Skip to content

Commit

Permalink
feat(ruler): separate out placing ruler
Browse files Browse the repository at this point in the history
A dedicated placing tool avoids mixing placing logic with regular tool
logic.
  • Loading branch information
floryst committed Sep 13, 2023
1 parent 370cbce commit c483e97
Show file tree
Hide file tree
Showing 16 changed files with 401 additions and 217 deletions.
163 changes: 163 additions & 0 deletions src/components/tools/ruler/PlacingRulerWidget2D.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<script lang="ts">
import vtkRulerWidget, {
InteractionState,
vtkRulerViewWidget,
vtkRulerWidgetState,
} from '@/src/vtk/RulerWidget';
import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager';
import {
computed,
defineComponent,
onMounted,
onUnmounted,
PropType,
toRefs,
watch,
watchEffect,
} from 'vue';
import vtkPlaneManipulator from '@kitware/vtk.js/Widgets/Manipulators/PlaneManipulator';
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 RulerSVG2D from '@/src/components/tools/ruler/RulerSVG2D.vue';
import createStandaloneState from '@/src/vtk/RulerWidget/standaloneState';
import {
RulerInitState,
useSyncedRulerState,
} from '@/src/components/tools/ruler/common';
import { useViewWidget } from '@/src/composables/useViewWidget';
export default defineComponent({
name: 'PlacingRulerWidget2D',
emits: ['placed'],
props: {
widgetManager: {
type: Object as PropType<vtkWidgetManager>,
required: true,
},
viewId: {
type: String,
required: true,
},
viewDirection: {
type: String as PropType<LPSAxisDir>,
required: true,
},
currentSlice: {
type: Number,
required: true,
},
color: String,
},
components: {
RulerSVG2D,
},
setup(props, { emit }) {
const { widgetManager, viewDirection, currentSlice } = toRefs(props);
const { currentImageID, currentImageMetadata } = useCurrentImage();
const widgetState = createStandaloneState() as vtkRulerWidgetState;
const widgetFactory = vtkRulerWidget.newInstance({
widgetState,
});
const syncedState = useSyncedRulerState(widgetFactory);
const widget = useViewWidget<vtkRulerViewWidget>(
widgetFactory,
widgetManager
);
onMounted(() => {
widget.value!.setInteractionState(InteractionState.PlacingFirst);
});
onUnmounted(() => {
widgetFactory.delete();
});
// --- reset on slice/image changes --- //
watch([currentSlice, currentImageID, widget], () => {
if (widget.value) {
widget.value.resetInteractions();
widget.value.setInteractionState(InteractionState.PlacingFirst);
}
});
// --- placed event --- //
onVTKEvent(widget, 'onPlacedEvent', () => {
const { firstPoint, secondPoint } = syncedState;
if (!firstPoint.origin || !secondPoint.origin)
throw new Error('Incomplete placing widget state');
const initState: RulerInitState = {
firstPoint: firstPoint.origin,
secondPoint: secondPoint.origin,
};
emit('placed', initState);
widget.value!.resetState();
widget.value!.setInteractionState(InteractionState.PlacingFirst);
});
// --- manipulator --- //
const manipulator = vtkPlaneManipulator.newInstance();
onMounted(() => {
if (!widget.value) {
return;
}
widget.value.setManipulator(manipulator);
});
watchEffect(() => {
updatePlaneManipulatorFor2DView(
manipulator,
viewDirection.value,
currentSlice.value,
currentImageMetadata.value
);
});
// --- visibility --- //
onMounted(() => {
if (!widget.value) {
return;
}
// hide handle visibility, but not picking visibility
widget.value.setHandleVisibility(false);
widgetManager.value.renderWidgets();
});
// --- //
return {
firstPoint: computed(() => {
return syncedState.firstPoint.visible
? syncedState.firstPoint.origin
: null;
}),
secondPoint: computed(() => {
return syncedState.secondPoint.visible
? syncedState.secondPoint.origin
: null;
}),
length: computed(() => syncedState.length),
};
},
});
</script>

<template>
<RulerSVG2D
:view-id="viewId"
:point1="firstPoint"
:point2="secondPoint"
:length="length"
:color="color"
/>
</template>
5 changes: 3 additions & 2 deletions src/components/tools/ruler/RulerSVG2D.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
watch,
inject,
} from 'vue';
import { Maybe } from '@/src/types';
type SVGPoint = {
x: number;
Expand All @@ -73,8 +74,8 @@ type SVGPoint = {
export default defineComponent({
props: {
point1: Array as PropType<Array<number>>,
point2: Array as PropType<Array<number>>,
point1: Array as PropType<Maybe<Array<number>>>,
point2: Array as PropType<Maybe<Array<number>>>,
color: String,
length: Number,
viewId: {
Expand Down
141 changes: 35 additions & 106 deletions src/components/tools/ruler/RulerTool.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
v-for="ruler in rulers"
:key="ruler.id"
:ruler-id="ruler.id"
:is-placing="ruler.id === placingRulerID"
:current-slice="currentSlice"
:view-id="viewId"
:view-direction="viewDirection"
:widget-manager="widgetManager"
@contextmenu="openContextMenu(ruler.id, $event)"
/>
<placing-ruler-widget-2D
v-if="isToolActive"
:current-slice="currentSlice"
:color="activeLabelColor"
:view-id="viewId"
:view-direction="viewDirection"
:widget-manager="widgetManager"
@placed="onRulerPlaced"
/>
</svg>
Expand All @@ -19,32 +26,24 @@
</template>

<script lang="ts">
import {
computed,
defineComponent,
onUnmounted,
PropType,
ref,
toRefs,
watch,
} from 'vue';
import { computed, defineComponent, PropType, toRefs } from 'vue';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import { useToolStore } from '@/src/store/tools';
import { Tools } from '@/src/store/tools/types';
import { useRulerStore } from '@/src/store/tools/rulers';
import { getLPSAxisFromDir } from '@/src/utils/lps';
import RulerWidget2D from '@/src/components/tools/ruler/RulerWidget2D.vue';
import PlacingRulerWidget2D from '@/src/components/tools/ruler/PlacingRulerWidget2D.vue';
import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager';
import type { Vector3 } from '@kitware/vtk.js/types';
import { LPSAxisDir } from '@/src/types/lps';
import { storeToRefs } from 'pinia';
import { FrameOfReference } from '@/src/utils/frameOfReference';
import { vec3 } from 'gl-matrix';
import {
useContextMenu,
useCurrentTools,
} from '@/src/composables/annotationTool';
import AnnotationContextMenu from '@/src/components/tools/AnnotationContextMenu.vue';
import { RulerInitState } from '@/src/components/tools/ruler/common';
import { useCurrentFrameOfReference } from '@/src/composables/useCurrentFrameOfReference';
export default defineComponent({
name: 'RulerTool',
Expand All @@ -68,6 +67,7 @@ export default defineComponent({
},
components: {
RulerWidget2D,
PlacingRulerWidget2D,
AnnotationContextMenu,
},
setup(props) {
Expand All @@ -76,105 +76,31 @@ export default defineComponent({
const rulerStore = useRulerStore();
const { activeLabel } = storeToRefs(rulerStore);
const { currentImageID, currentImageMetadata } = useCurrentImage();
const { currentImageID } = useCurrentImage();
const isToolActive = computed(() => toolStore.currentTool === Tools.Ruler);
const viewAxis = computed(() => getLPSAxisFromDir(viewDirection.value));
const placingRulerID = ref<string | null>(null);
// --- active ruler management --- //
watch(
placingRulerID,
(id, prevId) => {
if (prevId != null) {
rulerStore.updateRuler(prevId, { placing: false });
}
if (id != null) {
rulerStore.updateRuler(id, { placing: true });
}
},
{ immediate: true }
);
watch(
[isToolActive, currentImageID] as const,
([active, imageID]) => {
if (placingRulerID.value != null) {
rulerStore.removeRuler(placingRulerID.value);
placingRulerID.value = null;
}
if (active && imageID) {
placingRulerID.value = rulerStore.addRuler({
imageID,
placing: true,
});
}
},
{ immediate: true }
const currentFrameOfReference = useCurrentFrameOfReference(
viewDirection,
currentSlice
);
watch(
[activeLabel, placingRulerID],
([label, placingTool]) => {
if (placingTool != null) {
rulerStore.updateRuler(placingTool, {
label,
...(label && rulerStore.labels[label]),
});
}
},
{ immediate: true }
);
onUnmounted(() => {
if (placingRulerID.value != null) {
rulerStore.removeRuler(placingRulerID.value);
placingRulerID.value = null;
}
});
const onRulerPlaced = () => {
if (currentImageID.value) {
placingRulerID.value = rulerStore.addRuler({
imageID: currentImageID.value,
placing: true,
});
}
};
// --- updating active ruler frame --- //
// TODO useCurrentFrameOfReference(viewDirection)
const getCurrentFrameOfReference = (): FrameOfReference => {
const { lpsOrientation, indexToWorld } = currentImageMetadata.value;
const planeNormal = lpsOrientation[viewDirection.value] as Vector3;
const lpsIdx = lpsOrientation[viewAxis.value];
const planeOrigin: Vector3 = [0, 0, 0];
planeOrigin[lpsIdx] = currentSlice.value;
// convert index pt to world pt
vec3.transformMat4(planeOrigin, planeOrigin, indexToWorld);
return {
planeNormal,
planeOrigin,
};
const onRulerPlaced = (initState: RulerInitState) => {
if (!currentImageID.value) return;
rulerStore.addRuler({
imageID: currentImageID.value,
frameOfReference: currentFrameOfReference.value,
slice: currentSlice.value,
label: activeLabel.value,
color: activeLabel.value
? rulerStore.labels[activeLabel.value].color
: undefined,
firstPoint: initState.firstPoint,
secondPoint: initState.secondPoint,
});
};
// update active ruler's frame + slice, since the
// active ruler is not finalized.
watch(
[currentSlice, placingRulerID] as const,
([slice, rulerID]) => {
if (!rulerID) return;
rulerStore.updateRuler(rulerID, {
frameOfReference: getCurrentFrameOfReference(),
slice,
});
},
{ immediate: true }
);
// --- right-click menu --- //
const { contextMenu, openContextMenu } = useContextMenu();
Expand All @@ -192,7 +118,10 @@ export default defineComponent({
return {
rulers: currentRulers,
placingRulerID,
isToolActive,
activeLabelColor: computed(() => {
return activeLabel.value && rulerStore.labels[activeLabel.value].color;
}),
onRulerPlaced,
contextMenu,
openContextMenu,
Expand Down
Loading

0 comments on commit c483e97

Please sign in to comment.