Skip to content

Commit

Permalink
feat(widgets): smooth drag behavior
Browse files Browse the repository at this point in the history
Adds a worldDelta property to the return value of
manipulator.handleEvent.
  • Loading branch information
floryst committed Jun 5, 2024
1 parent f4923ff commit b19db90
Show file tree
Hide file tree
Showing 18 changed files with 258 additions and 170 deletions.
30 changes: 28 additions & 2 deletions Sources/Widgets/Manipulators/AbstractManipulator/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vector3>, worldDirection?: Matrix3x3 };
handleEvent(callData: any, glRenderWindow: vtkOpenGLRenderWindow): {
worldCoords: Nullable<Vector3>,
worldDelta: Vector3,
worldDirection?: Matrix3x3,
};

/* ------------------------------------------------------------------- */

Expand Down
24 changes: 24 additions & 0 deletions Sources/Widgets/Manipulators/AbstractManipulator/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import macro from 'vtk.js/Sources/macros';
import { subtract } from 'vtk.js/Sources/Common/Core/Math';

// ----------------------------------------------------------------------------
// vtkAbstractManipulator methods
Expand All @@ -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)
Expand All @@ -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,
};
};
}

// ----------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions Sources/Widgets/Manipulators/CPRManipulator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down
21 changes: 11 additions & 10 deletions Sources/Widgets/Manipulators/LineManipulator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
),
});
}

// ----------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions Sources/Widgets/Manipulators/PickerManipulator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ function vtkPickerManipulator(publicAPI, model) {
} else {
model.position = null;
}
return {
return model._addWorldDeltas({
worldCoords: model.position,
};
});
};
}

Expand Down
21 changes: 11 additions & 10 deletions Sources/Widgets/Manipulators/PlaneManipulator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
),
});
}

// ----------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion Sources/Widgets/Manipulators/TrackballManipulator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
30 changes: 18 additions & 12 deletions Sources/Widgets/Widgets3D/AngleWidget/behavior.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
26 changes: 17 additions & 9 deletions Sources/Widgets/Widgets3D/ImageCroppingWidget/behavior.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import macro from 'vtk.js/Sources/macros';
import { add } from 'vtk.js/Sources/Common/Core/Math';

import {
AXES,
Expand All @@ -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() ||
Expand All @@ -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);
Expand Down Expand Up @@ -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') {
Expand All @@ -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') {
Expand All @@ -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);
Expand All @@ -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;
Expand Down
28 changes: 22 additions & 6 deletions Sources/Widgets/Widgets3D/ImplicitPlaneWidget/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
};

Expand All @@ -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);
}
};

Expand Down
Loading

0 comments on commit b19db90

Please sign in to comment.