Skip to content

Commit

Permalink
Merge pull request #404 from PaulHax/tool-hover
Browse files Browse the repository at this point in the history
Annotation Tool Hover
  • Loading branch information
floryst authored Sep 18, 2023
2 parents bed260c + 43be39a commit 58d215f
Show file tree
Hide file tree
Showing 25 changed files with 580 additions and 33 deletions.
9 changes: 9 additions & 0 deletions src/components/ToolStrip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@
/>
</groupable-item>
<div class="my-1 tool-separator" />
<groupable-item v-slot:default="{ active, toggle }" :value="Tools.Select">
<tool-button
icon="mdi-cursor-default"
name="Select"
:buttonClass="['tool-btn', active ? 'tool-btn-selected' : '']"
:disabled="noCurrentImage"
@click="toggle"
/>
</groupable-item>
<groupable-item v-slot:default="{ active, toggle }" :value="Tools.Paint">
<menu-tool-button
icon="mdi-brush"
Expand Down
44 changes: 42 additions & 2 deletions src/components/tools/AnnotationContextMenu.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts" generic="ToolID extends string">
/* 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';
Expand Down Expand Up @@ -29,6 +29,10 @@ defineExpose({
open,
});
const tool = computed(() => {
return props.toolStore.toolByID[contextMenu.forToolID];
});
const deleteToolFromContextMenu = () => {
props.toolStore.removeTool(contextMenu.forToolID);
};
Expand All @@ -52,20 +56,56 @@ const hideToolFromContextMenu = () => {
close-on-content-click
>
<v-list density="compact">
<v-list-item>
<template v-slot:prepend>
<div
class="color-dot v-icon"
:style="{ backgroundColor: tool.color }"
/>
</template>
<v-list-item-title class="v-list-item--disabled">
{{ tool.labelName }}
</v-list-item-title>
</v-list-item>

<!-- Separate informative items from interactive items -->
<v-divider></v-divider>

<v-list-item @click="hideToolFromContextMenu">
<template v-slot:prepend>
<v-icon>mdi-eye</v-icon>
</template>
<v-list-item-title>Hide</v-list-item-title>
</v-list-item>

<v-list-item @click="deleteToolFromContextMenu">
<template v-slot:prepend>
<v-icon>mdi-delete</v-icon>
</template>
<v-list-item-title>Delete Annotation</v-list-item-title>
</v-list-item>
<!-- Optional items below stable item for muscle memory -->

<!-- Optional items below stable items for muscle memory -->
<v-list-item
v-for="action in contextMenu.widgetActions"
@click="action.func"
:key="action.name"
>
<template v-slot:prepend>
<v-icon></v-icon>
</template>
<v-list-item-title>{{ action.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>

<style scoped>
.color-dot {
width: 24px;
height: 24px;
background: yellow;
border-radius: 16px;
opacity: 1 !important;
}
</style>
63 changes: 63 additions & 0 deletions src/components/tools/AnnotationInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script setup lang="ts" generic="ToolID extends string">
/* global ToolID:readonly */
import { computed, ref } from 'vue';
import { useElementSize } from '@vueuse/core';
import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool';
import { OverlayInfo } from '@/src/composables/annotationTool';
// These seem to work ¯\_(ツ)_/¯
const TOOLTIP_PADDING_X = 30;
const TOOLTIP_PADDING_Y = 20;
const props = defineProps<{
info: OverlayInfo<ToolID>;
toolStore: AnnotationToolStore<ToolID>;
}>();
const visible = computed(() => {
return props.info.visible;
});
const label = computed(() => {
if (!props.info.visible) return '';
return props.toolStore.toolByID[props.info.toolID].labelName;
});
const tooltip = ref();
const content = computed(() => {
return tooltip.value?.contentEl;
});
const { width, height } = useElementSize(content);
const offset = computed(() => {
return {
// Tooltip location is above cursor and centered
// Don't know how to get ref to parent v-tooltip element, so adding fudge padding.
x: (width.value + TOOLTIP_PADDING_X) / 2,
y: height.value + TOOLTIP_PADDING_Y,
};
});
</script>

<template>
<v-tooltip
ref="tooltip"
v-if="info.visible"
v-model="visible"
:style="{
left: `${info.displayXY[0] - offset.x}px`,
top: `${info.displayXY[1] - offset.y}px`,
zIndex: 500, // stay under context menu
}"
class="better-contrast"
>
{{ label }}
</v-tooltip>
</template>

<style scoped>
.better-contrast :deep(.v-overlay__content) {
opacity: 1 !important;
background: rgba(255, 255, 255, 0.9) !important;
}
</style>
76 changes: 76 additions & 0 deletions src/components/tools/BoundingRectangle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script setup lang="ts">
import { computed, ref, watch, toRefs } from 'vue';
import { ANNOTATION_TOOL_HANDLE_RADIUS } from '@/src/constants';
import { useViewStore } from '@/src/store/views';
import { worldToSVG } from '@/src/utils/vtk-helpers';
import { nonNullable } from '@/src/utils/index';
import vtkLPSView2DProxy from '@/src/vtk/LPSView2DProxy';
import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox';
import { Bounds, Vector3 } from '@kitware/vtk.js/types';
import { onVTKEvent } from '@/src/composables/onVTKEvent';
const props = defineProps<{
points: Array<Vector3>;
viewId: string;
}>();
const viewStore = useViewStore();
const viewProxy = computed(
() => viewStore.getViewProxy<vtkLPSView2DProxy>(props.viewId)!
);
const visible = computed(() => {
return props.points.length > 0;
});
const rectangle = ref({
x: 0,
y: 0,
width: 0,
height: 0,
});
const updateRectangle = () => {
const viewRenderer = viewProxy.value.getRenderer();
const screenBounds = [...vtkBoundingBox.INIT_BOUNDS] as Bounds;
props.points
.map((point) => {
const point2D = worldToSVG(point, viewRenderer);
return point2D;
})
.filter(nonNullable)
.forEach(([x, y]) => {
vtkBoundingBox.addPoint(screenBounds, x, y, 0);
});
const [x, y] = vtkBoundingBox.getMinPoint(screenBounds);
const [maxX, maxY] = vtkBoundingBox.getMaxPoint(screenBounds);
// 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,
y: y - handleRadius,
width: maxX - x + handleDiameter,
height: maxY - y + handleDiameter,
};
};
const { points } = toRefs(props);
watch(points, updateRectangle, { immediate: true, deep: true });
onVTKEvent(viewProxy, 'onModified', updateRectangle);
</script>

<template>
<rect
v-if="visible"
:x="rectangle.x"
:y="rectangle.y"
:width="rectangle.width"
:height="rectangle.height"
stroke-width="2"
fill="transparent"
stroke="lightgray"
/>
</template>
8 changes: 4 additions & 4 deletions src/components/tools/polygon/PolygonSVG2D.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<g>
<!-- radius is related to the vtkRectangleWidget scale, specified in state -->
<!-- radius should match constants.ANNOTATION_TOOL_HANDLE_RADIUS and should be related to vtkHandleWidget scale. -->
<circle
v-for="({ point: [x, y], radius }, index) in handlePoints"
:key="index"
Expand All @@ -23,7 +23,7 @@
<script lang="ts">
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';
Expand All @@ -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: {
Expand Down
19 changes: 19 additions & 0 deletions src/components/tools/polygon/PolygonTool.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<div class="overlay-no-events">
<svg class="overlay-no-events">
<bounding-rectangle :points="points" :view-id="viewId" />
<polygon-widget-2D
v-for="tool in tools"
:key="tool.id"
Expand All @@ -12,9 +13,11 @@
:widget-manager="widgetManager"
@contextmenu="openContextMenu(tool.id, $event)"
@placed="onToolPlaced"
@widgetHover="onHover(tool.id, $event)"
/>
</svg>
<annotation-context-menu ref="contextMenu" :tool-store="activeToolStore" />
<annotation-info :info="overlayInfo" :tool-store="activeToolStore" />
</div>
</template>

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -74,6 +80,8 @@ export default defineComponent({
components: {
PolygonWidget2D,
AnnotationContextMenu,
AnnotationInfo,
BoundingRectangle,
},
setup(props) {
const { viewDirection, currentSlice } = toRefs(props);
Expand Down Expand Up @@ -181,13 +189,24 @@ 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,
onToolPlaced,
contextMenu,
openContextMenu,
activeToolStore,
onHover,
overlayInfo,
points,
};
},
});
Expand Down
9 changes: 7 additions & 2 deletions src/components/tools/polygon/PolygonWidget2D.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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,
Expand Down Expand Up @@ -103,6 +106,8 @@ export default defineComponent({
emit('placed');
});
useHoverEvent(emit, widget);
// --- right click handling --- //
useRightClickContextMenu(emit, widget);
Expand Down
2 changes: 1 addition & 1 deletion src/components/tools/rectangle/RectangleSVG2D.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
stroke-width="1"
:fill="fillColor"
/>
<!-- radius is related to the vtkRectangleWidget scale, specified in state -->
<!-- radius should match constants.ANNOTATION_TOOL_HANDLE_RADIUS and should be related to vtkHandleWidget scale. -->
<circle
v-if="first"
:cx="first.x"
Expand Down
Loading

0 comments on commit 58d215f

Please sign in to comment.