diff --git a/src/components/ToolStrip.vue b/src/components/ToolStrip.vue
index 73bfdba83..4d19e6d4b 100644
--- a/src/components/ToolStrip.vue
+++ b/src/components/ToolStrip.vue
@@ -48,6 +48,15 @@
/>
+
+
+
/* global ToolID:readonly */
-import { shallowReactive } from 'vue';
+import { computed, shallowReactive } from 'vue';
import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool';
import { ContextMenuEvent } from '@/src/types/annotation-tool';
import { WidgetAction } from '@/src/vtk/ToolWidgetUtils/utils';
@@ -29,6 +29,10 @@ defineExpose({
open,
});
+const tool = computed(() => {
+ return props.toolStore.toolByID[contextMenu.forToolID];
+});
+
const deleteToolFromContextMenu = () => {
props.toolStore.removeTool(contextMenu.forToolID);
};
@@ -52,20 +56,56 @@ const hideToolFromContextMenu = () => {
close-on-content-click
>
+
+
+
+
+
+ {{ tool.labelName }}
+
+
+
+
+
+
+
+ mdi-eye
+
Hide
+
+
+ mdi-delete
+
Delete Annotation
-
+
+
+
+
+
{{ action.name }}
+
+
diff --git a/src/components/tools/AnnotationInfo.vue b/src/components/tools/AnnotationInfo.vue
new file mode 100644
index 000000000..ca116eca6
--- /dev/null
+++ b/src/components/tools/AnnotationInfo.vue
@@ -0,0 +1,63 @@
+
+
+
+
+ {{ label }}
+
+
+
+
diff --git a/src/components/tools/BoundingRectangle.vue b/src/components/tools/BoundingRectangle.vue
new file mode 100644
index 000000000..65d786a5e
--- /dev/null
+++ b/src/components/tools/BoundingRectangle.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
diff --git a/src/components/tools/polygon/PolygonSVG2D.vue b/src/components/tools/polygon/PolygonSVG2D.vue
index bcc5e6d82..b2625321b 100644
--- a/src/components/tools/polygon/PolygonSVG2D.vue
+++ b/src/components/tools/polygon/PolygonSVG2D.vue
@@ -1,6 +1,6 @@
-
+
import { useResizeObserver } from '@/src/composables/useResizeObserver';
import { onVTKEvent } from '@/src/composables/onVTKEvent';
-import { ToolContainer } from '@/src/constants';
+import { ANNOTATION_TOOL_HANDLE_RADIUS, ToolContainer } from '@/src/constants';
import { useViewStore } from '@/src/store/views';
import { worldToSVG } from '@/src/utils/vtk-helpers';
import vtkLPSView2DProxy from '@/src/vtk/LPSView2DProxy';
@@ -38,8 +38,8 @@ import {
inject,
} from 'vue';
-const POINT_RADIUS = 10;
-const FINISHABLE_POINT_RADIUS = 16;
+const POINT_RADIUS = ANNOTATION_TOOL_HANDLE_RADIUS;
+const FINISHABLE_POINT_RADIUS = POINT_RADIUS + 6;
export default defineComponent({
props: {
diff --git a/src/components/tools/polygon/PolygonTool.vue b/src/components/tools/polygon/PolygonTool.vue
index 7284e495e..89c8c4fd8 100644
--- a/src/components/tools/polygon/PolygonTool.vue
+++ b/src/components/tools/polygon/PolygonTool.vue
@@ -1,6 +1,7 @@
@@ -43,8 +46,11 @@ import { PolygonID } from '@/src/types/polygon';
import {
useContextMenu,
useCurrentTools,
+ useHover,
} from '@/src/composables/annotationTool';
import AnnotationContextMenu from '@/src/components/tools/AnnotationContextMenu.vue';
+import AnnotationInfo from '@/src/components/tools/AnnotationInfo.vue';
+import BoundingRectangle from '@/src/components/tools/BoundingRectangle.vue';
import PolygonWidget2D from './PolygonWidget2D.vue';
type ToolID = PolygonID;
@@ -74,6 +80,8 @@ export default defineComponent({
components: {
PolygonWidget2D,
AnnotationContextMenu,
+ AnnotationInfo,
+ BoundingRectangle,
},
setup(props) {
const { viewDirection, currentSlice } = toRefs(props);
@@ -181,6 +189,14 @@ export default defineComponent({
const currentTools = useCurrentTools(activeToolStore, viewAxis);
+ const { onHover, overlayInfo } = useHover(currentTools, currentSlice);
+
+ const points = computed(() => {
+ if (!overlayInfo.value.visible) return [];
+ const tool = activeToolStore.toolByID[overlayInfo.value.toolID];
+ return tool.points;
+ });
+
return {
tools: currentTools,
placingToolID,
@@ -188,6 +204,9 @@ export default defineComponent({
contextMenu,
openContextMenu,
activeToolStore,
+ onHover,
+ overlayInfo,
+ points,
};
},
});
diff --git a/src/components/tools/polygon/PolygonWidget2D.vue b/src/components/tools/polygon/PolygonWidget2D.vue
index 9ebf50a68..2c25670a2 100644
--- a/src/components/tools/polygon/PolygonWidget2D.vue
+++ b/src/components/tools/polygon/PolygonWidget2D.vue
@@ -17,7 +17,10 @@ import { useCurrentImage } from '@/src/composables/useCurrentImage';
import { updatePlaneManipulatorFor2DView } from '@/src/utils/manipulators';
import { LPSAxisDir } from '@/src/types/lps';
import { onVTKEvent } from '@/src/composables/onVTKEvent';
-import { useRightClickContextMenu } from '@/src/composables/annotationTool';
+import {
+ useHoverEvent,
+ useRightClickContextMenu,
+} from '@/src/composables/annotationTool';
import { usePolygonStore as useStore } from '@/src/store/tools/polygons';
import { PolygonID as ToolID } from '@/src/types/polygon';
import vtkWidgetFactory, {
@@ -27,7 +30,7 @@ import SVG2DComponent from './PolygonSVG2D.vue';
export default defineComponent({
name: 'PolygonWidget2D',
- emits: ['placed', 'contextmenu'],
+ emits: ['placed', 'contextmenu', 'widgetHover'],
props: {
toolId: {
type: String,
@@ -103,6 +106,8 @@ export default defineComponent({
emit('placed');
});
+ useHoverEvent(emit, widget);
+
// --- right click handling --- //
useRightClickContextMenu(emit, widget);
diff --git a/src/components/tools/rectangle/RectangleSVG2D.vue b/src/components/tools/rectangle/RectangleSVG2D.vue
index f7e3dddab..e15cc8636 100644
--- a/src/components/tools/rectangle/RectangleSVG2D.vue
+++ b/src/components/tools/rectangle/RectangleSVG2D.vue
@@ -9,7 +9,7 @@
stroke-width="1"
:fill="fillColor"
/>
-
+
@@ -43,8 +46,11 @@ import { RectangleID } from '@/src/types/rectangle';
import {
useCurrentTools,
useContextMenu,
+ useHover,
} from '@/src/composables/annotationTool';
import AnnotationContextMenu from '@/src/components/tools/AnnotationContextMenu.vue';
+import AnnotationInfo from '@/src/components/tools/AnnotationInfo.vue';
+import BoundingRectangle from '@/src/components/tools/BoundingRectangle.vue';
import RectangleWidget2D from './RectangleWidget2D.vue';
type ToolID = RectangleID;
@@ -74,6 +80,8 @@ export default defineComponent({
components: {
RectangleWidget2D,
AnnotationContextMenu,
+ AnnotationInfo,
+ BoundingRectangle,
},
setup(props) {
const { viewDirection, currentSlice } = toRefs(props);
@@ -182,6 +190,14 @@ export default defineComponent({
const currentTools = useCurrentTools(activeToolStore, viewAxis);
+ const { onHover, overlayInfo } = useHover(currentTools, currentSlice);
+
+ const points = computed(() => {
+ if (!overlayInfo.value.visible) return [];
+ const tool = activeToolStore.toolByID[overlayInfo.value.toolID];
+ return [tool.firstPoint, tool.secondPoint];
+ });
+
return {
tools: currentTools,
placingToolID,
@@ -189,6 +205,9 @@ export default defineComponent({
contextMenu,
openContextMenu,
activeToolStore,
+ onHover,
+ overlayInfo,
+ points,
};
},
});
diff --git a/src/components/tools/rectangle/RectangleWidget2D.vue b/src/components/tools/rectangle/RectangleWidget2D.vue
index e73c6277c..1fda66452 100644
--- a/src/components/tools/rectangle/RectangleWidget2D.vue
+++ b/src/components/tools/rectangle/RectangleWidget2D.vue
@@ -26,7 +26,10 @@ import RectangleSVG2D from '@/src/components/tools/rectangle/RectangleSVG2D.vue'
import { vtkRulerWidgetPointState } from '@/src/vtk/RulerWidget';
import { watchOnce } from '@vueuse/core';
import { RectangleID } from '@/src/types/rectangle';
-import { useRightClickContextMenu } from '@/src/composables/annotationTool';
+import {
+ useRightClickContextMenu,
+ useHoverEvent,
+} from '@/src/composables/annotationTool';
const useStore = useRectangleStore;
const vtkWidgetFactory = vtkRectangleWidget;
@@ -37,7 +40,7 @@ const SVG2DComponent = RectangleSVG2D;
export default defineComponent({
name: 'RectangleWidget2D',
- emits: ['placed', 'contextmenu'],
+ emits: ['placed', 'contextmenu', 'widgetHover'],
props: {
toolId: {
type: String,
@@ -125,6 +128,8 @@ export default defineComponent({
emit('placed');
});
+ useHoverEvent(emit, widget);
+
// --- right click handling --- //
useRightClickContextMenu(emit, widget);
diff --git a/src/components/tools/ruler/RulerSVG2D.vue b/src/components/tools/ruler/RulerSVG2D.vue
index a2bea6bff..15033b9fa 100644
--- a/src/components/tools/ruler/RulerSVG2D.vue
+++ b/src/components/tools/ruler/RulerSVG2D.vue
@@ -9,7 +9,7 @@
:stroke="color"
stroke-width="1"
/>
-
+
@@ -43,8 +46,11 @@ import { vec3 } from 'gl-matrix';
import {
useContextMenu,
useCurrentTools,
+ useHover,
} from '@/src/composables/annotationTool';
import AnnotationContextMenu from '@/src/components/tools/AnnotationContextMenu.vue';
+import AnnotationInfo from '@/src/components/tools/AnnotationInfo.vue';
+import BoundingRectangle from '@/src/components/tools/BoundingRectangle.vue';
export default defineComponent({
name: 'RulerTool',
@@ -69,6 +75,8 @@ export default defineComponent({
components: {
RulerWidget2D,
AnnotationContextMenu,
+ AnnotationInfo,
+ BoundingRectangle,
},
setup(props) {
const { viewDirection, currentSlice } = toRefs(props);
@@ -190,6 +198,14 @@ export default defineComponent({
}));
});
+ const { onHover, overlayInfo } = useHover(currentTools, currentSlice);
+
+ const points = computed(() => {
+ if (!overlayInfo.value.visible) return [];
+ const tool = rulerStore.toolByID[overlayInfo.value.toolID];
+ return [tool.firstPoint, tool.secondPoint];
+ });
+
return {
rulers: currentRulers,
placingRulerID,
@@ -197,6 +213,9 @@ export default defineComponent({
contextMenu,
openContextMenu,
rulerStore,
+ onHover,
+ overlayInfo,
+ points,
};
},
});
diff --git a/src/components/tools/ruler/RulerWidget2D.vue b/src/components/tools/ruler/RulerWidget2D.vue
index 0cb6a8d3c..0dc542715 100644
--- a/src/components/tools/ruler/RulerWidget2D.vue
+++ b/src/components/tools/ruler/RulerWidget2D.vue
@@ -25,11 +25,14 @@ import { useRulerStore } from '@/src/store/tools/rulers';
import { onVTKEvent } from '@/src/composables/onVTKEvent';
import RulerSVG2D from '@/src/components/tools/ruler/RulerSVG2D.vue';
import { watchOnce } from '@vueuse/core';
-import { useRightClickContextMenu } from '@/src/composables/annotationTool';
+import {
+ useRightClickContextMenu,
+ useHoverEvent,
+} from '@/src/composables/annotationTool';
export default defineComponent({
name: 'RulerWidget2D',
- emits: ['placed', 'contextmenu'],
+ emits: ['placed', 'contextmenu', 'widgetHover'],
props: {
rulerId: {
type: String,
@@ -115,6 +118,8 @@ export default defineComponent({
emit('placed');
});
+ useHoverEvent(emit, widget);
+
// --- right click handling --- //
useRightClickContextMenu(emit, widget);
diff --git a/src/composables/annotationTool.ts b/src/composables/annotationTool.ts
index 4eb3011e6..3d672844b 100644
--- a/src/composables/annotationTool.ts
+++ b/src/composables/annotationTool.ts
@@ -1,12 +1,18 @@
-import { Ref, computed, ref } from 'vue';
+import { Ref, computed, ref, watch } from 'vue';
+import { Vector2 } from '@kitware/vtk.js/types';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import { frameOfReferenceToImageSliceAndAxis } from '@/src/utils/frameOfReference';
import { vtkAnnotationToolWidget } from '@/src/vtk/ToolWidgetUtils/utils';
import { onVTKEvent } from '@/src/composables/onVTKEvent';
-import { LPSAxis } from '../types/lps';
-import { AnnotationTool, ContextMenuEvent } from '../types/annotation-tool';
-import { AnnotationToolStore } from '../store/tools/useAnnotationTool';
-import { getCSSCoordinatesFromEvent } from '../utils/vtk-helpers';
+import { useToolStore } from '@/src/store/tools';
+import { Tools } from '@/src/store/tools/types';
+import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool';
+import { getCSSCoordinatesFromEvent } from '@/src//utils/vtk-helpers';
+import { LPSAxis } from '@/src/types/lps';
+import { AnnotationTool, ContextMenuEvent } from '@/src/types/annotation-tool';
+import { usePopperState } from './usePopperState';
+
+const SHOW_OVERLAY_DELAY = 250; // milliseconds
// does the tools's frame of reference match
// the view's axis
@@ -78,3 +84,103 @@ export const useRightClickContextMenu = (
}
});
};
+
+// --- Hover --- //
+
+export const useHoverEvent = (
+ emit: (event: 'widgetHover', ...args: any[]) => void,
+ widget: Ref
+) => {
+ onVTKEvent(widget, 'onHoverEvent', (eventData: any) => {
+ const displayXY = getCSSCoordinatesFromEvent(eventData);
+ if (displayXY) {
+ emit('widgetHover', {
+ displayXY,
+ hovering: eventData.hovering,
+ });
+ }
+ });
+};
+
+export type OverlayInfo =
+ | {
+ visible: false;
+ }
+ | {
+ visible: true;
+ toolID: ToolID;
+ displayXY: Vector2;
+ };
+
+// Maintains list of tools' hover states.
+// If one tool hovered, overlayInfo.visible === true with toolID and displayXY.
+export const useHover = (
+ tools: Ref>>,
+ currentSlice: Ref
+) => {
+ type Info = OverlayInfo;
+ const toolHoverState = ref({}) as Ref>;
+
+ const toolsOnCurrentSlice = computed(() =>
+ tools.value.filter((tool) => tool.slice === currentSlice.value)
+ );
+
+ watch(toolsOnCurrentSlice, () => {
+ // keep old hover states, default to false for new tools
+ toolHoverState.value = toolsOnCurrentSlice.value.reduce(
+ (toolsHovers, { id }) => {
+ const state = toolHoverState.value[id] ?? {
+ visible: false,
+ };
+ return Object.assign(toolsHovers, {
+ [id]: state,
+ });
+ },
+ {} as Record
+ );
+ });
+
+ const onHover = (id: ToolID, event: any) => {
+ toolHoverState.value[id] = event.hovering
+ ? {
+ visible: true,
+ toolID: id,
+ displayXY: event.displayXY,
+ }
+ : {
+ visible: false,
+ };
+ };
+
+ // If hovering true, debounce showing overlay.
+ // Immediately hide overlay if hovering false.
+ const synchronousOverlayInfo = computed(() => {
+ const visibleToolID = Object.keys(toolHoverState.value).find(
+ (toolID) => toolHoverState.value[toolID as ToolID].visible
+ ) as ToolID | undefined;
+
+ return visibleToolID
+ ? toolHoverState.value[visibleToolID]
+ : ({ visible: false } as Info);
+ });
+
+ const { isSet: showOverlay, reset: resetOverlay } =
+ usePopperState(SHOW_OVERLAY_DELAY);
+
+ watch(synchronousOverlayInfo, resetOverlay);
+
+ const overlayInfo = computed(() =>
+ showOverlay.value
+ ? synchronousOverlayInfo.value
+ : ({ visible: false } as Info)
+ );
+
+ const toolStore = useToolStore();
+ const noInfoWithoutSelect = computed(() => {
+ if (toolStore.currentTool !== Tools.Select)
+ return { visible: false } as Info;
+ return overlayInfo.value;
+ });
+
+ return { overlayInfo: noInfoWithoutSelect, onHover };
+};
diff --git a/src/composables/usePopperState.ts b/src/composables/usePopperState.ts
new file mode 100644
index 000000000..429007df0
--- /dev/null
+++ b/src/composables/usePopperState.ts
@@ -0,0 +1,18 @@
+import { useDebounceFn } from '@vueuse/core';
+import { ref } from 'vue';
+
+// reset: isSet = false immediately. After delay, isSet = true
+export const usePopperState = (delay: number) => {
+ const isSet = ref(true);
+
+ const delayedSet = useDebounceFn(() => {
+ isSet.value = true;
+ }, delay);
+
+ const reset = () => {
+ isSet.value = false;
+ delayedSet();
+ };
+
+ return { isSet, reset };
+};
diff --git a/src/constants.ts b/src/constants.ts
index 7192ccd86..aa6f5a25e 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -45,3 +45,5 @@ export const Messages = {
'Lost the WebGL context! Please reload the webpage. If the problem persists, you may need to restart your web browser.',
},
} as const;
+
+export const ANNOTATION_TOOL_HANDLE_RADIUS = 10; // pixels
diff --git a/src/store/tools/types.ts b/src/store/tools/types.ts
index 6ab9b36d3..f73e6e8d3 100644
--- a/src/store/tools/types.ts
+++ b/src/store/tools/types.ts
@@ -8,4 +8,5 @@ export enum Tools {
Crosshairs = 'Crosshairs',
Crop = 'Crop',
Polygon = 'Polygon',
+ Select = 'Select',
}
diff --git a/src/vtk/PolygonWidget/behavior.ts b/src/vtk/PolygonWidget/behavior.ts
index b82d31e9d..2e9c2d62f 100644
--- a/src/vtk/PolygonWidget/behavior.ts
+++ b/src/vtk/PolygonWidget/behavior.ts
@@ -1,12 +1,14 @@
import { distance2BetweenPoints } from '@kitware/vtk.js/Common/Core/Math';
import macro from '@kitware/vtk.js/macros';
import { Vector3 } from '@kitware/vtk.js/types';
+import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer';
import { WidgetAction } from '../ToolWidgetUtils/utils';
type Position3d = { x: number; y: number; z: number };
-type MouseEvent = {
+type vtkMouseEvent = {
position: Position3d;
+ pokedRenderer: vtkRenderer;
};
const FINISHABLE_DISTANCE = 10;
@@ -18,7 +20,7 @@ const DOUBLE_CLICK_SLIP_DISTANCE_MAX_SQUARED =
DOUBLE_CLICK_SLIP_DISTANCE_MAX ** 2;
export default function widgetBehavior(publicAPI: any, model: any) {
- model.classHierarchy.push('vtkPolygonWidgetProp');
+ model.classHierarchy.push('vtkPolygonWidgetBehavior');
model._isDragging = false;
// overUnselectedHandle is true if mouse is over handle that was created before a mouse move event.
@@ -42,9 +44,11 @@ export default function widgetBehavior(publicAPI: any, model: any) {
// support setting per-view widget manipulators
macro.setGet(publicAPI, model, ['manipulator']);
- // support forwarding events
+
+ // events to emit
macro.event(publicAPI, model, 'RightClickEvent');
macro.event(publicAPI, model, 'PlacedEvent');
+ macro.event(publicAPI, model, 'HoverEvent');
publicAPI.resetInteractions = () => {
model._interactor.cancelAnimation(publicAPI, true);
@@ -145,11 +149,18 @@ export default function widgetBehavior(publicAPI: any, model: any) {
// Left press: Select handle to drag / Add new handle
// --------------------------------------------------------------------------
- publicAPI.handleLeftButtonPress = (e: any) => {
+ publicAPI.handleLeftButtonPress = (event: vtkMouseEvent) => {
const activeWidget = model._widgetManager.getActiveWidget();
+
+ // turns off hover while dragging
+ publicAPI.invokeHoverEvent({
+ ...event,
+ hovering: false,
+ });
+
if (
!model.manipulator ||
- ignoreKey(e) ||
+ ignoreKey(event) ||
// If hovering over another widget, don't consume event.
(activeWidget && activeWidget !== publicAPI)
) {
@@ -165,7 +176,7 @@ export default function widgetBehavior(publicAPI: any, model: any) {
model.activeState = model.widgetState.getMoveHandle();
model._widgetManager.grabFocus(publicAPI);
}
- updateActiveStateHandle(e);
+ updateActiveStateHandle(event);
if (model.widgetState.getFinishable()) {
finishPlacing();
@@ -186,7 +197,7 @@ export default function widgetBehavior(publicAPI: any, model: any) {
// insert point
const insertIndex = model.activeState.getIndex() + 1;
const newHandle = model.widgetState.addHandle({ insertIndex });
- const coords = getWorldCoords(e);
+ const coords = getWorldCoords(event);
if (!coords) throw new Error('No world coords');
newHandle.setOrigin(coords);
// enable dragging immediately
@@ -213,14 +224,14 @@ export default function widgetBehavior(publicAPI: any, model: any) {
// Mouse move: Drag selected handle / Handle follow the mouse
// --------------------------------------------------------------------------
- publicAPI.handleMouseMove = (callData: any) => {
+ publicAPI.handleMouseMove = (event: vtkMouseEvent) => {
if (
model.pickable &&
model.dragable &&
model.activeState &&
- !ignoreKey(callData)
+ !ignoreKey(event)
) {
- if (updateActiveStateHandle(callData) === macro.EVENT_ABORT) {
+ if (updateActiveStateHandle(event) === macro.EVENT_ABORT) {
return macro.EVENT_ABORT;
}
}
@@ -234,6 +245,11 @@ export default function widgetBehavior(publicAPI: any, model: any) {
model._widgetManager.disablePicking();
}
+ publicAPI.invokeHoverEvent({
+ ...event,
+ hovering: !!model.activeState,
+ });
+
return macro.VOID;
};
@@ -245,7 +261,7 @@ export default function widgetBehavior(publicAPI: any, model: any) {
let lastReleaseTime = 0;
let lastReleasePosition: Vector3 | undefined;
- publicAPI.handleLeftButtonRelease = (event: MouseEvent) => {
+ publicAPI.handleLeftButtonRelease = (event: vtkMouseEvent) => {
if (
!model.activeState ||
!model.activeState.getActive() ||
diff --git a/src/vtk/PolygonWidget/state.ts b/src/vtk/PolygonWidget/state.ts
index 7f91aba8a..a580fce10 100644
--- a/src/vtk/PolygonWidget/state.ts
+++ b/src/vtk/PolygonWidget/state.ts
@@ -82,7 +82,6 @@ function vtkPolygonWidgetState(publicAPI: any, model: any) {
model.labels[HandlesLabel] = [...model.handles];
publicAPI.modified();
-
return handlePublicAPI;
};
diff --git a/src/vtk/RectangleWidget/RectangleLineRepresentation.js b/src/vtk/RectangleWidget/RectangleLineRepresentation.js
new file mode 100644
index 000000000..5b5086bc9
--- /dev/null
+++ b/src/vtk/RectangleWidget/RectangleLineRepresentation.js
@@ -0,0 +1,93 @@
+import macro from '@kitware/vtk.js/macros';
+import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox';
+import vtkStateBuilder from '@kitware/vtk.js/Widgets/Core/StateBuilder';
+import * as vtkMath from '@kitware/vtk.js/Common/Core/Math';
+import LineGlyphRepresentation from '../LineGlyphRepresentation';
+
+function vtkRectangleLineRepresentation(publicAPI, model) {
+ model.classHierarchy.push('vtkRectangleLineRepresentation');
+
+ const superGetRepresentationStates = publicAPI.getRepresentationStates;
+
+ const compositeState = vtkStateBuilder
+ .createBuilder()
+ .addDynamicMixinState({
+ labels: ['handles'],
+ mixins: ['origin', 'scale1'],
+ name: 'handle',
+ })
+ .build();
+
+ const cornerStates = Array.from({ length: 4 }, () =>
+ compositeState.addHandle()
+ );
+
+ // Save behavior model to access renderer
+ const superBehavior = model.widgetAPI.behavior;
+ let behaviorModel;
+ model.widgetAPI.behavior = (publicAPIy, bModel) => {
+ behaviorModel = bModel;
+ return superBehavior(publicAPIy, bModel);
+ };
+
+ publicAPI.getRepresentationStates = (input = model.inputData[0]) => {
+ // Map 2 handles to 4 corner states in display space
+ const states = superGetRepresentationStates(input);
+ if (states.length === 0) {
+ return states;
+ }
+
+ const box = [...vtkBoundingBox.INIT_BOUNDS];
+ states.forEach((handle) => {
+ const displayPos = behaviorModel._apiSpecificRenderWindow.worldToDisplay(
+ ...handle.getOrigin(),
+ behaviorModel._renderer
+ );
+ vtkBoundingBox.addPoint(box, ...displayPos);
+ });
+ const corners = vtkBoundingBox.getCorners(box, []);
+
+ // 8 corners on plane, remove duplicates to make 4 corners
+ const corners2D = corners.reduce((outCorners, corner) => {
+ const duplicate = outCorners.some((outCorner) =>
+ vtkMath.areEquals(outCorner, corner)
+ );
+ if (!duplicate) {
+ outCorners.push(corner);
+ }
+ return outCorners;
+ }, []);
+
+ const scale = states[0].getScale1();
+
+ // reorder corners
+ const outStates = [0, 2, 3, 1]
+ // if in handles are equal, corners2D length is 1
+ .map((index) => Math.min(index, corners2D.length - 1))
+ .map((cornerIndex, stateIndex) => {
+ const worldPos = behaviorModel._apiSpecificRenderWindow.displayToWorld(
+ ...corners2D[cornerIndex],
+ behaviorModel._renderer
+ );
+ const state = cornerStates[stateIndex];
+ state.setOrigin(worldPos);
+ state.setScale1(scale);
+ return state;
+ });
+
+ return outStates;
+ };
+}
+
+export function extend(publicAPI, model, initialValues = {}) {
+ LineGlyphRepresentation.extend(publicAPI, model, initialValues);
+
+ vtkRectangleLineRepresentation(publicAPI, model);
+}
+
+export const newInstance = macro.newInstance(
+ extend,
+ 'vtkRectangleLineRepresentation'
+);
+
+export default { newInstance, extend };
diff --git a/src/vtk/RectangleWidget/index.js b/src/vtk/RectangleWidget/index.js
index 3a043789c..737ae6d5d 100644
--- a/src/vtk/RectangleWidget/index.js
+++ b/src/vtk/RectangleWidget/index.js
@@ -1,14 +1,28 @@
import macro from '@kitware/vtk.js/macro';
import vtkRulerWidget from '../RulerWidget';
+import vtkRectangleLineRepresentation from './RectangleLineRepresentation';
export { InteractionState } from '../RulerWidget/behavior';
+
// ----------------------------------------------------------------------------
// Factory
// ----------------------------------------------------------------------------
function vtkRectangleWidget(publicAPI, model) {
model.classHierarchy.push('vtkRectangleWidget');
+
+ const superGetRepresentationsForViewType =
+ publicAPI.getRepresentationsForViewType;
+ publicAPI.getRepresentationsForViewType = () => {
+ const reps = superGetRepresentationsForViewType();
+ reps[1].builder = vtkRectangleLineRepresentation;
+ reps[1].initialValues = {
+ ...reps[1].initialValues,
+ widgetAPI: model,
+ };
+ return reps;
+ };
}
// ----------------------------------------------------------------------------
diff --git a/src/vtk/RulerWidget/behavior.ts b/src/vtk/RulerWidget/behavior.ts
index 029653142..28392d9fa 100644
--- a/src/vtk/RulerWidget/behavior.ts
+++ b/src/vtk/RulerWidget/behavior.ts
@@ -24,6 +24,7 @@ export default function widgetBehavior(publicAPI: any, model: any) {
// support forwarding events
macro.event(publicAPI, model, 'RightClickEvent');
macro.event(publicAPI, model, 'PlacedEvent');
+ macro.event(publicAPI, model, 'HoverEvent');
publicAPI.deactivateAllHandles = () => {
model.widgetState.deactivate();
@@ -55,6 +56,15 @@ export default function widgetBehavior(publicAPI: any, model: any) {
model._interactor.cancelAnimation(publicAPI, true);
};
+ // Check if mouse is over line segment between handles
+ const checkOverSegment = () => {
+ const selections = model._widgetManager.getSelections();
+ const overSegment =
+ selections[0]?.getProperties().prop ===
+ model.representations[1].getActors()[0]; // line representation is second representation
+ return overSegment;
+ };
+
/**
* Places or drags a point.
*/
@@ -63,6 +73,12 @@ export default function widgetBehavior(publicAPI: any, model: any) {
return macro.VOID;
}
+ // turns off hover while dragging
+ publicAPI.invokeHoverEvent({
+ ...eventData,
+ hovering: false,
+ });
+
// This ruler widget is passive, so if another widget
// is active, we don't do anything.
const activeWidget = model._widgetManager.getActiveWidget();
@@ -105,7 +121,11 @@ export default function widgetBehavior(publicAPI: any, model: any) {
}
// dragging
- if (model.activeState?.getActive() && model.pickable) {
+ if (
+ model.activeState?.getActive() &&
+ model.pickable &&
+ !checkOverSegment()
+ ) {
draggingState = model.activeState;
publicAPI.setInteractionState(InteractionState.Dragging);
model._apiSpecificRenderWindow.setCursor('grabbing');
@@ -148,6 +168,11 @@ export default function widgetBehavior(publicAPI: any, model: any) {
return macro.EVENT_ABORT;
}
+ publicAPI.invokeHoverEvent({
+ ...eventData,
+ hovering: !!model.activeState,
+ });
+
return macro.VOID;
};
diff --git a/src/vtk/RulerWidget/index.js b/src/vtk/RulerWidget/index.js
index 1c7179c3a..2da4766da 100644
--- a/src/vtk/RulerWidget/index.js
+++ b/src/vtk/RulerWidget/index.js
@@ -3,6 +3,8 @@ import vtkAbstractWidgetFactory from '@kitware/vtk.js/Widgets/Core/AbstractWidge
import vtkPlanePointManipulator from '@kitware/vtk.js/Widgets/Manipulators/PlaneManipulator';
import vtkSphereHandleRepresentation from '@kitware/vtk.js/Widgets/Representations/SphereHandleRepresentation';
import { distance2BetweenPoints } from '@kitware/vtk.js/Common/Core/Math';
+import { Behavior } from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation/Constants';
+import vtkLineGlyphRepresentation from '@/src/vtk/LineGlyphRepresentation';
import widgetBehavior from './behavior';
import stateGenerator, { PointsLabel } from './state';
@@ -26,6 +28,15 @@ function vtkRulerWidget(publicAPI, model) {
scaleInPixels: true,
},
},
+ {
+ builder: vtkLineGlyphRepresentation,
+ labels: [PointsLabel],
+ initialValues: {
+ scaleInPixels: true,
+ lineThickness: 0.25, // smaller than .5 default to prioritize picking handles
+ behavior: Behavior.HANDLE, // make pickable even if not visible
+ },
+ },
];
publicAPI.getLength = () => {
diff --git a/src/vtk/ToolWidgetUtils/pointState.js b/src/vtk/ToolWidgetUtils/pointState.js
index 4e369ab67..9c7ced295 100644
--- a/src/vtk/ToolWidgetUtils/pointState.js
+++ b/src/vtk/ToolWidgetUtils/pointState.js
@@ -2,8 +2,9 @@ import macro from '@kitware/vtk.js/macros';
import vtkWidgetState from '@kitware/vtk.js/Widgets/Core/WidgetState';
import visibleMixin from '@kitware/vtk.js/Widgets/Core/StateBuilder/visibleMixin';
import scale1Mixin from '@kitware/vtk.js/Widgets/Core/StateBuilder/scale1Mixin';
+import { ANNOTATION_TOOL_HANDLE_RADIUS } from '@/src/constants';
-const PIXEL_SIZE = 20;
+const PIXEL_SIZE = ANNOTATION_TOOL_HANDLE_RADIUS * 2;
function watchStore(publicAPI, store, getter, cmp) {
let cached = getter();
diff --git a/src/vtk/ToolWidgetUtils/utils.ts b/src/vtk/ToolWidgetUtils/utils.ts
index 2ec57fe44..ebdd7b7d8 100644
--- a/src/vtk/ToolWidgetUtils/utils.ts
+++ b/src/vtk/ToolWidgetUtils/utils.ts
@@ -26,5 +26,6 @@ export interface vtkAnnotationToolWidget extends vtkAbstractWidget {
getManipulator(): vtkPlaneManipulator;
onRightClickEvent(cb: (eventData: any) => void): vtkSubscription;
onPlacedEvent(cb: (eventData: any) => void): vtkSubscription;
+ onHoverEvent(cb: (eventData: any) => void): vtkSubscription;
resetInteractions(): void;
}