From b19db9076d3c86d30789c0457cdea3270003f7e1 Mon Sep 17 00:00:00 2001 From: Forrest Date: Tue, 21 May 2024 17:24:40 -0400 Subject: [PATCH] feat(widgets): smooth drag behavior Adds a worldDelta property to the return value of manipulator.handleEvent. --- .../AbstractManipulator/index.d.ts | 30 +++++++++- .../Manipulators/AbstractManipulator/index.js | 24 ++++++++ .../Manipulators/CPRManipulator/index.js | 4 +- .../Manipulators/LineManipulator/index.js | 21 +++---- .../Manipulators/PickerManipulator/index.js | 4 +- .../Manipulators/PlaneManipulator/index.js | 21 +++---- .../TrackballManipulator/index.js | 2 +- .../Widgets/Widgets3D/AngleWidget/behavior.js | 30 ++++++---- .../Widgets3D/ImageCroppingWidget/behavior.js | 26 ++++++--- .../Widgets3D/ImplicitPlaneWidget/index.js | 28 +++++++-- .../Widgets/Widgets3D/LabelWidget/behavior.js | 39 ++++++++----- .../Widgets/Widgets3D/LineWidget/behavior.js | 53 ++++++++--------- .../Widgets3D/PolyLineWidget/behavior.js | 27 ++++++--- .../Widgets/Widgets3D/PolyLineWidget/index.js | 9 --- .../Widgets/Widgets3D/SeedWidget/behavior.js | 8 +-- .../Widgets/Widgets3D/ShapeWidget/behavior.js | 19 +++--- .../Widgets3D/SphereWidget/behavior.js | 58 +++++++------------ .../Widgets3D/SplineWidget/behavior.js | 25 ++++++-- 18 files changed, 258 insertions(+), 170 deletions(-) diff --git a/Sources/Widgets/Manipulators/AbstractManipulator/index.d.ts b/Sources/Widgets/Manipulators/AbstractManipulator/index.d.ts index 0d1c46e0af4..5c2d5cfef2a 100644 --- a/Sources/Widgets/Manipulators/AbstractManipulator/index.d.ts +++ b/Sources/Widgets/Manipulators/AbstractManipulator/index.d.ts @@ -49,11 +49,37 @@ export interface vtkAbstractManipulator extends vtkObject { setUseCameraNormal(useCameraNormal: boolean): boolean; /** - * + * Processes a vtkRenderWindowInteractor event into 3D world positional info. + * + * Returns an object containing: + * - worldCoords: a 3D coordinate corresponding to the 2D event. + * - worldDelta: a 3D position delta between the current and the previous call to handleEvent. + * - worldDirection: a 3D directional vector. Only on select manipulators. + * + * worldCoords can be null if the pointer event enters an invalid manipulator region. For example, + * the PickerManipulator returns null when the pointer event is off of the picked geometry. + * + * worldDelta only makes sense between two calls of `handleEvent`. In a queue of `handleEvent` calls, + * the i-th call returns the delta between the i-th worldCoords and the (i-1)-th worldCoords. + * Thus, calling `handleEvent` is necessary for maintaining a valid worldDelta even when the return + * value is ignored. + * + * There are three cases where worldDelta needs to handle null events: + * 1. the first call to `handleEvent`, since there is no previously cached event position. + * worldDelta is set to [0, 0, 0]. + * 2. if the current `handleEvent` call returns a null worldCoords. worldDelta is set to [0, 0, 0]. + * 3. if the previous `handleEvent` call returned a null worldCoords. In this case, worldDelta is the + * delta between the current worldCoords and the previous non-null worldCoords, referring to the + * previous 2 cases when applicable. + * * @param callData * @param glRenderWindow */ - handleEvent(callData: any, glRenderWindow: vtkOpenGLRenderWindow): { worldCoords: Nullable, worldDirection?: Matrix3x3 }; + handleEvent(callData: any, glRenderWindow: vtkOpenGLRenderWindow): { + worldCoords: Nullable, + worldDelta: Vector3, + worldDirection?: Matrix3x3, + }; /* ------------------------------------------------------------------- */ diff --git a/Sources/Widgets/Manipulators/AbstractManipulator/index.js b/Sources/Widgets/Manipulators/AbstractManipulator/index.js index 6532a87515a..0a3975eec8b 100644 --- a/Sources/Widgets/Manipulators/AbstractManipulator/index.js +++ b/Sources/Widgets/Manipulators/AbstractManipulator/index.js @@ -1,4 +1,5 @@ import macro from 'vtk.js/Sources/macros'; +import { subtract } from 'vtk.js/Sources/Common/Core/Math'; // ---------------------------------------------------------------------------- // vtkAbstractManipulator methods @@ -8,6 +9,8 @@ function vtkAbstractManipulator(publicAPI, model) { // Set our className model.classHierarchy.push('vtkAbstractManipulator'); + model._prevWorldCoords = []; + publicAPI.getOrigin = (callData) => { if (model.userOrigin) return model.userOrigin; if (model.useCameraFocalPoint) @@ -27,6 +30,27 @@ function vtkAbstractManipulator(publicAPI, model) { if (model.widgetNormal) return model.widgetNormal; return [0, 0, 1]; }; + + model._computeDeltaFromPrevCoords = (curWorldCoords) => { + if (!model._prevWorldCoords?.length || !curWorldCoords?.length) + return [0, 0, 0]; + return subtract(curWorldCoords, model._prevWorldCoords, []); + }; + + model._addWorldDeltas = (manipulatorResults) => { + const { worldCoords: curWorldCoords } = manipulatorResults; + const worldDelta = model._computeDeltaFromPrevCoords(curWorldCoords); + if (curWorldCoords) model._prevWorldCoords = curWorldCoords; + + const deltas = { + worldDelta, + }; + + return { + ...manipulatorResults, + ...deltas, + }; + }; } // ---------------------------------------------------------------------------- diff --git a/Sources/Widgets/Manipulators/CPRManipulator/index.js b/Sources/Widgets/Manipulators/CPRManipulator/index.js index 766d7161ac5..bd512a00490 100644 --- a/Sources/Widgets/Manipulators/CPRManipulator/index.js +++ b/Sources/Widgets/Manipulators/CPRManipulator/index.js @@ -29,7 +29,7 @@ function vtkCPRManipulator(publicAPI, model) { publicAPI.handleEvent = (callData, glRenderWindow) => { const mapper = model.cprActor?.getMapper(); if (!mapper) { - return { worldCoords: null }; + return model._addWorldDeltas({ worldCoords: null }); } // Get normal and origin of the picking plane from the actor matrix @@ -59,7 +59,7 @@ function vtkCPRManipulator(publicAPI, model) { const height = mapper.getHeight(); const distance = height - modelPlanePicking[1]; - return publicAPI.distanceEvent(distance); + return model._addWorldDeltas(publicAPI.distanceEvent(distance)); }; publicAPI.distanceEvent = (distance) => { diff --git a/Sources/Widgets/Manipulators/LineManipulator/index.js b/Sources/Widgets/Manipulators/LineManipulator/index.js index 427c96bc222..7660068246e 100644 --- a/Sources/Widgets/Manipulators/LineManipulator/index.js +++ b/Sources/Widgets/Manipulators/LineManipulator/index.js @@ -55,16 +55,17 @@ function vtkLineManipulator(publicAPI, model) { // Set our className model.classHierarchy.push('vtkLineManipulator'); - publicAPI.handleEvent = (callData, glRenderWindow) => ({ - worldCoords: projectDisplayToLine( - callData.position.x, - callData.position.y, - publicAPI.getOrigin(callData), - publicAPI.getNormal(callData), - callData.pokedRenderer, - glRenderWindow - ), - }); + publicAPI.handleEvent = (callData, glRenderWindow) => + model._addWorldDeltas({ + worldCoords: projectDisplayToLine( + callData.position.x, + callData.position.y, + publicAPI.getOrigin(callData), + publicAPI.getNormal(callData), + callData.pokedRenderer, + glRenderWindow + ), + }); } // ---------------------------------------------------------------------------- diff --git a/Sources/Widgets/Manipulators/PickerManipulator/index.js b/Sources/Widgets/Manipulators/PickerManipulator/index.js index f89a5601b70..abf891b18ac 100644 --- a/Sources/Widgets/Manipulators/PickerManipulator/index.js +++ b/Sources/Widgets/Manipulators/PickerManipulator/index.js @@ -19,9 +19,9 @@ function vtkPickerManipulator(publicAPI, model) { } else { model.position = null; } - return { + return model._addWorldDeltas({ worldCoords: model.position, - }; + }); }; } diff --git a/Sources/Widgets/Manipulators/PlaneManipulator/index.js b/Sources/Widgets/Manipulators/PlaneManipulator/index.js index de178d9e2c4..6a54128f739 100644 --- a/Sources/Widgets/Manipulators/PlaneManipulator/index.js +++ b/Sources/Widgets/Manipulators/PlaneManipulator/index.js @@ -25,16 +25,17 @@ function vtkPlaneManipulator(publicAPI, model) { // Set our className model.classHierarchy.push('vtkPlaneManipulator'); - publicAPI.handleEvent = (callData, glRenderWindow) => ({ - worldCoords: intersectDisplayWithPlane( - callData.position.x, - callData.position.y, - publicAPI.getOrigin(callData), - publicAPI.getNormal(callData), - callData.pokedRenderer, - glRenderWindow - ), - }); + publicAPI.handleEvent = (callData, glRenderWindow) => + model._addWorldDeltas({ + worldCoords: intersectDisplayWithPlane( + callData.position.x, + callData.position.y, + publicAPI.getOrigin(callData), + publicAPI.getNormal(callData), + callData.pokedRenderer, + glRenderWindow + ), + }); } // ---------------------------------------------------------------------------- diff --git a/Sources/Widgets/Manipulators/TrackballManipulator/index.js b/Sources/Widgets/Manipulators/TrackballManipulator/index.js index 4750c694123..998de624e39 100644 --- a/Sources/Widgets/Manipulators/TrackballManipulator/index.js +++ b/Sources/Widgets/Manipulators/TrackballManipulator/index.js @@ -71,7 +71,7 @@ function vtkTrackballManipulator(publicAPI, model) { ); prevX = callData.position.x; prevY = callData.position.y; - return { worldCoords: newDirection }; + return model._addWorldDeltas({ worldCoords: newDirection }); }; publicAPI.reset = (callData) => { diff --git a/Sources/Widgets/Widgets3D/AngleWidget/behavior.js b/Sources/Widgets/Widgets3D/AngleWidget/behavior.js index 0d4f5848f1b..d76532aa4bd 100644 --- a/Sources/Widgets/Widgets3D/AngleWidget/behavior.js +++ b/Sources/Widgets/Widgets3D/AngleWidget/behavior.js @@ -1,4 +1,5 @@ import macro from 'vtk.js/Sources/macros'; +import { add } from 'vtk.js/Sources/Common/Core/Math'; import vtkPointPicker from 'vtk.js/Sources/Rendering/Core/PointPicker'; const MAX_POINTS = 3; @@ -43,15 +44,16 @@ export default function widgetBehavior(publicAPI, model) { picker.setPickList(publicAPI.getNestedProps()); const manipulator = model.activeState?.getManipulator?.() ?? model.manipulator; + const { worldCoords } = manipulator.handleEvent( + e, + model._apiSpecificRenderWindow + ); + if ( model.activeState === model.widgetState.getMoveHandle() && model.widgetState.getHandleList().length < MAX_POINTS && manipulator ) { - const { worldCoords } = manipulator.handleEvent( - e, - model._apiSpecificRenderWindow - ); // Commit handle to location const moveHandle = model.widgetState.getMoveHandle(); moveHandle.setOrigin(...worldCoords); @@ -85,18 +87,22 @@ export default function widgetBehavior(publicAPI, model) { model.activeState.getActive() && !ignoreKey(callData) ) { - const { worldCoords } = manipulator.handleEvent( + const { worldCoords, worldDelta } = manipulator.handleEvent( callData, model._apiSpecificRenderWindow ); - if ( - worldCoords.length && - (model.activeState === model.widgetState.getMoveHandle() || - model._isDragging) && - model.activeState.setOrigin // e.g. the line is pickable but not draggable - ) { - model.activeState.setOrigin(worldCoords); + const isHandleMoving = + model.activeState === model.widgetState.getMoveHandle() || + model._isDragging; + + if (isHandleMoving && worldCoords.length && model.activeState.setOrigin) { + const curOrigin = model.activeState.getOrigin(); + if (curOrigin) { + model.activeState.setOrigin(add(curOrigin, worldDelta, [])); + } else { + model.activeState.setOrigin(worldCoords); + } publicAPI.invokeInteractionEvent(); return macro.EVENT_ABORT; } diff --git a/Sources/Widgets/Widgets3D/ImageCroppingWidget/behavior.js b/Sources/Widgets/Widgets3D/ImageCroppingWidget/behavior.js index c5c11337583..4a700e2326c 100644 --- a/Sources/Widgets/Widgets3D/ImageCroppingWidget/behavior.js +++ b/Sources/Widgets/Widgets3D/ImageCroppingWidget/behavior.js @@ -1,4 +1,5 @@ import macro from 'vtk.js/Sources/macros'; +import { add } from 'vtk.js/Sources/Common/Core/Math'; import { AXES, @@ -14,7 +15,7 @@ export default function widgetBehavior(publicAPI, model) { publicAPI.setDisplayCallback = (callback) => model.representations[0].setDisplayCallback(callback); - publicAPI.handleLeftButtonPress = () => { + publicAPI.handleLeftButtonPress = (callData) => { if ( !model.activeState || !model.activeState.getActive() || @@ -23,6 +24,10 @@ export default function widgetBehavior(publicAPI, model) { return macro.VOID; } if (model.dragable) { + // updates worldDelta + model.activeState + .getManipulator() + .handleEvent(callData, model._apiSpecificRenderWindow); model._isDragging = true; model._apiSpecificRenderWindow.setCursor('grabbing'); model._interactor.requestAnimation(publicAPI); @@ -66,13 +71,14 @@ export default function widgetBehavior(publicAPI, model) { const indexToWorldT = model.widgetState.getIndexToWorldT(); let worldCoords = []; + let worldDelta = []; if (type === 'corners') { // manipulator should be a plane manipulator - worldCoords = manipulator.handleEvent( + ({ worldCoords, worldDelta } = manipulator.handleEvent( callData, model._apiSpecificRenderWindow - ).worldCoords; + )); } if (type === 'faces') { @@ -83,10 +89,10 @@ export default function widgetBehavior(publicAPI, model) { manipulator.setHandleNormal( calculateDirection(model.activeState.getOrigin(), worldCenter) ); - worldCoords = manipulator.handleEvent( + ({ worldCoords, worldDelta } = manipulator.handleEvent( callData, model._apiSpecificRenderWindow - ).worldCoords; + )); } if (type === 'edges') { @@ -100,13 +106,13 @@ export default function widgetBehavior(publicAPI, model) { manipulator.setHandleNormal( calculateDirection(handle.getOrigin(), worldCenter) ); - worldCoords = manipulator.handleEvent( + ({ worldCoords, worldDelta } = manipulator.handleEvent( callData, model._apiSpecificRenderWindow - ).worldCoords; + )); } - if (worldCoords.length) { + if (worldCoords.length && worldDelta.length) { // transform worldCoords to indexCoords, and then update the croppingPlanes() state with setPlanes(). const worldToIndexT = model.widgetState.getWorldToIndexT(); const indexCoords = transformVec3(worldCoords, worldToIndexT); @@ -119,7 +125,9 @@ export default function widgetBehavior(publicAPI, model) { } } - model.activeState.setOrigin(...worldCoords); + model.activeState.setOrigin( + add(model.activeState.getOrigin(), worldDelta, []) + ); model.widgetState.getCroppingPlanes().setPlanes(...planes); return macro.EVENT_ABORT; diff --git a/Sources/Widgets/Widgets3D/ImplicitPlaneWidget/index.js b/Sources/Widgets/Widgets3D/ImplicitPlaneWidget/index.js index 4e8305aff6f..5669c749712 100644 --- a/Sources/Widgets/Widgets3D/ImplicitPlaneWidget/index.js +++ b/Sources/Widgets/Widgets3D/ImplicitPlaneWidget/index.js @@ -1,4 +1,5 @@ import macro from 'vtk.js/Sources/macros'; +import { add } from 'vtk.js/Sources/Common/Core/Math'; import vtkAbstractWidgetFactory from 'vtk.js/Sources/Widgets/Core/AbstractWidgetFactory'; import vtkImplicitPlaneRepresentation from 'vtk.js/Sources/Widgets/Representations/ImplicitPlaneRepresentation'; import vtkLineManipulator from 'vtk.js/Sources/Widgets/Manipulators/LineManipulator'; @@ -14,6 +15,8 @@ import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants'; function widgetBehavior(publicAPI, model) { model.classHierarchy.push('vtkPlaneWidget'); model._isDragging = false; + // used to track the constrained widget position + model._draggingWidgetOrigin = [0, 0, 0]; publicAPI.setDisplayCallback = (callback) => model.representations[0].setDisplayCallback(callback); @@ -48,7 +51,15 @@ function widgetBehavior(publicAPI, model) { model.planeManipulator.setWidgetOrigin(model.widgetState.getOrigin()); model.trackballManipulator.reset(callData); // setup trackball delta + // updates worldDelta + model.lineManipulator.handleEvent(callData, model._apiSpecificRenderWindow); + model.planeManipulator.handleEvent( + callData, + model._apiSpecificRenderWindow + ); + if (model.dragable) { + model._draggingWidgetOrigin = model.widgetState.getOrigin(); model._isDragging = true; model._apiSpecificRenderWindow.setCursor('grabbing'); model._interactor.requestAnimation(publicAPI); @@ -100,13 +111,16 @@ function widgetBehavior(publicAPI, model) { publicAPI.updateFromOrigin = (callData) => { model.planeManipulator.setWidgetNormal(model.widgetState.getNormal()); - const { worldCoords } = model.planeManipulator.handleEvent( + const { worldCoords, worldDelta } = model.planeManipulator.handleEvent( callData, model._apiSpecificRenderWindow ); - if (model.widgetState.containsPoint(worldCoords)) { - model.activeState.setOrigin(worldCoords); + add(model._draggingWidgetOrigin, worldDelta, model._draggingWidgetOrigin); + + // test containment of interaction coords + if (model.widgetState.containsPoint(...worldCoords)) { + model.activeState.setOrigin(model._draggingWidgetOrigin); } }; @@ -115,13 +129,15 @@ function widgetBehavior(publicAPI, model) { publicAPI.updateFromPlane = (callData) => { // Move origin along normal axis model.lineManipulator.setWidgetNormal(model.activeState.getNormal()); - const { worldCoords } = model.lineManipulator.handleEvent( + const { worldDelta } = model.lineManipulator.handleEvent( callData, model._apiSpecificRenderWindow ); - if (model.widgetState.containsPoint(...worldCoords)) { - model.activeState.setOrigin(worldCoords); + add(model._draggingWidgetOrigin, worldDelta, model._draggingWidgetOrigin); + + if (model.widgetState.containsPoint(...model._draggingWidgetOrigin)) { + model.activeState.setOrigin(model._draggingWidgetOrigin); } }; diff --git a/Sources/Widgets/Widgets3D/LabelWidget/behavior.js b/Sources/Widgets/Widgets3D/LabelWidget/behavior.js index 4446067e526..26ba88eea0b 100644 --- a/Sources/Widgets/Widgets3D/LabelWidget/behavior.js +++ b/Sources/Widgets/Widgets3D/LabelWidget/behavior.js @@ -1,4 +1,5 @@ import macro from 'vtk.js/Sources/macros'; +import { add } from 'vtk.js/Sources/Common/Core/Math'; export default function widgetBehavior(publicAPI, model) { model.classHierarchy.push('vtkLabelWidgetProp'); @@ -46,14 +47,15 @@ export default function widgetBehavior(publicAPI, model) { const manipulator = model.activeState?.getManipulator?.() ?? model.manipulator; + const { worldCoords } = manipulator.handleEvent( + e, + model._apiSpecificRenderWindow + ); + if ( model.activeState === model.widgetState.getMoveHandle() && manipulator ) { - const { worldCoords } = manipulator.handleEvent( - e, - model._apiSpecificRenderWindow - ); // Commit handle to location const moveHandle = model.widgetState.getMoveHandle(); moveHandle.setOrigin(worldCoords); @@ -118,21 +120,28 @@ export default function widgetBehavior(publicAPI, model) { model.activeState.getActive() && !ignoreKey(callData) ) { - const { worldCoords } = manipulator.handleEvent( + const { worldCoords, worldDelta } = manipulator.handleEvent( callData, model._apiSpecificRenderWindow ); - if ( - worldCoords.length && - (model.activeState === model.widgetState.getMoveHandle() || - model._isDragging) - ) { - model.activeState.setOrigin(worldCoords); - model.widgetState.getText().setOrigin(model.activeState.getOrigin()); - publicAPI.invokeInteractionEvent(); - return macro.EVENT_ABORT; - } + const isHandleMoving = + model.widgetState.getMoveHandle() === model.activeState || + model._isDragging; + + if (!isHandleMoving || !worldCoords.length || !worldDelta.length) + return macro.VOID; + + const curOrigin = model.activeState.getOrigin(); + const newOrigin = curOrigin + ? add(curOrigin, worldDelta, []) + : worldCoords; + + model.activeState.setOrigin(newOrigin); + model.widgetState.getText().setOrigin(newOrigin); + + publicAPI.invokeInteractionEvent(); + return macro.EVENT_ABORT; } return macro.VOID; diff --git a/Sources/Widgets/Widgets3D/LineWidget/behavior.js b/Sources/Widgets/Widgets3D/LineWidget/behavior.js index 84486f50442..9c0813f938b 100644 --- a/Sources/Widgets/Widgets3D/LineWidget/behavior.js +++ b/Sources/Widgets/Widgets3D/LineWidget/behavior.js @@ -1,6 +1,6 @@ import Constants from 'vtk.js/Sources/Widgets/Widgets3D/LineWidget/Constants'; import macro from 'vtk.js/Sources/macros'; -import * as vtkMath from 'vtk.js/Sources/Common/Core/Math/'; +import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; import { calculateTextPosition, updateTextPosition, @@ -58,10 +58,7 @@ export default function widgetBehavior(publicAPI, model) { model._isDragging = true; const manipulator = model.activeState?.getManipulator?.() ?? model.manipulator; - model.previousPosition = manipulator.handleEvent( - callData, - model._apiSpecificRenderWindow - ).worldCoords; + manipulator.handleEvent(callData, model._apiSpecificRenderWindow); model._apiSpecificRenderWindow.setCursor('grabbing'); model._interactor.requestAnimation(publicAPI); } @@ -239,37 +236,41 @@ export default function widgetBehavior(publicAPI, model) { model.activeState.getActive() && !ignoreKey(callData) ) { - const { worldCoords } = manipulator.handleEvent( + const { worldCoords, worldDelta } = manipulator.handleEvent( callData, model._apiSpecificRenderWindow ); - const translation = model.previousPosition - ? vtkMath.subtract(worldCoords, model.previousPosition, []) - : [0, 0, 0]; - model.previousPosition = worldCoords; - if ( + + const isHandleMoving = // is placing first or second handle model.activeState === model.widgetState.getMoveHandle() || // is dragging already placed first or second handle - model._isDragging - ) { - if (model.activeState.setOrigin) { - model.activeState.setOrigin(worldCoords); + model._isDragging; + + // the line state doesn't have setOrigin + const isDraggingLine = !model.activeState.setOrigin; + + if (isHandleMoving) { + if (!isDraggingLine) { + const curOrigin = model.activeState.getOrigin(); + if (curOrigin) { + model.activeState.setOrigin( + vtkMath.add(model.activeState.getOrigin(), worldDelta, []) + ); + } else { + model.activeState.setOrigin(worldCoords); + } publicAPI.updateHandleVisibility( publicAPI.getHandleIndex(model.activeState) ); } else { - // Dragging line - publicAPI - .getHandle(0) - .setOrigin( - vtkMath.add(publicAPI.getHandle(0).getOrigin(), translation, []) - ); - publicAPI - .getHandle(1) - .setOrigin( - vtkMath.add(publicAPI.getHandle(1).getOrigin(), translation, []) - ); + // Dragging line; move all handles + for (let i = 0; i < 2; i++) { + const handleOrigin = publicAPI.getHandle(i).getOrigin(); + publicAPI + .getHandle(i) + .setOrigin(vtkMath.add(handleOrigin, worldDelta, [])); + } } publicAPI.updateHandleOrientations(); updateTextPosition(model); diff --git a/Sources/Widgets/Widgets3D/PolyLineWidget/behavior.js b/Sources/Widgets/Widgets3D/PolyLineWidget/behavior.js index 1f2d415980a..1ec7c6bd0f7 100644 --- a/Sources/Widgets/Widgets3D/PolyLineWidget/behavior.js +++ b/Sources/Widgets/Widgets3D/PolyLineWidget/behavior.js @@ -1,4 +1,5 @@ import macro from 'vtk.js/Sources/macros'; +import { add } from 'vtk.js/Sources/Common/Core/Math'; export default function widgetBehavior(publicAPI, model) { model.classHierarchy.push('vtkPolyLineWidgetProp'); @@ -26,18 +27,25 @@ export default function widgetBehavior(publicAPI, model) { return macro.VOID; } - const { worldCoords } = manipulator.handleEvent( + const { worldCoords, worldDelta } = manipulator.handleEvent( callData, model._apiSpecificRenderWindow ); - if ( - worldCoords.length && - (model.activeState === model.widgetState.getMoveHandle() || - model._isDragging) && - model.activeState.setOrigin // e.g. the line is pickable but not draggable - ) { - model.activeState.setOrigin(worldCoords); + const isHandleMoving = + model.activeState === model.widgetState.getMoveHandle() || + model._isDragging; + + // the line is pickable but not draggable + const isPickingLine = !model.activeState.setOrigin; + + if (worldCoords.length && isHandleMoving && !isPickingLine) { + const curOrigin = model.activeState.getOrigin(); + if (curOrigin) { + model.activeState.setOrigin(add(curOrigin, worldDelta, [])); + } else { + model.activeState.setOrigin(worldCoords); + } publicAPI.invokeInteractionEvent(); return macro.EVENT_ABORT; } @@ -87,6 +95,7 @@ export default function widgetBehavior(publicAPI, model) { } const manipulator = model.activeState?.getManipulator?.() ?? model.manipulator; + if ( model.activeState === model.widgetState.getMoveHandle() && manipulator @@ -99,6 +108,8 @@ export default function widgetBehavior(publicAPI, model) { newHandle.setScale1(moveHandle.getScale1()); newHandle.setManipulator(manipulator); } else if (model.dragable) { + // Update worldDelta + manipulator.handleEvent(e, model._apiSpecificRenderWindow); model._isDragging = true; model._apiSpecificRenderWindow.setCursor('grabbing'); model._interactor.requestAnimation(publicAPI); diff --git a/Sources/Widgets/Widgets3D/PolyLineWidget/index.js b/Sources/Widgets/Widgets3D/PolyLineWidget/index.js index a987be3f327..a291033a87e 100644 --- a/Sources/Widgets/Widgets3D/PolyLineWidget/index.js +++ b/Sources/Widgets/Widgets3D/PolyLineWidget/index.js @@ -68,15 +68,6 @@ function vtkPolyLineWidget(publicAPI, model) { // initialization // -------------------------------------------------------------------------- - model.widgetState.onBoundsChange((bounds) => { - const center = [ - (bounds[0] + bounds[1]) * 0.5, - (bounds[2] + bounds[3]) * 0.5, - (bounds[4] + bounds[5]) * 0.5, - ]; - model.widgetState.getMoveHandle().setOrigin(center); - }); - // Default manipulator publicAPI.setManipulator( model.manipulator || diff --git a/Sources/Widgets/Widgets3D/SeedWidget/behavior.js b/Sources/Widgets/Widgets3D/SeedWidget/behavior.js index 06f77ce089b..b23f5197391 100644 --- a/Sources/Widgets/Widgets3D/SeedWidget/behavior.js +++ b/Sources/Widgets/Widgets3D/SeedWidget/behavior.js @@ -6,7 +6,6 @@ export default function widgetBehavior(publicAPI, model) { const moveHandle = model.widgetState.getMoveHandle(); moveHandle.setVisible(true); model._isDragging = false; - model.previousPosition = null; function isValidHandle(handle) { return handle === moveHandle; @@ -40,7 +39,6 @@ export default function widgetBehavior(publicAPI, model) { if (model.activeState === moveHandle) { if (!moveHandle.getOrigin() && worldCoords) { moveHandle.setOrigin(worldCoords); - model.previousPosition = [...worldCoords]; } } model._isDragging = true; @@ -55,7 +53,6 @@ export default function widgetBehavior(publicAPI, model) { return macro.VOID; } if (isPlaced()) { - model.previousPosition = null; model._widgetManager.enablePicking(); model._apiSpecificRenderWindow.setCursor('pointer'); model._isDragging = false; @@ -73,10 +70,7 @@ export default function widgetBehavior(publicAPI, model) { } if (!model.activeState) throw Error('no activestate'); const worldCoords = currentWorldCoords(e); - if (worldCoords) { - model.activeState.setOrigin(worldCoords); - model.previousPosition = worldCoords; - } + model.activeState.setOrigin(worldCoords); return macro.VOID; }; diff --git a/Sources/Widgets/Widgets3D/ShapeWidget/behavior.js b/Sources/Widgets/Widgets3D/ShapeWidget/behavior.js index ee239ce5254..384a1605fce 100644 --- a/Sources/Widgets/Widgets3D/ShapeWidget/behavior.js +++ b/Sources/Widgets/Widgets3D/ShapeWidget/behavior.js @@ -479,7 +479,7 @@ export default function widgetBehavior(publicAPI, model) { model.shapeHandle.setRight(right); model.shapeHandle.setDirection(normal); } - const { worldCoords } = manipulator.handleEvent( + const { worldCoords, worldDelta } = manipulator.handleEvent( callData, model._apiSpecificRenderWindow ); @@ -498,11 +498,11 @@ export default function widgetBehavior(publicAPI, model) { } } else if (model._isDragging) { if (model.activeState === model.point1Handle) { - model.point1Handle.setOrigin(worldCoords); - model.point1 = worldCoords; + vtkMath.add(model.point1Handle.getOrigin(), worldDelta, model.point1); + model.point1Handle.setOrigin(model.point1); } else { - model.point2Handle.setOrigin(worldCoords); - model.point2 = worldCoords; + vtkMath.add(model.point2Handle.getOrigin(), worldDelta, model.point2); + model.point2Handle.setOrigin(model.point2); } publicAPI.updateShapeBounds(); publicAPI.invokeInteractionEvent(); @@ -527,11 +527,12 @@ export default function widgetBehavior(publicAPI, model) { return macro.VOID; } + const { worldCoords } = manipulator.handleEvent( + e, + model._apiSpecificRenderWindow + ); + if (model.hasFocus) { - const { worldCoords } = manipulator.handleEvent( - e, - model._apiSpecificRenderWindow - ); if (!model.point1) { model.point1Handle.setOrigin(worldCoords); publicAPI.placePoint1(model.point1Handle.getOrigin()); diff --git a/Sources/Widgets/Widgets3D/SphereWidget/behavior.js b/Sources/Widgets/Widgets3D/SphereWidget/behavior.js index 49fca9d45e8..c4f26eaa6ef 100644 --- a/Sources/Widgets/Widgets3D/SphereWidget/behavior.js +++ b/Sources/Widgets/Widgets3D/SphereWidget/behavior.js @@ -1,4 +1,5 @@ import macro from 'vtk.js/Sources/macros'; +import { add } from 'vtk.js/Sources/Common/Core/Math'; import { vec3 } from 'gl-matrix'; export default function widgetBehavior(publicAPI, model) { @@ -10,8 +11,6 @@ export default function widgetBehavior(publicAPI, model) { // Set while moving the center or border handle. model._isDragging = false; - // The last world coordinate of the mouse cursor during dragging. - model.previousPosition = null; model.classHierarchy.push('vtkSphereWidgetProp'); @@ -58,8 +57,7 @@ export default function widgetBehavior(publicAPI, model) { function currentWorldCoords(e) { const manipulator = model.activeState?.getManipulator?.() ?? model.manipulator; - return manipulator.handleEvent(e, model._apiSpecificRenderWindow) - .worldCoords; + return manipulator.handleEvent(e, model._apiSpecificRenderWindow); } // Update the sphere's center and radius. Example: @@ -92,7 +90,7 @@ export default function widgetBehavior(publicAPI, model) { model.activeState = null; return macro.VOID; } - const worldCoords = currentWorldCoords(e); + const { worldCoords } = currentWorldCoords(e); if (model.activeState === moveHandle) { // Initial sphere placement. @@ -100,12 +98,14 @@ export default function widgetBehavior(publicAPI, model) { centerHandle.setOrigin(worldCoords); } else if (!borderHandle.getOrigin()) { borderHandle.setOrigin(worldCoords); + publicAPI.loseFocus(); } updateSphere(); + return macro.EVENT_ABORT; } + model._isDragging = true; model._apiSpecificRenderWindow.setCursor('grabbing'); - model.previousPosition = [...currentWorldCoords(e)]; publicAPI.invokeStartInteractionEvent(); return macro.EVENT_ABORT; }; @@ -116,7 +116,6 @@ export default function widgetBehavior(publicAPI, model) { return macro.VOID; } if (isPlaced()) { - model.previousPosition = null; model._widgetManager.enablePicking(); model._apiSpecificRenderWindow.setCursor('pointer'); model._isDragging = false; @@ -128,51 +127,38 @@ export default function widgetBehavior(publicAPI, model) { }; publicAPI.handleMouseMove = (e) => { - if (!model._isDragging) { - model.activeState = null; - return macro.VOID; + if (!model.activeState) return macro.VOID; + const { worldCoords, worldDelta } = currentWorldCoords(e); + + if (model.hasFocus) { + model.activeState.setOrigin(worldCoords); + } else if (model._isDragging) { + model.activeState.setOrigin( + add(model.activeState.getOrigin(), worldDelta, []) + ); } - if (!model.activeState) throw Error('no activestate'); - const worldCoords = currentWorldCoords(e); - model.activeState.setOrigin(worldCoords); - if (model.activeState === centerHandle) { - // When the sphere is fully placed, and the user is moving the - // center, we move the whole sphere. - if (borderHandle.getOrigin()) { - if (!model.previousPosition) { - // !previousPosition here happens only immediately - // after grabFocus, but grabFocus resets - // borderHandle.origin. - throw Error(`no pos ${model.activeState} ${model.previousPosition}`); - } - const translation = vec3.sub( - vec3.create(), - worldCoords, - model.previousPosition - ); - borderHandle.setOrigin( - vec3.add(vec3.create(), borderHandle.getOrigin(), translation) - ); - } - } - model.previousPosition = worldCoords; + updateSphere(); return macro.VOID; }; + const superGrabFocus = publicAPI.grabFocus; + publicAPI.grabFocus = () => { + superGrabFocus(); moveHandle.setVisible(true); centerHandle.setVisible(false); borderHandle.setVisible(false); centerHandle.setOrigin(null); borderHandle.setOrigin(null); - model._isDragging = true; model.activeState = moveHandle; model._interactor.render(); }; + const superLoseFocus = publicAPI.loseFocus; + publicAPI.loseFocus = () => { - model._isDragging = false; + superLoseFocus(); model.activeState = null; }; } diff --git a/Sources/Widgets/Widgets3D/SplineWidget/behavior.js b/Sources/Widgets/Widgets3D/SplineWidget/behavior.js index 8b424f1832a..d9f6beeea81 100644 --- a/Sources/Widgets/Widgets3D/SplineWidget/behavior.js +++ b/Sources/Widgets/Widgets3D/SplineWidget/behavior.js @@ -1,4 +1,5 @@ import macro from 'vtk.js/Sources/macros'; +import { add } from 'vtk.js/Sources/Common/Core/Math'; import { vec3 } from 'gl-matrix'; export default function widgetBehavior(publicAPI, model) { @@ -197,7 +198,10 @@ export default function widgetBehavior(publicAPI, model) { // -------------------------------------------------------------------------- publicAPI.handleLeftButtonPress = (e) => { + const manipulator = + model.activeState?.getManipulator?.() ?? model.manipulator; if ( + !manipulator || !model.activeState || !model.activeState.getActive() || !model.pickable @@ -205,6 +209,9 @@ export default function widgetBehavior(publicAPI, model) { return macro.VOID; } + // update worldDelta + manipulator.handleEvent(e, model._apiSpecificRenderWindow); + if (model.activeState === model.moveHandle) { if (model.widgetState.getHandleList().length === 0) { addPoint(); @@ -215,6 +222,7 @@ export default function widgetBehavior(publicAPI, model) { model.moveHandle.setVisible(false); model.activeState = hoveredHandle; hoveredHandle.activate(); + model._isDragging = true; model.lastHandle.setVisible(true); } else { @@ -302,7 +310,7 @@ export default function widgetBehavior(publicAPI, model) { ) { return macro.VOID; } - const { worldCoords } = manipulator.handleEvent( + const { worldCoords, worldDelta } = manipulator.handleEvent( callData, model._apiSpecificRenderWindow ); @@ -322,11 +330,16 @@ export default function widgetBehavior(publicAPI, model) { model.lastHandle.setVisible(true); } - if ( - worldCoords.length && - (model._isDragging || model.activeState === model.moveHandle) - ) { - model.activeState.setOrigin(worldCoords); + const isHandleMoving = + model._isDragging || model.activeState === model.moveHandle; + + if (worldCoords.length && isHandleMoving) { + const curOrigin = model.activeState.getOrigin(); + if (curOrigin) { + model.activeState.setOrigin(add(curOrigin, worldDelta, [])); + } else { + model.activeState.setOrigin(worldCoords); + } if (model._isDragging) { model.draggedPoint = true; }