Skip to content

Commit 67c8815

Browse files
committed
fix(ImageResliceMapper): plane polydata construction
Fix the construction of slicing plane's polydata for non-trivial image orientations.
1 parent bd4fe74 commit 67c8815

File tree

2 files changed

+86
-28
lines changed

2 files changed

+86
-28
lines changed

Sources/Rendering/Core/ImageResliceMapper/index.d.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export interface vtkImageResliceMapper extends vtkAbstractImageMapper {
7878
*
7979
* Get the slab composite function.
8080
*/
81-
getSlabType(): boolean;
81+
getSlabType(): SlabTypes;
8282

8383
/**
8484
*
@@ -167,7 +167,7 @@ export interface vtkImageResliceMapper extends vtkAbstractImageMapper {
167167
* Enable slab slicing mode and set the slab thickness in world space (mm).
168168
* @param {Number} slabThickness The slab thickness in world space (mm). Default: 0.
169169
*/
170-
setSlabThickness(slabThickness: number): number;
170+
setSlabThickness(slabThickness: number): boolean;
171171

172172
/**
173173
*
@@ -178,7 +178,7 @@ export interface vtkImageResliceMapper extends vtkAbstractImageMapper {
178178
* @param {Number} slabTrapezoidIntegration Enable/disable trapezoid integration for slab slicing.
179179
* Default: 0
180180
*/
181-
setSlabTrapezoidIntegration(slabTrapezoidIntegration: number): number;
181+
setSlabTrapezoidIntegration(slabTrapezoidIntegration: number): boolean;
182182

183183
/**
184184
*
@@ -201,7 +201,7 @@ export interface vtkImageResliceMapper extends vtkAbstractImageMapper {
201201
* bounds.
202202
* @param {vtkPlane} slicePlane The implicit plane to slice the volume with. Default: null
203203
*/
204-
setSlicePlane(slicePlane: vtkPlane): vtkPlane;
204+
setSlicePlane(slicePlane: vtkPlane): boolean;
205205

206206
/**
207207
*
@@ -214,7 +214,7 @@ export interface vtkImageResliceMapper extends vtkAbstractImageMapper {
214214
* bounds.
215215
* @param {vtkPolyData} slicePolyData The polydata to slice the volume with. Default: null
216216
*/
217-
setSlicePolyData(slicePolyData: vtkPolyData): vtkPolyData;
217+
setSlicePolyData(slicePolyData: vtkPolyData): boolean;
218218
}
219219

220220
/**

Sources/Rendering/OpenGL/ImageResliceMapper/index.js

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as macro from 'vtk.js/Sources/macros';
22

3-
import { mat4, vec3 } from 'gl-matrix';
3+
import { mat4, mat3, vec3 } from 'gl-matrix';
44

55
import vtkClosedPolyLineToSurfaceFilter from 'vtk.js/Sources/Filters/General/ClosedPolyLineToSurfaceFilter';
66
import vtkCubeSource from 'vtk.js/Sources/Filters/Sources/CubeSource';
@@ -1067,6 +1067,52 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
10671067
return [false, 2];
10681068
}
10691069

1070+
function transformPoints(points, transform) {
1071+
const tmp = [0, 0, 0];
1072+
for (let i = 0; i < points.length; i += 3) {
1073+
const p = points.subarray(i, i + 3);
1074+
if (transform.length === 9) {
1075+
vec3.transformMat3(tmp, p, transform);
1076+
} else {
1077+
vec3.transformMat4(tmp, p, transform);
1078+
}
1079+
p[0] = tmp[0];
1080+
p[1] = tmp[1];
1081+
p[2] = tmp[2];
1082+
}
1083+
}
1084+
1085+
function imageToCubePolyData(image, outPD) {
1086+
// First create a cube polydata in the index-space of the image.
1087+
const sext = image?.getSpatialExtent();
1088+
1089+
if (sext) {
1090+
model.cubeSource.setXLength(sext[1] - sext[0]);
1091+
model.cubeSource.setYLength(sext[3] - sext[2]);
1092+
model.cubeSource.setZLength(sext[5] - sext[4]);
1093+
} else {
1094+
model.cubeSource.setXLength(1);
1095+
model.cubeSource.setYLength(1);
1096+
model.cubeSource.setZLength(1);
1097+
}
1098+
1099+
model.cubeSource.setCenter(
1100+
model.cubeSource.getXLength() / 2.0,
1101+
model.cubeSource.getYLength() / 2.0,
1102+
model.cubeSource.getZLength() / 2.0
1103+
);
1104+
1105+
model.cubeSource.update();
1106+
const out = model.cubeSource.getOutputData();
1107+
outPD.getPoints().setData(Float32Array.from(out.getPoints().getData()), 3);
1108+
outPD.getPolys().setData(Uint32Array.from(out.getPolys().getData()), 1);
1109+
1110+
// Now, transform the cube polydata points in-place
1111+
// using the image's indexToWorld transformation.
1112+
const points = outPD.getPoints().getData();
1113+
transformPoints(points, image.getIndexToWorld());
1114+
}
1115+
10701116
publicAPI.updateResliceGeometry = () => {
10711117
let resGeomString = '';
10721118
const image = model.currentInput;
@@ -1084,7 +1130,15 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
10841130
resGeomString = resGeomString.concat(`Image${image.getMTime()}`);
10851131
}
10861132
// Check to see if we can bypass oblique slicing related bounds computation
1087-
[orthoSlicing, orthoAxis] = isVectorAxisAligned(slicePlane.getNormal());
1133+
// Compute a world-to-image-orientation matrix.
1134+
// Ignore the translation component since we are
1135+
// using it on vectors rather than positions.
1136+
const w2io = mat3.fromValues(image?.getDirection());
1137+
mat3.invert(w2io, w2io);
1138+
// transform the cutting plane normal to image local coords
1139+
const imageLocalNormal = [...slicePlane.getNormal()];
1140+
vec3.transformMat3(imageLocalNormal, imageLocalNormal, w2io);
1141+
[orthoSlicing, orthoAxis] = isVectorAxisAligned(imageLocalNormal);
10881142
} else {
10891143
// Create a default slice plane here
10901144
const plane = vtkPlane.newInstance();
@@ -1112,27 +1166,18 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
11121166
.getPointData()
11131167
.setNormals(slicePD.getPointData().getNormals());
11141168
} else if (slicePlane) {
1115-
const bounds = image ? imageBounds : [0, 1, 0, 1, 0, 1];
11161169
if (!orthoSlicing) {
1117-
const cube = vtkCubeSource.newInstance();
1118-
cube.setCenter(
1119-
0.5 * (bounds[0] + bounds[1]),
1120-
0.5 * (bounds[2] + bounds[3]),
1121-
0.5 * (bounds[4] + bounds[5])
1170+
imageToCubePolyData(image, model.cubePolyData);
1171+
model.cutter.setInputData(model.cubePolyData);
1172+
model.cutter.setCutFunction(slicePlane);
1173+
model.lineToSurfaceFilter.setInputConnection(
1174+
model.cutter.getOutputPort()
11221175
);
1123-
cube.setXLength(bounds[1] - bounds[0]);
1124-
cube.setYLength(bounds[3] - bounds[2]);
1125-
cube.setZLength(bounds[5] - bounds[4]);
1126-
const cutter = vtkCutter.newInstance();
1127-
cutter.setInputConnection(cube.getOutputPort());
1128-
cutter.setCutFunction(slicePlane);
1129-
const pds = vtkClosedPolyLineToSurfaceFilter.newInstance();
1130-
pds.setInputConnection(cutter.getOutputPort());
1131-
pds.update();
1176+
model.lineToSurfaceFilter.update();
11321177
if (!model.resliceGeom) {
11331178
model.resliceGeom = vtkPolyData.newInstance();
11341179
}
1135-
const planePD = pds.getOutputData();
1180+
const planePD = model.lineToSurfaceFilter.getOutputData();
11361181
model.resliceGeom
11371182
.getPoints()
11381183
.setData(planePD.getPoints().getData(), 3);
@@ -1158,18 +1203,26 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
11581203
});
11591204
model.resliceGeom.getPointData().setNormals(normals);
11601205
} else {
1206+
// Since the image-local normal is axis-aligned, we
1207+
// can quickly construct the cutting plane using indexToWorld transforms.
11611208
const ptsArray = new Float32Array(12);
1162-
const o = slicePlane.getOrigin();
1209+
const indexSpacePlaneOrigin = image.worldToIndex(
1210+
slicePlane.getOrigin(),
1211+
[0, 0, 0]
1212+
);
11631213
const otherAxes = [(orthoAxis + 1) % 3, (orthoAxis + 2) % 3].sort();
1214+
const dim = image.getDimensions();
1215+
const ext = [0, dim[0] - 1, 0, dim[1] - 1, 0, dim[2] - 1];
11641216
let ptIdx = 0;
11651217
for (let i = 0; i < 2; ++i) {
11661218
for (let j = 0; j < 2; ++j) {
1167-
ptsArray[ptIdx + orthoAxis] = o[orthoAxis];
1168-
ptsArray[ptIdx + otherAxes[0]] = bounds[2 * otherAxes[0] + j];
1169-
ptsArray[ptIdx + otherAxes[1]] = bounds[2 * otherAxes[1] + i];
1219+
ptsArray[ptIdx + orthoAxis] = indexSpacePlaneOrigin[orthoAxis];
1220+
ptsArray[ptIdx + otherAxes[0]] = ext[2 * otherAxes[0] + j];
1221+
ptsArray[ptIdx + otherAxes[1]] = ext[2 * otherAxes[1] + i];
11701222
ptIdx += 3;
11711223
}
11721224
}
1225+
transformPoints(ptsArray, image.getIndexToWorld());
11731226

11741227
const cellArray = new Uint16Array(8);
11751228
cellArray[0] = 3;
@@ -1273,9 +1326,14 @@ export function extend(publicAPI, model, initialValues = {}) {
12731326
model.VBOBuildTime = {};
12741327
macro.obj(model.VBOBuildTime);
12751328

1276-
// model.modelToView = mat4.identity(new Float64Array(16));
12771329
model.tmpMat4 = mat4.identity(new Float64Array(16));
12781330

1331+
// Implicit plane to polydata related cache:
1332+
model.cubeSource = vtkCubeSource.newInstance();
1333+
model.cubePolyData = vtkPolyData.newInstance();
1334+
model.cutter = vtkCutter.newInstance();
1335+
model.lineToSurfaceFilter = vtkClosedPolyLineToSurfaceFilter.newInstance();
1336+
12791337
macro.get(publicAPI, model, ['openGLTexture']);
12801338

12811339
// Object methods

0 commit comments

Comments
 (0)