From 67f822a7bd3662fdfb467c53071d42b486667bb2 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Tue, 30 Apr 2024 15:47:14 -0400 Subject: [PATCH 1/5] feat(PolygonWidget): Add freehand dropping of points When holding left mouse down, points are dropped. --- src/vtk/PolygonWidget/behavior.ts | 96 ++++++++++++++++++------------- 1 file changed, 57 insertions(+), 39 deletions(-) diff --git a/src/vtk/PolygonWidget/behavior.ts b/src/vtk/PolygonWidget/behavior.ts index 6d4c80f0..fd3b1e24 100644 --- a/src/vtk/PolygonWidget/behavior.ts +++ b/src/vtk/PolygonWidget/behavior.ts @@ -25,14 +25,16 @@ export default function widgetBehavior(publicAPI: any, model: any) { model._isDragging = false; // overUnselectedHandle is true if mouse is over handle that was created before a mouse move event. - // If creating new handle and immediately dragging, - // widgetManager.getSelections() still points to the last actor - // after the mouse button is released. In this widgets case the last actor is part of the LineGlyphRepresentation. - // So overUnselectedHandle tracks if the mouse is over the new handle so we + // That happens if creating new handle and immediately dragging. + // Then widgetManager.getSelections() still points to the last actor + // after the mouse button is released. In this widgets case, the last actor is part of the LineGlyphRepresentation. + // overUnselectedHandle tracks if the mouse is over the new handle so we // don't create a another handle when clicking after without mouse move. // A mouse move event sets overUnselectedHandle to false as we can then rely on widgetManager.getSelections(). let overUnselectedHandle = false; + let freeHanding = false; + // Check if mouse is over line segment between handles const checkOverSegment = () => { // overSegment guards against clicking anywhere in view @@ -113,6 +115,8 @@ export default function widgetBehavior(publicAPI: any, model: any) { const getWorldCoords = computeWorldCoords(model); + // returns macro.EVENT_ABORT if dragging handle or finishing + // to indicate event should be consumed. function updateActiveStateHandle(callData: any) { const worldCoords = getWorldCoords(callData); @@ -138,6 +142,12 @@ export default function widgetBehavior(publicAPI: any, model: any) { return macro.VOID; } + function addHandle() { + const moveHandle = model.widgetState.getMoveHandle(); + const newHandle = model.widgetState.addHandle(); + newHandle.setOrigin(moveHandle.getOrigin()); + } + // -------------------------------------------------------------------------- // Left press: Select handle to drag / Add new handle // -------------------------------------------------------------------------- @@ -164,12 +174,13 @@ export default function widgetBehavior(publicAPI: any, model: any) { return macro.VOID; } + // Drop point? const manipulator = model.activeState?.getManipulator?.() ?? model.manipulator; if (model.widgetState.getPlacing() && manipulator) { // Dropping first point? if (model.widgetState.getHandles().length === 0) { - // For updateActiveStateHandle + // update variables used by updateActiveStateHandle model.activeState = model.widgetState.getMoveHandle(); model._widgetManager.grabFocus(publicAPI); } @@ -181,12 +192,9 @@ export default function widgetBehavior(publicAPI: any, model: any) { return macro.EVENT_ABORT; } - // Add handle - const moveHandle = model.widgetState.getMoveHandle(); - const newHandle = model.widgetState.addHandle(); - newHandle.setOrigin(moveHandle.getOrigin()); - + addHandle(); publicAPI.invokeStartInteractionEvent(); + freeHanding = true; return macro.EVENT_ABORT; } @@ -194,7 +202,6 @@ export default function widgetBehavior(publicAPI: any, model: any) { model._isDragging = true; model._apiSpecificRenderWindow.setCursor('grabbing'); model._interactor.requestAnimation(publicAPI); - publicAPI.invokeStartInteractionEvent(); return macro.EVENT_ABORT; } @@ -211,16 +218,18 @@ export default function widgetBehavior(publicAPI: any, model: any) { model.pickable && model.dragable && model.activeState && - !ignoreKey(event) + !ignoreKey(event) && + updateActiveStateHandle(event) === macro.EVENT_ABORT // side effect! ) { - if (updateActiveStateHandle(event) === macro.EVENT_ABORT) { - return macro.EVENT_ABORT; + if (freeHanding) { + addHandle(); } + return macro.EVENT_ABORT; // consume event } // widgetManager.getSelections() updates on mouse move and not animating. // (Widget triggers animation when dragging.) - // So we can rely on getSelections() to be up to date now. + // So we can rely on getSelections() to be up to date now overUnselectedHandle = false; if (model.hasFocus) { @@ -239,9 +248,33 @@ export default function widgetBehavior(publicAPI: any, model: any) { // Left release: Finish drag // -------------------------------------------------------------------------- - // Detect double click by comparing these values. + // Detect double click by diffing these. let lastReleaseTime = 0; let lastReleasePosition: Vector3 | undefined; + function isDoubleClick(event: vtkMouseEvent) { + const currentTime = Date.now(); + const currentDisplayPos = [ + event.position.x, + event.position.y, + event.position.z, + ] as Vector3; + const elapsed = currentTime - lastReleaseTime; + + const distance = lastReleasePosition + ? distance2BetweenPoints( + [event.position.x, event.position.y, event.position.z], + lastReleasePosition + ) + : Number.POSITIVE_INFINITY; + + const doubleClicked = + elapsed < DOUBLE_CLICK_TIMEOUT && + distance < DOUBLE_CLICK_SLIP_DISTANCE_MAX_SQUARED; + + lastReleaseTime = currentTime; + lastReleasePosition = currentDisplayPos; + return doubleClicked; + } publicAPI.handleLeftButtonRelease = (event: vtkMouseEvent) => { if ( @@ -252,6 +285,8 @@ export default function widgetBehavior(publicAPI: any, model: any) { return macro.VOID; } + freeHanding = false; + if (model._isDragging) { model._apiSpecificRenderWindow.setCursor('pointer'); model._interactor.cancelAnimation(publicAPI); @@ -259,37 +294,20 @@ export default function widgetBehavior(publicAPI: any, model: any) { model._widgetManager.enablePicking(); // So a following left click without moving the mouse can immediately grab the handle, // we don't call model.widgetState.deactivate() here. - publicAPI.invokeEndInteractionEvent(); return macro.EVENT_ABORT; } - // If not placing, (and not dragging) don't consume event + // If not placing (and not dragging) don't consume event // so camera control widgets can react. if (!model.widgetState.getPlacing()) { return macro.VOID; } - // Double click? Then finish. - const currentDisplayPos = [ - event.position.x, - event.position.y, - event.position.z, - ] as Vector3; - - const distance = lastReleasePosition - ? distance2BetweenPoints(currentDisplayPos, lastReleasePosition) - : Number.POSITIVE_INFINITY; - lastReleasePosition = currentDisplayPos; - - const currentTime = Date.now(); - const elapsed = currentTime - lastReleaseTime; - - const distanceThreshold = - DOUBLE_CLICK_SLIP_DISTANCE_MAX_SQUARED * - model._apiSpecificRenderWindow.getComputedDevicePixelRatio(); - - if (elapsed < DOUBLE_CLICK_TIMEOUT && distance < distanceThreshold) { + if (model.widgetState.getFinishable()) { + finishPlacing(); + } else if (isDoubleClick(event)) { + // try to finish placing const handles = model.widgetState.getHandles(); // Need 3 handles to finish. Double click created 2 handles, 1 extra. if (handles.length >= 4) { @@ -297,12 +315,12 @@ export default function widgetBehavior(publicAPI: any, model: any) { finishPlacing(); } } - lastReleaseTime = currentTime; if ( (model.hasFocus && !model.activeState) || (model.activeState && !model.activeState.getActive()) ) { + // update if mouse hovered over handle/activeState for next onDown model._widgetManager.enablePicking(); model._interactor.render(); } From 6530a0d5e1868cbb91e43a72149daa69d67d7364 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Wed, 1 May 2024 11:02:50 -0400 Subject: [PATCH 2/5] perf(polygon): decimate polygon points after placing --- src/components/tools/polygon/PolygonSVG2D.vue | 2 +- src/vtk/PolygonWidget/decimate.ts | 132 ++++++++++++++++++ src/vtk/PolygonWidget/state.ts | 21 ++- 3 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 src/vtk/PolygonWidget/decimate.ts diff --git a/src/components/tools/polygon/PolygonSVG2D.vue b/src/components/tools/polygon/PolygonSVG2D.vue index 5d805414..89f1e7e5 100644 --- a/src/components/tools/polygon/PolygonSVG2D.vue +++ b/src/components/tools/polygon/PolygonSVG2D.vue @@ -74,7 +74,7 @@ export default defineComponent({ }); // Indicate finishable - if (finishable.value && placing.value) { + if (svgPoints.length > 0 && finishable.value && placing.value) { svgPoints[0].radius = FINISHABLE_POINT_RADIUS; } diff --git a/src/vtk/PolygonWidget/decimate.ts b/src/vtk/PolygonWidget/decimate.ts new file mode 100644 index 00000000..8e78dc05 --- /dev/null +++ b/src/vtk/PolygonWidget/decimate.ts @@ -0,0 +1,132 @@ +/* eslint-disable no-continue */ + +/* +File modified from https://github.com/cornerstonejs/cornerstone3D/blob/main/packages/tools/src/utilities/math/polyline/decimate.ts + +MIT License + +Copyright (c) 2019 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import { Vector3 } from '@kitware/vtk.js/types'; +import vtkLine from '@kitware/vtk.js/Common/DataModel/Line'; + +const DEFAULT_EPSILON = 0.1; + +type Line = Array; + +/** + * Ramer–Douglas–Peucker algorithm implementation to decimate a polyline + * to a similar polyline with fewer points + * + * https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm + * https://rosettacode.org/wiki/Ramer-Douglas-Peucker_line_simplification + * https://karthaus.nl/rdp/ + * + * @param polyline - Polyline to decimate + * @param epsilon - A maximum given distance 'epsilon' to decide if a point + * should or shouldn't be added the decimated polyline version. In each + * iteration the polyline is split into two polylines and the distance of each + * point from those new polylines are checked against the line that connects + * the first and last points. + * @returns Decimated polyline + */ +export default function decimate(polyline: Line, epsilon = DEFAULT_EPSILON) { + const numPoints = polyline.length; + + // The polyline must have at least a start and end points + if (numPoints < 3) { + return polyline; + } + + const epsilonSquared = epsilon * epsilon; + const partitionQueue = [[0, numPoints - 1]]; + + // Used a boolean array to set each point that will be in the decimated polyline + // because pre-allocated arrays are 3-4x faster than thousands of push() calls + // to add all points to a new array. + const polylinePointFlags = new Array(numPoints).fill(false); + + // Start and end points are always added to the decimated polyline + let numDecimatedPoints = 2; + + // Add start and end points to the decimated polyline + polylinePointFlags[0] = true; + polylinePointFlags[numPoints - 1] = true; + + // Iterative approach using a queue instead of recursion to reduce the number + // of function calls (performance) + while (partitionQueue.length) { + const [startIndex, endIndex] = partitionQueue.pop()!; + + // Return if there is no point between the start and end points + if (endIndex - startIndex === 1) { + continue; + } + + const startPoint = polyline[startIndex]; + const endPoint = polyline[endIndex]; + let maxDistSquared = -Infinity; + let maxDistIndex = -1; + + // Search for the furthest point + for (let i = startIndex + 1; i < endIndex; i++) { + const currentPoint = polyline[i]; + const { distance: distSquared } = vtkLine.distanceToLine( + currentPoint, + startPoint, + endPoint + ); + + if (distSquared > maxDistSquared) { + maxDistSquared = distSquared; + maxDistIndex = i; + } + } + + // Do not add any of the points because the fursthest one is very close to + // the line based on the epsilon value + if (maxDistSquared < epsilonSquared) { + continue; + } + + // Update the flag for the furthest point because it will be added to the + // decimated polyline + polylinePointFlags[maxDistIndex] = true; + numDecimatedPoints++; + + // Partition the points into two parts using maxDistIndex as the pivot point + // and process both sides + partitionQueue.push([maxDistIndex, endIndex]); + partitionQueue.push([startIndex, maxDistIndex]); + } + + // A pre-allocated array is 3-4x faster then multiple push() calls + const decimatedPolyline: Line = new Array(numDecimatedPoints); + + for (let srcIndex = 0, dstIndex = 0; srcIndex < numPoints; srcIndex++) { + if (polylinePointFlags[srcIndex]) { + decimatedPolyline[dstIndex++] = polyline[srcIndex]; + } + } + + return decimatedPolyline; +} diff --git a/src/vtk/PolygonWidget/state.ts b/src/vtk/PolygonWidget/state.ts index 57aa9a3f..c27bdbe1 100644 --- a/src/vtk/PolygonWidget/state.ts +++ b/src/vtk/PolygonWidget/state.ts @@ -9,6 +9,7 @@ import { Polygon } from '@/src/types/polygon'; import { AnnotationToolType } from '@/src/store/tools/types'; import createPointState from '../ToolWidgetUtils/pointState'; import { watchState } from '../ToolWidgetUtils/utils'; +import decimate from './decimate'; export const MoveHandleLabel = 'moveHandle'; export const HandlesLabel = 'handles'; @@ -110,16 +111,26 @@ function vtkPolygonWidgetState(publicAPI: any, model: any) { } }; + const addPointsAsHandles = () => { + getTool().points.forEach((point) => { + const handle = publicAPI.addHandle({ addPoint: false }); + handle.setOrigin(point); + }); + }; + publicAPI.getPlacing = () => getTool().placing; + publicAPI.setPlacing = (placing: boolean) => { - getTool().placing = placing; + const tool = getTool(); + const optimizedLine = decimate(tool.points); + publicAPI.clearHandles(); + tool.points = optimizedLine; + addPointsAsHandles(); + tool.placing = placing; }; // Setup after deserialization - getTool().points.forEach((point: Vector3) => { - const handle = publicAPI.addHandle({ addPoint: false }); - handle.setOrigin(point); - }); + addPointsAsHandles(); } const defaultValues = (initialValues: any) => ({ From 7ff3a4839f64b7bbda9d9eee83df27d566d0359a Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Wed, 1 May 2024 12:58:46 -0400 Subject: [PATCH 3/5] fix(polygon): use pixel size during line decimation --- src/utils/frameOfReference.ts | 12 ++++++++++++ src/vtk/PolygonWidget/state.ts | 34 ++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/utils/frameOfReference.ts b/src/utils/frameOfReference.ts index 9636fd4d..3c9b357a 100644 --- a/src/utils/frameOfReference.ts +++ b/src/utils/frameOfReference.ts @@ -76,3 +76,15 @@ export function frameOfReferenceToImageSliceAndAxis( return { axis, slice }; } + +export function getPixelSizeSquared( + frame: FrameOfReference, + metadata: ImageMetadata +): number | null { + const toolImageFrame = frameOfReferenceToImageSliceAndAxis(frame, metadata); + if (!toolImageFrame) return null; + const axisIndex = metadata.lpsOrientation[toolImageFrame.axis]; + const spacing = [...metadata.spacing]; + spacing.splice(axisIndex, 1); + return spacing[0] * spacing[0] + spacing[1] * spacing[1]; +} diff --git a/src/vtk/PolygonWidget/state.ts b/src/vtk/PolygonWidget/state.ts index c27bdbe1..82f0d574 100644 --- a/src/vtk/PolygonWidget/state.ts +++ b/src/vtk/PolygonWidget/state.ts @@ -7,6 +7,8 @@ import { Vector3 } from '@kitware/vtk.js/types'; import vtkAnnotationWidgetState from '@/src/vtk/ToolWidgetUtils/annotationWidgetState'; import { Polygon } from '@/src/types/polygon'; import { AnnotationToolType } from '@/src/store/tools/types'; +import { getImageMetadata } from '@/src/composables/useCurrentImage'; +import { getPixelSizeSquared } from '@/src/utils/frameOfReference'; import createPointState from '../ToolWidgetUtils/pointState'; import { watchState } from '../ToolWidgetUtils/utils'; import decimate from './decimate'; @@ -14,7 +16,8 @@ import decimate from './decimate'; export const MoveHandleLabel = 'moveHandle'; export const HandlesLabel = 'handles'; -const PIXEL_SIZE = 20; +const HANDLE_PIXEL_SIZE = 20; +const DECIMATE_PIXEL_SIZE_FACTOR = 0.01; type VtkObjectModel = { classHierarchy: string[]; @@ -66,7 +69,9 @@ function vtkPolygonWidgetState(publicAPI: any, model: any) { }; vtkWidgetState.extend(handlePublicAPI, handleModel, {}); visibleMixin.extend(handlePublicAPI, handleModel, { visible: true }); - scale1Mixin.extend(handlePublicAPI, handleModel, { scale1: PIXEL_SIZE }); + scale1Mixin.extend(handlePublicAPI, handleModel, { + scale1: HANDLE_PIXEL_SIZE, + }); const handleModelPromoted = handleModel as HandleModel; handleModelPromoted.classHierarchy.push('vtkPolygonHandleState'); @@ -122,11 +127,28 @@ function vtkPolygonWidgetState(publicAPI: any, model: any) { publicAPI.setPlacing = (placing: boolean) => { const tool = getTool(); - const optimizedLine = decimate(tool.points); - publicAPI.clearHandles(); - tool.points = optimizedLine; - addPointsAsHandles(); tool.placing = placing; + if (placing) return; + + // Decimate points + const imageMeta = getImageMetadata(tool.imageID); + const pixelSizeSquared = getPixelSizeSquared( + tool.frameOfReference, + imageMeta + ); + if (pixelSizeSquared) { + const optimizedLine = decimate( + tool.points, + pixelSizeSquared * DECIMATE_PIXEL_SIZE_FACTOR + ); + publicAPI.clearHandles(); + tool.points = optimizedLine; + addPointsAsHandles(); + } else { + console.error( + 'Off LPS axis pixel sizing not implemented. Not decimating line.' + ); + } }; // Setup after deserialization From 253d2611406f9fc44e81d69e3cb41640f8bcf9bc Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 2 May 2024 15:03:39 -0400 Subject: [PATCH 4/5] feat(polygon): decimate with smallest pixel dimension --- src/utils/frameOfReference.ts | 12 ++++++------ src/vtk/PolygonWidget/state.ts | 27 +++++++++------------------ 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/utils/frameOfReference.ts b/src/utils/frameOfReference.ts index 3c9b357a..016e6999 100644 --- a/src/utils/frameOfReference.ts +++ b/src/utils/frameOfReference.ts @@ -77,14 +77,14 @@ export function frameOfReferenceToImageSliceAndAxis( return { axis, slice }; } -export function getPixelSizeSquared( +export function getSmallestSpacing( frame: FrameOfReference, metadata: ImageMetadata -): number | null { - const toolImageFrame = frameOfReferenceToImageSliceAndAxis(frame, metadata); - if (!toolImageFrame) return null; - const axisIndex = metadata.lpsOrientation[toolImageFrame.axis]; +): number { + const sliceAxis = frameOfReferenceToImageSliceAndAxis(frame, metadata); + if (!sliceAxis) return Math.min(...metadata.spacing); // off orthogonal + const axisIndex = metadata.lpsOrientation[sliceAxis.axis]; const spacing = [...metadata.spacing]; spacing.splice(axisIndex, 1); - return spacing[0] * spacing[0] + spacing[1] * spacing[1]; + return Math.min(...spacing); } diff --git a/src/vtk/PolygonWidget/state.ts b/src/vtk/PolygonWidget/state.ts index 82f0d574..8d956423 100644 --- a/src/vtk/PolygonWidget/state.ts +++ b/src/vtk/PolygonWidget/state.ts @@ -8,7 +8,7 @@ import vtkAnnotationWidgetState from '@/src/vtk/ToolWidgetUtils/annotationWidget import { Polygon } from '@/src/types/polygon'; import { AnnotationToolType } from '@/src/store/tools/types'; import { getImageMetadata } from '@/src/composables/useCurrentImage'; -import { getPixelSizeSquared } from '@/src/utils/frameOfReference'; +import { getSmallestSpacing } from '@/src/utils/frameOfReference'; import createPointState from '../ToolWidgetUtils/pointState'; import { watchState } from '../ToolWidgetUtils/utils'; import decimate from './decimate'; @@ -17,7 +17,7 @@ export const MoveHandleLabel = 'moveHandle'; export const HandlesLabel = 'handles'; const HANDLE_PIXEL_SIZE = 20; -const DECIMATE_PIXEL_SIZE_FACTOR = 0.01; +const DECIMATE_PIXEL_SIZE_FACTOR = 0.2; type VtkObjectModel = { classHierarchy: string[]; @@ -132,23 +132,14 @@ function vtkPolygonWidgetState(publicAPI: any, model: any) { // Decimate points const imageMeta = getImageMetadata(tool.imageID); - const pixelSizeSquared = getPixelSizeSquared( - tool.frameOfReference, - imageMeta + const pixelScale = getSmallestSpacing(tool.frameOfReference, imageMeta); + const optimizedLine = decimate( + tool.points, + pixelScale * DECIMATE_PIXEL_SIZE_FACTOR ); - if (pixelSizeSquared) { - const optimizedLine = decimate( - tool.points, - pixelSizeSquared * DECIMATE_PIXEL_SIZE_FACTOR - ); - publicAPI.clearHandles(); - tool.points = optimizedLine; - addPointsAsHandles(); - } else { - console.error( - 'Off LPS axis pixel sizing not implemented. Not decimating line.' - ); - } + publicAPI.clearHandles(); + tool.points = optimizedLine; + addPointsAsHandles(); }; // Setup after deserialization From 7dc882341b7c49e5151ec4419728e76faba4a998 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 2 May 2024 18:51:22 -0400 Subject: [PATCH 5/5] feat(polygon): only show handles on hover --- src/components/tools/polygon/PolygonSVG2D.vue | 38 +++++++++++++++++-- .../tools/polygon/PolygonWidget2D.vue | 27 ++++++++++++- src/vtk/PolygonWidget/behavior.ts | 23 +++++------ src/vtk/PolygonWidget/index.d.ts | 1 + 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/components/tools/polygon/PolygonSVG2D.vue b/src/components/tools/polygon/PolygonSVG2D.vue index 89f1e7e5..fe50614a 100644 --- a/src/components/tools/polygon/PolygonSVG2D.vue +++ b/src/components/tools/polygon/PolygonSVG2D.vue @@ -9,6 +9,7 @@ :stroke-width="strokeWidth" fill="transparent" :r="radius" + :visibility="index === 0 ? firstHandleVisibility : handleVisibility" /> { + return points.value.length > 0 && placing.value && finishable.value; + }); + + const handleVisibility = computed(() => { + return showHandles.value ? 'visible' : 'hidden'; + }); + const firstHandleVisibility = computed(() => { + if (finishPossible.value) { + return 'visible'; + } + return handleVisibility.value; + }); + type SVGPoint = { point: Vector2; radius: number }; const handlePoints = ref>([]); const linePoints = ref(''); @@ -74,7 +102,7 @@ export default defineComponent({ }); // Indicate finishable - if (svgPoints.length > 0 && finishable.value && placing.value) { + if (finishPossible.value) { svgPoints[0].radius = FINISHABLE_POINT_RADIUS; } @@ -117,6 +145,8 @@ export default defineComponent({ devicePixelRatio, handlePoints, linePoints, + firstHandleVisibility, + handleVisibility, }; }, }); diff --git a/src/components/tools/polygon/PolygonWidget2D.vue b/src/components/tools/polygon/PolygonWidget2D.vue index 8bb82c6a..6c151f6e 100644 --- a/src/components/tools/polygon/PolygonWidget2D.vue +++ b/src/components/tools/polygon/PolygonWidget2D.vue @@ -9,6 +9,7 @@ import { watchEffect, inject, onUnmounted, + ref, } from 'vue'; import vtkPlaneManipulator from '@kitware/vtk.js/Widgets/Manipulators/PlaneManipulator'; import { useImage } from '@/src/composables/useCurrentImage'; @@ -16,10 +17,10 @@ import { updatePlaneManipulatorFor2DView } from '@/src/utils/manipulators'; import { LPSAxisDir } from '@/src/types/lps'; import { onVTKEvent } from '@/src/composables/onVTKEvent'; import { - useHoverEvent, useRightClickContextMenu, useWidgetVisibility, } from '@/src/composables/annotationTool'; +import { getCSSCoordinatesFromEvent } from '@/src/utils/vtk-helpers'; import { usePolygonStore as useStore } from '@/src/store/tools/polygons'; import vtkWidgetFactory, { vtkPolygonViewWidget as WidgetView, @@ -92,7 +93,27 @@ export default defineComponent({ emit('placed'); }); - useHoverEvent(emit, widget); + const lastHoverEventData = ref(null); + onVTKEvent(widget, 'onHoverEvent', (eventData: any) => { + lastHoverEventData.value = eventData; + }); + const dragging = ref(false); + onVTKEvent(widget, 'onDraggingEvent', (eventData: any) => { + dragging.value = eventData.dragging; + }); + const showHandles = computed(() => { + return lastHoverEventData.value?.hovering && !dragging.value; + }); + watchEffect(() => { + if (!lastHoverEventData.value) return; + const displayXY = getCSSCoordinatesFromEvent(lastHoverEventData.value); + if (displayXY) { + emit('widgetHover', { + displayXY, + hovering: lastHoverEventData.value?.hovering && !dragging.value, + }); + } + }); // --- right click handling --- // @@ -134,6 +155,7 @@ export default defineComponent({ slice, tool, editState, + showHandles, }; }, }); @@ -148,5 +170,6 @@ export default defineComponent({ :move-point="editState.movePoint" :placing="tool.placing" :finishable="editState.finishable" + :show-handles="showHandles" /> diff --git a/src/vtk/PolygonWidget/behavior.ts b/src/vtk/PolygonWidget/behavior.ts index fd3b1e24..b2f63b13 100644 --- a/src/vtk/PolygonWidget/behavior.ts +++ b/src/vtk/PolygonWidget/behavior.ts @@ -22,7 +22,13 @@ const DOUBLE_CLICK_SLIP_DISTANCE_MAX_SQUARED = export default function widgetBehavior(publicAPI: any, model: any) { model.classHierarchy.push('vtkPolygonWidgetBehavior'); - model._isDragging = false; + + const setDragging = (isDragging: boolean) => { + model._dragging = isDragging; + publicAPI.invokeDraggingEvent({ + dragging: isDragging, + }); + }; // overUnselectedHandle is true if mouse is over handle that was created before a mouse move event. // That happens if creating new handle and immediately dragging. @@ -52,6 +58,7 @@ export default function widgetBehavior(publicAPI: any, model: any) { macro.event(publicAPI, model, 'RightClickEvent'); macro.event(publicAPI, model, 'PlacedEvent'); macro.event(publicAPI, model, 'HoverEvent'); + macro.event(publicAPI, model, 'DraggingEvent'); publicAPI.resetInteractions = () => { model._interactor.cancelAnimation(publicAPI, true); @@ -123,7 +130,7 @@ export default function widgetBehavior(publicAPI: any, model: any) { if ( worldCoords?.length && (model.activeState === model.widgetState.getMoveHandle() || - model._isDragging) + model._dragging) ) { model.activeState.setOrigin(worldCoords); @@ -155,12 +162,6 @@ 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, - }); - if ( !model.manipulator || ignoreKey(event) || @@ -199,7 +200,7 @@ export default function widgetBehavior(publicAPI: any, model: any) { } if (model.activeState?.getActive() && model.pickable && model.dragable) { - model._isDragging = true; + setDragging(true); model._apiSpecificRenderWindow.setCursor('grabbing'); model._interactor.requestAnimation(publicAPI); publicAPI.invokeStartInteractionEvent(); @@ -287,10 +288,10 @@ export default function widgetBehavior(publicAPI: any, model: any) { freeHanding = false; - if (model._isDragging) { + if (model._dragging) { model._apiSpecificRenderWindow.setCursor('pointer'); model._interactor.cancelAnimation(publicAPI); - model._isDragging = false; + setDragging(false); model._widgetManager.enablePicking(); // So a following left click without moving the mouse can immediately grab the handle, // we don't call model.widgetState.deactivate() here. diff --git a/src/vtk/PolygonWidget/index.d.ts b/src/vtk/PolygonWidget/index.d.ts index 0c793a84..513a82c6 100644 --- a/src/vtk/PolygonWidget/index.d.ts +++ b/src/vtk/PolygonWidget/index.d.ts @@ -25,6 +25,7 @@ export interface vtkPolygonWidgetState extends vtkAnnotationWidgetState { export interface vtkPolygonViewWidget extends vtkAnnotationToolWidget { getWidgetState(): vtkPolygonWidgetState; + onDraggingEvent(callback: (e: any) => void): vtkSubscription; } export interface IPolygonWidgetInitialValues