Skip to content

Commit 129222d

Browse files
dakerfinetjul
authored andcommitted
refactor(ImageResliceMapper): extract shared code into helpers
1 parent b4ab4d0 commit 129222d

8 files changed

Lines changed: 628 additions & 610 deletions

File tree

Examples/Rendering/ImageResliceMapperLabelOutline/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice';
1414
import vtkInteractorStyleImage from '@kitware/vtk.js/Interaction/Style/InteractorStyleImage';
1515
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
1616
import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane';
17+
import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract';
1718
import vtkPlaneWidget from '@kitware/vtk.js/Widgets/Widgets3D/ImplicitPlaneWidget';
1819
import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager';
1920
import { InterpolationType } from '@kitware/vtk.js/Rendering/Core/ImageProperty/Constants';
@@ -28,8 +29,12 @@ import { unzipSync } from 'fflate';
2829

2930
import GUI from 'lil-gui';
3031
// ----------------------------------------------------------------------------
32+
const userParams = vtkURLExtract.extractURLParameters();
33+
const viewAPI = userParams.viewAPI || 'WebGL';
34+
3135
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
3236
background: [0.3, 0.3, 0.34],
37+
defaultViewAPI: viewAPI,
3338
});
3439
const renderer = fullScreenRenderer.getRenderer();
3540
const renderWindow = fullScreenRenderer.getRenderWindow();
@@ -345,6 +350,7 @@ actor.setProperty(1, labelPpty);
345350

346351
const gui = new GUI();
347352
const params = {
353+
viewAPI,
348354
slabEnabled: false,
349355
slabType: 'MAX',
350356
slabThickness: 20,
@@ -358,6 +364,15 @@ const params = {
358364
labelmapResolution: 2,
359365
};
360366

367+
gui
368+
.add(params, 'viewAPI', ['WebGL', 'WebGPU'])
369+
.name('Renderer')
370+
.onChange((api) => {
371+
const query = new URLSearchParams(window.location.search);
372+
query.set('viewAPI', api);
373+
window.location.search = query.toString();
374+
});
375+
361376
function applySlabEnabled() {
362377
if (params.slabEnabled) {
363378
mapper.setSlabThickness(Number(params.slabThickness));
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export function getInputProperty(actor, inputIndex) {
2+
if (!actor) {
3+
return null;
4+
}
5+
return actor.getProperty(inputIndex) || actor.getProperty();
6+
}
7+
8+
export function findLabelOutlineProperties(actor, currentValidInputs) {
9+
const labelOutlineProperties = [];
10+
for (let i = 0; i < currentValidInputs.length; i++) {
11+
const property = getInputProperty(actor, currentValidInputs[i].inputIndex);
12+
if (property?.getUseLabelOutline()) {
13+
labelOutlineProperties.push({ property, arrayIndex: i });
14+
}
15+
}
16+
return labelOutlineProperties;
17+
}
18+
19+
export function getLabelOutlineTextureParameters(
20+
labelOutlineProperties,
21+
getDataArray
22+
) {
23+
const dataArrays = [];
24+
const hashParts = [];
25+
let width = 0;
26+
for (let row = 0; row < labelOutlineProperties.length; row++) {
27+
const dataArray = getDataArray(labelOutlineProperties[row].property);
28+
dataArrays.push(dataArray);
29+
hashParts.push(dataArray.join('-'));
30+
width = Math.max(width, dataArray.length);
31+
}
32+
33+
return {
34+
dataArrays,
35+
hash: hashParts.join('|'),
36+
width,
37+
height: dataArrays.length,
38+
};
39+
}
40+
41+
export function fillLabelOutlineTextureTable(table, dataArrays, width) {
42+
for (let row = 0; row < dataArrays.length; row++) {
43+
const dataArray = dataArrays[row];
44+
const rowOffset = row * width;
45+
for (let col = 0; col < width; col++) {
46+
table[rowOffset + col] = dataArray[col] ?? dataArray[0];
47+
}
48+
}
49+
return table;
50+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { mat3, vec3 } from 'gl-matrix';
2+
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
3+
import { EPSILON } from 'vtk.js/Sources/Common/Core/Math/Constants';
4+
5+
const tmpMat3 = new Float64Array(9);
6+
const tmpTangent1 = [0, 0, 0];
7+
const tmpTangent2 = [0, 0, 0];
8+
const tmpVec3a = vec3.create();
9+
const tmpVec3b = vec3.create();
10+
11+
export function computeSliceTangents(slicePlane, planeNormalOut = [0, 0, 1]) {
12+
planeNormalOut[0] = 0;
13+
planeNormalOut[1] = 0;
14+
planeNormalOut[2] = 1;
15+
if (slicePlane) {
16+
const n = slicePlane.getNormal();
17+
planeNormalOut[0] = n[0];
18+
planeNormalOut[1] = n[1];
19+
planeNormalOut[2] = n[2];
20+
}
21+
vtkMath.normalize(planeNormalOut);
22+
23+
tmpTangent1[0] = 0;
24+
tmpTangent1[1] = 0;
25+
tmpTangent1[2] = 0;
26+
tmpTangent2[0] = 0;
27+
tmpTangent2[1] = 0;
28+
tmpTangent2[2] = 0;
29+
30+
if (slicePlane) {
31+
vtkMath.perpendiculars(planeNormalOut, tmpTangent1, tmpTangent2, 0);
32+
if (
33+
vtkMath.norm(tmpTangent1) < EPSILON ||
34+
vtkMath.norm(tmpTangent2) < EPSILON
35+
) {
36+
tmpTangent1[0] = 1;
37+
tmpTangent1[1] = 0;
38+
tmpTangent1[2] = 0;
39+
tmpTangent2[0] = 0;
40+
tmpTangent2[1] = 1;
41+
tmpTangent2[2] = 0;
42+
}
43+
} else {
44+
tmpTangent1[0] = 1;
45+
tmpTangent2[1] = 1;
46+
}
47+
48+
return {
49+
planeNormal: planeNormalOut,
50+
tangent1: tmpTangent1,
51+
tangent2: tmpTangent2,
52+
};
53+
}
54+
55+
export function computeInputOutlineBasis(
56+
imageData,
57+
tangent1,
58+
tangent2,
59+
texelSizeOut
60+
) {
61+
mat3.set(tmpMat3, ...imageData.getDirection());
62+
mat3.invert(tmpMat3, tmpMat3);
63+
64+
vec3.transformMat3(tmpVec3a, tangent1, tmpMat3);
65+
vec3.transformMat3(tmpVec3b, tangent2, tmpMat3);
66+
67+
const dims = imageData.getDimensions();
68+
const spacing = imageData.getSpacing();
69+
const minSpacing = Math.min(
70+
Math.abs(spacing[0]),
71+
Math.abs(spacing[1]),
72+
Math.abs(spacing[2])
73+
);
74+
texelSizeOut[0] = minSpacing / (dims[0] * Math.abs(spacing[0]));
75+
texelSizeOut[1] = minSpacing / (dims[1] * Math.abs(spacing[1]));
76+
texelSizeOut[2] = minSpacing / (dims[2] * Math.abs(spacing[2]));
77+
78+
return { tangent1: tmpVec3a, tangent2: tmpVec3b, texelSize: texelSizeOut };
79+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
2+
import vtkPlane from 'vtk.js/Sources/Common/DataModel/Plane';
3+
import { mat3, vec3 } from 'gl-matrix';
4+
5+
const tmpAxisAlignedN = [0, 0, 0];
6+
7+
export function isVectorAxisAligned(n) {
8+
vtkMath.normalize(n);
9+
for (let i = 0; i < 3; ++i) {
10+
vec3.zero(tmpAxisAlignedN);
11+
tmpAxisAlignedN[i] = 1.0;
12+
const dotP = vtkMath.dot(n, tmpAxisAlignedN);
13+
if (dotP < -0.999999 || dotP > 0.999999) {
14+
return [true, i];
15+
}
16+
}
17+
return [false, 2];
18+
}
19+
20+
export function fillPackedNormals(values, normal, numberOfPoints) {
21+
for (let i = 0; i < numberOfPoints; ++i) {
22+
values[3 * i] = normal[0];
23+
values[3 * i + 1] = normal[1];
24+
values[3 * i + 2] = normal[2];
25+
}
26+
}
27+
28+
export function computeResliceGeometryState(renderable, firstImageData) {
29+
let resGeomString = '';
30+
let orthoSlicing = true;
31+
let orthoAxis = 2;
32+
const imageBounds = firstImageData?.getBounds();
33+
const slicePD = renderable.getSlicePolyData();
34+
let slicePlane = renderable.getSlicePlane();
35+
36+
if (slicePD) {
37+
resGeomString = `PolyData${slicePD.getMTime()}`;
38+
} else if (slicePlane) {
39+
resGeomString = `Plane${slicePlane.getMTime()}`;
40+
if (firstImageData) {
41+
resGeomString = `${resGeomString}Image${firstImageData.getMTime()}`;
42+
const w2io = mat3.create();
43+
mat3.set(w2io, ...firstImageData.getDirection());
44+
mat3.invert(w2io, w2io);
45+
const imageLocalNormal = [...slicePlane.getNormal()];
46+
vec3.transformMat3(imageLocalNormal, imageLocalNormal, w2io);
47+
[orthoSlicing, orthoAxis] = isVectorAxisAligned(imageLocalNormal);
48+
}
49+
} else {
50+
slicePlane = vtkPlane.newInstance();
51+
slicePlane.setNormal(0, 0, 1);
52+
let bds = [0, 1, 0, 1, 0, 1];
53+
if (firstImageData) {
54+
bds = imageBounds;
55+
}
56+
slicePlane.setOrigin(bds[0], bds[2], 0.5 * (bds[4] + bds[5]));
57+
renderable.setSlicePlane(slicePlane);
58+
resGeomString = `Plane${slicePlane.getMTime()}Image${
59+
firstImageData?.getMTime?.() ?? 0
60+
}`;
61+
}
62+
63+
return {
64+
resGeomString,
65+
slicePD,
66+
slicePlane,
67+
orthoSlicing,
68+
orthoAxis,
69+
};
70+
}
71+
72+
export function computeObliqueSliceGeometryData(
73+
firstImageData,
74+
slicePlane,
75+
outlineFilter,
76+
cutter,
77+
lineToSurfaceFilter
78+
) {
79+
outlineFilter.setInputData(firstImageData);
80+
cutter.setInputConnection(outlineFilter.getOutputPort());
81+
cutter.setCutFunction(slicePlane);
82+
lineToSurfaceFilter.setInputConnection(cutter.getOutputPort());
83+
lineToSurfaceFilter.update();
84+
const planePD = lineToSurfaceFilter.getOutputData();
85+
const points = planePD.getPoints().getData();
86+
const polys = planePD.getPolys().getData();
87+
const normal = [...slicePlane.getNormal()];
88+
vtkMath.normalize(normal);
89+
const numberOfPoints = planePD.getPoints().getNumberOfPoints();
90+
const normalsData = new Float32Array(numberOfPoints * 3);
91+
fillPackedNormals(normalsData, normal, numberOfPoints);
92+
return { points, polys, normalsData };
93+
}
94+
95+
export function computeOrthoSliceGeometryData(
96+
firstImageData,
97+
slicePlane,
98+
orthoAxis,
99+
transform
100+
) {
101+
const points = new Float32Array(12);
102+
const indexSpacePlaneOrigin = firstImageData.worldToIndex(
103+
slicePlane.getOrigin(),
104+
[0, 0, 0]
105+
);
106+
const otherAxes = [(orthoAxis + 1) % 3, (orthoAxis + 2) % 3].sort();
107+
const ext = firstImageData.getSpatialExtent();
108+
let ptIdx = 0;
109+
for (let i = 0; i < 2; ++i) {
110+
for (let j = 0; j < 2; ++j) {
111+
points[ptIdx + orthoAxis] = indexSpacePlaneOrigin[orthoAxis];
112+
points[ptIdx + otherAxes[0]] = ext[2 * otherAxes[0] + j];
113+
points[ptIdx + otherAxes[1]] = ext[2 * otherAxes[1] + i];
114+
ptIdx += 3;
115+
}
116+
}
117+
transform.setMatrix(firstImageData.getIndexToWorld());
118+
transform.transformPoints(points, points);
119+
120+
const polys = new Uint16Array([3, 0, 1, 3, 3, 0, 3, 2]);
121+
const normal = [...slicePlane.getNormal()];
122+
vtkMath.normalize(normal);
123+
const normalsData = new Float32Array(12);
124+
fillPackedNormals(normalsData, normal, 4);
125+
return { points, polys, normalsData };
126+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
export function computeColorShiftScale({
2+
colorWindow,
3+
colorLevel,
4+
useLookupTableScalarRange,
5+
colorRange,
6+
volumeScale = 1.0,
7+
volumeOffset = 0.0,
8+
}) {
9+
let cw = colorWindow;
10+
let cl = colorLevel;
11+
if (useLookupTableScalarRange && colorRange) {
12+
cw = colorRange[1] - colorRange[0];
13+
cl = 0.5 * (colorRange[1] + colorRange[0]);
14+
}
15+
return {
16+
colorScale: volumeScale / cw,
17+
colorShift: (volumeOffset - cl) / cw + 0.5,
18+
};
19+
}
20+
21+
export function computeOpacityShiftScale({
22+
pwfRange,
23+
volumeScale = 1.0,
24+
volumeOffset = 0.0,
25+
}) {
26+
if (!pwfRange) {
27+
return { opacityScale: 1.0, opacityShift: 0.0 };
28+
}
29+
const length = pwfRange[1] - pwfRange[0];
30+
const mid = 0.5 * (pwfRange[0] + pwfRange[1]);
31+
return {
32+
opacityScale: volumeScale / length,
33+
opacityShift: (volumeOffset - mid) / length + 0.5,
34+
};
35+
}
36+
37+
export function getTransferFunctionHash({
38+
currentValidInputs,
39+
independentComponents,
40+
numberOfRows,
41+
kind,
42+
getInputProperty,
43+
}) {
44+
if (!currentValidInputs.length) {
45+
return '0';
46+
}
47+
const fnName =
48+
kind === 'color' ? 'getRGBTransferFunction' : 'getPiecewiseFunction';
49+
const rows = [];
50+
for (let i = 0; i < numberOfRows; i++) {
51+
const property = independentComponents
52+
? getInputProperty(currentValidInputs[i].inputIndex)
53+
: getInputProperty(currentValidInputs[0].inputIndex);
54+
const fn = property?.[fnName]?.(independentComponents ? 0 : i);
55+
rows.push(`${property?.getMTime?.() ?? 0}:${fn?.getMTime?.() ?? 0}`);
56+
}
57+
return rows.join('|');
58+
}

Sources/Rendering/Core/ImageResliceMapper/example/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const viewAPI = userParams.viewAPI || 'WebGL';
3232

3333
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
3434
background: [0.3, 0.3, 0.34],
35+
defaultViewAPI: viewAPI,
3536
});
3637
const renderer = fullScreenRenderer.getRenderer();
3738
const renderWindow = fullScreenRenderer.getRenderWindow();
@@ -226,7 +227,9 @@ gui
226227
.add(params, 'viewAPI', ['WebGL', 'WebGPU'])
227228
.name('Renderer')
228229
.onChange((api) => {
229-
window.location = `?viewAPI=${api}`;
230+
const query = new URLSearchParams(window.location.search);
231+
query.set('viewAPI', api);
232+
window.location.search = query.toString();
230233
});
231234

232235
gui

0 commit comments

Comments
 (0)