From d7510ba84feee1078e19d8fef712033323c4b929 Mon Sep 17 00:00:00 2001 From: James Petts Date: Fri, 9 Oct 2020 11:30:29 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Mouse=20wheel=20scrollab?= =?UTF-8?q?le=20ref=20lines=20+=20pan=20fix=20(#116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 Mouse wheel scrollable ref lines + pan fix * Fix placement of color indicator * Add setGet for disable scroll --- examples/VTKRotatableCrosshairsExample.js | 12 + src/VTKViewport/View2D.js | 19 ++ src/VTKViewport/vtkInteractorStyleMPRSlice.js | 50 ++-- ...tkInteractorStyleRotatableMPRCrosshairs.js | 218 +++++++++++++++--- .../vtkSVGRotatableCrosshairsWidget.js | 29 ++- 5 files changed, 274 insertions(+), 54 deletions(-) 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) { -