@acransac/vtk.js
Version:
Visualization Toolkit for the Web
585 lines (501 loc) • 18.5 kB
JavaScript
import macro from 'vtk.js/Sources/macro';
import vtkAbstractMapper3D from 'vtk.js/Sources/Rendering/Core/AbstractMapper3D';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkLookupTable from 'vtk.js/Sources/Common/Core/LookupTable';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
import vtkScalarsToColors from 'vtk.js/Sources/Common/Core/ScalarsToColors/Constants'; // Need to go inside Constants otherwise dependency loop
import CoincidentTopologyHelper from 'vtk.js/Sources/Rendering/Core/Mapper/CoincidentTopologyHelper';
import Constants from 'vtk.js/Sources/Rendering/Core/Mapper/Constants';
const { staticOffsetAPI, otherStaticMethods } = CoincidentTopologyHelper;
const { ColorMode, ScalarMode, GetArray } = Constants;
const { VectorMode } = vtkScalarsToColors;
const { VtkDataTypes } = vtkDataArray;
// ----------------------------------------------------------------------------
function notImplemented(method) {
return () => macro.vtkErrorMacro(`vtkMapper::${method} - NOT IMPLEMENTED`);
}
// ----------------------------------------------------------------------------
// vtkMapper methods
// ----------------------------------------------------------------------------
function vtkMapper(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkMapper');
publicAPI.getBounds = () => {
const input = publicAPI.getInputData();
if (!input) {
model.bounds = vtkMath.createUninitializedBounds();
} else {
if (!model.static) {
publicAPI.update();
}
model.bounds = input.getBounds();
}
return model.bounds;
};
publicAPI.setForceCompileOnly = (v) => {
model.forceCompileOnly = v;
// make sure we do NOT call modified()
};
publicAPI.createDefaultLookupTable = () => {
model.lookupTable = vtkLookupTable.newInstance();
};
publicAPI.getColorModeAsString = () =>
macro.enumToString(ColorMode, model.colorMode);
publicAPI.setColorModeToDefault = () => publicAPI.setColorMode(0);
publicAPI.setColorModeToMapScalars = () => publicAPI.setColorMode(1);
publicAPI.setColorModeToDirectScalars = () => publicAPI.setColorMode(2);
publicAPI.getScalarModeAsString = () =>
macro.enumToString(ScalarMode, model.scalarMode);
publicAPI.setScalarModeToDefault = () => publicAPI.setScalarMode(0);
publicAPI.setScalarModeToUsePointData = () => publicAPI.setScalarMode(1);
publicAPI.setScalarModeToUseCellData = () => publicAPI.setScalarMode(2);
publicAPI.setScalarModeToUsePointFieldData = () => publicAPI.setScalarMode(3);
publicAPI.setScalarModeToUseCellFieldData = () => publicAPI.setScalarMode(4);
publicAPI.setScalarModeToUseFieldData = () => publicAPI.setScalarMode(5);
publicAPI.getAbstractScalars = (
input,
scalarMode,
arrayAccessMode,
arrayId,
arrayName
) => {
// make sure we have an input
if (!input || !model.scalarVisibility) {
return { scalars: null, cellFLag: false };
}
let scalars = null;
let cellFlag = false;
// get and scalar data according to scalar mode
if (scalarMode === ScalarMode.DEFAULT) {
scalars = input.getPointData().getScalars();
if (!scalars) {
scalars = input.getCellData().getScalars();
cellFlag = true;
}
} else if (scalarMode === ScalarMode.USE_POINT_DATA) {
scalars = input.getPointData().getScalars();
} else if (scalarMode === ScalarMode.USE_CELL_DATA) {
scalars = input.getCellData().getScalars();
cellFlag = true;
} else if (scalarMode === ScalarMode.USE_POINT_FIELD_DATA) {
const pd = input.getPointData();
if (arrayAccessMode === GetArray.BY_ID) {
scalars = pd.getArrayByIndex(arrayId);
} else {
scalars = pd.getArrayByName(arrayName);
}
} else if (scalarMode === ScalarMode.USE_CELL_FIELD_DATA) {
const cd = input.getCellData();
cellFlag = true;
if (arrayAccessMode === GetArray.BY_ID) {
scalars = cd.getArrayByIndex(arrayId);
} else {
scalars = cd.getArrayByName(arrayName);
}
} else if (scalarMode === ScalarMode.USE_FIELD_DATA) {
const fd = input.getFieldData();
if (arrayAccessMode === GetArray.BY_ID) {
scalars = fd.getArrayByIndex(arrayId);
} else {
scalars = fd.getArrayByName(arrayName);
}
}
return { scalars, cellFlag };
};
publicAPI.mapScalars = (input, alpha) => {
const scalars = publicAPI.getAbstractScalars(
input,
model.scalarMode,
model.arrayAccessMode,
model.arrayId,
model.colorByArrayName
).scalars;
if (!scalars) {
model.colorCoordinates = null;
model.colorTextureMap = null;
model.colorMapColors = null;
return;
}
// we want to only recompute when something has changed
const toString = `${publicAPI.getMTime()}${scalars.getMTime()}${alpha}`;
if (model.colorBuildString === toString) return;
if (!model.useLookupTableScalarRange) {
publicAPI
.getLookupTable()
.setRange(model.scalarRange[0], model.scalarRange[1]);
}
// Decide between texture color or vertex color.
// Cell data always uses vertex color.
// Only point data can use both texture and vertex coloring.
if (publicAPI.canUseTextureMapForColoring(input)) {
publicAPI.mapScalarsToTexture(scalars, alpha);
} else {
model.colorCoordinates = null;
model.colorTextureMap = null;
const lut = publicAPI.getLookupTable();
if (lut) {
// Ensure that the lookup table is built
lut.build();
model.colorMapColors = lut.mapScalars(scalars, model.colorMode, -1);
}
}
model.colorBuildString = `${publicAPI.getMTime()}${scalars.getMTime()}${alpha}`;
};
//-----------------------------------------------------------------------------
publicAPI.scalarToTextureCoordinate = (
scalarValue, // Input scalar
rangeMin, // range[0]
invRangeWidth
) => {
// 1/(range[1]-range[0])
let texCoordS = 0.5; // Scalar value is arbitrary when NaN
let texCoordT = 1.0; // 1.0 in t coordinate means NaN
if (!vtkMath.isNan(scalarValue)) {
// 0.0 in t coordinate means not NaN. So why am I setting it to 0.49?
// Because when you are mapping scalars and you have a NaN adjacent to
// anything else, the interpolation everywhere should be NaN. Thus, I
// want the NaN color everywhere except right on the non-NaN neighbors.
// To simulate this, I set the t coord for the real numbers close to
// the threshold so that the interpolation almost immediately looks up
// the NaN value.
texCoordT = 0.49;
texCoordS = (scalarValue - rangeMin) * invRangeWidth;
// Some implementations apparently don't handle relatively large
// numbers (compared to the range [0.0, 1.0]) very well. In fact,
// values above 1122.0f appear to cause texture wrap-around on
// some systems even when edge clamping is enabled. Why 1122.0f? I
// don't know. For safety, we'll clamp at +/- 1000. This will
// result in incorrect images when the texture value should be
// above or below 1000, but I don't have a better solution.
if (texCoordS > 1000.0) {
texCoordS = 1000.0;
} else if (texCoordS < -1000.0) {
texCoordS = -1000.0;
}
}
return { texCoordS, texCoordT };
};
//-----------------------------------------------------------------------------
publicAPI.createColorTextureCoordinates = (
input,
output,
numScalars,
numComps,
component,
range,
tableRange,
tableNumberOfColors,
useLogScale
) => {
// We have to change the range used for computing texture
// coordinates slightly to accommodate the special above- and
// below-range colors that are the first and last texels,
// respectively.
const scalarTexelWidth = (range[1] - range[0]) / tableNumberOfColors;
const paddedRange = [];
paddedRange[0] = range[0] - scalarTexelWidth;
paddedRange[1] = range[1] + scalarTexelWidth;
const invRangeWidth = 1.0 / (paddedRange[1] - paddedRange[0]);
const outputV = output.getData();
const inputV = input.getData();
let count = 0;
let outputCount = 0;
if (component < 0 || component >= numComps) {
for (let scalarIdx = 0; scalarIdx < numScalars; ++scalarIdx) {
let sum = 0;
for (let compIdx = 0; compIdx < numComps; ++compIdx) {
sum += inputV[count] * inputV[count];
count++;
}
let magnitude = Math.sqrt(sum);
if (useLogScale) {
magnitude = vtkLookupTable.applyLogScale(
magnitude,
tableRange,
range
);
}
const outputs = publicAPI.scalarToTextureCoordinate(
magnitude,
paddedRange[0],
invRangeWidth
);
outputV[outputCount] = outputs.texCoordS;
outputV[outputCount + 1] = outputs.texCoordT;
outputCount += 2;
}
} else {
count += component;
for (let scalarIdx = 0; scalarIdx < numScalars; ++scalarIdx) {
let inputValue = inputV[count];
if (useLogScale) {
inputValue = vtkLookupTable.applyLogScale(
inputValue,
tableRange,
range
);
}
const outputs = publicAPI.scalarToTextureCoordinate(
inputValue,
paddedRange[0],
invRangeWidth
);
outputV[outputCount] = outputs.texCoordS;
outputV[outputCount + 1] = outputs.texCoordT;
outputCount += 2;
count += numComps;
}
}
};
publicAPI.mapScalarsToTexture = (scalars, alpha) => {
const range = model.lookupTable.getRange();
const useLogScale = model.lookupTable.usingLogScale();
if (useLogScale) {
// convert range to log.
vtkLookupTable.getLogRange(range, range);
}
const origAlpha = model.lookupTable.getAlpha();
// Get rid of vertex color array. Only texture or vertex coloring
// can be active at one time. The existence of the array is the
// signal to use that technique.
model.colorMapColors = null;
// If the lookup table has changed, then recreate the color texture map.
// Set a new lookup table changes this->MTime.
if (
model.colorTextureMap == null ||
publicAPI.getMTime() > model.colorTextureMap.getMTime() ||
model.lookupTable.getMTime() > model.colorTextureMap.getMTime() ||
model.lookupTable.getAlpha() !== alpha
) {
model.lookupTable.setAlpha(alpha);
model.colorTextureMap = null;
// Get the texture map from the lookup table.
// Create a dummy ramp of scalars.
// In the future, we could extend vtkScalarsToColors.
model.lookupTable.build();
let numberOfColors = model.lookupTable.getNumberOfAvailableColors();
if (numberOfColors > 4094) {
numberOfColors = 4094;
}
numberOfColors += 2;
const k = (range[1] - range[0]) / (numberOfColors - 1 - 2);
const newArray = new Float64Array(numberOfColors * 2);
for (let i = 0; i < numberOfColors; ++i) {
newArray[i] = range[0] + i * k - k; // minus k to start at below range color
if (useLogScale) {
newArray[i] = 10.0 ** newArray[i];
}
}
// Dimension on NaN.
for (let i = 0; i < numberOfColors; ++i) {
newArray[i + numberOfColors] = NaN;
}
model.colorTextureMap = vtkImageData.newInstance();
model.colorTextureMap.setExtent(0, numberOfColors - 1, 0, 1, 0, 0);
const tmp = vtkDataArray.newInstance({
numberOfComponents: 1,
values: newArray,
});
model.colorTextureMap
.getPointData()
.setScalars(model.lookupTable.mapScalars(tmp, model.colorMode, 0));
model.lookupTable.setAlpha(origAlpha);
}
// Create new coordinates if necessary.
// Need to compare lookup table in case the range has changed.
if (
!model.colorCoordinates ||
publicAPI.getMTime() > model.colorCoordinates.getMTime() ||
publicAPI.getInputData(0).getMTime() >
model.colorCoordinates.getMTime() ||
model.lookupTable.getMTime() > model.colorCoordinates.getMTime()
) {
// Get rid of old colors
model.colorCoordinates = null;
// Now create the color texture coordinates.
const numComps = scalars.getNumberOfComponents();
const num = scalars.getNumberOfTuples();
// const fArray = new FloatArray(num * 2);
model.colorCoordinates = vtkDataArray.newInstance({
numberOfComponents: 2,
values: new Float32Array(num * 2),
});
let scalarComponent = model.lookupTable.getVectorComponent();
// Although I like the feature of applying magnitude to single component
// scalars, it is not how the old MapScalars for vertex coloring works.
if (
model.lookupTable.getVectorMode() === VectorMode.MAGNITUDE &&
scalars.getNumberOfComponents() > 1
) {
scalarComponent = -1;
}
publicAPI.createColorTextureCoordinates(
scalars,
model.colorCoordinates,
num,
numComps,
scalarComponent,
range,
model.lookupTable.getRange(),
model.colorTextureMap.getPointData().getScalars().getNumberOfTuples() /
2 -
2,
useLogScale
);
}
};
publicAPI.getIsOpaque = () => {
const lut = publicAPI.getLookupTable();
if (lut) {
// Ensure that the lookup table is built
lut.build();
return lut.isOpaque();
}
return true;
};
publicAPI.canUseTextureMapForColoring = (input) => {
if (!model.interpolateScalarsBeforeMapping) {
return false; // user doesn't want us to use texture maps at all.
}
// index color does not use textures
if (model.lookupTable && model.lookupTable.getIndexedLookup()) {
return false;
}
const gasResult = publicAPI.getAbstractScalars(
input,
model.scalarMode,
model.arrayAccessMode,
model.arrayId,
model.colorByArrayName
);
const scalars = gasResult.scalars;
if (!scalars) {
// no scalars on this dataset, we don't care if texture is used at all.
return false;
}
if (gasResult.cellFlag) {
return false; // cell data colors, don't use textures.
}
if (
(model.colorMode === ColorMode.DEFAULT &&
scalars.getDataType() === VtkDataTypes.UNSIGNED_CHAR) ||
model.colorMode === ColorMode.DIRECT_SCALARS
) {
// Don't use texture is direct coloring using RGB unsigned chars is
// requested.
return false;
}
return true;
};
publicAPI.clearColorArrays = () => {
model.colorMapColors = null;
model.colorCoordinates = null;
model.colorTextureMap = null;
};
publicAPI.getLookupTable = () => {
if (!model.lookupTable) {
publicAPI.createDefaultLookupTable();
}
return model.lookupTable;
};
publicAPI.getMTime = () => {
let mt = model.mtime;
if (model.lookupTable !== null) {
const time = model.lookupTable.getMTime();
mt = time > mt ? time : mt;
}
return mt;
};
publicAPI.getPrimitiveCount = () => {
const input = publicAPI.getInputData();
const pcount = {
points: input.getPoints().getNumberOfValues() / 3,
verts:
input.getVerts().getNumberOfValues() -
input.getVerts().getNumberOfCells(),
lines:
input.getLines().getNumberOfValues() -
2 * input.getLines().getNumberOfCells(),
triangles:
input.getPolys().getNumberOfValues() -
3 * input.getLines().getNumberOfCells(),
};
return pcount;
};
publicAPI.acquireInvertibleLookupTable = notImplemented(
'AcquireInvertibleLookupTable'
);
publicAPI.valueToColor = notImplemented('ValueToColor');
publicAPI.colorToValue = notImplemented('ColorToValue');
publicAPI.useInvertibleColorFor = notImplemented('UseInvertibleColorFor');
publicAPI.clearInvertibleColor = notImplemented('ClearInvertibleColor');
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
colorMapColors: null, // Same as this->Colors
static: false,
lookupTable: null,
scalarVisibility: true,
scalarRange: [0, 1],
useLookupTableScalarRange: false,
colorMode: 0,
scalarMode: 0,
arrayAccessMode: 1, // By_NAME
renderTime: 0,
colorByArrayName: null,
fieldDataTupleId: -1,
interpolateScalarsBeforeMapping: false,
colorCoordinates: null,
colorTextureMap: null,
forceCompileOnly: 0,
useInvertibleColors: false,
invertibleScalars: null,
viewSpecificProperties: null,
customShaderAttributes: [],
};
// ----------------------------------------------------------------------------
export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Inheritance
vtkAbstractMapper3D.extend(publicAPI, model, initialValues);
macro.get(publicAPI, model, [
'colorCoordinates',
'colorMapColors',
'colorTextureMap',
]);
macro.setGet(publicAPI, model, [
'colorByArrayName',
'arrayAccessMode',
'colorMode',
'fieldDataTupleId',
'interpolateScalarsBeforeMapping',
'lookupTable',
'renderTime',
'scalarMode',
'scalarVisibility',
'static',
'useLookupTableScalarRange',
'viewSpecificProperties',
'customShaderAttributes', // point data array names that will be transferred to the VBO
]);
macro.setGetArray(publicAPI, model, ['scalarRange'], 2);
if (!model.viewSpecificProperties) {
model.viewSpecificProperties = {};
}
CoincidentTopologyHelper.implementCoincidentTopologyMethods(publicAPI, model);
// Object methods
vtkMapper(publicAPI, model);
}
// ----------------------------------------------------------------------------
export const newInstance = macro.newInstance(extend, 'vtkMapper');
// ----------------------------------------------------------------------------
export default {
newInstance,
extend,
...staticOffsetAPI,
...otherStaticMethods,
...Constants,
};