UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

824 lines (761 loc) 33.6 kB
import * as d3 from 'd3-scale'; import { T as nearestPowerOfTwo } from '../../Common/Core/Math/index.js'; import { m as macro } from '../../macros2.js'; import vtkActor from './Actor.js'; import vtkDataArray from '../../Common/Core/DataArray.js'; import vtkScalarsToColors from '../../Common/Core/ScalarsToColors.js'; import vtkMapper from './Mapper.js'; import vtkPolyData from '../../Common/DataModel/PolyData.js'; import vtkTexture from './Texture.js'; import Constants from './ScalarBarActor/Constants.js'; const { VectorMode } = vtkScalarsToColors; const { Orientation } = Constants; // ---------------------------------------------------------------------------- // vtkScalarBarActor // // Note log scales are currently not supported // // Developer note: This class is broken into the main class and a helper // class. The main class holds view independent properties (those properties // that do not change as the view's resolution/aspect ratio change). The // helper class is instantiated one per view and holds properties that can // depend on view specific values such as resolution. The helper class code // could have been left to the View specific implementation (such as // vtkWebGPUScalarBarActor) but is instead placed here to it can be shared by // multiple rendering backends. // // ---------------------------------------------------------------------------- function applyTextStyle(ctx, style, defaultFontSize) { ctx.strokeStyle = style.strokeColor; ctx.lineWidth = style.strokeSize; ctx.fillStyle = style.fontColor; const fontSize = style.fontSize ?? defaultFontSize; ctx.font = `${style.fontStyle} ${fontSize}px ${style.fontFamily}`; } // ---------------------------------------------------------------------------- // Default autoLayout function // ---------------------------------------------------------------------------- // compute good values to use based on window size etc a bunch of heuristics // here with hand tuned constants These values worked for me but really this // method could be redically changed. The basic gist is // 1) compute a resonable font size // 2) render the text atlas using those font sizes // 3) pick horizontal or vertical bsed on window size // 4) based on the size of the title and tick labels rendered // compute the box size and position such that // the text will all fit nicely and the bar will be a resonable size // 5) compute the bar segments based on the above settings // // Note that this function can and should read values from the // ScalarBarActor but should only write values to the view dependent helper // instance that is provided as those values are the ones that will be used // for rendering. // function defaultAutoLayout(publicAPI, model) { return helper => { // we don't do a linear scale, the proportions for // a 700 pixel window differ from a 1400 const lastSize = helper.getLastSize(); const xAxisAdjust = (lastSize[0] / 700) ** 0.8; const yAxisAdjust = (lastSize[1] / 700) ** 0.8; const minAdjust = Math.min(xAxisAdjust, yAxisAdjust); const axisTextStyle = helper.getAxisTextStyle(); const tickTextStyle = helper.getTickTextStyle(); Object.assign(axisTextStyle, model.axisTextStyle); Object.assign(tickTextStyle, model.tickTextStyle); // compute a reasonable font size first, but only if user hasn't explicitly set it if (axisTextStyle.fontSize === undefined) { axisTextStyle.fontSize = Math.max(24 * minAdjust, 12); } if (tickTextStyle.fontSize === undefined) { if (helper.getLastAspectRatio() > 1.0) { tickTextStyle.fontSize = Math.max(20 * minAdjust, 10); } else { tickTextStyle.fontSize = Math.max(16 * minAdjust, 10); } } // rebuild the text atlas const textSizes = helper.updateTextureAtlas(); // now compute the boxSize and pixel offsets, different algorithm // for horizonal versus vertical helper.setTopTitle(false); const boxSize = helper.getBoxSizeByReference(); // Determine orientation: user-forced, or auto based on aspect ratio let isVertical = false; if (model.orientation === Orientation.VERTICAL) { isVertical = true; } else if (model.orientation === Orientation.HORIZONTAL) { isVertical = false; } else { // auto: use aspect ratio (original behavior) isVertical = helper.getLastAspectRatio() > 1.0; } // if vertical if (isVertical) { helper.setTickLabelPixelOffset(0.3 * tickTextStyle.fontSize); // if the title will fit within the width of the bar then that looks // nicer to put it at the top (helper.topTitle), otherwise rotate it // and place it sideways if (textSizes.titleWidth <= textSizes.tickWidth + helper.getTickLabelPixelOffset() + 0.8 * tickTextStyle.fontSize) { helper.setTopTitle(true); helper.setAxisTitlePixelOffset(0.2 * tickTextStyle.fontSize); boxSize[0] = 2.0 * (textSizes.tickWidth + helper.getTickLabelPixelOffset() + 0.8 * tickTextStyle.fontSize) / lastSize[0]; helper.setBoxPosition([0.98 - boxSize[0], -0.92]); } else { helper.setAxisTitlePixelOffset(0.2 * tickTextStyle.fontSize); boxSize[0] = 2.0 * (textSizes.titleHeight + helper.getAxisTitlePixelOffset() + textSizes.tickWidth + helper.getTickLabelPixelOffset() + 0.8 * tickTextStyle.fontSize) / lastSize[0]; helper.setBoxPosition([0.99 - boxSize[0], -0.92]); } boxSize[1] = Math.max(1.2, Math.min(1.84 / yAxisAdjust, 1.84)); } else { // horizontal helper.setAxisTitlePixelOffset(1.2 * tickTextStyle.fontSize); helper.setTickLabelPixelOffset(0.1 * tickTextStyle.fontSize); const titleHeight = // total offset from top of bar (includes ticks) 2.0 * (0.8 * tickTextStyle.fontSize + textSizes.titleHeight + helper.getAxisTitlePixelOffset()) / lastSize[1]; const tickWidth = 2.0 * textSizes.tickWidth / lastSize[0]; boxSize[0] = Math.min(1.9, Math.max(1.4, 1.4 * tickWidth * (helper.getTicks().length + 3))); boxSize[1] = titleHeight; helper.setBoxPosition([-0.5 * boxSize[0], -0.97]); } // recomute bar segments based on positioning helper.recomputeBarSegments(textSizes); }; } // ---------------------------------------------------------------------------- // Default generateTicks function // ---------------------------------------------------------------------------- // This function returns the default function used to generate vtkScalarBarActor ticks. // The default function makes use of d3.scaleLinear() to generate 5 tick marks between // the minimum and maximum values of the scalar bar. Customize this behavior by passing // a function to vtkScalarBarActor.newInstance({ generateTicks: customGenerateTicks }) // or by calling scalarBarActor.setGenerateTicks(customGenerateTicks). function defaultGenerateTicks(publicApi, model) { return helper => { const lastTickBounds = helper.getLastTickBounds(); const scale = d3.scaleLinear().domain([lastTickBounds[0], lastTickBounds[1]]); const ticks = scale.ticks(5); const format = scale.tickFormat(5); helper.setTicks(ticks); helper.setTickStrings(ticks.map(format)); }; } // many properties of this actor depend on the API specific view The main // dependency being the resolution as that drives what font sizes to use. // Bacause of this we need to do some of the calculations in a API specific // subclass. But... we don't want a lot of duplicated code between WebGL and // WebGPU for example so we have this helper class, that is designed to be // fairly API independent so that API specific views can call this to do // most of the work. function vtkScalarBarActorHelper(publicAPI, model) { // Set our className model.classHierarchy.push('vtkScalarBarActorHelper'); publicAPI.setRenderable = renderable => { if (model.renderable === renderable) { return; } model.renderable = renderable; model.barActor.setProperty(renderable.getProperty()); model.barActor.setParentProp(renderable); model.barActor.setCoordinateSystemToDisplay(); model.tmActor.setProperty(renderable.getProperty()); model.tmActor.setParentProp(renderable); model.tmActor.setCoordinateSystemToDisplay(); model.generateTicks = renderable.generateTicks; model.axisTextStyle = { ...renderable.getAxisTextStyle() }; model.tickTextStyle = { ...renderable.getTickTextStyle() }; publicAPI.modified(); }; publicAPI.updateAPISpecificData = (size, camera, renderWindow) => { // has the size changed? if (model.lastSize[0] !== size[0] || model.lastSize[1] !== size[1]) { model.lastSize[0] = size[0]; model.lastSize[1] = size[1]; model.lastAspectRatio = size[0] / size[1]; model.forceUpdate = true; } const scalarsToColors = model.renderable.getScalarsToColors(); if (!scalarsToColors || !model.renderable.getVisibility()) { return; } // make sure the lut is assigned to our mapper model.barMapper.setLookupTable(scalarsToColors); // camera should be the same for all views model.camera = camera; model.renderWindow = renderWindow; // did something significant change? If so rebuild a lot of things if (model.forceUpdate || Math.max(scalarsToColors.getMTime(), publicAPI.getMTime(), model.renderable.getMTime()) > model.lastRebuildTime.getMTime()) { const range = scalarsToColors.getMappingRange(); model.lastTickBounds = [...range]; // compute tick marks for axes (update for log scale) model.renderable.getGenerateTicks()(publicAPI); if (model.renderable.getAutomated()) { model.renderable.getAutoLayout()(publicAPI); } else { // copy values from renderable model.axisTextStyle = { ...model.renderable.getAxisTextStyle() }; model.tickTextStyle = { ...model.renderable.getTickTextStyle() }; model.barPosition = [...model.renderable.getBarPosition()]; model.barSize = [...model.renderable.getBarSize()]; model.boxPosition = [...model.renderable.getBoxPosition()]; model.boxSize = [...model.renderable.getBoxSize()]; model.axisTitlePixelOffset = model.renderable.getAxisTitlePixelOffset(); model.tickLabelPixelOffset = model.renderable.getTickLabelPixelOffset(); // rebuild the texture only when force or changed bounds, face // visibility changes do to change the atlas const textSizes = publicAPI.updateTextureAtlas(); // recompute bar segments based on positioning publicAPI.recomputeBarSegments(textSizes); } publicAPI.updatePolyDataForLabels(); publicAPI.updatePolyDataForBarSegments(); model.lastRebuildTime.modified(); model.forceUpdate = false; } }; // create the texture map atlas that contains the rendering of // all the text strings. Only needs to be called when the text strings // have changed (labels and ticks) publicAPI.updateTextureAtlas = () => { // set the text properties model.tmContext.textBaseline = 'bottom'; model.tmContext.textAlign = 'left'; // return some factors about the text atlas const results = {}; // first the axislabel const newTmAtlas = new Map(); let maxWidth = 0; let totalHeight = 1; // start one pixel in so we have a border applyTextStyle(model.tmContext, model.axisTextStyle, 18); let metrics = model.tmContext.measureText(model.renderable.getAxisLabel()); let entry = { height: metrics.actualBoundingBoxAscent + 2, startingHeight: totalHeight, width: metrics.width + 2, textStyle: model.axisTextStyle }; newTmAtlas.set(model.renderable.getAxisLabel(), entry); totalHeight += entry.height; maxWidth = entry.width; results.titleWidth = entry.width; results.titleHeight = entry.height; // and the ticks, NaN Below and Above results.tickWidth = 0; results.tickHeight = 0; applyTextStyle(model.tmContext, model.tickTextStyle, 14); const strings = [...publicAPI.getTickStrings(), 'NaN', 'Below', 'Above']; for (let t = 0; t < strings.length; t++) { if (!newTmAtlas.has(strings[t])) { metrics = model.tmContext.measureText(strings[t]); entry = { height: metrics.actualBoundingBoxAscent + 2, startingHeight: totalHeight, width: metrics.width + 2, textStyle: model.tickTextStyle }; newTmAtlas.set(strings[t], entry); totalHeight += entry.height; if (maxWidth < entry.width) { maxWidth = entry.width; } if (results.tickWidth < entry.width) { results.tickWidth = entry.width; } if (results.tickHeight < entry.height) { results.tickHeight = entry.height; } } } // always use power of two to avoid interpolation // in cases where PO2 is required maxWidth = nearestPowerOfTwo(maxWidth); totalHeight = nearestPowerOfTwo(totalHeight); // set the tcoord values newTmAtlas.forEach(value => { value.tcoords = [0.0, (totalHeight - value.startingHeight - value.height) / totalHeight, value.width / maxWidth, (totalHeight - value.startingHeight - value.height) / totalHeight, value.width / maxWidth, (totalHeight - value.startingHeight) / totalHeight, 0.0, (totalHeight - value.startingHeight) / totalHeight]; }); // make sure we have power of two dimensions model.tmCanvas.width = maxWidth; model.tmCanvas.height = totalHeight; model.tmContext.textBaseline = 'bottom'; model.tmContext.textAlign = 'left'; model.tmContext.clearRect(0, 0, maxWidth, totalHeight); // draw the text onto the texture newTmAtlas.forEach((value, key) => { const defaultSize = value.textStyle === model.axisTextStyle ? 18 : 14; applyTextStyle(model.tmContext, value.textStyle, defaultSize); model.tmContext.fillText(key, 1, value.startingHeight + value.height - 1); }); model.tmTexture.setCanvas(model.tmCanvas); // mark as modified since the canvas typically doesn't change model.tmTexture.modified(); model._tmAtlas = newTmAtlas; return results; }; publicAPI.computeBarSize = textSizes => { // compute orientation model.vertical = model.boxSize[1] > model.boxSize[0]; const tickHeight = 2.0 * textSizes.tickHeight / model.lastSize[1]; const segSize = [1, 1]; // horizontal and vertical have different astetics so adjust based on // orientation if (model.vertical) { const tickWidth = 2.0 * (textSizes.tickWidth + model.tickLabelPixelOffset) / model.lastSize[0]; if (model.topTitle) { const titleHeight = 2.0 * (textSizes.titleHeight + model.axisTitlePixelOffset) / model.lastSize[1]; model.barSize[0] = model.boxSize[0] - tickWidth; model.barSize[1] = model.boxSize[1] - titleHeight; } else { // rotated title so width is based off height const titleWidth = 2.0 * (textSizes.titleHeight + model.axisTitlePixelOffset) / model.lastSize[0]; model.barSize[0] = model.boxSize[0] - titleWidth - tickWidth; model.barSize[1] = model.boxSize[1]; } model.barPosition[0] = model.boxPosition[0] + tickWidth; model.barPosition[1] = model.boxPosition[1]; segSize[1] = tickHeight; } else { const tickWidth = (2.0 * textSizes.tickWidth - 8) / model.lastSize[0]; const titleHeight = 2.0 * (textSizes.titleHeight + model.axisTitlePixelOffset) / model.lastSize[1]; model.barSize[0] = model.boxSize[0]; model.barPosition[0] = model.boxPosition[0]; model.barSize[1] = model.boxSize[1] - titleHeight; model.barPosition[1] = model.boxPosition[1]; segSize[0] = tickWidth; } return segSize; }; // based on all the settins compute a barSegments array // containing the segments of the scalar bar // each segment contains // corners[4][2] // title - e.g. NaN, Above, ticks // scalars - the normalized scalars values to use for that segment // // Note that the bar consumes the space in the box that remains after // leaving room for the text labels publicAPI.recomputeBarSegments = textSizes => { // first compute the barSize/Position const segSize = publicAPI.computeBarSize(textSizes); model.barSegments = []; const startPos = [0.0, 0.0]; // horizontal and vertical have different astetics so adjust based on // orientation const barAxis = model.vertical ? 1 : 0; const segSpace = model.vertical ? 0.01 : 0.02; function pushSeg(title, scalars) { model.barSegments.push({ corners: [[...startPos], [startPos[0] + segSize[0], startPos[1]], [startPos[0] + segSize[0], startPos[1] + segSize[1]], [startPos[0], startPos[1] + segSize[1]]], scalars, title }); startPos[barAxis] += segSize[barAxis] + segSpace; } if (model.renderable.getDrawNanAnnotation() && model.renderable.getScalarsToColors().getNanColor()) { pushSeg('NaN', [NaN, NaN, NaN, NaN]); } if (model.renderable.getDrawBelowRangeSwatch() && model.renderable.getScalarsToColors().getUseBelowRangeColor?.()) { pushSeg('Below', [-0.1, -0.1, -0.1, -0.1]); } const haveAbove = model.renderable.getScalarsToColors().getUseAboveRangeColor?.(); // extra space around the ticks section startPos[barAxis] += segSpace; const oldSegSize = segSize[barAxis]; segSize[barAxis] = haveAbove ? 1.0 - 2.0 * segSpace - segSize[barAxis] - startPos[barAxis] : 1.0 - segSpace - startPos[barAxis]; pushSeg('ticks', model.vertical ? [0, 0, 0.995, 0.995] : [0, 0.995, 0.995, 0]); if (model.renderable.getDrawAboveRangeSwatch() && haveAbove) { segSize[barAxis] = oldSegSize; startPos[barAxis] += segSpace; pushSeg('Above', [1.1, 1.1, 1.1, 1.1]); } }; // called by updatePolyDataForLabels // modifies class constants tmp2v3 const tmp2v3 = new Float64Array(3); // anchor point = pos // H alignment = left, middle, right // V alignment = bottom, middle, top // Text Orientation = horizontal, vertical // orientation publicAPI.createPolyDataForOneLabel = (text, pos, alignment, orientation, offset, results) => { const value = model._tmAtlas.get(text); if (!value) { return; } // have to find the four corners of the texture polygon for this label let ptIdx = results.ptIdx; let cellIdx = results.cellIdx; // get achor point in pixels tmp2v3[0] = (0.5 * pos[0] + 0.5) * model.lastSize[0]; tmp2v3[1] = (0.5 * pos[1] + 0.5) * model.lastSize[1]; tmp2v3[2] = pos[2]; tmp2v3[0] += offset[0]; tmp2v3[1] += offset[1]; // get text size in display pixels const textSize = []; const textAxes = orientation === 'vertical' ? [1, 0] : [0, 1]; if (orientation === 'vertical') { textSize[0] = value.width; textSize[1] = -value.height; // update anchor point based on alignment if (alignment[0] === 'middle') { tmp2v3[1] -= value.width / 2.0; } else if (alignment[0] === 'right') { tmp2v3[1] -= value.width; } if (alignment[1] === 'middle') { tmp2v3[0] += value.height / 2.0; } else if (alignment[1] === 'top') { tmp2v3[0] += value.height; } } else { textSize[0] = value.width; textSize[1] = value.height; // update anchor point based on alignment if (alignment[0] === 'middle') { tmp2v3[0] -= value.width / 2.0; } else if (alignment[0] === 'right') { tmp2v3[0] -= value.width; } if (alignment[1] === 'middle') { tmp2v3[1] -= value.height / 2.0; } else if (alignment[1] === 'top') { tmp2v3[1] -= value.height; } } results.points[ptIdx * 3] = tmp2v3[0]; results.points[ptIdx * 3 + 1] = tmp2v3[1]; results.points[ptIdx * 3 + 2] = tmp2v3[2]; results.tcoords[ptIdx * 2] = value.tcoords[0]; results.tcoords[ptIdx * 2 + 1] = value.tcoords[1]; ptIdx++; tmp2v3[textAxes[0]] += textSize[0]; results.points[ptIdx * 3] = tmp2v3[0]; results.points[ptIdx * 3 + 1] = tmp2v3[1]; results.points[ptIdx * 3 + 2] = tmp2v3[2]; results.tcoords[ptIdx * 2] = value.tcoords[2]; results.tcoords[ptIdx * 2 + 1] = value.tcoords[3]; ptIdx++; tmp2v3[textAxes[1]] += textSize[1]; results.points[ptIdx * 3] = tmp2v3[0]; results.points[ptIdx * 3 + 1] = tmp2v3[1]; results.points[ptIdx * 3 + 2] = tmp2v3[2]; results.tcoords[ptIdx * 2] = value.tcoords[4]; results.tcoords[ptIdx * 2 + 1] = value.tcoords[5]; ptIdx++; tmp2v3[textAxes[0]] -= textSize[0]; results.points[ptIdx * 3] = tmp2v3[0]; results.points[ptIdx * 3 + 1] = tmp2v3[1]; results.points[ptIdx * 3 + 2] = tmp2v3[2]; results.tcoords[ptIdx * 2] = value.tcoords[6]; results.tcoords[ptIdx * 2 + 1] = value.tcoords[7]; ptIdx++; // add the two triangles to represent the quad results.polys[cellIdx * 4] = 3; results.polys[cellIdx * 4 + 1] = ptIdx - 4; results.polys[cellIdx * 4 + 2] = ptIdx - 3; results.polys[cellIdx * 4 + 3] = ptIdx - 2; cellIdx++; results.polys[cellIdx * 4] = 3; results.polys[cellIdx * 4 + 1] = ptIdx - 4; results.polys[cellIdx * 4 + 2] = ptIdx - 2; results.polys[cellIdx * 4 + 3] = ptIdx - 1; results.ptIdx += 4; results.cellIdx += 2; }; // update the polydata associated with drawing the text labels // specifically the quads used for each label and their associated tcoords // etc. This changes every time the camera viewpoint changes const tmpv3 = new Float64Array(3); publicAPI.updatePolyDataForLabels = () => { // update the polydata const numLabels = publicAPI.getTickStrings().length + model.barSegments.length; const numPts = numLabels * 4; const numTris = numLabels * 2; const points = new Float64Array(numPts * 3); const polys = new Uint16Array(numTris * 4); const tcoords = new Float32Array(numPts * 2); const results = { ptIdx: 0, cellIdx: 0, polys, points, tcoords }; // compute the direction vector const offsetAxis = model.vertical ? 0 : 1; const spacedAxis = model.vertical ? 1 : 0; tmpv3[2] = -0.99; // near plane // draw the title const alignment = model.vertical ? ['right', 'middle'] : ['middle', 'bottom']; let dir = [0, 1]; const tickOffsets = [0, 0]; if (model.vertical) { tickOffsets[0] = -model.tickLabelPixelOffset; if (model.topTitle) { tmpv3[0] = model.boxPosition[0] + 0.5 * model.boxSize[0]; tmpv3[1] = model.barPosition[1] + model.barSize[1]; // write the axis label publicAPI.createPolyDataForOneLabel(model.renderable.getAxisLabel(), tmpv3, ['middle', 'bottom'], 'horizontal', [0, model.axisTitlePixelOffset], results); } else { tmpv3[0] = model.barPosition[0] + model.barSize[0]; tmpv3[1] = model.barPosition[1] + 0.5 * model.barSize[1]; // write the axis label publicAPI.createPolyDataForOneLabel(model.renderable.getAxisLabel(), tmpv3, ['middle', 'top'], 'vertical', [model.axisTitlePixelOffset, 0], results); } dir = [-1, 0]; } else { tickOffsets[1] = model.tickLabelPixelOffset; tmpv3[0] = model.barPosition[0] + 0.5 * model.barSize[0]; tmpv3[1] = model.barPosition[1] + model.barSize[1]; publicAPI.createPolyDataForOneLabel(model.renderable.getAxisLabel(), tmpv3, ['middle', 'bottom'], 'horizontal', [0, model.axisTitlePixelOffset], results); } tmpv3[offsetAxis] = model.barPosition[offsetAxis] + (0.5 * dir[offsetAxis] + 0.5) * model.barSize[offsetAxis]; tmpv3[spacedAxis] = model.barPosition[spacedAxis] + model.barSize[spacedAxis] * 0.5; // draw bar segment labels let tickSeg = null; for (let i = 0; i < model.barSegments.length; i++) { const seg = model.barSegments[i]; if (seg.title === 'ticks') { // handle ticks below tickSeg = seg; } else { tmpv3[spacedAxis] = model.barPosition[spacedAxis] + 0.5 * model.barSize[spacedAxis] * (seg.corners[2][spacedAxis] + seg.corners[0][spacedAxis]); publicAPI.createPolyDataForOneLabel(seg.title, tmpv3, alignment, 'horizontal', tickOffsets, results); } } // write the tick labels const tickSegmentStart = model.barPosition[spacedAxis] + model.barSize[spacedAxis] * tickSeg.corners[0][spacedAxis]; const tickSegmentSize = model.barSize[spacedAxis] * (tickSeg.corners[2][spacedAxis] - tickSeg.corners[0][spacedAxis]); const ticks = publicAPI.getTicks(); const tickStrings = publicAPI.getTickStrings(); const tickPositions = publicAPI.getTickPositions(); for (let t = 0; t < ticks.length; t++) { // If tickPositions is not set, use a normalized position const tickPos = tickPositions ? tickPositions[t] : (ticks[t] - model.lastTickBounds[0]) / (model.lastTickBounds[1] - model.lastTickBounds[0]); tmpv3[spacedAxis] = tickSegmentStart + tickSegmentSize * tickPos; publicAPI.createPolyDataForOneLabel(tickStrings[t], tmpv3, alignment, 'horizontal', tickOffsets, results); } const tcoordDA = vtkDataArray.newInstance({ numberOfComponents: 2, values: tcoords, name: 'TextureCoordinates' }); model.tmPolyData.getPointData().setTCoords(tcoordDA); model.tmPolyData.getPoints().setData(points, 3); model.tmPolyData.getPoints().modified(); model.tmPolyData.getPolys().setData(polys, 1); model.tmPolyData.getPolys().modified(); model.tmPolyData.modified(); }; publicAPI.updatePolyDataForBarSegments = () => { const scalarsToColors = model.renderable.getScalarsToColors(); let numberOfExtraColors = 0; if (model.renderable.getDrawNanAnnotation() && scalarsToColors.getNanColor()) { numberOfExtraColors += 1; } if (model.renderable.getDrawBelowRangeSwatch() && scalarsToColors.getUseBelowRangeColor?.()) { numberOfExtraColors += 1; } if (model.renderable.getDrawAboveRangeSwatch() && scalarsToColors.getUseAboveRangeColor?.()) { numberOfExtraColors += 1; } const numPts = 4 * (1 + numberOfExtraColors); const numQuads = numPts; // handle vector component mode let numComps = 1; if (scalarsToColors.getVectorMode() === VectorMode.COMPONENT) { numComps = scalarsToColors.getVectorComponent() + 1; } // create the colored bars const points = new Float64Array(numPts * 3); const cells = new Uint16Array(numQuads * 5); const scalars = new Float32Array(numPts * numComps); let ptIdx = 0; let cellIdx = 0; for (let i = 0; i < model.barSegments.length; i++) { const seg = model.barSegments[i]; for (let e = 0; e < 4; e++) { tmpv3[0] = model.barPosition[0] + seg.corners[e][0] * model.barSize[0]; tmpv3[1] = model.barPosition[1] + seg.corners[e][1] * model.barSize[1]; points[ptIdx * 3] = (0.5 * tmpv3[0] + 0.5) * model.lastSize[0]; points[ptIdx * 3 + 1] = (0.5 * tmpv3[1] + 0.5) * model.lastSize[1]; points[ptIdx * 3 + 2] = tmpv3[2]; for (let nc = 0; nc < numComps; nc++) { scalars[ptIdx * numComps + nc] = model.lastTickBounds[0] + seg.scalars[e] * (model.lastTickBounds[1] - model.lastTickBounds[0]); } ptIdx++; } cells[cellIdx * 5] = 4; cells[cellIdx * 5 + 1] = ptIdx - 4; cells[cellIdx * 5 + 2] = ptIdx - 3; cells[cellIdx * 5 + 3] = ptIdx - 2; cells[cellIdx * 5 + 4] = ptIdx - 1; cellIdx++; } const scalarsDA = vtkDataArray.newInstance({ numberOfComponents: numComps, values: scalars, name: 'Scalars' }); model.polyData.getPointData().setScalars(scalarsDA); model.polyData.getPoints().setData(points, 3); model.polyData.getPoints().modified(); model.polyData.getPolys().setData(cells, 1); model.polyData.getPolys().modified(); model.polyData.modified(); }; } const newScalarBarActorHelper = macro.newInstance((publicAPI, model, initialValues = { renderable: null }) => { Object.assign(model, {}, initialValues); // Inheritance macro.obj(publicAPI, model); macro.setGet(publicAPI, model, ['axisTitlePixelOffset', 'tickLabelPixelOffset', 'renderable', 'topTitle', 'ticks', 'tickStrings', 'tickPositions']); macro.get(publicAPI, model, ['lastSize', 'lastAspectRatio', 'lastTickBounds', 'axisTextStyle', 'tickTextStyle', 'barActor', 'tmActor']); macro.getArray(publicAPI, model, ['boxPosition', 'boxSize']); macro.setArray(publicAPI, model, ['boxPosition', 'boxSize'], 2); model.forceUpdate = false; model.lastRebuildTime = {}; macro.obj(model.lastRebuildTime, { mtime: 0 }); model.lastSize = [-1, -1]; model.tmCanvas = document.createElement('canvas'); model.tmContext = model.tmCanvas.getContext('2d'); model._tmAtlas = new Map(); model.barMapper = vtkMapper.newInstance(); model.barMapper.setInterpolateScalarsBeforeMapping(true); model.barMapper.setUseLookupTableScalarRange(true); model.polyData = vtkPolyData.newInstance(); model.barMapper.setInputData(model.polyData); model.barActor = vtkActor.newInstance(); model.barActor.setMapper(model.barMapper); // for texture atlas model.tmPolyData = vtkPolyData.newInstance(); model.tmMapper = vtkMapper.newInstance(); model.tmMapper.setInputData(model.tmPolyData); model.tmTexture = vtkTexture.newInstance({ resizable: true }); model.tmTexture.setInterpolate(false); model.tmActor = vtkActor.newInstance({ parentProp: publicAPI }); model.tmActor.setMapper(model.tmMapper); model.tmActor.addTexture(model.tmTexture); model.barPosition = [0, 0]; model.barSize = [0, 0]; model.boxPosition = [0.88, -0.92]; model.boxSize = [0.1, 1.1]; // internal variables model.lastTickBounds = []; vtkScalarBarActorHelper(publicAPI, model); }, 'vtkScalarBarActorHelper'); // // Now we define the public class that the application sets view independent // properties on. This class is fairly small as it mainly just holds // properties setter and getters leaving all calculations to the helper // class. // function vtkScalarBarActor(publicAPI, model) { // Set our className model.classHierarchy.push('vtkScalarBarActor'); publicAPI.setTickTextStyle = tickStyle => { model.tickTextStyle = { ...model.tickTextStyle, ...tickStyle }; publicAPI.modified(); }; publicAPI.setAxisTextStyle = axisStyle => { model.axisTextStyle = { ...model.axisTextStyle, ...axisStyle }; publicAPI.modified(); }; publicAPI.setOrientationToHorizontal = () => publicAPI.setOrientation(Orientation.HORIZONTAL); publicAPI.setOrientationToVertical = () => publicAPI.setOrientation(Orientation.VERTICAL); publicAPI.resetAutoLayoutToDefault = () => { publicAPI.setAutoLayout(defaultAutoLayout(publicAPI, model)); }; publicAPI.resetGenerateTicksToDefault = () => { publicAPI.setGenerateTicks(defaultGenerateTicks()); }; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- function defaultValues(initialValues) { return { automated: true, autoLayout: null, axisLabel: 'Scalar Value', barPosition: [0, 0], barSize: [0, 0], boxPosition: [0.88, -0.92], boxSize: [0.1, 1.1], scalarToColors: null, axisTitlePixelOffset: 36.0, axisTextStyle: { fontColor: 'white', fontStyle: 'normal', fontSize: undefined, fontFamily: 'serif' }, tickLabelPixelOffset: 14.0, tickTextStyle: { fontColor: 'white', fontStyle: 'normal', fontSize: undefined, fontFamily: 'serif' }, generateTicks: null, drawNanAnnotation: true, drawBelowRangeSwatch: true, drawAboveRangeSwatch: true, orientation: null, ...initialValues }; } // ---------------------------------------------------------------------------- function extend(publicAPI, model, initialValues = {}) { Object.assign(model, defaultValues(initialValues)); if (!model.autoLayout) model.autoLayout = defaultAutoLayout(publicAPI, model); if (!model.generateTicks) model.generateTicks = defaultGenerateTicks(); // Inheritance vtkActor.extend(publicAPI, model, initialValues); publicAPI.getProperty().setDiffuse(0.0); publicAPI.getProperty().setAmbient(1.0); macro.setGet(publicAPI, model, ['automated', 'autoLayout', 'axisTitlePixelOffset', 'axisLabel', 'scalarsToColors', 'tickLabelPixelOffset', 'generateTicks', 'drawNanAnnotation', 'drawBelowRangeSwatch', 'drawAboveRangeSwatch', 'orientation']); macro.get(publicAPI, model, ['axisTextStyle', 'tickTextStyle']); macro.getArray(publicAPI, model, ['barPosition', 'barSize', 'boxPosition', 'boxSize']); macro.setArray(publicAPI, model, ['barPosition', 'barSize', 'boxPosition', 'boxSize'], 2); // Object methods vtkScalarBarActor(publicAPI, model); } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkScalarBarActor'); // ---------------------------------------------------------------------------- var vtkScalarBarActor$1 = { newInstance, extend, newScalarBarActorHelper, ...Constants }; export { vtkScalarBarActor$1 as default, extend, newInstance };