From 617e53cd1c528841504fd20ebce69aaef16e2367 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 4 Sep 2023 18:36:43 -0400 Subject: [PATCH 01/15] feat(annotationTool): add label info popover on polygon hover --- src/components/tools/AnnotationInfo.vue | 46 +++++++++++ src/components/tools/polygon/PolygonTool.vue | 16 ++++ .../tools/polygon/PolygonWidget2D.vue | 17 ++-- src/composables/annotationTool.ts | 80 ++++++++++++++++++- src/vtk/PolygonWidget/behavior.ts | 15 +++- src/vtk/ToolWidgetUtils/utils.ts | 1 + 6 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 src/components/tools/AnnotationInfo.vue diff --git a/src/components/tools/AnnotationInfo.vue b/src/components/tools/AnnotationInfo.vue new file mode 100644 index 000000000..b7e2685dc --- /dev/null +++ b/src/components/tools/AnnotationInfo.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/components/tools/polygon/PolygonTool.vue b/src/components/tools/polygon/PolygonTool.vue index 7284e495e..628cb53b4 100644 --- a/src/components/tools/polygon/PolygonTool.vue +++ b/src/components/tools/polygon/PolygonTool.vue @@ -12,9 +12,15 @@ :widget-manager="widgetManager" @contextmenu="openContextMenu(tool.id, $event)" @placed="onToolPlaced" + @widgetHover="onHover(tool.id, $event)" /> + @@ -43,8 +49,10 @@ 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 PolygonWidget2D from './PolygonWidget2D.vue'; type ToolID = PolygonID; @@ -74,6 +82,7 @@ export default defineComponent({ components: { PolygonWidget2D, AnnotationContextMenu, + AnnotationInfo, }, setup(props) { const { viewDirection, currentSlice } = toRefs(props); @@ -181,6 +190,11 @@ export default defineComponent({ const currentTools = useCurrentTools(activeToolStore, viewAxis); + const { onHover, overlayInfo } = useHover( + currentTools, + currentSlice + ); + return { tools: currentTools, placingToolID, @@ -188,6 +202,8 @@ export default defineComponent({ contextMenu, openContextMenu, activeToolStore, + onHover, + overlayInfo, }; }, }); diff --git a/src/components/tools/polygon/PolygonWidget2D.vue b/src/components/tools/polygon/PolygonWidget2D.vue index 9ebf50a68..debe669fd 100644 --- a/src/components/tools/polygon/PolygonWidget2D.vue +++ b/src/components/tools/polygon/PolygonWidget2D.vue @@ -17,7 +17,7 @@ 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 +27,7 @@ import SVG2DComponent from './PolygonSVG2D.vue'; export default defineComponent({ name: 'PolygonWidget2D', - emits: ['placed', 'contextmenu'], + emits: ['placed', 'contextmenu', 'widgetHover'], props: { toolId: { type: String, @@ -103,6 +103,8 @@ export default defineComponent({ emit('placed'); }); + useHoverEvent(emit, widget); + // --- right click handling --- // useRightClickContextMenu(emit, widget); @@ -165,13 +167,6 @@ export default defineComponent({ diff --git a/src/composables/annotationTool.ts b/src/composables/annotationTool.ts index 4eb3011e6..465f9d629 100644 --- a/src/composables/annotationTool.ts +++ b/src/composables/annotationTool.ts @@ -1,4 +1,5 @@ -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'; @@ -78,3 +79,80 @@ 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; + }; + +export const useHover = ( + tools: Ref>>, + currentSlice: Ref +) => { + type Info = OverlayInfo; + const toolHoverState = ref({}) as Ref>; + + const overlayInfo = 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 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, + }; + }; + return { overlayInfo, onHover }; +}; diff --git a/src/vtk/PolygonWidget/behavior.ts b/src/vtk/PolygonWidget/behavior.ts index b82d31e9d..0fb385f5f 100644 --- a/src/vtk/PolygonWidget/behavior.ts +++ b/src/vtk/PolygonWidget/behavior.ts @@ -42,9 +42,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); @@ -147,6 +149,12 @@ export default function widgetBehavior(publicAPI: any, model: any) { publicAPI.handleLeftButtonPress = (e: any) => { const activeWidget = model._widgetManager.getActiveWidget(); + + publicAPI.invokeHoverEvent({ + ...e, + hovering: false, + }); + if ( !model.manipulator || ignoreKey(e) || @@ -234,6 +242,11 @@ export default function widgetBehavior(publicAPI: any, model: any) { model._widgetManager.disablePicking(); } + publicAPI.invokeHoverEvent({ + ...callData, + hovering: !!model.activeState, + }); + return macro.VOID; }; 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; } From 812eb828183836ab454df2a091135278c40fae83 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Tue, 5 Sep 2023 14:38:29 -0400 Subject: [PATCH 02/15] feat(PolygonWidget): check for hover from handles bounds --- src/vtk/PolygonWidget/behavior.ts | 62 ++++++++++++++++++++++++------- src/vtk/PolygonWidget/state.ts | 31 ++++++++++++++-- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/vtk/PolygonWidget/behavior.ts b/src/vtk/PolygonWidget/behavior.ts index 0fb385f5f..841ce8778 100644 --- a/src/vtk/PolygonWidget/behavior.ts +++ b/src/vtk/PolygonWidget/behavior.ts @@ -1,12 +1,15 @@ import { distance2BetweenPoints } from '@kitware/vtk.js/Common/Core/Math'; +import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox'; import macro from '@kitware/vtk.js/macros'; -import { Vector3 } from '@kitware/vtk.js/types'; +import { Bounds, 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 +21,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. @@ -147,17 +150,17 @@ 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(); publicAPI.invokeHoverEvent({ - ...e, + ...event, hovering: false, }); if ( !model.manipulator || - ignoreKey(e) || + ignoreKey(event) || // If hovering over another widget, don't consume event. (activeWidget && activeWidget !== publicAPI) ) { @@ -173,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(); @@ -194,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 @@ -221,14 +224,45 @@ export default function widgetBehavior(publicAPI: any, model: any) { // Mouse move: Drag selected handle / Handle follow the mouse // -------------------------------------------------------------------------- - publicAPI.handleMouseMove = (callData: any) => { + const checkInScreenBounds = (event: vtkMouseEvent) => { + const b = publicAPI.getBounds(); + if (!vtkBoundingBox.isValid(b)) return false; + + const corners = [] as Array; + vtkBoundingBox.getCorners(b, corners); + + const screenBounds = [...vtkBoundingBox.INIT_BOUNDS] as Bounds; + corners.forEach((corner) => { + const pos = model._apiSpecificRenderWindow.worldToDisplay( + ...corner, + event.pokedRenderer + ) as Vector3; + vtkBoundingBox.addPoint(screenBounds, ...pos); + }); + + const pointerScreenPos = [ + event.position.x, + event.position.y, + event.position.z, + ] as Vector3; + + screenBounds[4] = -1; + screenBounds[5] = 1; + const isInside = vtkBoundingBox.containsPoint( + screenBounds, + ...pointerScreenPos + ); + return isInside; + }; + + 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; } } @@ -243,8 +277,8 @@ export default function widgetBehavior(publicAPI: any, model: any) { } publicAPI.invokeHoverEvent({ - ...callData, - hovering: !!model.activeState, + ...event, + hovering: checkInScreenBounds(event), }); return macro.VOID; @@ -258,7 +292,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..27bae711b 100644 --- a/src/vtk/PolygonWidget/state.ts +++ b/src/vtk/PolygonWidget/state.ts @@ -1,9 +1,10 @@ import macro from '@kitware/vtk.js/macros'; import vtkWidgetState from '@kitware/vtk.js/Widgets/Core/WidgetState'; -import bounds from '@kitware/vtk.js/Widgets/Core/StateBuilder/boundsMixin'; +import boundsMixin from '@kitware/vtk.js/Widgets/Core/StateBuilder/boundsMixin'; import visibleMixin from '@kitware/vtk.js/Widgets/Core/StateBuilder/visibleMixin'; import scale1Mixin from '@kitware/vtk.js/Widgets/Core/StateBuilder/scale1Mixin'; -import { Vector3 } from '@kitware/vtk.js/types'; +import { Bounds, Vector3 } from '@kitware/vtk.js/types'; +import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox'; import createPointState from '../ToolWidgetUtils/pointState'; import { watchState } from '../ToolWidgetUtils/utils'; @@ -82,7 +83,6 @@ function vtkPolygonWidgetState(publicAPI: any, model: any) { model.labels[HandlesLabel] = [...model.handles]; publicAPI.modified(); - return handlePublicAPI; }; @@ -109,6 +109,29 @@ function vtkPolygonWidgetState(publicAPI: any, model: any) { } }; + const computeBounds = () => { + if (model.handles.length === 0) return undefined; + + const bounds = vtkBoundingBox.reset([] as unknown as Bounds); + model.handles + .map((handle: any) => handle.getOrigin()) + .forEach((point: Vector3) => { + vtkBoundingBox.addPoint(bounds, ...point); + }); + return bounds; + }; + + let savedBounds: Bounds | undefined; + publicAPI.onModified(() => { + savedBounds = undefined; + }); + + publicAPI.getBounds = () => { + if (savedBounds) return savedBounds; + savedBounds = computeBounds(); + return savedBounds; + }; + publicAPI.getPlacing = () => getTool().placing; publicAPI.setPlacing = (placing: boolean) => { getTool().placing = placing; @@ -132,7 +155,7 @@ function _createPolygonWidgetState( ) { Object.assign(model, defaultValues(initialValues)); vtkWidgetState.extend(publicAPI, model, initialValues); - bounds.extend(publicAPI, model); + boundsMixin.extend(publicAPI, model); macro.get(publicAPI, model, ['id', 'handles', 'moveHandle', 'finishable']); macro.setGet(publicAPI, model, ['finishable']); From d15b65aa4bbf9219554cbadd74fe24504a2f8488 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Tue, 5 Sep 2023 15:57:45 -0400 Subject: [PATCH 03/15] feat(PolygonWidget): show bounding box upon hover --- src/components/tools/BoundingRectangle.vue | 64 ++++++++++++++++++++ src/components/tools/polygon/PolygonTool.vue | 16 +++-- 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 src/components/tools/BoundingRectangle.vue diff --git a/src/components/tools/BoundingRectangle.vue b/src/components/tools/BoundingRectangle.vue new file mode 100644 index 000000000..67248fa80 --- /dev/null +++ b/src/components/tools/BoundingRectangle.vue @@ -0,0 +1,64 @@ + + + diff --git a/src/components/tools/polygon/PolygonTool.vue b/src/components/tools/polygon/PolygonTool.vue index 628cb53b4..80b787cd3 100644 --- a/src/components/tools/polygon/PolygonTool.vue +++ b/src/components/tools/polygon/PolygonTool.vue @@ -1,6 +1,7 @@ @@ -53,6 +50,7 @@ import { } 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; @@ -83,6 +81,7 @@ export default defineComponent({ PolygonWidget2D, AnnotationContextMenu, AnnotationInfo, + BoundingRectangle, }, setup(props) { const { viewDirection, currentSlice } = toRefs(props); @@ -195,6 +194,12 @@ export default defineComponent({ currentSlice ); + const pointsToBound = computed(() => { + if (!overlayInfo.value.visible) return []; + const tool = activeToolStore.toolByID[overlayInfo.value.toolID]; + return tool.points; + }); + return { tools: currentTools, placingToolID, @@ -204,6 +209,7 @@ export default defineComponent({ activeToolStore, onHover, overlayInfo, + pointsToBound, }; }, }); From eb2ddbc4a656339159876276c821b08e83e837e1 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 14 Sep 2023 16:44:36 -0400 Subject: [PATCH 04/15] feat(polygon): hover on when cursor over line or handle --- src/vtk/PolygonWidget/behavior.ts | 36 ++----------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/src/vtk/PolygonWidget/behavior.ts b/src/vtk/PolygonWidget/behavior.ts index 841ce8778..467d3a5b4 100644 --- a/src/vtk/PolygonWidget/behavior.ts +++ b/src/vtk/PolygonWidget/behavior.ts @@ -1,7 +1,6 @@ import { distance2BetweenPoints } from '@kitware/vtk.js/Common/Core/Math'; -import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox'; import macro from '@kitware/vtk.js/macros'; -import { Bounds, Vector3 } from '@kitware/vtk.js/types'; +import { Vector3 } from '@kitware/vtk.js/types'; import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; import { WidgetAction } from '../ToolWidgetUtils/utils'; @@ -224,37 +223,6 @@ export default function widgetBehavior(publicAPI: any, model: any) { // Mouse move: Drag selected handle / Handle follow the mouse // -------------------------------------------------------------------------- - const checkInScreenBounds = (event: vtkMouseEvent) => { - const b = publicAPI.getBounds(); - if (!vtkBoundingBox.isValid(b)) return false; - - const corners = [] as Array; - vtkBoundingBox.getCorners(b, corners); - - const screenBounds = [...vtkBoundingBox.INIT_BOUNDS] as Bounds; - corners.forEach((corner) => { - const pos = model._apiSpecificRenderWindow.worldToDisplay( - ...corner, - event.pokedRenderer - ) as Vector3; - vtkBoundingBox.addPoint(screenBounds, ...pos); - }); - - const pointerScreenPos = [ - event.position.x, - event.position.y, - event.position.z, - ] as Vector3; - - screenBounds[4] = -1; - screenBounds[5] = 1; - const isInside = vtkBoundingBox.containsPoint( - screenBounds, - ...pointerScreenPos - ); - return isInside; - }; - publicAPI.handleMouseMove = (event: vtkMouseEvent) => { if ( model.pickable && @@ -278,7 +246,7 @@ export default function widgetBehavior(publicAPI: any, model: any) { publicAPI.invokeHoverEvent({ ...event, - hovering: checkInScreenBounds(event), + hovering: !!model.activeState, }); return macro.VOID; From 0f8e55f686f92b369b5583a0e85830385350ed38 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 14 Sep 2023 18:04:17 -0400 Subject: [PATCH 05/15] feat(annotationTool): debouce showing overlay in useHover --- src/components/tools/BoundingRectangle.vue | 14 ++++-- .../tools/polygon/PolygonWidget2D.vue | 16 +++++-- src/composables/annotationTool.ts | 47 +++++++++++++++---- 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/src/components/tools/BoundingRectangle.vue b/src/components/tools/BoundingRectangle.vue index 67248fa80..ff809da18 100644 --- a/src/components/tools/BoundingRectangle.vue +++ b/src/components/tools/BoundingRectangle.vue @@ -55,10 +55,18 @@ const updateRectangle = () => { const { points } = toRefs(props); watch([points], updateRectangle, { immediate: true, deep: true }); -onVTKEvent(viewProxy, 'onModified', updateRectangle) +onVTKEvent(viewProxy, 'onModified', updateRectangle); diff --git a/src/components/tools/polygon/PolygonWidget2D.vue b/src/components/tools/polygon/PolygonWidget2D.vue index debe669fd..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 { useHoverEvent, 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, { @@ -167,6 +170,13 @@ export default defineComponent({ diff --git a/src/composables/annotationTool.ts b/src/composables/annotationTool.ts index 465f9d629..884513578 100644 --- a/src/composables/annotationTool.ts +++ b/src/composables/annotationTool.ts @@ -1,4 +1,5 @@ import { Ref, computed, ref, watch } from 'vue'; +import { useDebounceFn } from '@vueuse/core'; import { Vector2 } from '@kitware/vtk.js/types'; import { useCurrentImage } from '@/src/composables/useCurrentImage'; import { frameOfReferenceToImageSliceAndAxis } from '@/src/utils/frameOfReference'; @@ -9,6 +10,8 @@ import { AnnotationTool, ContextMenuEvent } from '../types/annotation-tool'; import { AnnotationToolStore } from '../store/tools/useAnnotationTool'; import { getCSSCoordinatesFromEvent } from '../utils/vtk-helpers'; +const SHOW_OVERLAY_DELAY = 500; // milliseconds + // does the tools's frame of reference match // the view's axis const useDoesToolFrameMatchViewAxis = < @@ -107,6 +110,8 @@ export type OverlayInfo = 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 @@ -114,16 +119,6 @@ export const useHover = ( type Info = OverlayInfo; const toolHoverState = ref({}) as Ref>; - const overlayInfo = 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 toolsOnCurrentSlice = computed(() => tools.value.filter((tool) => tool.slice === currentSlice.value) ); @@ -154,5 +149,37 @@ export const useHover = ( 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); + }); + + // Debounced output + const overlayInfo = ref(synchronousOverlayInfo.value) as Ref; + + const debouncedOverlayInfo = useDebounceFn((info: Info) => { + // if we moved off the tool (syncOverlay.visible === false), don't show overlay + if (synchronousOverlayInfo.value.visible) overlayInfo.value = info; + }, SHOW_OVERLAY_DELAY); + + watch(synchronousOverlayInfo, () => { + if (!synchronousOverlayInfo.value.visible) + overlayInfo.value = synchronousOverlayInfo.value; + else { + // Immediately set visible = false to hide overlay on mouse move, even if hovering true. + // Depends on widget sending hover events with every mouse move. + overlayInfo.value = { visible: false }; + debouncedOverlayInfo({ ...synchronousOverlayInfo.value }); + } + }); + return { overlayInfo, onHover }; }; From e6a552a52f021650086413ce842bcaa75fd72d43 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Fri, 15 Sep 2023 11:24:52 -0400 Subject: [PATCH 06/15] fix(BoundingRectangle): fit handle circles within rectangle --- src/components/tools/BoundingRectangle.vue | 13 ++++++++----- src/components/tools/polygon/PolygonSVG2D.vue | 2 +- src/constants.ts | 2 ++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/tools/BoundingRectangle.vue b/src/components/tools/BoundingRectangle.vue index ff809da18..64c722697 100644 --- a/src/components/tools/BoundingRectangle.vue +++ b/src/components/tools/BoundingRectangle.vue @@ -1,11 +1,12 @@ - diff --git a/src/components/tools/BoundingRectangle.vue b/src/components/tools/BoundingRectangle.vue index 64c722697..ac53f72d0 100644 --- a/src/components/tools/BoundingRectangle.vue +++ b/src/components/tools/BoundingRectangle.vue @@ -56,7 +56,7 @@ const updateRectangle = () => { }; const { points } = toRefs(props); -watch([points], updateRectangle, { immediate: true, deep: true }); +watch(points, updateRectangle, { immediate: true, deep: true }); onVTKEvent(viewProxy, 'onModified', updateRectangle); diff --git a/src/vtk/PolygonWidget/state.ts b/src/vtk/PolygonWidget/state.ts index 27bae711b..a580fce10 100644 --- a/src/vtk/PolygonWidget/state.ts +++ b/src/vtk/PolygonWidget/state.ts @@ -1,10 +1,9 @@ import macro from '@kitware/vtk.js/macros'; import vtkWidgetState from '@kitware/vtk.js/Widgets/Core/WidgetState'; -import boundsMixin from '@kitware/vtk.js/Widgets/Core/StateBuilder/boundsMixin'; +import bounds from '@kitware/vtk.js/Widgets/Core/StateBuilder/boundsMixin'; import visibleMixin from '@kitware/vtk.js/Widgets/Core/StateBuilder/visibleMixin'; import scale1Mixin from '@kitware/vtk.js/Widgets/Core/StateBuilder/scale1Mixin'; -import { Bounds, Vector3 } from '@kitware/vtk.js/types'; -import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox'; +import { Vector3 } from '@kitware/vtk.js/types'; import createPointState from '../ToolWidgetUtils/pointState'; import { watchState } from '../ToolWidgetUtils/utils'; @@ -109,29 +108,6 @@ function vtkPolygonWidgetState(publicAPI: any, model: any) { } }; - const computeBounds = () => { - if (model.handles.length === 0) return undefined; - - const bounds = vtkBoundingBox.reset([] as unknown as Bounds); - model.handles - .map((handle: any) => handle.getOrigin()) - .forEach((point: Vector3) => { - vtkBoundingBox.addPoint(bounds, ...point); - }); - return bounds; - }; - - let savedBounds: Bounds | undefined; - publicAPI.onModified(() => { - savedBounds = undefined; - }); - - publicAPI.getBounds = () => { - if (savedBounds) return savedBounds; - savedBounds = computeBounds(); - return savedBounds; - }; - publicAPI.getPlacing = () => getTool().placing; publicAPI.setPlacing = (placing: boolean) => { getTool().placing = placing; @@ -155,7 +131,7 @@ function _createPolygonWidgetState( ) { Object.assign(model, defaultValues(initialValues)); vtkWidgetState.extend(publicAPI, model, initialValues); - boundsMixin.extend(publicAPI, model); + bounds.extend(publicAPI, model); macro.get(publicAPI, model, ['id', 'handles', 'moveHandle', 'finishable']); macro.setGet(publicAPI, model, ['finishable']); From 7d23b658cfccb176ddb1a0c49aa657e1ae904e33 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Fri, 15 Sep 2023 17:16:18 -0400 Subject: [PATCH 09/15] fix(ruler): turn off hover when dragging --- src/composables/annotationTool.ts | 2 +- src/vtk/PolygonWidget/behavior.ts | 1 + src/vtk/RulerWidget/behavior.ts | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/composables/annotationTool.ts b/src/composables/annotationTool.ts index 884513578..27c4d1792 100644 --- a/src/composables/annotationTool.ts +++ b/src/composables/annotationTool.ts @@ -10,7 +10,7 @@ import { AnnotationTool, ContextMenuEvent } from '../types/annotation-tool'; import { AnnotationToolStore } from '../store/tools/useAnnotationTool'; import { getCSSCoordinatesFromEvent } from '../utils/vtk-helpers'; -const SHOW_OVERLAY_DELAY = 500; // milliseconds +const SHOW_OVERLAY_DELAY = 250; // milliseconds // does the tools's frame of reference match // the view's axis diff --git a/src/vtk/PolygonWidget/behavior.ts b/src/vtk/PolygonWidget/behavior.ts index 467d3a5b4..2e9c2d62f 100644 --- a/src/vtk/PolygonWidget/behavior.ts +++ b/src/vtk/PolygonWidget/behavior.ts @@ -152,6 +152,7 @@ export default function widgetBehavior(publicAPI: any, model: any) { publicAPI.handleLeftButtonPress = (event: vtkMouseEvent) => { const activeWidget = model._widgetManager.getActiveWidget(); + // turns off hover while dragging publicAPI.invokeHoverEvent({ ...event, hovering: false, diff --git a/src/vtk/RulerWidget/behavior.ts b/src/vtk/RulerWidget/behavior.ts index 6cc30853a..b04c3914e 100644 --- a/src/vtk/RulerWidget/behavior.ts +++ b/src/vtk/RulerWidget/behavior.ts @@ -73,6 +73,11 @@ export default function widgetBehavior(publicAPI: any, model: any) { return macro.VOID; } + 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(); @@ -135,11 +140,6 @@ export default function widgetBehavior(publicAPI: any, model: any) { * Moves a point around. */ publicAPI.handleMouseMove = (eventData: any) => { - publicAPI.invokeHoverEvent({ - ...eventData, - hovering: !!model.activeState, - }); - const worldCoords = model.manipulator.handleEvent( eventData, model._apiSpecificRenderWindow From f28a2570f00e7c63d5300e6d87443f392bdafb6e Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Fri, 15 Sep 2023 17:33:06 -0400 Subject: [PATCH 10/15] refactor(pointState): use ANNOTATION_TOOL_HANDLE_RADIUS everywhere --- src/components/tools/BoundingRectangle.vue | 3 ++- src/components/tools/polygon/PolygonSVG2D.vue | 6 +++--- src/vtk/RulerWidget/behavior.ts | 1 + src/vtk/ToolWidgetUtils/pointState.js | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/tools/BoundingRectangle.vue b/src/components/tools/BoundingRectangle.vue index ac53f72d0..65d786a5e 100644 --- a/src/components/tools/BoundingRectangle.vue +++ b/src/components/tools/BoundingRectangle.vue @@ -45,7 +45,8 @@ const updateRectangle = () => { }); const [x, y] = vtkBoundingBox.getMinPoint(screenBounds); const [maxX, maxY] = vtkBoundingBox.getMaxPoint(screenBounds); - const handleRadius = ANNOTATION_TOOL_HANDLE_RADIUS / devicePixelRatio; + // Plus 2 to account for the stroke width + const handleRadius = (ANNOTATION_TOOL_HANDLE_RADIUS + 2) / devicePixelRatio; const handleDiameter = 2 * handleRadius; rectangle.value = { x: x - handleRadius, diff --git a/src/components/tools/polygon/PolygonSVG2D.vue b/src/components/tools/polygon/PolygonSVG2D.vue index 0b80ed199..b2625321b 100644 --- a/src/components/tools/polygon/PolygonSVG2D.vue +++ b/src/components/tools/polygon/PolygonSVG2D.vue @@ -23,7 +23,7 @@