diff --git a/examples/VTKRotatableCrosshairsExample.js b/examples/VTKRotatableCrosshairsExample.js
index 23766562..20e19ecb 100644
--- a/examples/VTKRotatableCrosshairsExample.js
+++ b/examples/VTKRotatableCrosshairsExample.js
@@ -207,6 +207,17 @@ class VTKRotatableCrosshairsExample extends Component {
this.setState({ displayCrosshairs: shouldDisplayCrosshairs });
};
+ resetCrosshairs = () => {
+ const apis = this.apis;
+
+ apis.forEach(api => {
+ api.resetOrientation();
+ });
+
+ // Reset the crosshairs
+ apis[0].svgWidgets.rotatableCrosshairsWidget.resetCrosshairs(apis, 0);
+ };
+
render() {
if (!this.state.volumes || !this.state.volumes.length) {
return
Loading...
;
@@ -241,6 +252,7 @@ class VTKRotatableCrosshairsExample extends Component {
? 'Switch To WL/Zoom/Pan/Scroll'
: 'Switch To Crosshairs'}
+
diff --git a/src/VTKViewport/View2D.js b/src/VTKViewport/View2D.js
index 6636517a..1b42660b 100644
--- a/src/VTKViewport/View2D.js
+++ b/src/VTKViewport/View2D.js
@@ -217,6 +217,7 @@ export default class View2D extends Component {
const boundUpdateVOI = this.updateVOI.bind(this);
const boundGetOrienation = this.getOrientation.bind(this);
const boundSetOrientation = this.setOrientation.bind(this);
+ const boundResetOrientation = this.resetOrientation.bind(this);
const boundGetViewUp = this.getViewUp.bind(this);
const boundGetSliceNormal = this.getSliceNormal.bind(this);
const boundSetInteractorStyle = this.setInteractorStyle.bind(this);
@@ -261,6 +262,7 @@ export default class View2D extends Component {
updateVOI: boundUpdateVOI,
getOrientation: boundGetOrienation,
setOrientation: boundSetOrientation,
+ resetOrientation: boundResetOrientation,
getViewUp: boundGetViewUp,
getSliceNormal: boundGetSliceNormal,
setInteractorStyle: boundSetInteractorStyle,
@@ -307,6 +309,23 @@ export default class View2D extends Component {
currentIStyle.setSliceOrientation(sliceNormal, viewUp);
}
+ resetOrientation() {
+ const orientation = this.props.orientation || {
+ sliceNormal: [0, 0, 1],
+ viewUp: [0, -1, 0],
+ };
+
+ // Reset orientation.
+ this.setOrientation(orientation.sliceNormal, orientation.viewUp);
+
+ // Reset slice.
+ const renderWindow = this.genericRenderWindow.getRenderWindow();
+ const currentIStyle = renderWindow.getInteractor().getInteractorStyle();
+ const range = currentIStyle.getSliceRange();
+
+ currentIStyle.setSlice((range[0] + range[1]) / 2);
+ }
+
getApiProperty(propertyName) {
return this.apiProperties[propertyName];
}
diff --git a/src/VTKViewport/vtkInteractorStyleMPRSlice.js b/src/VTKViewport/vtkInteractorStyleMPRSlice.js
index ba1bae07..871706fd 100644
--- a/src/VTKViewport/vtkInteractorStyleMPRSlice.js
+++ b/src/VTKViewport/vtkInteractorStyleMPRSlice.js
@@ -74,7 +74,7 @@ function vtkInteractorStyleMPRSlice(publicAPI, model) {
sliceCenter: [],
};
- function updateScrollManipulator() {
+ publicAPI.updateScrollManipulator = () => {
const range = publicAPI.getSliceRange();
model.scrollManipulator.removeScrollListener();
// The Scroll listener has min, max, step, and getValue setValue as params.
@@ -86,7 +86,7 @@ function vtkInteractorStyleMPRSlice(publicAPI, model) {
publicAPI.getSlice,
publicAPI.scrollToSlice
);
- }
+ };
function setManipulators() {
publicAPI.removeAllMouseManipulators();
@@ -94,7 +94,7 @@ function vtkInteractorStyleMPRSlice(publicAPI, model) {
publicAPI.addMouseManipulator(model.panManipulator);
publicAPI.addMouseManipulator(model.zoomManipulator);
publicAPI.addMouseManipulator(model.scrollManipulator);
- updateScrollManipulator();
+ publicAPI.updateScrollManipulator();
}
function isCameraViewInitialized(camera) {
@@ -219,7 +219,7 @@ function vtkInteractorStyleMPRSlice(publicAPI, model) {
const camera = renderer.getActiveCamera();
cameraSub = camera.onModified(() => {
- updateScrollManipulator();
+ publicAPI.updateScrollManipulator();
publicAPI.modified();
});
@@ -238,7 +238,7 @@ function vtkInteractorStyleMPRSlice(publicAPI, model) {
// TODO -> When we want a modular framework we'll have to rethink all this.
// TODO -> We need to think of a more generic way to do this for all widget types eventually.
// TODO -> We certainly need to be able to register widget types on instantiation.
- function handleButtonPress() {
+ function handleButtonPress(callData) {
const { apis, apiIndex } = model;
if (apis && apis[apiIndex] && apis[apiIndex].type === 'VIEW2D') {
@@ -250,11 +250,33 @@ function vtkInteractorStyleMPRSlice(publicAPI, model) {
api.svgWidgets.crosshairsWidget.updateCrosshairForApi(api);
}
if (api.svgWidgets.rotatableCrosshairsWidget) {
- api.svgWidgets.rotatableCrosshairsWidget.updateCrosshairForApi(api);
+ updateRotatableCrosshairs(callData);
}
}
}
+ function updateRotatableCrosshairs(callData) {
+ const { apis, apiIndex } = model;
+ const thisApi = apis[apiIndex];
+ const { rotatableCrosshairsWidget } = thisApi.svgWidgets;
+ const renderer = callData.pokedRenderer;
+ const worldPos = thisApi.get('cachedCrosshairWorldPosition');
+
+ const camera = renderer.getActiveCamera();
+ const directionOfProjection = camera.getDirectionOfProjection();
+
+ const halfSlabThickness = thisApi.getSlabThickness() / 2;
+
+ // Add half of the slab thickness to the world position, such that we select
+ // The center of the slice.
+
+ for (let i = 0; i < worldPos.length; i++) {
+ worldPos[i] += halfSlabThickness * directionOfProjection[i];
+ }
+
+ rotatableCrosshairsWidget.moveCrosshairs(worldPos, apis, apiIndex);
+ }
+
publicAPI.handleMiddleButtonPress = macro.chain(
publicAPI.handleMiddleButtonPress,
handleButtonPress
@@ -280,12 +302,12 @@ function vtkInteractorStyleMPRSlice(publicAPI, model) {
api.svgWidgets.crosshairsWidget.updateCrosshairForApi(api);
}
if (api.svgWidgets.rotatableCrosshairsWidget) {
- api.svgWidgets.rotatableCrosshairsWidget.updateCrosshairForApi(api);
+ updateRotatableCrosshairs(callData);
}
}
};
- function handleButtonRelease(superButtonRelease) {
+ function handleButtonRelease(superButtonRelease, callData) {
if (model.state === States.IS_PAN) {
publicAPI.endPan();
const { apis, apiIndex } = model;
@@ -295,7 +317,7 @@ function vtkInteractorStyleMPRSlice(publicAPI, model) {
api.svgWidgets.crosshairsWidget.updateCrosshairForApi(api);
}
if (api.svgWidgets.rotatableCrosshairsWidget) {
- api.svgWidgets.rotatableCrosshairsWidget.updateCrosshairForApi(api);
+ updateRotatableCrosshairs(callData);
}
}
@@ -304,13 +326,13 @@ function vtkInteractorStyleMPRSlice(publicAPI, model) {
publicAPI.superHandleMiddleButtonRelease =
publicAPI.handleMiddleButtonRelease;
- publicAPI.handleMiddleButtonRelease = () => {
- handleButtonRelease(publicAPI.superHandleMiddleButtonRelease);
+ publicAPI.handleMiddleButtonRelease = callData => {
+ handleButtonRelease(publicAPI.superHandleMiddleButtonRelease, callData);
};
publicAPI.superHandleRightButtonRelease = publicAPI.handleRightButtonRelease;
- publicAPI.handleRightButtonRelease = () => {
- handleButtonRelease(publicAPI.superHandleRightButtonRelease);
+ publicAPI.handleRightButtonRelease = callData => {
+ handleButtonRelease(publicAPI.superHandleRightButtonRelease, callData);
};
publicAPI.setVolumeActor = actor => {
@@ -328,7 +350,7 @@ function vtkInteractorStyleMPRSlice(publicAPI, model) {
setViewUpInternal(viewportData.getCurrentViewUp());
}
- updateScrollManipulator();
+ publicAPI.updateScrollManipulator();
// NOTE: Disabling this because it makes it more difficult to switch
// interactor styles. Need to find a better way to do this!
//publicAPI.setSliceNormal(...publicAPI.getSliceNormal());
diff --git a/src/VTKViewport/vtkInteractorStyleRotatableMPRCrosshairs.js b/src/VTKViewport/vtkInteractorStyleRotatableMPRCrosshairs.js
index 1a8f8092..fed1d2b4 100644
--- a/src/VTKViewport/vtkInteractorStyleRotatableMPRCrosshairs.js
+++ b/src/VTKViewport/vtkInteractorStyleRotatableMPRCrosshairs.js
@@ -81,6 +81,14 @@ function vtkInteractorStyleRotatableMPRCrosshairs(publicAPI, model) {
lineRotateHandles.selected = true;
+ if (lineIndex === 0) {
+ lines[0].active = true;
+ lines[1].active = false;
+ } else {
+ lines[0].active = false;
+ lines[1].active = true;
+ }
+
return;
}
}
@@ -99,21 +107,27 @@ function vtkInteractorStyleRotatableMPRCrosshairs(publicAPI, model) {
distanceFromFirstLine < distanceFromSecondLine ? 0 : 1;
lines[selectedLineIndex].selected = true;
+ lines[selectedLineIndex].active = true;
- // TODO -> MOVE LINE
+ // Deactivate other line if active
+ const otherLineIndex = selectedLineIndex === 0 ? 1 : 0;
- const snapToLineIndex = selectedLineIndex === 0 ? 1 : 0;
+ lines[otherLineIndex].active = false;
- // Get the line
+ // Set operation data.
model.operation = {
type: operations.MOVE_REFERENCE_LINE,
- snapToLineIndex,
+ snapToLineIndex: selectedLineIndex === 0 ? 1 : 0,
};
return;
}
+ lines.forEach(line => {
+ line.active = false;
+ });
+
// What is the fallback? Pan? Do nothing for now.
model.operation = { type: null };
}
@@ -142,17 +156,22 @@ function vtkInteractorStyleRotatableMPRCrosshairs(publicAPI, model) {
const { operation } = model;
const { type } = operation;
+ let snapToLineIndex;
+ let pos;
+
switch (type) {
case operations.MOVE_CROSSHAIRS:
+ moveCrosshairs(callData.position, callData.pokedRenderer);
+ break;
case operations.MOVE_REFERENCE_LINE:
- moveCrosshairs(callData);
+ snapToLineIndex = operation.snapToLineIndex;
+ pos = snapPosToLine(callData.position, snapToLineIndex);
+
+ moveCrosshairs(pos, callData.pokedRenderer);
break;
case operations.ROTATE_CROSSHAIRS:
rotateCrosshairs(callData);
break;
- case operations.PAN:
- pan(callData);
- break;
}
}
@@ -315,21 +334,9 @@ function vtkInteractorStyleRotatableMPRCrosshairs(publicAPI, model) {
};
}
- function moveCrosshairs(callData) {
+ function moveCrosshairs(pos, renderer) {
const { apis, apiIndex } = model;
const api = apis[apiIndex];
- const { operation } = model;
- const { snapToLineIndex } = operation;
-
- let pos;
-
- if (snapToLineIndex !== undefined) {
- pos = snapPosToLine(callData.position, snapToLineIndex);
- } else {
- pos = callData.position;
- }
-
- const renderer = callData.pokedRenderer;
const dPos = vtkCoordinate.newInstance();
dPos.setCoordinateSystemToDisplay();
@@ -358,6 +365,118 @@ function vtkInteractorStyleRotatableMPRCrosshairs(publicAPI, model) {
publicAPI.invokeInteractionEvent({ type: 'InteractionEvent' });
}
+ function scrollCrosshairs(lineIndex, direction) {
+ const { apis, apiIndex } = model;
+ const thisApi = apis[apiIndex];
+ const { svgWidgetManager, volumes } = thisApi;
+ const volume = volumes[0];
+ const size = svgWidgetManager.getSize();
+ const scale = svgWidgetManager.getScale();
+ const height = size[1];
+ const renderer = thisApi.genericRenderWindow.getRenderer();
+
+ const { rotatableCrosshairsWidget } = thisApi.svgWidgets;
+
+ if (!rotatableCrosshairsWidget) {
+ throw new Error(
+ 'Must use rotatable crosshair svg widget with this istyle.'
+ );
+ }
+
+ const lines = rotatableCrosshairsWidget.getReferenceLines();
+ const otherLineIndex = lineIndex === 0 ? 1 : 0;
+
+ const point = rotatableCrosshairsWidget.getPoint();
+ // Transform point to SVG coordinates
+ const p = [point[0] * scale, height - point[1] * scale];
+
+ // Get the unit vector to move the line in.
+
+ const linePoints = lines[otherLineIndex].points;
+ let lowToHighPoints;
+
+ // If line is horizontal (<1 pix difference in height), move right when scroll forward.
+ if (Math.abs(linePoints[0].y - linePoints[1].y) < 1.0) {
+ if (linePoints[0].x < linePoints[1].x) {
+ lowToHighPoints = [linePoints[0], linePoints[1]];
+ } else {
+ lowToHighPoints = [linePoints[1], linePoints[0]];
+ }
+ }
+ // If end is higher on screen, scroll moves crosshairs that way.
+ else if (linePoints[0].y < linePoints[1].y) {
+ lowToHighPoints = [linePoints[0], linePoints[1]];
+ } else {
+ lowToHighPoints = [linePoints[1], linePoints[0]];
+ }
+
+ const unitVector = [];
+ vec2.subtract(
+ unitVector,
+ [lowToHighPoints[1].x, lowToHighPoints[1].y],
+ [lowToHighPoints[0].x, lowToHighPoints[0].y]
+ );
+ vec2.normalize(unitVector, unitVector);
+
+ if (direction === 'forwards') {
+ unitVector[0] *= -1;
+ unitVector[1] *= -1;
+ }
+
+ const displayCoordintateScrollIncrement = getDisplayCoordinateScrollIncrement(
+ point
+ );
+
+ const newCenterPointSVG = [
+ p[0] + unitVector[0] * displayCoordintateScrollIncrement,
+ p[1] + unitVector[1] * displayCoordintateScrollIncrement,
+ ];
+
+ // translate to the display coordinates.
+ const displayCoordinate = {
+ x: newCenterPointSVG[0] / scale,
+ y: (height - newCenterPointSVG[1]) / scale,
+ };
+
+ // Move point.
+ moveCrosshairs(
+ displayCoordinate,
+ //{ x: newCenterPointSVG[0], y: newCenterPointSVG[1] },
+ renderer
+ );
+ }
+
+ function getDisplayCoordinateScrollIncrement(point) {
+ const { apis, apiIndex } = model;
+ const thisApi = apis[apiIndex];
+ const { volumes, genericRenderWindow } = thisApi;
+ const renderer = genericRenderWindow.getRenderer();
+ const volume = volumes[0];
+ const diagonalWorldLength = volume
+ .getMapper()
+ .getInputData()
+ .getSpacing()
+ .map(v => v * v)
+ .reduce((a, b) => a + b, 0);
+
+ const dPos = vtkCoordinate.newInstance();
+ dPos.setCoordinateSystemToDisplay();
+ dPos.setValue(point[0], point[1], 0);
+
+ let worldPosCenter = dPos.getComputedWorldValue(renderer);
+
+ dPos.setValue(point[0] + 1, point[1], 0);
+
+ let worldPosOnePixelOver = dPos.getComputedWorldValue(renderer);
+
+ const distanceOfOnePixelInWorld = vec2.distance(
+ worldPosCenter,
+ worldPosOnePixelOver
+ );
+
+ return diagonalWorldLength / distanceOfOnePixelInWorld;
+ }
+
function handlePassiveMouseMove(callData) {
const { apis, apiIndex, lineGrabDistance } = model;
const thisApi = apis[apiIndex];
@@ -472,17 +591,13 @@ function vtkInteractorStyleRotatableMPRCrosshairs(publicAPI, model) {
}
};
- const superHandleLeftButtonPress = publicAPI.handleLeftButtonPress;
+ //const superHandleLeftButtonPress = publicAPI.handleLeftButtonPress;
publicAPI.handleLeftButtonPress = callData => {
- if (!callData.shiftKey && !callData.controlKey) {
- if (model.volumeActor) {
- selectOpperation(callData);
- performOperation(callData);
+ if (model.volumeActor) {
+ selectOpperation(callData);
+ performOperation(callData);
- publicAPI.startWindowLevel();
- }
- } else if (superHandleLeftButtonPress) {
- superHandleLeftButtonPress(callData);
+ publicAPI.startWindowLevel();
}
};
@@ -524,13 +639,53 @@ function vtkInteractorStyleRotatableMPRCrosshairs(publicAPI, model) {
publicAPI.endWindowLevel();
}
+
+ const superScrollToSlice = publicAPI.scrollToSlice;
+ publicAPI.scrollToSlice = slice => {
+ const { apis, apiIndex, lineGrabDistance } = model;
+ const thisApi = apis[apiIndex];
+
+ const { rotatableCrosshairsWidget } = thisApi.svgWidgets;
+
+ if (!rotatableCrosshairsWidget) {
+ throw new Error(
+ 'Must use rotatable crosshair svg widget with this istyle.'
+ );
+ }
+
+ const lines = rotatableCrosshairsWidget.getReferenceLines();
+
+ let activeLineIndex;
+
+ lines.forEach((line, lineIndex) => {
+ if (line.active) {
+ activeLineIndex = lineIndex;
+ }
+ });
+
+ if (activeLineIndex === undefined) {
+ if (!model.disableNormalMPRScroll) {
+ superScrollToSlice(slice);
+ }
+ } else {
+ const direction = publicAPI.getSlice() - slice;
+
+ const scrollDirection = direction > 0 ? 'forwards' : 'backwards';
+
+ scrollCrosshairs(activeLineIndex, scrollDirection);
+ }
+ };
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
-const DEFAULT_VALUES = { operation: { type: null }, lineGrabDistance: 20 };
+const DEFAULT_VALUES = {
+ operation: { type: null },
+ lineGrabDistance: 20,
+ disableNormalMPRScroll: false,
+};
// ----------------------------------------------------------------------------
@@ -547,6 +702,7 @@ export function extend(publicAPI, model, initialValues = {}) {
'onScroll',
'operation',
'lineGrabDistance',
+ 'disableNormalMPRScroll',
]);
// Object specific methods
diff --git a/src/VTKViewport/vtkSVGRotatableCrosshairsWidget.js b/src/VTKViewport/vtkSVGRotatableCrosshairsWidget.js
index c9d8c66d..7c55e688 100644
--- a/src/VTKViewport/vtkSVGRotatableCrosshairsWidget.js
+++ b/src/VTKViewport/vtkSVGRotatableCrosshairsWidget.js
@@ -47,6 +47,9 @@ function vtkSVGRotatableCrosshairsWidget(publicAPI, model) {
const quarterSmallestDimension = Math.min(width, height) / 4;
+ const halfWidth = width / 2;
+ const halfHeight = height / 2;
+
// A "far" distance for line clipping algorithm.
const farDistance = Math.sqrt(bottom * bottom + right * right);
@@ -119,10 +122,12 @@ function vtkSVGRotatableCrosshairsWidget(publicAPI, model) {
let lineSelected = false;
let rotateSelected = false;
+ let lineActive = false;
if (oldReferenceLine) {
lineSelected = oldReferenceLine.selected;
rotateSelected = oldReferenceLine.rotateHandles.selected;
+ lineActive = oldReferenceLine.active;
}
const firstRotateHandle = {
@@ -150,6 +155,7 @@ function vtkSVGRotatableCrosshairsWidget(publicAPI, model) {
color: strokeColors[i],
apiIndex: i,
selected: lineSelected,
+ active: lineActive,
};
referenceLines.push(referenceLine);
@@ -213,12 +219,17 @@ function vtkSVGRotatableCrosshairsWidget(publicAPI, model) {
p
);
- const firstLineStrokeWidth = firstLine.selected
- ? selectedStrokeWidth
- : strokeWidth;
- const secondLineStrokeWidth = secondLine.selected
- ? selectedStrokeWidth
- : strokeWidth;
+ const firstLineStrokeColor = strokeColors[firstLine.apiIndex];
+ const secondLineStrokeColor = strokeColors[secondLine.apiIndex];
+
+ const firstLineStrokeWidth =
+ firstLine.selected || firstLine.active
+ ? selectedStrokeWidth
+ : strokeWidth;
+ const secondLineStrokeWidth =
+ secondLine.selected || secondLine.active
+ ? selectedStrokeWidth
+ : strokeWidth;
const firstLineRotateWidth = firstLineRotateSelected
? selectedStrokeWidth
@@ -234,7 +245,7 @@ function vtkSVGRotatableCrosshairsWidget(publicAPI, model) {