Skip to content

Commit a47d9bb

Browse files
committed
feat(tools): hover over tool fill shows tooltip
1 parent f41e8a1 commit a47d9bb

File tree

6 files changed

+262
-12
lines changed

6 files changed

+262
-12
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import macro from '@kitware/vtk.js/macros';
2+
import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
3+
import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
4+
import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData';
5+
import vtkWidgetRepresentation from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation';
6+
import { Behavior } from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation/Constants';
7+
8+
function vtkPolygonFillRepresentation(publicAPI, model) {
9+
model.classHierarchy.push('vtkPolygonFillRepresentation');
10+
11+
model.internalPolyData = vtkPolyData.newInstance();
12+
13+
model._pipeline = {
14+
source: publicAPI,
15+
mapper: vtkMapper.newInstance(),
16+
actor: vtkActor.newInstance({ pickable: true }),
17+
};
18+
19+
model._pipeline.actor.setMapper(model._pipeline.mapper);
20+
21+
vtkWidgetRepresentation.connectPipeline(model._pipeline);
22+
publicAPI.addActor(model._pipeline.actor);
23+
24+
publicAPI.getSelectedState = () => model.inputData[0];
25+
26+
publicAPI.requestData = (inData, outData) => {
27+
const states = publicAPI.getRepresentationStates(inData[0]);
28+
29+
if (states.length < 3) {
30+
model.internalPolyData.getPoints().setData(new Float32Array(0));
31+
model.internalPolyData.getPolys().setData(new Uint32Array(0));
32+
model.internalPolyData.modified();
33+
outData[0] = model.internalPolyData;
34+
return;
35+
}
36+
37+
const points = new Float32Array(states.length * 3);
38+
states.forEach((state, i) => {
39+
const origin = state.getOrigin();
40+
points[i * 3] = origin[0];
41+
points[i * 3 + 1] = origin[1];
42+
points[i * 3 + 2] = origin[2];
43+
});
44+
45+
const polys = new Uint32Array(states.length + 1);
46+
polys[0] = states.length;
47+
for (let i = 0; i < states.length; i++) {
48+
polys[i + 1] = i;
49+
}
50+
51+
model.internalPolyData.getPoints().setData(points);
52+
model.internalPolyData.getPolys().setData(polys);
53+
model.internalPolyData.modified();
54+
55+
outData[0] = model.internalPolyData;
56+
};
57+
}
58+
59+
const DEFAULT_VALUES = {
60+
behavior: Behavior.HANDLE,
61+
};
62+
63+
export function extend(publicAPI, model, initialValues = {}) {
64+
Object.assign(model, DEFAULT_VALUES, initialValues);
65+
vtkWidgetRepresentation.extend(publicAPI, model, initialValues);
66+
macro.get(publicAPI, model._pipeline, ['mapper', 'actor']);
67+
vtkPolygonFillRepresentation(publicAPI, model);
68+
}
69+
70+
export const newInstance = macro.newInstance(
71+
extend,
72+
'vtkPolygonFillRepresentation'
73+
);
74+
75+
export default { newInstance, extend };

src/vtk/PolygonWidget/behavior.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ export default function widgetBehavior(publicAPI: any, model: any) {
5151
return overSegment && !overUnselectedHandle;
5252
};
5353

54+
// Check if mouse is over fill representation (for hover but not interaction)
55+
const checkOverFill = () => {
56+
const selections = model._widgetManager.getSelections();
57+
return (
58+
model.representations[2] &&
59+
selections?.[0]?.getProperties().prop ===
60+
model.representations[2].getActors()[0]
61+
);
62+
};
63+
5464
// support setting per-view widget manipulators
5565
macro.setGet(publicAPI, model, ['manipulator']);
5666

@@ -156,17 +166,23 @@ export default function widgetBehavior(publicAPI: any, model: any) {
156166
// --------------------------------------------------------------------------
157167

158168
publicAPI.handleLeftButtonPress = (event: vtkMouseEvent) => {
169+
if (!model.manipulator) {
170+
return macro.VOID;
171+
}
172+
159173
const activeWidget = model._widgetManager.getActiveWidget();
160174

175+
// If not placing and hovering over another widget, don't consume event.
161176
if (
162-
!model.manipulator ||
163-
// If hovering over another widget, don't consume event.
164-
(activeWidget && activeWidget !== publicAPI)
177+
!model.widgetState.getPlacing() &&
178+
activeWidget &&
179+
activeWidget !== publicAPI
165180
) {
166181
return macro.VOID;
167182
}
168183

169-
if (checkOverSegment()) {
184+
// Ignore clicks on this widget's segment or fill
185+
if (checkOverSegment() || checkOverFill()) {
170186
return macro.VOID;
171187
}
172188

@@ -194,7 +210,12 @@ export default function widgetBehavior(publicAPI: any, model: any) {
194210
return macro.EVENT_ABORT;
195211
}
196212

197-
if (model.activeState?.getActive() && model.pickable && model.dragable) {
213+
if (
214+
model.activeState?.getActive() &&
215+
model.activeState?.setOrigin &&
216+
model.pickable &&
217+
model.dragable
218+
) {
198219
setDragging(true);
199220
model._apiSpecificRenderWindow.setCursor('grabbing');
200221
model._interactor.requestAnimation(publicAPI);
@@ -233,7 +254,7 @@ export default function widgetBehavior(publicAPI: any, model: any) {
233254

234255
publicAPI.invokeHoverEvent({
235256
...event,
236-
hovering: !!model.activeState,
257+
hovering: !!model.activeState || checkOverFill(),
237258
});
238259

239260
return macro.VOID;

src/vtk/PolygonWidget/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import vtkPlanePointManipulator from '@kitware/vtk.js/Widgets/Manipulators/Plane
44
import vtkSphereHandleRepresentation from '@kitware/vtk.js/Widgets/Representations/SphereHandleRepresentation';
55
import { Behavior } from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation/Constants';
66
import vtkLineGlyphRepresentation from '@/src/vtk/LineGlyphRepresentation';
7+
import vtkPolygonFillRepresentation from '@/src/vtk/PolygonFillRepresentation';
78

89
import widgetBehavior from './behavior';
910
import stateGenerator, { HandlesLabel, MoveHandleLabel } from './state';
@@ -34,6 +35,13 @@ function vtkPolygonWidget(publicAPI, model) {
3435
behavior: Behavior.HANDLE, // make pickable even if not visible
3536
},
3637
},
38+
{
39+
builder: vtkPolygonFillRepresentation,
40+
labels: [HandlesLabel],
41+
initialValues: {
42+
behavior: Behavior.HANDLE,
43+
},
44+
},
3745
];
3846

3947
// Default manipulator
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import macro from '@kitware/vtk.js/macros';
2+
import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox';
3+
import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
4+
import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
5+
import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData';
6+
import vtkWidgetRepresentation from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation';
7+
import { Behavior } from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation/Constants';
8+
import * as vtkMath from '@kitware/vtk.js/Common/Core/Math';
9+
10+
function vtkRectangleFillRepresentation(publicAPI, model) {
11+
model.classHierarchy.push('vtkRectangleFillRepresentation');
12+
13+
model.internalPolyData = vtkPolyData.newInstance();
14+
15+
model._pipeline = {
16+
source: publicAPI,
17+
mapper: vtkMapper.newInstance(),
18+
actor: vtkActor.newInstance({ pickable: true }),
19+
};
20+
21+
model._pipeline.actor.setMapper(model._pipeline.mapper);
22+
23+
vtkWidgetRepresentation.connectPipeline(model._pipeline);
24+
publicAPI.addActor(model._pipeline.actor);
25+
26+
publicAPI.getSelectedState = () => model.inputData[0];
27+
28+
const superBehavior = model.widgetAPI.behavior;
29+
let behaviorModel;
30+
model.widgetAPI.behavior = (publicAPIy, bModel) => {
31+
behaviorModel = bModel;
32+
return superBehavior(publicAPIy, bModel);
33+
};
34+
35+
publicAPI.requestData = (inData, outData) => {
36+
const states = publicAPI.getRepresentationStates(inData[0]);
37+
38+
if (states.length < 2 || !behaviorModel) {
39+
model.internalPolyData.getPoints().setData(new Float32Array(0));
40+
model.internalPolyData.getPolys().setData(new Uint32Array(0));
41+
model.internalPolyData.modified();
42+
outData[0] = model.internalPolyData;
43+
return;
44+
}
45+
46+
const box = [...vtkBoundingBox.INIT_BOUNDS];
47+
states.forEach((handle) => {
48+
const displayPos = behaviorModel._apiSpecificRenderWindow.worldToDisplay(
49+
...handle.getOrigin(),
50+
behaviorModel._renderer
51+
);
52+
vtkBoundingBox.addPoint(box, ...displayPos);
53+
});
54+
const corners = vtkBoundingBox.getCorners(box, []);
55+
56+
const corners2D = corners.reduce((outCorners, corner) => {
57+
const duplicate = outCorners.some((outCorner) =>
58+
vtkMath.areEquals(outCorner, corner)
59+
);
60+
if (!duplicate) {
61+
outCorners.push(corner);
62+
}
63+
return outCorners;
64+
}, []);
65+
66+
if (corners2D.length < 4) {
67+
model.internalPolyData.getPoints().setData(new Float32Array(0));
68+
model.internalPolyData.getPolys().setData(new Uint32Array(0));
69+
model.internalPolyData.modified();
70+
outData[0] = model.internalPolyData;
71+
return;
72+
}
73+
74+
const worldCorners = [0, 2, 3, 1].map((cornerIndex) =>
75+
behaviorModel._apiSpecificRenderWindow.displayToWorld(
76+
...corners2D[cornerIndex],
77+
behaviorModel._renderer
78+
)
79+
);
80+
81+
const points = new Float32Array(12);
82+
worldCorners.forEach((corner, i) => {
83+
points[i * 3] = corner[0];
84+
points[i * 3 + 1] = corner[1];
85+
points[i * 3 + 2] = corner[2];
86+
});
87+
88+
const polys = new Uint32Array([4, 0, 1, 2, 3]);
89+
90+
model.internalPolyData.getPoints().setData(points);
91+
model.internalPolyData.getPolys().setData(polys);
92+
model.internalPolyData.modified();
93+
94+
outData[0] = model.internalPolyData;
95+
};
96+
}
97+
98+
const DEFAULT_VALUES = {
99+
behavior: Behavior.HANDLE,
100+
widgetAPI: null,
101+
};
102+
103+
export function extend(publicAPI, model, initialValues = {}) {
104+
Object.assign(model, DEFAULT_VALUES, initialValues);
105+
vtkWidgetRepresentation.extend(publicAPI, model, initialValues);
106+
macro.setGet(publicAPI, model, ['widgetAPI']);
107+
macro.get(publicAPI, model._pipeline, ['mapper', 'actor']);
108+
vtkRectangleFillRepresentation(publicAPI, model);
109+
}
110+
111+
export const newInstance = macro.newInstance(
112+
extend,
113+
'vtkRectangleFillRepresentation'
114+
);
115+
116+
export default { newInstance, extend };

src/vtk/RectangleWidget/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import macro from '@kitware/vtk.js/macro';
2+
import { Behavior } from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation/Constants';
23

34
import { AnnotationToolType } from '@/src/store/tools/types';
45
import vtkRulerWidget from '../RulerWidget';
56
import vtkRectangleLineRepresentation from './RectangleLineRepresentation';
7+
import vtkRectangleFillRepresentation from './RectangleFillRepresentation';
8+
import { PointsLabel } from '../RulerWidget/state';
69

710
export { InteractionState } from '../RulerWidget/behavior';
811

@@ -22,6 +25,14 @@ function vtkRectangleWidget(publicAPI, model) {
2225
...reps[1].initialValues,
2326
widgetAPI: model,
2427
};
28+
reps.push({
29+
builder: vtkRectangleFillRepresentation,
30+
labels: [PointsLabel],
31+
initialValues: {
32+
behavior: Behavior.HANDLE,
33+
widgetAPI: model,
34+
},
35+
});
2536
return reps;
2637
};
2738
}

src/vtk/RulerWidget/behavior.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ export default function widgetBehavior(publicAPI: any, model: any) {
6666
return overSegment;
6767
};
6868

69+
// Check if mouse is over fill representation (for hover but not interaction)
70+
const checkOverFill = () => {
71+
const selections = model._widgetManager.getSelections();
72+
return (
73+
model.representations[2] &&
74+
selections?.[0]?.getProperties().prop ===
75+
model.representations[2].getActors()[0]
76+
);
77+
};
78+
6979
const getWorldCoords = computeWorldCoords(model);
7080

7181
/**
@@ -76,16 +86,26 @@ export default function widgetBehavior(publicAPI: any, model: any) {
7686
return macro.VOID;
7787
}
7888

89+
// Ignore clicks on fill - let them pass through
90+
if (checkOverFill()) {
91+
return macro.VOID;
92+
}
93+
7994
// turns off hover while dragging
8095
publicAPI.invokeHoverEvent({
8196
...eventData,
8297
hovering: false,
8398
});
8499

85-
// This ruler widget is passive, so if another widget
86-
// is active, we don't do anything.
100+
const intState = publicAPI.getInteractionState();
101+
102+
// If not placing and another widget is active, don't consume event.
87103
const activeWidget = model._widgetManager.getActiveWidget();
88-
if (activeWidget && activeWidget !== publicAPI) {
104+
if (
105+
intState === InteractionState.Select &&
106+
activeWidget &&
107+
activeWidget !== publicAPI
108+
) {
89109
return macro.VOID;
90110
}
91111

@@ -94,8 +114,6 @@ export default function widgetBehavior(publicAPI: any, model: any) {
94114
return macro.VOID;
95115
}
96116

97-
const intState = publicAPI.getInteractionState();
98-
99117
if (intState === InteractionState.PlacingFirst) {
100118
publicAPI.setFirstPoint(worldCoords);
101119
publicAPI.setSecondPoint(worldCoords);
@@ -123,6 +141,7 @@ export default function widgetBehavior(publicAPI: any, model: any) {
123141
// dragging
124142
if (
125143
model.activeState?.getActive() &&
144+
model.activeState?.setOrigin &&
126145
model.pickable &&
127146
!checkOverSegment()
128147
) {
@@ -167,7 +186,7 @@ export default function widgetBehavior(publicAPI: any, model: any) {
167186

168187
publicAPI.invokeHoverEvent({
169188
...eventData,
170-
hovering: !!model.activeState,
189+
hovering: !!model.activeState || checkOverFill(),
171190
});
172191

173192
return macro.VOID;

0 commit comments

Comments
 (0)