Skip to content

Commit 7710284

Browse files
mix3dJamesAPetts
authored andcommitted
feat: 🎸 Allows rotation by relative and absolute rotations (#71)
* Update rotation handling to include absolute and relative rotations. * Refactor interactors to remove redundant logic.
1 parent b29d78c commit 7710284

10 files changed

+146
-294
lines changed

‎examples/VTKLoadImageDataExample.js‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,7 @@ class VTKLoadImageDataExample extends Component {
141141

142142
const { slicePlaneNormal, sliceViewUp } = ORIENTATION[orientation];
143143

144-
istyle.setSliceNormal(...slicePlaneNormal);
145-
istyle.setViewUp(...sliceViewUp);
144+
istyle.setSliceOrientation(slicePlaneNormal, sliceViewUp);
146145

147146
this.imageDataObject.insertPixelDataPromises.forEach(promise => {
148147
promise.then(() => renderWindow.render());

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
"style-loader": "^0.23.1",
8383
"stylelint": "^10.1.0",
8484
"stylelint-config-recommended": "^2.2.0",
85-
"vtk.js": "^11.7.2",
85+
"vtk.js": "^11.11.1",
8686
"webpack": "4.34.0",
8787
"webpack-cli": "^3.3.4",
8888
"webpack-dev-server": "^3.8.0",

‎src/Custom/VTKMPRViewport.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export default class VtkMpr extends Component {
134134
// must be added AFTER the data volume is added so that this can be rendered in front
135135
this.renderer.addVolume(this.labelPipeline.actor);
136136

137-
istyle.setVolumeMapper(this.pipeline.mapper);
137+
istyle.setVolumeActor(this.pipeline.actor);
138138
istyle.setSliceNormal([0, 0, 1]);
139139
const range = istyle.getSliceRange();
140140
istyle.setSlice((range[0] + range[1]) / 2);

‎src/VTKViewport/Manipulators/vtkMouseRangeRotateManipulator.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function vtkMouseRangeRotateManipulator(publicAPI, model) {
2020
dThetaY => {
2121
let thetaY = dThetaY % 360;
2222

23-
model.viewportData.rotate(0, thetaY);
23+
model.viewportData.rotateRelative(0, thetaY);
2424

2525
// onInteractiveRotationChanged();
2626
}

‎src/VTKViewport/View2D.js‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export default class View2D extends Component {
2323
onPaint: PropTypes.func,
2424
onPaintStart: PropTypes.func,
2525
onPaintEnd: PropTypes.func,
26-
interactorStyleVolumeMapper: PropTypes.object,
2726
dataDetails: PropTypes.object,
2827
onCreated: PropTypes.func,
2928
onDestroyed: PropTypes.func,
@@ -172,16 +171,11 @@ export default class View2D extends Component {
172171
renderer.getActiveCamera().setClippingRange(...r);
173172
});*/
174173

175-
const istyleVolumeMapper =
176-
this.props.interactorStyleVolumeMapper ||
177-
this.props.volumes[0].getMapper();
178-
179174
// Set orientation based on props
180175
if (this.props.orientation) {
181176
const { orientation } = this.props;
182177

183-
istyle.setSliceNormal(...orientation.sliceNormal);
184-
istyle.setViewUp(...orientation.viewUp);
178+
istyle.setSliceOrientation(orientation.sliceNormal, orientation.viewUp);
185179
} else {
186180
istyle.setSliceNormal(0, 0, 1);
187181
}
@@ -191,7 +185,7 @@ export default class View2D extends Component {
191185
camera.setParallelProjection(true);
192186
this.renderer.resetCamera();
193187

194-
istyle.setVolumeMapper(istyleVolumeMapper);
188+
istyle.setVolumeActor(this.props.volumes[0]);
195189
const range = istyle.getSliceRange();
196190
istyle.setSlice((range[0] + range[1]) / 2);
197191

@@ -315,14 +309,12 @@ export default class View2D extends Component {
315309
istyle.setViewport(currentViewport);
316310
}
317311

318-
istyle.getVolumeMapper();
319-
320-
if (istyle.getVolumeMapper() !== volumes[0]) {
312+
if (istyle.getVolumeActor() !== volumes[0]) {
321313
if (slabThickness && istyle.setSlabThickness) {
322314
istyle.setSlabThickness(slabThickness);
323315
}
324316

325-
istyle.setVolumeMapper(volumes[0]);
317+
istyle.setVolumeActor(volumes[0]);
326318
}
327319

328320
// Add appropriate callbacks

‎src/VTKViewport/ViewportData.js‎

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { vec3, mat4 } from 'gl-matrix';
1+
import { vec3, mat4, quat } from 'gl-matrix';
22
import { degrees2radians } from '../lib/math/angles.js';
33
import EVENTS from '../events.js';
44

@@ -32,47 +32,66 @@ export default class {
3232
} else {
3333
this._state = JSON.parse(JSON.stringify(state));
3434
}
35+
36+
// copy the state to a cache, so we can modify internally,
37+
// but remember the original values for absolute rotations
38+
this._state.cache = {
39+
...this._state,
40+
};
3541
}
3642

3743
getEventWindow = () => {
3844
return this.eventWindow;
3945
};
4046

41-
rotate = (dThetaX, dThetaY) => {
47+
_rotate = (viewUp, sliceNormal, dThetaX, dThetaY, dThetaZ = 0) => {
4248
validateNumber(dThetaX);
4349
validateNumber(dThetaY);
50+
validateNumber(dThetaZ);
4451

4552
let xAxis = [];
46-
vec3.cross(xAxis, this._state.viewUp, this._state.sliceNormal);
53+
vec3.cross(xAxis, viewUp, sliceNormal);
4754
vec3.normalize(xAxis, xAxis);
4855

49-
let yAxis = this._state.viewUp;
56+
let yAxis = viewUp;
5057
// rotate around the vector of the cross product of the
5158
// plane and viewup as the X component
5259

53-
const sliceNormal = [];
54-
const sliceViewUp = [];
60+
const nSliceNormal = [];
61+
const nViewUp = [];
5562

5663
const planeMat = mat4.create();
5764

58-
//Rotate around the vertical (slice-up) vector
65+
// Rotate around the vertical (slice-up) vector
5966
mat4.rotate(planeMat, planeMat, degrees2radians(dThetaY), yAxis);
6067

61-
//Rotate around the horizontal (screen-x) vector
68+
// Rotate around the horizontal (screen-x) vector
6269
mat4.rotate(planeMat, planeMat, degrees2radians(dThetaX), xAxis);
6370

64-
vec3.transformMat4(sliceNormal, this._state.sliceNormal, planeMat);
65-
vec3.transformMat4(sliceViewUp, this._state.viewUp, planeMat);
71+
vec3.transformMat4(nSliceNormal, sliceNormal, planeMat);
72+
vec3.transformMat4(nViewUp, viewUp, planeMat);
73+
74+
if (dThetaZ !== 0) {
75+
// Rotate the viewUp in 90 degree increments
76+
const zRotQuat = quat.create();
77+
// Use negative degrees clockwise rotation since the axis should really be the direction of projection, which is the negative of the plane normal
78+
quat.setAxisAngle(zRotQuat, nSliceNormal, degrees2radians(-dThetaZ));
79+
quat.normalize(zRotQuat, zRotQuat);
80+
81+
// rotate the ViewUp with the z rotation
82+
vec3.transformQuat(nViewUp, nViewUp, zRotQuat);
83+
}
6684

67-
this._state.sliceNormal = sliceNormal;
68-
this._state.viewUp = sliceViewUp;
85+
this._state.cache.sliceNormal = nSliceNormal;
86+
this._state.cache.viewUp = nViewUp;
6987

7088
var event = new CustomEvent(EVENTS.VIEWPORT_ROTATED, {
7189
detail: {
72-
sliceNormal,
73-
sliceViewUp,
90+
sliceNormal: nSliceNormal,
91+
sliceViewUp: nViewUp,
7492
dThetaX,
7593
dThetaY,
94+
dThetaZ,
7695
},
7796
bubbles: true,
7897
cancelable: true,
@@ -81,9 +100,30 @@ export default class {
81100
this.eventWindow.dispatchEvent(event);
82101
};
83102

103+
rotateAbsolute = (dThetaX, dThetaY, dThetaZ = 0) => {
104+
this._rotate(
105+
this._state.viewUp,
106+
this._state.sliceNormal,
107+
dThetaX,
108+
dThetaY,
109+
dThetaZ
110+
);
111+
};
112+
rotateRelative = (dThetaX, dThetaY, dThetaZ = 0) => {
113+
this._rotate(
114+
this._state.cache.viewUp,
115+
this._state.cache.sliceNormal,
116+
dThetaX,
117+
dThetaY,
118+
dThetaZ
119+
);
120+
};
121+
84122
setOrientation = (sliceNormal, viewUp = [0, 1, 0]) => {
85-
this._state.sliceNormal = sliceNormal;
86-
this._state.viewUp = viewUp;
123+
this._state.sliceNormal = [...sliceNormal];
124+
this._state.viewUp = [...viewUp];
125+
this._state.cache.sliceNormal = [...sliceNormal];
126+
this._state.cache.viewUp = [...viewUp];
87127
};
88128

89129
getViewUp = () => {
@@ -94,6 +134,14 @@ export default class {
94134
return this._state.sliceNormal;
95135
};
96136

137+
getCurrentViewUp = () => {
138+
return this._state.cache.viewUp;
139+
};
140+
141+
getCurrentSliceNormal = () => {
142+
return this._state.cache.sliceNormal;
143+
};
144+
97145
getReadOnlyViewPort = () => {
98146
const readOnlyState = JSON.parse(JSON.stringify(this._state));
99147

‎src/VTKViewport/vtkInteractorStyleMPRCrosshairs.js‎

Lines changed: 2 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import macro from 'vtk.js/Sources/macro';
2-
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
32
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
4-
import vtkInteractorStyleManipulator from 'vtk.js/Sources/Interaction/Style/InteractorStyleManipulator';
5-
import vtkMouseCameraTrackballRotateManipulator from 'vtk.js/Sources/Interaction/Manipulators/MouseCameraTrackballRotateManipulator';
6-
import vtkMouseCameraTrackballPanManipulator from 'vtk.js/Sources/Interaction/Manipulators/MouseCameraTrackballPanManipulator';
7-
import vtkMouseCameraTrackballZoomManipulator from 'vtk.js/Sources/Interaction/Manipulators/MouseCameraTrackballZoomManipulator';
8-
import vtkMouseRangeManipulator from 'vtk.js/Sources/Interaction/Manipulators/MouseRangeManipulator';
93
import vtkInteractorStyleMPRSlice from './vtkInteractorStyleMPRSlice.js';
104
import Constants from 'vtk.js/Sources/Rendering/Core/InteractorStyle/Constants';
115
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
@@ -24,44 +18,6 @@ function vtkInteractorStyleMPRCrosshairs(publicAPI, model) {
2418
// Set our className
2519
model.classHierarchy.push('vtkInteractorStyleMPRCrosshairs');
2620

27-
model.trackballManipulator = vtkMouseCameraTrackballRotateManipulator.newInstance(
28-
{
29-
button: 1,
30-
}
31-
);
32-
model.panManipulator = vtkMouseCameraTrackballPanManipulator.newInstance({
33-
button: 1,
34-
shift: true,
35-
});
36-
model.zoomManipulator = vtkMouseCameraTrackballZoomManipulator.newInstance({
37-
button: 3,
38-
});
39-
model.scrollManipulator = vtkMouseRangeManipulator.newInstance({
40-
scrollEnabled: true,
41-
dragEnabled: false,
42-
});
43-
44-
function updateScrollManipulator() {
45-
const range = publicAPI.getSliceRange();
46-
model.scrollManipulator.removeScrollListener();
47-
model.scrollManipulator.setScrollListener(
48-
range[0],
49-
range[1],
50-
1,
51-
publicAPI.getSlice,
52-
publicAPI.setSlice
53-
);
54-
}
55-
56-
function setManipulators() {
57-
publicAPI.removeAllMouseManipulators();
58-
publicAPI.addMouseManipulator(model.trackballManipulator);
59-
publicAPI.addMouseManipulator(model.panManipulator);
60-
publicAPI.addMouseManipulator(model.zoomManipulator);
61-
publicAPI.addMouseManipulator(model.scrollManipulator);
62-
updateScrollManipulator();
63-
}
64-
6521
function moveCrosshairs(callData) {
6622
const { apis, apiIndex } = model;
6723
const pos = [callData.position.x, callData.position.y];
@@ -148,7 +104,7 @@ function vtkInteractorStyleMPRCrosshairs(publicAPI, model) {
148104
const superHandleLeftButtonPress = publicAPI.handleLeftButtonPress;
149105
publicAPI.handleLeftButtonPress = callData => {
150106
if (!callData.shiftKey && !callData.controlKey) {
151-
if (model.volumeMapper) {
107+
if (model.volumeActor) {
152108
moveCrosshairs(callData);
153109
publicAPI.startWindowLevel();
154110
}
@@ -157,24 +113,6 @@ function vtkInteractorStyleMPRCrosshairs(publicAPI, model) {
157113
}
158114
};
159115

160-
const superSetVolumeMapper = publicAPI.setVolumeMapper;
161-
publicAPI.setVolumeMapper = mapper => {
162-
if (superSetVolumeMapper(mapper)) {
163-
const renderer = model.interactor.getCurrentRenderer();
164-
const camera = renderer.getActiveCamera();
165-
if (mapper) {
166-
// prevent zoom manipulator from messing with our focal point
167-
camera.setFreezeFocalPoint(true);
168-
updateScrollManipulator();
169-
// NOTE: Disabling this because it makes it more difficult to switch
170-
// interactor styles. Need to find a better way to do this!
171-
//publicAPI.setSliceNormal(...publicAPI.getSliceNormal());
172-
} else {
173-
camera.setFreezeFocalPoint(false);
174-
}
175-
}
176-
};
177-
178116
publicAPI.superHandleLeftButtonRelease = publicAPI.handleLeftButtonRelease;
179117
publicAPI.handleLeftButtonRelease = () => {
180118
switch (model.state) {
@@ -194,8 +132,6 @@ function vtkInteractorStyleMPRCrosshairs(publicAPI, model) {
194132
publicAPI.setApiIndex = apiIndex => {
195133
model.apiIndex = apiIndex;
196134
};
197-
198-
setManipulators();
199135
}
200136

201137
// ----------------------------------------------------------------------------
@@ -212,7 +148,7 @@ export function extend(publicAPI, model, initialValues = {}) {
212148
// Inheritance
213149
vtkInteractorStyleMPRSlice.extend(publicAPI, model, initialValues);
214150

215-
macro.setGet(publicAPI, model, ['volumeMapper', 'callback']);
151+
macro.setGet(publicAPI, model, ['callback']);
216152

217153
// Object specific methods
218154
vtkInteractorStyleMPRCrosshairs(publicAPI, model);

‎src/VTKViewport/vtkInteractorStyleMPRRotate.js‎

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import macro from 'vtk.js/Sources/macro';
22
import vtkInteractorStyleMPRSlice from './vtkInteractorStyleMPRSlice.js';
33
import Constants from 'vtk.js/Sources/Rendering/Core/InteractorStyle/Constants';
4-
import { vec3, mat4 } from 'gl-matrix';
5-
import { degrees2radians } from '../lib/math/angles.js';
6-
import ViewportData from './ViewportData.js';
74

85
const { States } = Constants;
9-
const MAX_SAFE_INTEGER = 2147483647;
106

117
// ----------------------------------------------------------------------------
128
// Global methods
@@ -48,7 +44,7 @@ function vtkInteractorStyleMPRRotate(publicAPI, model) {
4844
const dThetaY = -((pos[0] - model.rotateStartPos[0]) * xSensitivity);
4945
const viewport = publicAPI.getViewport();
5046

51-
viewport.rotate(dThetaX, dThetaY);
47+
viewport.rotateRelative(dThetaX, dThetaY);
5248

5349
model.rotateStartPos[0] = Math.round(pos[0]);
5450
model.rotateStartPos[1] = Math.round(pos[1]);
@@ -59,7 +55,7 @@ function vtkInteractorStyleMPRRotate(publicAPI, model) {
5955
model.rotateStartPos[0] = Math.round(callData.position.x);
6056
model.rotateStartPos[1] = Math.round(callData.position.y);
6157
if (!callData.shiftKey && !callData.controlKey) {
62-
const property = model.volumeMapper.getProperty();
58+
const property = model.volumeActor.getProperty();
6359
if (property) {
6460
publicAPI.startRotate();
6561
}
@@ -68,20 +64,6 @@ function vtkInteractorStyleMPRRotate(publicAPI, model) {
6864
}
6965
};
7066

71-
const superSetVolumeMapper = publicAPI.setVolumeMapper;
72-
publicAPI.setVolumeMapper = mapper => {
73-
if (superSetVolumeMapper(mapper)) {
74-
const renderer = model.interactor.getCurrentRenderer();
75-
const camera = renderer.getActiveCamera();
76-
if (mapper) {
77-
// prevent zoom manipulator from messing with our focal point
78-
camera.setFreezeFocalPoint(true);
79-
} else {
80-
camera.setFreezeFocalPoint(false);
81-
}
82-
}
83-
};
84-
8567
publicAPI.superHandleLeftButtonRelease = publicAPI.handleLeftButtonRelease;
8668
publicAPI.handleLeftButtonRelease = () => {
8769
switch (model.state) {
@@ -114,10 +96,7 @@ export function extend(publicAPI, model, initialValues = {}) {
11496
// Inheritance
11597
vtkInteractorStyleMPRSlice.extend(publicAPI, model, initialValues);
11698

117-
macro.setGet(publicAPI, model, [
118-
'volumeMapper',
119-
'onInteractiveRotateChanged',
120-
]);
99+
macro.setGet(publicAPI, model, ['onInteractiveRotateChanged']);
121100

122101
// Object specific methods
123102
vtkInteractorStyleMPRRotate(publicAPI, model);

0 commit comments

Comments
 (0)