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 }} + + + + + + + Hide + + 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 @@ + + + + + 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 @@ @@ -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; }