Skip to content

Commit 4d62ebc

Browse files
bruyeretfinetjul
authored andcommitted
feat(cprExample): add angle input
The angle has two way binding with the widget Move default distance step in CPR manipulator: from defaultValues() to getDistanceStep()
1 parent 1087439 commit 4d62ebc

File tree

3 files changed

+82
-46
lines changed

3 files changed

+82
-46
lines changed

Sources/Rendering/Core/ImageCPRMapper/example/controller.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@
44
<select id='centerline' style="width: 100%"></select>
55
</td>
66
</tr>
7+
<td>
8+
<label>Angle</label>
9+
<input id='angle' type='range' min='0' max='360' value='0' step="1"/>
10+
</td>
711
</table>

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

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import '@kitware/vtk.js/Rendering/Profiles/All';
77
import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper';
88

99
import { radiansFromDegrees } from 'vtk.js/Sources/Common/Core/Math';
10+
import { updateState } from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/helpers';
1011
import { vec3, mat3, mat4 } from 'gl-matrix';
1112
import { ViewTypes } from '@kitware/vtk.js/Widgets/Core/WidgetManager/Constants';
1213
import vtkCPRManipulator from '@kitware/vtk.js/Widgets/Manipulators/CPRManipulator';
@@ -39,7 +40,11 @@ const centerlineKeys = Object.keys(centerlineJsons);
3940
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance();
4041
const stretchRenderer = fullScreenRenderer.getRenderer();
4142
const renderWindow = fullScreenRenderer.getRenderWindow();
43+
4244
fullScreenRenderer.addController(controlPanel);
45+
const angleEl = document.getElementById('angle');
46+
const centerlineEl = document.getElementById('centerline');
47+
4348
const interactor = renderWindow.getInteractor();
4449
interactor.setInteractorStyle(vtkInteractorStyleImage.newInstance());
4550
interactor.setDesiredUpdateRate(15.0);
@@ -122,6 +127,8 @@ function updateDistanceAndDirection() {
122127
const widgetPlanes = widgetState.getPlanes();
123128
const worldBitangent = widgetPlanes[stretchViewType].normal;
124129
const worldNormal = widgetPlanes[stretchViewType].viewUp;
130+
widgetPlanes[crossViewType].normal = worldNormal;
131+
widgetPlanes[crossViewType].viewUp = worldBitangent;
125132
const worldTangent = vec3.cross([], worldBitangent, worldNormal);
126133
vec3.normalize(worldTangent, worldTangent);
127134
const worldWidgetCenter = widgetState.getCenter();
@@ -144,21 +151,13 @@ function updateDistanceAndDirection() {
144151
distance - height
145152
);
146153
const worldActorTransform = mat4.fromValues(
147-
worldTangent[0],
148-
worldTangent[1],
149-
worldTangent[2],
154+
...worldTangent,
150155
0,
151-
worldNormal[0],
152-
worldNormal[1],
153-
worldNormal[2],
156+
...worldNormal,
154157
0,
155-
-worldBitangent[0],
156-
-worldBitangent[1],
157-
-worldBitangent[2],
158+
...vec3.scale([], worldBitangent, -1),
158159
0,
159-
worldActorTranslation[0],
160-
worldActorTranslation[1],
161-
worldActorTranslation[2],
160+
...worldActorTranslation,
162161
1
163162
);
164163
actor.setUserMatrix(worldActorTransform);
@@ -195,15 +194,9 @@ function updateDistanceAndDirection() {
195194
const modelDirections = mat3.fromQuat([], orientation);
196195
const inverseModelDirections = mat3.invert([], modelDirections);
197196
const worldDirections = mat3.fromValues(
198-
worldTangent[0],
199-
worldTangent[1],
200-
worldTangent[2],
201-
worldBitangent[0],
202-
worldBitangent[1],
203-
worldBitangent[2],
204-
worldNormal[0],
205-
worldNormal[1],
206-
worldNormal[2]
197+
...worldTangent,
198+
...worldBitangent,
199+
...worldNormal
207200
);
208201
const baseDirections = mat3.mul([], inverseModelDirections, worldDirections);
209202
mapper.setDirectionMatrix(baseDirections);
@@ -222,6 +215,18 @@ function updateDistanceAndDirection() {
222215
// Update plane manipulator origin / normal for the cross view
223216
planeManipulator.setUserOrigin(worldWidgetCenter);
224217
planeManipulator.setUserNormal(worldNormal);
218+
219+
// Find the angle
220+
const signedRadAngle = Math.atan2(baseDirections[1], baseDirections[0]);
221+
const signedDegAngle = (signedRadAngle * 180) / Math.PI;
222+
const degAngle = signedDegAngle > 0 ? signedDegAngle : 360 + signedDegAngle;
223+
angleEl.value = degAngle;
224+
updateState(
225+
widgetState,
226+
widget.getScaleInPixels(),
227+
widget.getRotationHandlePosition()
228+
);
229+
renderWindow.render();
225230
}
226231

227232
// The centerline JSON contains positions (vec3) and orientations (mat4)
@@ -272,7 +277,6 @@ function setCenterlineKey(centerlineKey) {
272277
}
273278

274279
// Create an option for each centerline
275-
const centerlineEl = document.getElementById('centerline');
276280
for (let i = 0; i < centerlineKeys.length; ++i) {
277281
const name = centerlineKeys[i];
278282
const optionEl = document.createElement('option');
@@ -291,7 +295,6 @@ reader.setUrl(volumePath).then(() => {
291295
widget.setImage(image);
292296
const imageDimensions = image.getDimensions();
293297
const imageSpacing = image.getSpacing();
294-
cprManipulator.setDistanceStep(Math.min(...imageSpacing));
295298
const diagonal = vec3.mul([], imageDimensions, imageSpacing);
296299
mapper.setWidth(2 * vec3.len(diagonal));
297300

@@ -318,6 +321,35 @@ reader.setUrl(volumePath).then(() => {
318321
});
319322
});
320323

324+
function setAngleFromSlider(radAngle) {
325+
// Compute normal and bitangent directions from angle
326+
const origin = [0, 0, 0];
327+
const normalDir = [0, 0, 1];
328+
const bitangentDir = [0, 1, 0];
329+
vec3.rotateZ(bitangentDir, bitangentDir, origin, radAngle);
330+
331+
// Get orientation from distance
332+
const distance = cprManipulator.getCurrentDistance();
333+
const { orientation } = mapper.getCenterlinePositionAndOrientation(distance);
334+
const modelDirections = mat3.fromQuat([], orientation);
335+
336+
// Set widget normal and viewUp from orientation and directions
337+
const worldBitangent = vec3.transformMat3([], bitangentDir, modelDirections);
338+
const worldNormal = vec3.transformMat3([], normalDir, modelDirections);
339+
const widgetPlanes = widgetState.getPlanes();
340+
widgetPlanes[stretchViewType].normal = worldBitangent;
341+
widgetPlanes[stretchViewType].viewUp = worldNormal;
342+
widgetPlanes[crossViewType].normal = worldNormal;
343+
widgetPlanes[crossViewType].viewUp = worldBitangent;
344+
widgetState.setPlanes(widgetPlanes);
345+
346+
updateDistanceAndDirection();
347+
}
348+
349+
angleEl.addEventListener('input', () =>
350+
setAngleFromSlider(radiansFromDegrees(Number.parseFloat(angleEl.value, 10)))
351+
);
352+
321353
stretchViewWidgetInstance.onInteractionEvent(updateDistanceAndDirection);
322354
crossViewWidgetInstance.onInteractionEvent(updateDistanceAndDirection);
323355

Sources/Widgets/Manipulators/CPRManipulator/index.js

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function vtkCPRManipulator(publicAPI, model) {
2929
publicAPI.handleEvent = (callData, glRenderWindow) => {
3030
const mapper = model.cprActor?.getMapper();
3131
if (!mapper) {
32-
return { worldCoords: [0, 0, 0] };
32+
return { worldCoords: null };
3333
}
3434

3535
// Get normal and origin of the picking plane from the actor matrix
@@ -65,7 +65,7 @@ function vtkCPRManipulator(publicAPI, model) {
6565
publicAPI.distanceEvent = (distance) => {
6666
const mapper = model.cprActor?.getMapper();
6767
if (!mapper) {
68-
return { worldCoords: [0, 0, 0] };
68+
return { worldCoords: null };
6969
}
7070
const height = mapper.getHeight();
7171
const clampedDistance = Math.max(0, Math.min(height, distance));
@@ -84,9 +84,25 @@ function vtkCPRManipulator(publicAPI, model) {
8484
};
8585

8686
publicAPI.handleScroll = (nbSteps) => {
87-
const distance = model.currentDistance + model.distanceStep * nbSteps;
87+
const distance =
88+
model.currentDistance + publicAPI.getDistanceStep() * nbSteps;
8889
return publicAPI.distanceEvent(distance);
8990
};
91+
92+
publicAPI.getDistanceStep = () => {
93+
// Find default distanceStep from image spacing
94+
// This only works if the mapper in the actor already has an ImageData
95+
if (!model.distanceStep) {
96+
const imageSpacing = model.cprActor
97+
?.getMapper()
98+
?.getInputData(0)
99+
?.getSpacing?.();
100+
if (imageSpacing) {
101+
return Math.min(...imageSpacing);
102+
}
103+
}
104+
return model.distanceStep;
105+
};
90106
}
91107

92108
// ----------------------------------------------------------------------------
@@ -96,37 +112,21 @@ function vtkCPRManipulator(publicAPI, model) {
96112
// currentDistance is the distance from the first point of the centerline
97113
// cprActor.getMapper() should be a vtkImageCPRMapper
98114
function defaultValues(initialValues) {
99-
const values = {
100-
distanceStep: 1,
115+
return {
116+
distanceStep: 0,
101117
currentDistance: 0,
102118
cprActor: null,
103119
...initialValues,
104120
};
105-
// Find default distanceStep from image spacing
106-
// This only works if the mapper in the actor already has an ImageData
107-
if (!initialValues.distanceStep) {
108-
const imageSpacing = initialValues.cprActor
109-
?.getMapper()
110-
?.getInputData(0)
111-
?.getSpacing?.();
112-
if (imageSpacing) {
113-
values.distanceStep = Math.min(...imageSpacing);
114-
}
115-
}
116-
return values;
117121
}
118122

119123
// ----------------------------------------------------------------------------
120124

121125
export function extend(publicAPI, model, initialValues = {}) {
122126
vtkAbstractManipulator.extend(publicAPI, model, defaultValues(initialValues));
123127

124-
macro.setGet(publicAPI, model, [
125-
'distance',
126-
'distanceStep',
127-
'currentDistance',
128-
'cprActor',
129-
]);
128+
macro.setGet(publicAPI, model, ['distance', 'currentDistance', 'cprActor']);
129+
macro.set(publicAPI, model, ['distanceStep']);
130130

131131
vtkCPRManipulator(publicAPI, model);
132132
}

0 commit comments

Comments
 (0)