@kitware/vtk.js 31.1.0 → 32.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,6 +35,234 @@ function notImplemented(method) {
35
35
  return () => macro.vtkErrorMacro(`vtkMapper::${method} - NOT IMPLEMENTED`);
36
36
  }
37
37
 
38
+ /**
39
+ * Increase by one the 3D coordinates
40
+ * It will follow a zigzag pattern so that each coordinate is the neighbor of the next coordinate
41
+ * This enables interpolation between two texels without issues
42
+ * Note: texture coordinates can't be interpolated using this pattern
43
+ * @param {vec3} coordinates The 3D coordinates using integers for each coorinate
44
+ * @param {vec3} dimensions The 3D dimensions of the volume
45
+ */
46
+ function updateZigzaggingCoordinates(coordinates, dimensions) {
47
+ const directionX = coordinates[1] % 2 === 0 ? 1 : -1;
48
+ coordinates[0] += directionX;
49
+ if (coordinates[0] >= dimensions[0] || coordinates[0] < 0) {
50
+ const directionY = coordinates[2] % 2 === 0 ? 1 : -1;
51
+ coordinates[0] -= directionX;
52
+ coordinates[1] += directionY;
53
+ if (coordinates[1] >= dimensions[1] || coordinates[1] < 0) {
54
+ coordinates[1] -= directionY;
55
+ coordinates[2]++;
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Returns the index in the array representing the volume from a 3D coordinate
62
+ * @param {vec3} coordinates The 3D integer coordinates
63
+ * @param {vec3} dimensions The 3D dimensions of the volume
64
+ * @returns The index in a flat array representing the volume
65
+ */
66
+ function getIndexFromCoordinates(coordinates, dimensions) {
67
+ return coordinates[0] + dimensions[0] * (coordinates[1] + dimensions[1] * coordinates[2]);
68
+ }
69
+
70
+ /**
71
+ * Write texture coordinates for the given `texelIndexPosition` in `textureCoordinate`.
72
+ * The `texelIndexPosition` is a floating point number that represents the distance in index space
73
+ * from the center of the first texel to the final output position.
74
+ * The output is given in texture coordinates and not in index coordinates (this is done at the very end of the function)
75
+ * @param {vec3} textureCoordinate The output texture coordinates (to avoid allocating a new Array)
76
+ * @param {Number} texelIndexPosition The floating point distance from the center of the first texel, following a zigzag pattern
77
+ * @param {vec3} dimensions The 3D dimensions of the volume
78
+ */
79
+ function getZigZagTextureCoordinatesFromTexelPosition(textureCoordinate, texelIndexPosition, dimensions) {
80
+ // First compute the integer textureCoordinate
81
+ const intTexelIndex = Math.floor(texelIndexPosition);
82
+ const xCoordBeforeWrap = intTexelIndex % (2 * dimensions[0]);
83
+ let xDirection;
84
+ let xEndFlag;
85
+ if (xCoordBeforeWrap < dimensions[0]) {
86
+ textureCoordinate[0] = xCoordBeforeWrap;
87
+ xDirection = 1;
88
+ xEndFlag = textureCoordinate[0] === dimensions[0] - 1;
89
+ } else {
90
+ textureCoordinate[0] = 2 * dimensions[0] - 1 - xCoordBeforeWrap;
91
+ xDirection = -1;
92
+ xEndFlag = textureCoordinate[0] === 0;
93
+ }
94
+ const intRowIndex = Math.floor(intTexelIndex / dimensions[0]);
95
+ const yCoordBeforeWrap = intRowIndex % (2 * dimensions[1]);
96
+ let yDirection;
97
+ let yEndFlag;
98
+ if (yCoordBeforeWrap < dimensions[1]) {
99
+ textureCoordinate[1] = yCoordBeforeWrap;
100
+ yDirection = 1;
101
+ yEndFlag = textureCoordinate[1] === dimensions[1] - 1;
102
+ } else {
103
+ textureCoordinate[1] = 2 * dimensions[1] - 1 - yCoordBeforeWrap;
104
+ yDirection = -1;
105
+ yEndFlag = textureCoordinate[1] === 0;
106
+ }
107
+ textureCoordinate[2] = Math.floor(intRowIndex / dimensions[1]);
108
+
109
+ // Now add the remainder either in x, y or z
110
+ const remainder = texelIndexPosition - intTexelIndex;
111
+ if (xEndFlag) {
112
+ if (yEndFlag) {
113
+ textureCoordinate[2] += remainder;
114
+ } else {
115
+ textureCoordinate[1] += yDirection * remainder;
116
+ }
117
+ } else {
118
+ textureCoordinate[0] += xDirection * remainder;
119
+ }
120
+
121
+ // textureCoordinates are in index space, convert to texture space
122
+ textureCoordinate[0] = (textureCoordinate[0] + 0.5) / dimensions[0];
123
+ textureCoordinate[1] = (textureCoordinate[1] + 0.5) / dimensions[1];
124
+ textureCoordinate[2] = (textureCoordinate[2] + 0.5) / dimensions[2];
125
+ }
126
+
127
+ // Associate an input vtkDataArray to an object { stringHash, textureCoordinates }
128
+ // A single dataArray only caches one array of texture coordinates, so this cache is useless when
129
+ // the input data array is used with two different lookup tables (which is very unlikely)
130
+ const colorTextureCoordinatesCache = new WeakMap();
131
+ /**
132
+ * The minimum of the range is mapped to the center of the first texel excluding min texel (texel at index distance 1)
133
+ * The maximum of the range is mapped to the center of the last texel excluding max and NaN texels (texel at index distance numberOfColorsInRange)
134
+ * The result is cached, and is reused if the arguments are the same and the input doesn't change
135
+ * @param {vtkDataArray} input The input data array used for coloring
136
+ * @param {Number} component The component of the input data array that is used for coloring (-1 for magnitude of the vectors)
137
+ * @param {Range} range The range of the scalars
138
+ * @param {Number} numberOfColorsInRange The number of colors that are used in the range
139
+ * @param {vec3} dimensions The dimensions of the texture
140
+ * @param {boolean} useLogScale If log scale should be used to transform input scalars
141
+ * @param {boolean} useZigzagPattern If a zigzag pattern should be used. Otherwise 1 row for colors (including min and max) and 1 row for NaN are used.
142
+ * @returns A vtkDataArray containing the texture coordinates (2D or 3D)
143
+ */
144
+ function getOrCreateColorTextureCoordinates(input, component, range, numberOfColorsInRange, dimensions, useLogScale, useZigzagPattern) {
145
+ // Caching using the "arguments" special object (because it is a pure function)
146
+ const argStrings = new Array(arguments.length);
147
+ for (let argIndex = 0; argIndex < arguments.length; ++argIndex) {
148
+ // eslint-disable-next-line prefer-rest-params
149
+ const arg = arguments[argIndex];
150
+ argStrings[argIndex] = arg.getMTime?.() ?? arg;
151
+ }
152
+ const stringHash = argStrings.join('/');
153
+ const cachedResult = colorTextureCoordinatesCache.get(input);
154
+ if (cachedResult && cachedResult.stringHash === stringHash) {
155
+ return cachedResult.textureCoordinates;
156
+ }
157
+
158
+ // The range used for computing coordinates have to change
159
+ // slightly to accommodate the special above- and below-range
160
+ // colors that are the first and last texels, respectively.
161
+ const scalarTexelWidth = (range[1] - range[0]) / (numberOfColorsInRange - 1);
162
+ const [paddedRangeMin, paddedRangeMax] = [range[0] - scalarTexelWidth, range[1] + scalarTexelWidth];
163
+
164
+ // Use the center of the voxel
165
+ const textureSOrigin = paddedRangeMin - 0.5 * scalarTexelWidth;
166
+ const textureSCoeff = 1.0 / (paddedRangeMax - paddedRangeMin + scalarTexelWidth);
167
+
168
+ // Compute in index space first
169
+ const texelIndexOrigin = paddedRangeMin;
170
+ const texelIndexCoeff = (numberOfColorsInRange + 1) / (paddedRangeMax - paddedRangeMin);
171
+ const inputV = input.getData();
172
+ const numScalars = input.getNumberOfTuples();
173
+ const numComps = input.getNumberOfComponents();
174
+ const useMagnitude = component < 0 || component >= numComps;
175
+ const numberOfOutputComponents = dimensions[2] <= 1 ? 2 : 3;
176
+ const output = vtkDataArray.newInstance({
177
+ numberOfComponents: numberOfOutputComponents,
178
+ values: new Float32Array(numScalars * numberOfOutputComponents)
179
+ });
180
+ const outputV = output.getData();
181
+ const nanTextureCoordinate = [0, 0, 0];
182
+ // Distance of NaN from the beginning:
183
+ // min: 0, ...colorsInRange, max: numberOfColorsInRange + 1, NaN = numberOfColorsInRange + 2
184
+ getZigZagTextureCoordinatesFromTexelPosition(nanTextureCoordinate, numberOfColorsInRange + 2, dimensions);
185
+
186
+ // Set a texture coordinate in the output for each tuple in the input
187
+ let inputIdx = 0;
188
+ let outputIdx = 0;
189
+ const textureCoordinate = [0.5, 0.5, 0.5];
190
+ for (let scalarIdx = 0; scalarIdx < numScalars; ++scalarIdx) {
191
+ // Get scalar value from magnitude or a single component
192
+ let scalarValue;
193
+ if (useMagnitude) {
194
+ let sum = 0;
195
+ for (let compIdx = 0; compIdx < numComps; ++compIdx) {
196
+ const compValue = inputV[inputIdx + compIdx];
197
+ sum += compValue * compValue;
198
+ }
199
+ scalarValue = Math.sqrt(sum);
200
+ } else {
201
+ scalarValue = inputV[inputIdx + component];
202
+ }
203
+ inputIdx += numComps;
204
+
205
+ // Apply log scale if necessary
206
+ if (useLogScale) {
207
+ scalarValue = vtkLookupTable.applyLogScale(scalarValue, range, range);
208
+ }
209
+
210
+ // Convert to texture coordinates and update output
211
+ if (isNan(scalarValue)) {
212
+ // Last texels are NaN colors (there is at least one NaN color)
213
+ textureCoordinate[0] = nanTextureCoordinate[0];
214
+ textureCoordinate[1] = nanTextureCoordinate[1];
215
+ textureCoordinate[2] = nanTextureCoordinate[2];
216
+ } else if (useZigzagPattern) {
217
+ // Texel position is in [0, numberOfColorsInRange + 1]
218
+ let texelIndexPosition = (scalarValue - texelIndexOrigin) * texelIndexCoeff;
219
+ if (texelIndexPosition < 1) {
220
+ // Use min color when smaller than range
221
+ texelIndexPosition = 0;
222
+ } else if (texelIndexPosition > numberOfColorsInRange) {
223
+ // Use max color when greater than range
224
+ texelIndexPosition = numberOfColorsInRange + 1;
225
+ }
226
+
227
+ // Convert the texel position into texture coordinate following a zigzag pattern
228
+ getZigZagTextureCoordinatesFromTexelPosition(textureCoordinate, texelIndexPosition, dimensions);
229
+ } else {
230
+ // 0.0 in t coordinate means not NaN. So why am I setting it to 0.49?
231
+ // Because when you are mapping scalars and you have a NaN adjacent to
232
+ // anything else, the interpolation everywhere should be NaN. Thus, I
233
+ // want the NaN color everywhere except right on the non-NaN neighbors.
234
+ // To simulate this, I set the t coord for the real numbers close to
235
+ // the threshold so that the interpolation almost immediately looks up
236
+ // the NaN value.
237
+ textureCoordinate[1] = 0.49;
238
+
239
+ // Some implementations apparently don't handle relatively large
240
+ // numbers (compared to the range [0.0, 1.0]) very well. In fact,
241
+ // values above 1122.0f appear to cause texture wrap-around on
242
+ // some systems even when edge clamping is enabled. Why 1122.0f? I
243
+ // don't know. For safety, we'll clamp at +/- 1000. This will
244
+ // result in incorrect images when the texture value should be
245
+ // above or below 1000, but I don't have a better solution.
246
+ const textureS = (scalarValue - textureSOrigin) * textureSCoeff;
247
+ if (textureS > 1000.0) {
248
+ textureCoordinate[0] = 1000.0;
249
+ } else if (textureS < -1000.0) {
250
+ textureCoordinate[0] = -1000.0;
251
+ } else {
252
+ textureCoordinate[0] = textureS;
253
+ }
254
+ }
255
+ for (let i = 0; i < numberOfOutputComponents; ++i) {
256
+ outputV[outputIdx++] = textureCoordinate[i];
257
+ }
258
+ }
259
+ colorTextureCoordinatesCache.set(input, {
260
+ stringHash,
261
+ textureCoordinates: output
262
+ });
263
+ return output;
264
+ }
265
+
38
266
  // ----------------------------------------------------------------------------
39
267
  // vtkMapper methods
40
268
  // ----------------------------------------------------------------------------
@@ -85,7 +313,7 @@ function vtkMapper(publicAPI, model) {
85
313
  if (!input || !model.scalarVisibility) {
86
314
  return {
87
315
  scalars: null,
88
- cellFLag: false
316
+ cellFlag: false
89
317
  };
90
318
  }
91
319
  let scalars = null;
@@ -155,7 +383,7 @@ function vtkMapper(publicAPI, model) {
155
383
  // Cell data always uses vertex color.
156
384
  // Only point data can use both texture and vertex coloring.
157
385
  if (publicAPI.canUseTextureMapForColoring(scalars, cellFlag)) {
158
- publicAPI.mapScalarsToTexture(scalars, alpha);
386
+ model.mapScalarsToTexture(scalars, cellFlag, alpha);
159
387
  } else {
160
388
  model.colorCoordinates = null;
161
389
  model.colorTextureMap = null;
@@ -169,98 +397,8 @@ function vtkMapper(publicAPI, model) {
169
397
  model.colorBuildString = `${publicAPI.getMTime()}${scalars.getMTime()}${alpha}`;
170
398
  };
171
399
 
172
- //-----------------------------------------------------------------------------
173
- publicAPI.scalarToTextureCoordinate = (scalarValue,
174
- // Input scalar
175
- rangeMin,
176
- // range[0]
177
- invRangeWidth) => {
178
- // 1/(range[1]-range[0])
179
- let texCoordS = 0.5; // Scalar value is arbitrary when NaN
180
- let texCoordT = 1.0; // 1.0 in t coordinate means NaN
181
- if (!isNan(scalarValue)) {
182
- // 0.0 in t coordinate means not NaN. So why am I setting it to 0.49?
183
- // Because when you are mapping scalars and you have a NaN adjacent to
184
- // anything else, the interpolation everywhere should be NaN. Thus, I
185
- // want the NaN color everywhere except right on the non-NaN neighbors.
186
- // To simulate this, I set the t coord for the real numbers close to
187
- // the threshold so that the interpolation almost immediately looks up
188
- // the NaN value.
189
- texCoordT = 0.49;
190
- texCoordS = (scalarValue - rangeMin) * invRangeWidth;
191
-
192
- // Some implementations apparently don't handle relatively large
193
- // numbers (compared to the range [0.0, 1.0]) very well. In fact,
194
- // values above 1122.0f appear to cause texture wrap-around on
195
- // some systems even when edge clamping is enabled. Why 1122.0f? I
196
- // don't know. For safety, we'll clamp at +/- 1000. This will
197
- // result in incorrect images when the texture value should be
198
- // above or below 1000, but I don't have a better solution.
199
- if (texCoordS > 1000.0) {
200
- texCoordS = 1000.0;
201
- } else if (texCoordS < -1000.0) {
202
- texCoordS = -1000.0;
203
- }
204
- }
205
- return {
206
- texCoordS,
207
- texCoordT
208
- };
209
- };
210
-
211
- //-----------------------------------------------------------------------------
212
- publicAPI.createColorTextureCoordinates = (input, component, range, tableNumberOfColors, useLogScale) => {
213
- // We have to change the range used for computing texture
214
- // coordinates slightly to accommodate the special above- and
215
- // below-range colors that are the first and last texels,
216
- // respectively.
217
- const scalarTexelWidth = (range[1] - range[0]) / tableNumberOfColors;
218
- const paddedRange = [range[0] - scalarTexelWidth, range[1] + scalarTexelWidth];
219
- const invRangeWidth = 1.0 / (paddedRange[1] - paddedRange[0]);
220
- const inputV = input.getData();
221
- const numScalars = input.getNumberOfTuples();
222
- const numComps = input.getNumberOfComponents();
223
- const output = vtkDataArray.newInstance({
224
- numberOfComponents: 2,
225
- values: new Float32Array(numScalars * 2)
226
- });
227
- const outputV = output.getData();
228
- if (component < 0 || component >= numComps) {
229
- // Convert the magnitude of all components to texture coordinates
230
- let inputIdx = 0;
231
- let outputIdx = 0;
232
- for (let scalarIdx = 0; scalarIdx < numScalars; ++scalarIdx) {
233
- let sum = 0;
234
- for (let compIdx = 0; compIdx < numComps; ++compIdx) {
235
- sum += inputV[inputIdx] * inputV[inputIdx];
236
- inputIdx++;
237
- }
238
- let magnitude = Math.sqrt(sum);
239
- if (useLogScale) {
240
- magnitude = vtkLookupTable.applyLogScale(magnitude, range, range);
241
- }
242
- const outputs = publicAPI.scalarToTextureCoordinate(magnitude, paddedRange[0], invRangeWidth);
243
- outputV[outputIdx++] = outputs.texCoordS;
244
- outputV[outputIdx++] = outputs.texCoordT;
245
- }
246
- } else {
247
- // Convert one of the components to texture coordinates
248
- let inputIdx = component;
249
- let outputIdx = 0;
250
- for (let scalarIdx = 0; scalarIdx < numScalars; ++scalarIdx) {
251
- let inputValue = inputV[inputIdx];
252
- if (useLogScale) {
253
- inputValue = vtkLookupTable.applyLogScale(inputValue, range, range);
254
- }
255
- const outputs = publicAPI.scalarToTextureCoordinate(inputValue, paddedRange[0], invRangeWidth);
256
- outputV[outputIdx++] = outputs.texCoordS;
257
- outputV[outputIdx++] = outputs.texCoordT;
258
- inputIdx += numComps;
259
- }
260
- }
261
- return output;
262
- };
263
- publicAPI.mapScalarsToTexture = (scalars, alpha) => {
400
+ // Protected method
401
+ model.mapScalarsToTexture = (scalars, cellFlag, alpha) => {
264
402
  const range = model.lookupTable.getRange();
265
403
  const useLogScale = model.lookupTable.usingLogScale();
266
404
  if (useLogScale) {
@@ -284,28 +422,52 @@ function vtkMapper(publicAPI, model) {
284
422
  // Create a dummy ramp of scalars.
285
423
  // In the future, we could extend vtkScalarsToColors.
286
424
  model.lookupTable.build();
287
- const numberOfColorsInRange = Math.min(Math.max(model.lookupTable.getNumberOfAvailableColors(), 64), 4094);
288
- // Texel width is computed before min and max colors that are out of range
289
- const scalarTexelWidth = (range[1] - range[0]) / numberOfColorsInRange;
290
- // Add two colors for special min and max colors
291
- const totalNumberOfColors = numberOfColorsInRange + 2;
292
- const newArray = new Float64Array(totalNumberOfColors * 2);
293
- for (let i = 0; i < totalNumberOfColors; ++i) {
294
- // minus 0.5 to start at below range color
295
- newArray[i] = range[0] + (i - 0.5) * scalarTexelWidth;
296
- if (useLogScale) {
297
- newArray[i] = 10.0 ** newArray[i];
298
- }
425
+ const numberOfAvailableColors = model.lookupTable.getNumberOfAvailableColors();
426
+
427
+ // Maximum dimensions and number of colors in range
428
+ const maxTextureWidthForCells = 2048;
429
+ const maxColorsInRangeForCells = maxTextureWidthForCells ** 3 - 3; // 3D but keep a color for min, max and NaN
430
+ const maxTextureWidthForPoints = 4096;
431
+ const maxColorsInRangeForPoints = maxTextureWidthForPoints - 2; // 1D but keep a color for min and max (NaN is in a different row)
432
+ // Minimum number of colors in range (excluding special colors like minColor, maxColor and NaNColor)
433
+ const minColorsInRange = 2;
434
+ // Maximum number of colors, limited by the maximum possible texture size
435
+ const maxColorsInRange = cellFlag ? maxColorsInRangeForCells : maxColorsInRangeForPoints;
436
+ model.numberOfColorsInRange = Math.min(Math.max(numberOfAvailableColors, minColorsInRange), maxColorsInRange);
437
+ const numberOfColorsForCells = model.numberOfColorsInRange + 3; // Add min, max and NaN
438
+ const numberOfColorsInUpperRowForPoints = model.numberOfColorsInRange + 2; // Add min and max ; the lower row will be used for NaN color
439
+ const textureDimensions = cellFlag ? [Math.min(Math.ceil(numberOfColorsForCells / maxTextureWidthForCells ** 0), maxTextureWidthForCells), Math.min(Math.ceil(numberOfColorsForCells / maxTextureWidthForCells ** 1), maxTextureWidthForCells), Math.min(Math.ceil(numberOfColorsForCells / maxTextureWidthForCells ** 2), maxTextureWidthForCells)] : [numberOfColorsInUpperRowForPoints, 2, 1];
440
+ const textureSize = textureDimensions[0] * textureDimensions[1] * textureDimensions[2];
441
+ const scalarsArray = new Float64Array(textureSize);
442
+
443
+ // Colors for NaN by default
444
+ scalarsArray.fill(NaN);
445
+
446
+ // Colors in range
447
+ // Add 2 to also get color for min and max
448
+ const numberOfNonSpecialColors = model.numberOfColorsInRange;
449
+ const numberOfNonNaNColors = numberOfNonSpecialColors + 2;
450
+ const textureCoordinates = [0, 0, 0];
451
+ const rangeMin = range[0];
452
+ const rangeDifference = range[1] - range[0];
453
+ for (let i = 0; i < numberOfNonNaNColors; ++i) {
454
+ const scalarsArrayIndex = getIndexFromCoordinates(textureCoordinates, textureDimensions);
455
+
456
+ // Minus 1 start at min color
457
+ const scalarValue = rangeMin + rangeDifference * (i - 1) / (numberOfNonSpecialColors - 1);
458
+ scalarsArray[scalarsArrayIndex] = useLogScale ? 10.0 ** scalarValue : scalarValue;
459
+
460
+ // Colors are zigzagging to allow interpolation between two neighbor colors when coloring cells
461
+ updateZigzaggingCoordinates(textureCoordinates, textureDimensions);
299
462
  }
300
- // Dimension on NaN.
301
- newArray.fill(NaN, totalNumberOfColors);
302
- model.colorTextureMap = vtkImageData.newInstance();
303
- model.colorTextureMap.setExtent(0, totalNumberOfColors - 1, 0, 1, 0, 0);
304
- const tmp = vtkDataArray.newInstance({
463
+ const scalarsDataArray = vtkDataArray.newInstance({
305
464
  numberOfComponents: 1,
306
- values: newArray
465
+ values: scalarsArray
307
466
  });
308
- model.colorTextureMap.getPointData().setScalars(model.lookupTable.mapScalars(tmp, model.colorMode, 0));
467
+ const colorsDataArray = model.lookupTable.mapScalars(scalarsDataArray, model.colorMode, 0);
468
+ model.colorTextureMap = vtkImageData.newInstance();
469
+ model.colorTextureMap.setDimensions(textureDimensions);
470
+ model.colorTextureMap.getPointData().setScalars(colorsDataArray);
309
471
  model.lookupTable.setAlpha(origAlpha);
310
472
  }
311
473
 
@@ -313,17 +475,12 @@ function vtkMapper(publicAPI, model) {
313
475
  // scalars, it is not how the old MapScalars for vertex coloring works.
314
476
  const scalarComponent = model.lookupTable.getVectorMode() === VectorMode.MAGNITUDE && scalars.getNumberOfComponents() > 1 ? -1 : model.lookupTable.getVectorComponent();
315
477
 
316
- // Reverse the computation of numberOfColorsInRange that is used to compute model.colorTextureMap
317
- const textureMapTuples = model.colorTextureMap.getPointData().getScalars().getNumberOfTuples();
318
- const totalNumberOfColors = textureMapTuples / 2;
319
- const numberOfColorsInRange = totalNumberOfColors - 2;
320
-
321
- // Create new coordinates if necessary.
322
- const colorCoordinatesString = `${scalars.getMTime()}/${scalarComponent}/${range}/${numberOfColorsInRange}/${useLogScale}`;
323
- if (colorCoordinatesString !== model.colorCoordinatesString) {
324
- model.colorCoordinates = publicAPI.createColorTextureCoordinates(scalars, scalarComponent, range, numberOfColorsInRange, useLogScale);
325
- model.colorCoordinatesString = colorCoordinatesString;
326
- }
478
+ // Create new coordinates if necessary, this function uses cache if possible.
479
+ // A zigzag pattern can't be used with point data, as interpolation of texture coordinates will be wrong
480
+ // A zigzag pattern can be used with cell data, as there will be no texture coordinates interpolation
481
+ // The texture generated using a zigzag pattern in one dimension is the same as without zigzag
482
+ // Therefore, the same code can be used for texture generation of point/cell data but not for texture coordinates
483
+ model.colorCoordinates = getOrCreateColorTextureCoordinates(scalars, scalarComponent, range, model.numberOfColorsInRange, model.colorTextureMap.getDimensions(), useLogScale, cellFlag);
327
484
  };
328
485
  publicAPI.getIsOpaque = () => {
329
486
  const input = publicAPI.getInputData();
@@ -478,6 +635,7 @@ const DEFAULT_VALUES = {
478
635
  interpolateScalarsBeforeMapping: false,
479
636
  colorCoordinates: null,
480
637
  colorTextureMap: null,
638
+ numberOfColorsInRange: 0,
481
639
  forceCompileOnly: 0,
482
640
  useInvertibleColors: false,
483
641
  invertibleScalars: null,
@@ -492,7 +650,7 @@ function extend(publicAPI, model) {
492
650
 
493
651
  // Inheritance
494
652
  vtkAbstractMapper3D.extend(publicAPI, model, initialValues);
495
- macro.get(publicAPI, model, ['areScalarsMappedFromCells', 'colorCoordinates', 'colorMapColors', 'colorTextureMap', 'selectionWebGLIdsToVTKIds']);
653
+ macro.get(publicAPI, model, ['areScalarsMappedFromCells', 'colorCoordinates', 'colorMapColors', 'colorTextureMap', 'numberOfColorsInRange', 'selectionWebGLIdsToVTKIds']);
496
654
  macro.setGet(publicAPI, model, ['colorByArrayName', 'arrayAccessMode', 'colorMode', 'fieldDataTupleId', 'interpolateScalarsBeforeMapping', 'lookupTable', 'populateSelectionSettings', 'renderTime', 'scalarMode', 'scalarVisibility', 'static', 'useLookupTableScalarRange', 'customShaderAttributes' // point data array names that will be transferred to the VBO
497
655
  ]);
498
656
 
@@ -34,11 +34,6 @@ interface IAbstractScalars {
34
34
  cellFlag: boolean;
35
35
  }
36
36
 
37
- interface IScalarToTextureCoordinate {
38
- texCoordS: number;
39
- texCoordT: number;
40
- }
41
-
42
37
  export interface IMapper2DInitialValues extends IAbstractMapperInitialValues {
43
38
  arrayAccessMode?: number;
44
39
  colorMode?: number;
@@ -5,6 +5,7 @@ export declare enum BlendMode {
5
5
  AVERAGE_INTENSITY_BLEND = 3,
6
6
  ADDITIVE_INTENSITY_BLEND = 4,
7
7
  RADON_TRANSFORM_BLEND = 5,
8
+ LABELMAP_EDGE_PROJECTION_BLEND = 6,
8
9
  }
9
10
 
10
11
  export declare enum FilterMode {
@@ -4,7 +4,8 @@ const BlendMode = {
4
4
  MINIMUM_INTENSITY_BLEND: 2,
5
5
  AVERAGE_INTENSITY_BLEND: 3,
6
6
  ADDITIVE_INTENSITY_BLEND: 4,
7
- RADON_TRANSFORM_BLEND: 5
7
+ RADON_TRANSFORM_BLEND: 5,
8
+ LABELMAP_EDGE_PROJECTION_BLEND: 6
8
9
  };
9
10
  const FilterMode = {
10
11
  OFF: 0,
@@ -14,6 +14,8 @@ import vtkViewNode from '../SceneGraph/ViewNode.js';
14
14
  import { v as vtkPolyDataVS } from './glsl/vtkPolyDataVS.glsl.js';
15
15
  import { v as vtkPolyDataFS } from './glsl/vtkPolyDataFS.glsl.js';
16
16
  import { registerOverride } from './ViewNodeFactory.js';
17
+ import '../Core/Mapper/CoincidentTopologyHelper.js';
18
+ import { Resolve } from '../Core/Mapper/Static.js';
17
19
 
18
20
  const {
19
21
  vtkErrorMacro
@@ -68,7 +70,7 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) {
68
70
  }
69
71
  };
70
72
  publicAPI.getCoincidentParameters = (ren, actor) => {
71
- if (model.renderable.getResolveCoincidentTopology()) {
73
+ if (model.renderable.getResolveCoincidentTopology() === Resolve.PolygonOffset) {
72
74
  return model.renderable.getCoincidentTopologyPolygonOffsetParameters();
73
75
  }
74
76
  return null;
@@ -14,7 +14,9 @@ import { InterpolationType } from '../Core/ImageProperty/Constants.js';
14
14
  import { v as vtkPolyDataVS } from './glsl/vtkPolyDataVS.glsl.js';
15
15
  import { v as vtkPolyDataFS } from './glsl/vtkPolyDataFS.glsl.js';
16
16
  import vtkReplacementShaderMapper from './ReplacementShaderMapper.js';
17
+ import '../Core/Mapper/CoincidentTopologyHelper.js';
17
18
  import { registerOverride } from './ViewNodeFactory.js';
19
+ import { Resolve } from '../Core/Mapper/Static.js';
18
20
 
19
21
  const {
20
22
  vtkErrorMacro
@@ -103,7 +105,10 @@ function vtkOpenGLImageMapper(publicAPI, model) {
103
105
  }
104
106
  };
105
107
  publicAPI.getCoincidentParameters = (ren, actor) => {
106
- if (model.renderable.getResolveCoincidentTopology()) {
108
+ if (
109
+ // backwards compat with code that (errorneously) set this to boolean
110
+ // eslint-disable-next-line eqeqeq
111
+ model.renderable.getResolveCoincidentTopology() == Resolve.PolygonOffset) {
107
112
  return model.renderable.getCoincidentTopologyPolygonOffsetParameters();
108
113
  }
109
114
  return null;
@@ -202,7 +207,7 @@ function vtkOpenGLImageMapper(publicAPI, model) {
202
207
  case 1:
203
208
  FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::TCoord::Impl', [...splitStringOnEnter(`
204
209
  #ifdef vtkImageLabelOutlineOn
205
- vec3 centerPosIS = fragCoordToIndexSpace(gl_FragCoord);
210
+ vec3 centerPosIS = fragCoordToIndexSpace(gl_FragCoord);
206
211
  float centerValue = texture2D(texture1, centerPosIS.xy).r;
207
212
  bool pixelOnBorder = false;
208
213
  vec3 tColor = texture2D(colorTexture1, vec2(centerValue * cscale0 + cshift0, 0.5)).rgb;
@@ -20,6 +20,8 @@ import { InterpolationType } from '../Core/ImageProperty/Constants.js';
20
20
  import { Representation } from '../Core/Property/Constants.js';
21
21
  import { VtkDataTypes } from '../../Common/Core/DataArray/Constants.js';
22
22
  import { registerOverride } from './ViewNodeFactory.js';
23
+ import '../Core/Mapper/CoincidentTopologyHelper.js';
24
+ import { Resolve } from '../Core/Mapper/Static.js';
23
25
 
24
26
  const {
25
27
  vtkErrorMacro
@@ -94,7 +96,10 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
94
96
  }
95
97
  };
96
98
  publicAPI.getCoincidentParameters = (ren, actor) => {
97
- if (model.renderable.getResolveCoincidentTopology()) {
99
+ if (
100
+ // backwards compat with code that (errorneously) set this to boolean
101
+ // eslint-disable-next-line eqeqeq
102
+ model.renderable.getResolveCoincidentTopology() == Resolve.PolygonOffset) {
98
103
  return model.renderable.getCoincidentTopologyPolygonOffsetParameters();
99
104
  }
100
105
  return null;
@@ -14,6 +14,8 @@ import vtkReplacementShaderMapper from './ReplacementShaderMapper.js';
14
14
  import { registerOverride } from './ViewNodeFactory.js';
15
15
  import { PassTypes } from './HardwareSelector/Constants.js';
16
16
  import vtkDataSet from '../../Common/DataModel/DataSet.js';
17
+ import '../Core/Mapper/CoincidentTopologyHelper.js';
18
+ import { Resolve } from '../Core/Mapper/Static.js';
17
19
 
18
20
  const {
19
21
  FieldAssociations
@@ -391,7 +393,10 @@ function vtkOpenGLPolyDataMapper(publicAPI, model) {
391
393
  offset: 0.0
392
394
  };
393
395
  const prop = actor.getProperty();
394
- if (model.renderable.getResolveCoincidentTopology() || prop.getEdgeVisibility() && prop.getRepresentation() === Representation.SURFACE) {
396
+ if (
397
+ // backwards compat with code that (errorneously) set this to boolean
398
+ // eslint-disable-next-line eqeqeq
399
+ model.renderable.getResolveCoincidentTopology() == Resolve.PolygonOffset || prop.getEdgeVisibility() && prop.getRepresentation() === Representation.SURFACE) {
395
400
  const primType = model.lastBoundBO.getPrimitiveType();
396
401
  if (primType === primTypes.Points || prop.getRepresentation() === Representation.POINTS) {
397
402
  cp = model.renderable.getCoincidentTopologyPointOffsetParameter();
@@ -175,10 +175,14 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
175
175
  if (iType === InterpolationType.LINEAR) {
176
176
  FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::TrilinearOn', '#define vtkTrilinearOn').result;
177
177
  }
178
- const vtkImageLabelOutline = actorProps.getUseLabelOutline();
178
+ const vtkImageLabelOutline = publicAPI.isLabelmapOutlineRequired(actor);
179
179
  if (vtkImageLabelOutline === true) {
180
180
  FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::ImageLabelOutlineOn', '#define vtkImageLabelOutlineOn').result;
181
181
  }
182
+ const LabelEdgeProjection = model.renderable.getBlendMode() === BlendMode.LABELMAP_EDGE_PROJECTION_BLEND;
183
+ if (LabelEdgeProjection) {
184
+ FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::LabelEdgeProjectionOn', '#define vtkLabelEdgeProjectionOn').result;
185
+ }
182
186
  const numComp = model.scalarTexture.getComponents();
183
187
  FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::NumComponents', `#define vtkNumComponents ${numComp}`).result;
184
188
  const useIndependentComps = publicAPI.useIndependentComponents(actorProps);
@@ -350,7 +354,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
350
354
  iComps: actorProps.getIndependentComponents(),
351
355
  colorMixPreset: actorProps.getColorMixPreset(),
352
356
  interpolationType: actorProps.getInterpolationType(),
353
- useLabelOutline: actorProps.getUseLabelOutline(),
357
+ useLabelOutline: publicAPI.isLabelmapOutlineRequired(actor),
354
358
  numComp,
355
359
  maxSamples,
356
360
  useGradientOpacity: actorProps.getUseGradientOpacity(0),
@@ -512,6 +516,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
512
516
  vec3.divide(vctoijk, vctoijk, vsize);
513
517
  program.setUniform3f('vVCToIJK', vctoijk[0], vctoijk[1], vctoijk[2]);
514
518
  program.setUniform3i('volumeDimensions', dims[0], dims[1], dims[2]);
519
+ program.setUniform3f('volumeSpacings', spc[0], spc[1], spc[2]);
515
520
  if (!model._openGLRenderWindow.getWebgl2()) {
516
521
  const volInfo = model.scalarTexture.getVolumeInfo();
517
522
  program.setUniformf('texWidth', model.scalarTexture.getWidth());
@@ -562,7 +567,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
562
567
  program.setUniform3f(`vPlaneNormal${i}`, normal[0], normal[1], normal[2]);
563
568
  program.setUniformf(`vPlaneDistance${i}`, dist);
564
569
  }
565
- if (actor.getProperty().getUseLabelOutline()) {
570
+ if (publicAPI.isLabelmapOutlineRequired(actor)) {
566
571
  const image = model.currentInput;
567
572
  const worldToIndex = image.getWorldToIndex();
568
573
  program.setUniformMatrix('vWCtoIDX', worldToIndex);
@@ -750,7 +755,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
750
755
  program.setUniformf('goshift0', -goRange[0] * (gomax - gomin) / (goRange[1] - goRange[0]) + gomin);
751
756
  }
752
757
  }
753
- const vtkImageLabelOutline = actor.getProperty().getUseLabelOutline();
758
+ const vtkImageLabelOutline = publicAPI.isLabelmapOutlineRequired(actor);
754
759
  if (vtkImageLabelOutline === true) {
755
760
  const labelOutlineOpacity = actor.getProperty().getLabelOutlineOpacity();
756
761
  program.setUniformf('outlineOpacity', labelOutlineOpacity);
@@ -1252,6 +1257,11 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
1252
1257
  model.labelOutlineThicknessTexture = lTex.oglObject;
1253
1258
  }
1254
1259
  };
1260
+ publicAPI.isLabelmapOutlineRequired = actor => {
1261
+ const prop = actor.getProperty();
1262
+ const renderable = model.renderable;
1263
+ return prop.getUseLabelOutline() || renderable.getBlendMode() === BlendMode.LABELMAP_EDGE_PROJECTION_BLEND;
1264
+ };
1255
1265
  }
1256
1266
 
1257
1267
  // ----------------------------------------------------------------------------