Skip to content

Commit

Permalink
Merge pull request #428 from Kitware/tool-selection
Browse files Browse the repository at this point in the history
Annotation Tool Selection
  • Loading branch information
floryst authored Oct 10, 2023
2 parents 431e60b + a50892d commit 2043d06
Show file tree
Hide file tree
Showing 37 changed files with 645 additions and 325 deletions.
10 changes: 4 additions & 6 deletions src/components/AnnotationsModule.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
<script setup lang="ts">
import { AnnotationToolType } from '@/src/store/tools/types';
import MeasurementsToolList from './MeasurementsToolList.vue';
import LabelmapList from './LabelmapList.vue';
import ToolControls from './ToolControls.vue';
import { usePolygonStore } from '../store/tools/polygons';
import { useRectangleStore } from '../store/tools/rectangles';
import { useRulerStore } from '../store/tools/rulers';
import MeasurementRulerDetails from './MeasurementRulerDetails.vue';
const tools = [
{
store: useRulerStore(),
type: AnnotationToolType.Ruler,
icon: 'mdi-ruler',
details: MeasurementRulerDetails,
},
{
store: useRectangleStore(),
type: AnnotationToolType.Rectangle,
icon: 'mdi-vector-square',
},
{
store: usePolygonStore(),
type: AnnotationToolType.Polygon,
icon: 'mdi-pentagon-outline',
},
];
Expand Down
77 changes: 50 additions & 27 deletions src/components/MeasurementsToolList.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool';
import { frameOfReferenceToImageSliceAndAxis } from '@/src/utils/frameOfReference';
import { nonNullable } from '@/src/utils/index';
import { AnnotationToolType } from '@/src/store/tools/types';
import { useAnnotationToolStore } from '@/src/store/tools';
import {
useMultipleToolSelection,
MultipleSelectionState,
} from '@/src/composables/useMultipleToolSelection';
import { useToolSelectionStore } from '@/src/store/tools/toolSelection';
import MeasurementToolDetails from './MeasurementToolDetails.vue';
import { useMultiSelection } from '../composables/useMultiSelection';
import { AnnotationTool } from '../types/annotation-tool';
type AnnotationToolConfig = {
store: AnnotationToolStore;
type: AnnotationToolType;
icon: string;
details?: typeof MeasurementToolDetails;
};
Expand All @@ -23,7 +28,8 @@ const props = defineProps<{
const { currentImageID, currentImageMetadata } = useCurrentImage();
// Filter and add axis for specific annotation type
const getTools = (toolStore: AnnotationToolStore) => {
const getTools = (type: AnnotationToolType) => {
const toolStore = useAnnotationToolStore(type);
return toolStore.finishedTools
.filter((tool) => tool.imageID === currentImageID.value)
.map((tool) => {
Expand All @@ -44,10 +50,13 @@ const getTools = (toolStore: AnnotationToolStore) => {
// Flatten all tool types and add actions
const tools = computed(() => {
return props.tools.flatMap(
({ store, icon, details = MeasurementToolDetails }) => {
const toolsWithAxis = getTools(store);
({ type, icon, details = MeasurementToolDetails }) => {
const store = useAnnotationToolStore(type);
const toolsWithAxis = getTools(type);
return toolsWithAxis.map((tool) => ({
...tool,
id: tool.id,
type,
toolData: tool,
icon,
details,
remove: () => store.removeTool(tool.id),
Expand All @@ -66,22 +75,31 @@ const tools = computed(() => {
// --- selection and batch actions --- //
const toolIds = computed(() => tools.value.map((tool) => tool.id));
const selectionStore = useToolSelectionStore();
const { selectAll, deselectAll, selected, selectionState } =
useMultipleToolSelection(tools);
const { selected, selectedAll, selectedSome, toggleSelectAll } =
useMultiSelection(toolIds);
const toggleSelectAll = (shouldSelectAll: boolean) => {
if (shouldSelectAll) {
selectAll();
} else {
deselectAll();
}
};
const forEachSelectedTool = (
callback: (tool: (typeof tools.value)[number]) => void
) =>
selected.value
.map((id) => tools.value.find((tool) => id === tool.id))
.filter(nonNullable)
tools.value
.filter((tool) => selectionStore.isSelected(tool.id))
.forEach(callback);
function removeAll() {
forEachSelectedTool((tool) => tool.remove());
selected.value = [];
selectionStore.selection.forEach((sel) => {
const store = useAnnotationToolStore(sel.type);
store.removeTool(sel.id);
});
}
// If all selected tools are already hidden, it should be "show".
Expand All @@ -90,7 +108,7 @@ const allHidden = computed(() => {
return selected.value
.map((id) => tools.value.find((tool) => id === tool.id))
.filter(nonNullable)
.every((tool) => tool.hidden);
.every((tool) => tool.toolData.hidden);
});
function toggleGlobalHidden() {
Expand All @@ -105,10 +123,11 @@ function toggleGlobalHidden() {
<v-row no-gutters justify="space-between" align="center" class="mb-1">
<v-col class="d-flex">
<v-checkbox
:indeterminate="selectedSome && !selectedAll"
class="ml-3"
:indeterminate="selectionState === MultipleSelectionState.Some"
label="Select All"
v-model="selectedAll"
@click.stop="toggleSelectAll"
:model-value="selectionState === MultipleSelectionState.All"
@update:model-value="toggleSelectAll"
density="compact"
hide-details
/>
Expand All @@ -123,7 +142,7 @@ function toggleGlobalHidden() {
<v-btn
icon
variant="text"
:disabled="!selectedSome"
:disabled="selectionState === MultipleSelectionState.None"
@click.stop="toggleGlobalHidden"
>
<v-icon v-if="allHidden">mdi-eye-off</v-icon>
Expand All @@ -135,11 +154,15 @@ function toggleGlobalHidden() {
<v-btn
icon
variant="text"
:disabled="!selectedSome"
:disabled="selectionState === MultipleSelectionState.None"
@click.stop="removeAll"
>
<v-icon>mdi-delete</v-icon>
<v-tooltip :disabled="!selectedSome" location="top" activator="parent">
<v-tooltip
:disabled="selectionState === MultipleSelectionState.None"
location="top"
activator="parent"
>
Delete selected
</v-tooltip>
</v-btn>
Expand All @@ -163,10 +186,10 @@ function toggleGlobalHidden() {

<div
class="color-dot flex-shrink-0 mr-2"
:style="{ backgroundColor: tool.color }"
:style="{ backgroundColor: tool.toolData.color }"
/>
<v-list-item-title v-bind="$attrs">
{{ tool.labelName }}
{{ tool.toolData.labelName }}
</v-list-item-title>

<span class="ml-auto flex-shrink-0">
Expand All @@ -177,10 +200,10 @@ function toggleGlobalHidden() {
</v-tooltip>
</v-btn>
<v-btn icon variant="text" @click="tool.toggleHidden()">
<v-icon v-if="tool.hidden">mdi-eye-off</v-icon>
<v-icon v-if="tool.toolData.hidden">mdi-eye-off</v-icon>
<v-icon v-else>mdi-eye</v-icon>
<v-tooltip location="top" activator="parent">{{
tool.hidden ? 'Show' : 'Hide'
tool.toolData.hidden ? 'Show' : 'Hide'
}}</v-tooltip>
</v-btn>
<v-btn icon variant="text" @click="tool.remove()">
Expand All @@ -192,7 +215,7 @@ function toggleGlobalHidden() {

<v-row class="mt-4">
<v-list-item-subtitle class="w-100">
<component :is="tool.details" :tool="tool" />
<component :is="tool.details" :tool="tool.toolData" />
</v-list-item-subtitle>
</v-row>
</v-container>
Expand Down
31 changes: 31 additions & 0 deletions src/components/VtkTwoView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@
/>
</div>
<div class="overlay-no-events tool-layer" ref="toolContainer">
<svg class="overlay-no-events">
<bounding-rectangle :points="selectionPoints" :view-id="viewID" />
</svg>
<pan-tool :view-id="viewID" />
<zoom-tool :view-id="viewID" />
<slice-scroll-tool :view-id="viewID" />
<window-level-tool :view-id="viewID" />
<select-tool :view-id="viewID" :widget-manager="widgetManager" />
<ruler-tool
:view-id="viewID"
:widget-manager="widgetManager"
Expand Down Expand Up @@ -194,6 +198,11 @@ import { Mode as LookupTableProxyMode } from '@kitware/vtk.js/Proxy/Core/LookupT
import { manageVTKSubscription } from '@/src/composables/manageVTKSubscription';
import SliceSlider from '@/src/components/SliceSlider.vue';
import ViewOverlayGrid from '@/src/components/ViewOverlayGrid.vue';
import SelectTool from '@/src/components/tools/SelectTool.vue';
import BoundingRectangle from '@/src/components/tools/BoundingRectangle.vue';
import { useToolSelectionStore } from '@/src/store/tools/toolSelection';
import { useAnnotationToolStore } from '@/src/store/tools';
import { doesToolFrameMatchViewAxis } from '@/src/composables/annotationTool';
import { useResizeObserver } from '../composables/useResizeObserver';
import { useOrientationLabels } from '../composables/useOrientationLabels';
import { getLPSAxisFromDir } from '../utils/lps';
Expand Down Expand Up @@ -256,6 +265,7 @@ export default defineComponent({
components: {
SliceSlider,
ViewOverlayGrid,
BoundingRectangle,
WindowLevelTool,
SliceScrollTool,
PanTool,
Expand All @@ -266,6 +276,7 @@ export default defineComponent({
PaintTool,
CrosshairsTool,
CropTool,
SelectTool,
},
setup(props) {
const windowingStore = useWindowingStore();
Expand Down Expand Up @@ -740,6 +751,25 @@ export default defineComponent({
{ immediate: true, deep: true }
);
// --- selection points --- //
const selectionStore = useToolSelectionStore();
const selectionPoints = computed(() => {
return selectionStore.selection
.map((sel) => {
const store = useAnnotationToolStore(sel.type);
return { store, tool: store.toolByID[sel.id] };
})
.filter(
({ tool }) =>
tool.slice === currentSlice.value &&
doesToolFrameMatchViewAxis(viewAxis, tool)
)
.flatMap(({ store, tool }) => store.getPoints(tool.id));
});
// --- //
return {
vtkContainerRef,
toolContainer,
Expand All @@ -762,6 +792,7 @@ export default defineComponent({
resizeToFit.value = true;
},
hover,
selectionPoints,
};
},
});
Expand Down
7 changes: 5 additions & 2 deletions src/components/tools/AnnotationContextMenu.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<script setup lang="ts">
import { computed, shallowReactive } from 'vue';
import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool';
import { ContextMenuEvent, ToolID } from '@/src/types/annotation-tool';
import { WidgetAction } from '@/src/vtk/ToolWidgetUtils/utils';
import {
ContextMenuEvent,
WidgetAction,
} from '@/src/vtk/ToolWidgetUtils/types';
import { ToolID } from '@/src/types/annotation-tool';
const props = defineProps<{
toolStore: AnnotationToolStore;
Expand Down
33 changes: 25 additions & 8 deletions src/components/tools/BoundingRectangle.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
<script setup lang="ts">
import { computed, ref, watch, toRefs } from 'vue';
import { ANNOTATION_TOOL_HANDLE_RADIUS } from '@/src/constants';
import { computed, ref, watch, toRefs, toRaw, inject } from 'vue';
import { ANNOTATION_TOOL_HANDLE_RADIUS, ToolContainer } 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 type { Bounds, Vector3 } from '@kitware/vtk.js/types';
import { onVTKEvent } from '@/src/composables/onVTKEvent';
import { useResizeObserver } from '@/src/composables/useResizeObserver';
const props = defineProps<{
points: Array<Vector3>;
viewId: string;
}>();
const DEFAULT_PADDING = 2;
const props = withDefaults(
defineProps<{
points: Array<Vector3>;
viewId: string;
padding?: number;
}>(),
{
padding: DEFAULT_PADDING,
}
);
const viewStore = useViewStore();
const viewProxy = computed(
Expand All @@ -34,7 +43,7 @@ const updateRectangle = () => {
const viewRenderer = viewProxy.value.getRenderer();
const screenBounds = [...vtkBoundingBox.INIT_BOUNDS] as Bounds;
props.points
toRaw(props.points)
.map((point) => {
const point2D = worldToSVG(point, viewRenderer);
return point2D;
Expand All @@ -43,6 +52,8 @@ const updateRectangle = () => {
.forEach(([x, y]) => {
vtkBoundingBox.addPoint(screenBounds, x, y, 0);
});
vtkBoundingBox.inflate(screenBounds, props.padding);
const [x, y] = vtkBoundingBox.getMinPoint(screenBounds);
const [maxX, maxY] = vtkBoundingBox.getMaxPoint(screenBounds);
// Plus 2 to account for the stroke width
Expand All @@ -59,7 +70,13 @@ const updateRectangle = () => {
const { points } = toRefs(props);
watch(points, updateRectangle, { immediate: true, deep: true });
onVTKEvent(viewProxy, 'onModified', updateRectangle);
const camera = computed(() => viewProxy.value.getCamera());
onVTKEvent(camera, 'onModified', updateRectangle);
const containerEl = inject(ToolContainer)!;
useResizeObserver(containerEl, () => {
updateRectangle();
});
</script>

<template>
Expand Down
Loading

0 comments on commit 2043d06

Please sign in to comment.