Skip to content

Commit b19db90

Browse files
committed
feat(widgets): smooth drag behavior
Adds a worldDelta property to the return value of manipulator.handleEvent.
1 parent f4923ff commit b19db90

File tree

18 files changed

+258
-170
lines changed

18 files changed

+258
-170
lines changed

Sources/Widgets/Manipulators/AbstractManipulator/index.d.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,37 @@ export interface vtkAbstractManipulator extends vtkObject {
4949
setUseCameraNormal(useCameraNormal: boolean): boolean;
5050

5151
/**
52-
*
52+
* Processes a vtkRenderWindowInteractor event into 3D world positional info.
53+
*
54+
* Returns an object containing:
55+
* - worldCoords: a 3D coordinate corresponding to the 2D event.
56+
* - worldDelta: a 3D position delta between the current and the previous call to handleEvent.
57+
* - worldDirection: a 3D directional vector. Only on select manipulators.
58+
*
59+
* worldCoords can be null if the pointer event enters an invalid manipulator region. For example,
60+
* the PickerManipulator returns null when the pointer event is off of the picked geometry.
61+
*
62+
* worldDelta only makes sense between two calls of `handleEvent`. In a queue of `handleEvent` calls,
63+
* the i-th call returns the delta between the i-th worldCoords and the (i-1)-th worldCoords.
64+
* Thus, calling `handleEvent` is necessary for maintaining a valid worldDelta even when the return
65+
* value is ignored.
66+
*
67+
* There are three cases where worldDelta needs to handle null events:
68+
* 1. the first call to `handleEvent`, since there is no previously cached event position.
69+
* worldDelta is set to [0, 0, 0].
70+
* 2. if the current `handleEvent` call returns a null worldCoords. worldDelta is set to [0, 0, 0].
71+
* 3. if the previous `handleEvent` call returned a null worldCoords. In this case, worldDelta is the
72+
* delta between the current worldCoords and the previous non-null worldCoords, referring to the
73+
* previous 2 cases when applicable.
74+
*
5375
* @param callData
5476
* @param glRenderWindow
5577
*/
56-
handleEvent(callData: any, glRenderWindow: vtkOpenGLRenderWindow): { worldCoords: Nullable<Vector3>, worldDirection?: Matrix3x3 };
78+
handleEvent(callData: any, glRenderWindow: vtkOpenGLRenderWindow): {
79+
worldCoords: Nullable<Vector3>,
80+
worldDelta: Vector3,
81+
worldDirection?: Matrix3x3,
82+
};
5783

5884
/* ------------------------------------------------------------------- */
5985

Sources/Widgets/Manipulators/AbstractManipulator/index.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import macro from 'vtk.js/Sources/macros';
2+
import { subtract } from 'vtk.js/Sources/Common/Core/Math';
23

34
// ----------------------------------------------------------------------------
45
// vtkAbstractManipulator methods
@@ -8,6 +9,8 @@ function vtkAbstractManipulator(publicAPI, model) {
89
// Set our className
910
model.classHierarchy.push('vtkAbstractManipulator');
1011

12+
model._prevWorldCoords = [];
13+
1114
publicAPI.getOrigin = (callData) => {
1215
if (model.userOrigin) return model.userOrigin;
1316
if (model.useCameraFocalPoint)
@@ -27,6 +30,27 @@ function vtkAbstractManipulator(publicAPI, model) {
2730
if (model.widgetNormal) return model.widgetNormal;
2831
return [0, 0, 1];
2932
};
33+
34+
model._computeDeltaFromPrevCoords = (curWorldCoords) => {
35+
if (!model._prevWorldCoords?.length || !curWorldCoords?.length)
36+
return [0, 0, 0];
37+
return subtract(curWorldCoords, model._prevWorldCoords, []);
38+
};
39+
40+
model._addWorldDeltas = (manipulatorResults) => {
41+
const { worldCoords: curWorldCoords } = manipulatorResults;
42+
const worldDelta = model._computeDeltaFromPrevCoords(curWorldCoords);
43+
if (curWorldCoords) model._prevWorldCoords = curWorldCoords;
44+
45+
const deltas = {
46+
worldDelta,
47+
};
48+
49+
return {
50+
...manipulatorResults,
51+
...deltas,
52+
};
53+
};
3054
}
3155

3256
// ----------------------------------------------------------------------------

Sources/Widgets/Manipulators/CPRManipulator/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function vtkCPRManipulator(publicAPI, model) {
2929
publicAPI.handleEvent = (callData, glRenderWindow) => {
3030
const mapper = model.cprActor?.getMapper();
3131
if (!mapper) {
32-
return { worldCoords: null };
32+
return model._addWorldDeltas({ worldCoords: null });
3333
}
3434

3535
// Get normal and origin of the picking plane from the actor matrix
@@ -59,7 +59,7 @@ function vtkCPRManipulator(publicAPI, model) {
5959
const height = mapper.getHeight();
6060
const distance = height - modelPlanePicking[1];
6161

62-
return publicAPI.distanceEvent(distance);
62+
return model._addWorldDeltas(publicAPI.distanceEvent(distance));
6363
};
6464

6565
publicAPI.distanceEvent = (distance) => {

Sources/Widgets/Manipulators/LineManipulator/index.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,17 @@ function vtkLineManipulator(publicAPI, model) {
5555
// Set our className
5656
model.classHierarchy.push('vtkLineManipulator');
5757

58-
publicAPI.handleEvent = (callData, glRenderWindow) => ({
59-
worldCoords: projectDisplayToLine(
60-
callData.position.x,
61-
callData.position.y,
62-
publicAPI.getOrigin(callData),
63-
publicAPI.getNormal(callData),
64-
callData.pokedRenderer,
65-
glRenderWindow
66-
),
67-
});
58+
publicAPI.handleEvent = (callData, glRenderWindow) =>
59+
model._addWorldDeltas({
60+
worldCoords: projectDisplayToLine(
61+
callData.position.x,
62+
callData.position.y,
63+
publicAPI.getOrigin(callData),
64+
publicAPI.getNormal(callData),
65+
callData.pokedRenderer,
66+
glRenderWindow
67+
),
68+
});
6869
}
6970

7071
// ----------------------------------------------------------------------------

Sources/Widgets/Manipulators/PickerManipulator/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ function vtkPickerManipulator(publicAPI, model) {
1919
} else {
2020
model.position = null;
2121
}
22-
return {
22+
return model._addWorldDeltas({
2323
worldCoords: model.position,
24-
};
24+
});
2525
};
2626
}
2727

Sources/Widgets/Manipulators/PlaneManipulator/index.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,17 @@ function vtkPlaneManipulator(publicAPI, model) {
2525
// Set our className
2626
model.classHierarchy.push('vtkPlaneManipulator');
2727

28-
publicAPI.handleEvent = (callData, glRenderWindow) => ({
29-
worldCoords: intersectDisplayWithPlane(
30-
callData.position.x,
31-
callData.position.y,
32-
publicAPI.getOrigin(callData),
33-
publicAPI.getNormal(callData),
34-
callData.pokedRenderer,
35-
glRenderWindow
36-
),
37-
});
28+
publicAPI.handleEvent = (callData, glRenderWindow) =>
29+
model._addWorldDeltas({
30+
worldCoords: intersectDisplayWithPlane(
31+
callData.position.x,
32+
callData.position.y,
33+
publicAPI.getOrigin(callData),
34+
publicAPI.getNormal(callData),
35+
callData.pokedRenderer,
36+
glRenderWindow
37+
),
38+
});
3839
}
3940

4041
// ----------------------------------------------------------------------------

Sources/Widgets/Manipulators/TrackballManipulator/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function vtkTrackballManipulator(publicAPI, model) {
7171
);
7272
prevX = callData.position.x;
7373
prevY = callData.position.y;
74-
return { worldCoords: newDirection };
74+
return model._addWorldDeltas({ worldCoords: newDirection });
7575
};
7676

7777
publicAPI.reset = (callData) => {

Sources/Widgets/Widgets3D/AngleWidget/behavior.js

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import macro from 'vtk.js/Sources/macros';
2+
import { add } from 'vtk.js/Sources/Common/Core/Math';
23
import vtkPointPicker from 'vtk.js/Sources/Rendering/Core/PointPicker';
34

45
const MAX_POINTS = 3;
@@ -43,15 +44,16 @@ export default function widgetBehavior(publicAPI, model) {
4344
picker.setPickList(publicAPI.getNestedProps());
4445
const manipulator =
4546
model.activeState?.getManipulator?.() ?? model.manipulator;
47+
const { worldCoords } = manipulator.handleEvent(
48+
e,
49+
model._apiSpecificRenderWindow
50+
);
51+
4652
if (
4753
model.activeState === model.widgetState.getMoveHandle() &&
4854
model.widgetState.getHandleList().length < MAX_POINTS &&
4955
manipulator
5056
) {
51-
const { worldCoords } = manipulator.handleEvent(
52-
e,
53-
model._apiSpecificRenderWindow
54-
);
5557
// Commit handle to location
5658
const moveHandle = model.widgetState.getMoveHandle();
5759
moveHandle.setOrigin(...worldCoords);
@@ -85,18 +87,22 @@ export default function widgetBehavior(publicAPI, model) {
8587
model.activeState.getActive() &&
8688
!ignoreKey(callData)
8789
) {
88-
const { worldCoords } = manipulator.handleEvent(
90+
const { worldCoords, worldDelta } = manipulator.handleEvent(
8991
callData,
9092
model._apiSpecificRenderWindow
9193
);
9294

93-
if (
94-
worldCoords.length &&
95-
(model.activeState === model.widgetState.getMoveHandle() ||
96-
model._isDragging) &&
97-
model.activeState.setOrigin // e.g. the line is pickable but not draggable
98-
) {
99-
model.activeState.setOrigin(worldCoords);
95+
const isHandleMoving =
96+
model.activeState === model.widgetState.getMoveHandle() ||
97+
model._isDragging;
98+
99+
if (isHandleMoving && worldCoords.length && model.activeState.setOrigin) {
100+
const curOrigin = model.activeState.getOrigin();
101+
if (curOrigin) {
102+
model.activeState.setOrigin(add(curOrigin, worldDelta, []));
103+
} else {
104+
model.activeState.setOrigin(worldCoords);
105+
}
100106
publicAPI.invokeInteractionEvent();
101107
return macro.EVENT_ABORT;
102108
}

Sources/Widgets/Widgets3D/ImageCroppingWidget/behavior.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import macro from 'vtk.js/Sources/macros';
2+
import { add } from 'vtk.js/Sources/Common/Core/Math';
23

34
import {
45
AXES,
@@ -14,7 +15,7 @@ export default function widgetBehavior(publicAPI, model) {
1415
publicAPI.setDisplayCallback = (callback) =>
1516
model.representations[0].setDisplayCallback(callback);
1617

17-
publicAPI.handleLeftButtonPress = () => {
18+
publicAPI.handleLeftButtonPress = (callData) => {
1819
if (
1920
!model.activeState ||
2021
!model.activeState.getActive() ||
@@ -23,6 +24,10 @@ export default function widgetBehavior(publicAPI, model) {
2324
return macro.VOID;
2425
}
2526
if (model.dragable) {
27+
// updates worldDelta
28+
model.activeState
29+
.getManipulator()
30+
.handleEvent(callData, model._apiSpecificRenderWindow);
2631
model._isDragging = true;
2732
model._apiSpecificRenderWindow.setCursor('grabbing');
2833
model._interactor.requestAnimation(publicAPI);
@@ -66,13 +71,14 @@ export default function widgetBehavior(publicAPI, model) {
6671
const indexToWorldT = model.widgetState.getIndexToWorldT();
6772

6873
let worldCoords = [];
74+
let worldDelta = [];
6975

7076
if (type === 'corners') {
7177
// manipulator should be a plane manipulator
72-
worldCoords = manipulator.handleEvent(
78+
({ worldCoords, worldDelta } = manipulator.handleEvent(
7379
callData,
7480
model._apiSpecificRenderWindow
75-
).worldCoords;
81+
));
7682
}
7783

7884
if (type === 'faces') {
@@ -83,10 +89,10 @@ export default function widgetBehavior(publicAPI, model) {
8389
manipulator.setHandleNormal(
8490
calculateDirection(model.activeState.getOrigin(), worldCenter)
8591
);
86-
worldCoords = manipulator.handleEvent(
92+
({ worldCoords, worldDelta } = manipulator.handleEvent(
8793
callData,
8894
model._apiSpecificRenderWindow
89-
).worldCoords;
95+
));
9096
}
9197

9298
if (type === 'edges') {
@@ -100,13 +106,13 @@ export default function widgetBehavior(publicAPI, model) {
100106
manipulator.setHandleNormal(
101107
calculateDirection(handle.getOrigin(), worldCenter)
102108
);
103-
worldCoords = manipulator.handleEvent(
109+
({ worldCoords, worldDelta } = manipulator.handleEvent(
104110
callData,
105111
model._apiSpecificRenderWindow
106-
).worldCoords;
112+
));
107113
}
108114

109-
if (worldCoords.length) {
115+
if (worldCoords.length && worldDelta.length) {
110116
// transform worldCoords to indexCoords, and then update the croppingPlanes() state with setPlanes().
111117
const worldToIndexT = model.widgetState.getWorldToIndexT();
112118
const indexCoords = transformVec3(worldCoords, worldToIndexT);
@@ -119,7 +125,9 @@ export default function widgetBehavior(publicAPI, model) {
119125
}
120126
}
121127

122-
model.activeState.setOrigin(...worldCoords);
128+
model.activeState.setOrigin(
129+
add(model.activeState.getOrigin(), worldDelta, [])
130+
);
123131
model.widgetState.getCroppingPlanes().setPlanes(...planes);
124132

125133
return macro.EVENT_ABORT;

Sources/Widgets/Widgets3D/ImplicitPlaneWidget/index.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import macro from 'vtk.js/Sources/macros';
2+
import { add } from 'vtk.js/Sources/Common/Core/Math';
23
import vtkAbstractWidgetFactory from 'vtk.js/Sources/Widgets/Core/AbstractWidgetFactory';
34
import vtkImplicitPlaneRepresentation from 'vtk.js/Sources/Widgets/Representations/ImplicitPlaneRepresentation';
45
import vtkLineManipulator from 'vtk.js/Sources/Widgets/Manipulators/LineManipulator';
@@ -14,6 +15,8 @@ import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';
1415
function widgetBehavior(publicAPI, model) {
1516
model.classHierarchy.push('vtkPlaneWidget');
1617
model._isDragging = false;
18+
// used to track the constrained widget position
19+
model._draggingWidgetOrigin = [0, 0, 0];
1720

1821
publicAPI.setDisplayCallback = (callback) =>
1922
model.representations[0].setDisplayCallback(callback);
@@ -48,7 +51,15 @@ function widgetBehavior(publicAPI, model) {
4851
model.planeManipulator.setWidgetOrigin(model.widgetState.getOrigin());
4952
model.trackballManipulator.reset(callData); // setup trackball delta
5053

54+
// updates worldDelta
55+
model.lineManipulator.handleEvent(callData, model._apiSpecificRenderWindow);
56+
model.planeManipulator.handleEvent(
57+
callData,
58+
model._apiSpecificRenderWindow
59+
);
60+
5161
if (model.dragable) {
62+
model._draggingWidgetOrigin = model.widgetState.getOrigin();
5263
model._isDragging = true;
5364
model._apiSpecificRenderWindow.setCursor('grabbing');
5465
model._interactor.requestAnimation(publicAPI);
@@ -100,13 +111,16 @@ function widgetBehavior(publicAPI, model) {
100111

101112
publicAPI.updateFromOrigin = (callData) => {
102113
model.planeManipulator.setWidgetNormal(model.widgetState.getNormal());
103-
const { worldCoords } = model.planeManipulator.handleEvent(
114+
const { worldCoords, worldDelta } = model.planeManipulator.handleEvent(
104115
callData,
105116
model._apiSpecificRenderWindow
106117
);
107118

108-
if (model.widgetState.containsPoint(worldCoords)) {
109-
model.activeState.setOrigin(worldCoords);
119+
add(model._draggingWidgetOrigin, worldDelta, model._draggingWidgetOrigin);
120+
121+
// test containment of interaction coords
122+
if (model.widgetState.containsPoint(...worldCoords)) {
123+
model.activeState.setOrigin(model._draggingWidgetOrigin);
110124
}
111125
};
112126

@@ -115,13 +129,15 @@ function widgetBehavior(publicAPI, model) {
115129
publicAPI.updateFromPlane = (callData) => {
116130
// Move origin along normal axis
117131
model.lineManipulator.setWidgetNormal(model.activeState.getNormal());
118-
const { worldCoords } = model.lineManipulator.handleEvent(
132+
const { worldDelta } = model.lineManipulator.handleEvent(
119133
callData,
120134
model._apiSpecificRenderWindow
121135
);
122136

123-
if (model.widgetState.containsPoint(...worldCoords)) {
124-
model.activeState.setOrigin(worldCoords);
137+
add(model._draggingWidgetOrigin, worldDelta, model._draggingWidgetOrigin);
138+
139+
if (model.widgetState.containsPoint(...model._draggingWidgetOrigin)) {
140+
model.activeState.setOrigin(model._draggingWidgetOrigin);
125141
}
126142
};
127143

0 commit comments

Comments
 (0)