Skip to content

Commit 3bc0c47

Browse files
committed
refactor(cine): render cine through an explicit effective-view layer
Replace the shims that masqueraded a cine clip as a Coronal/Sagittal/2D slice viewer with a discriminated EffectiveView resolver. Slot ViewInfo keeps representing user intent; the resolver computes render truth on read, so a 3D-stored slot bound to cine renders CineViewer and snaps back to the volume viewer when cine is replaced — without mutating viewInfo. - CineViewer / CineViewerOverlay with cine-specific scrub manipulators - cinePlaybackStore gains frame; useCineFrame clamps against live range - useSliceInfo / useAnnotationTool / paint / crosshairs route through computeEffectiveView; getEffectiveViewAxis and getRenderSlice deleted - Annotation tools place via a Locator adapter, gaining tool.frame for cine; locatorMatches gates per-frame visibility and locatorPatch pins cine annotations to a degenerate FoR + frame - isToolAllowedFor coerces Paint/Crop/Crosshairs to Select on cine and reacts to active-view kind changes (not just slot switches) - ControlsStripTools isObliqueLayout derives from effective kind so cine in a stored-Oblique slot enables annotation buttons
1 parent 96841be commit 3bc0c47

53 files changed

Lines changed: 1472 additions & 325 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/components/CineViewer.vue

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<template>
2+
<div
3+
class="vtk-container-wrapper"
4+
tabindex="0"
5+
@pointerenter="hover = true"
6+
@pointerleave="hover = false"
7+
@focusin="hover = true"
8+
@focusout="hover = false"
9+
>
10+
<div class="vtk-gutter mt-1">
11+
<v-btn dark icon size="medium" variant="text" @click="resetCamera">
12+
<v-icon size="medium" class="py-1">mdi-camera-flip-outline</v-icon>
13+
<v-tooltip
14+
location="right"
15+
activator="parent"
16+
transition="slide-x-transition"
17+
>
18+
Reset Camera
19+
</v-tooltip>
20+
</v-btn>
21+
<slice-slider
22+
v-model="currentFrame"
23+
class="slice-slider"
24+
:min="frameRange[0]"
25+
:max="frameRange[1]"
26+
:step="1"
27+
:handle-height="20"
28+
/>
29+
</div>
30+
<div class="vtk-container" data-testid="two-view-container">
31+
<v-progress-linear
32+
v-if="isImageLoading"
33+
indeterminate
34+
class="loading-indicator"
35+
height="2"
36+
color="grey"
37+
/>
38+
<div class="vtk-sub-container">
39+
<vtk-slice-view
40+
class="vtk-view"
41+
ref="vtkView"
42+
data-testid="vtk-view vtk-cine-view"
43+
:view-id="viewId"
44+
:image-id="currentImageID"
45+
:view-direction="VIEW_DIRECTION"
46+
:view-up="VIEW_UP"
47+
>
48+
<vtk-mouse-interaction-manipulator
49+
v-if="currentTool === Tools.Pan"
50+
:manipulator-constructor="vtkMouseCameraTrackballPanManipulator"
51+
:manipulator-props="{ button: 1 }"
52+
></vtk-mouse-interaction-manipulator>
53+
<vtk-mouse-interaction-manipulator
54+
:manipulator-constructor="vtkMouseCameraTrackballPanManipulator"
55+
:manipulator-props="{ button: 1, shift: true }"
56+
></vtk-mouse-interaction-manipulator>
57+
<vtk-mouse-interaction-manipulator
58+
:manipulator-constructor="vtkMouseCameraTrackballPanManipulator"
59+
:manipulator-props="{ button: 2 }"
60+
></vtk-mouse-interaction-manipulator>
61+
<vtk-mouse-interaction-manipulator
62+
v-if="currentTool === Tools.Zoom"
63+
:manipulator-constructor="
64+
vtkMouseCameraTrackballZoomToMouseManipulator
65+
"
66+
:manipulator-props="{ button: 1 }"
67+
></vtk-mouse-interaction-manipulator>
68+
<vtk-mouse-interaction-manipulator
69+
:manipulator-constructor="
70+
vtkMouseCameraTrackballZoomToMouseManipulator
71+
"
72+
:manipulator-props="{ button: 3 }"
73+
></vtk-mouse-interaction-manipulator>
74+
<vtk-cine-scrub-manipulator
75+
:view-id="viewId"
76+
:image-id="currentImageID"
77+
></vtk-cine-scrub-manipulator>
78+
<vtk-cine-scrub-key-manipulator
79+
:view-id="viewId"
80+
:image-id="currentImageID"
81+
></vtk-cine-scrub-key-manipulator>
82+
<cine-viewer-overlay
83+
:view-id="viewId"
84+
:image-id="currentImageID"
85+
></cine-viewer-overlay>
86+
<vtk-base-slice-representation
87+
ref="baseSliceRep"
88+
:view-id="viewId"
89+
:image-id="currentImageID"
90+
:axis="VIEW_AXIS"
91+
:frame="currentFrame"
92+
></vtk-base-slice-representation>
93+
<polygon-tool
94+
:view-id="viewId"
95+
:image-id="currentImageID"
96+
:view-direction="VIEW_DIRECTION"
97+
/>
98+
<ruler-tool
99+
:view-id="viewId"
100+
:image-id="currentImageID"
101+
:view-direction="VIEW_DIRECTION"
102+
/>
103+
<rectangle-tool
104+
:view-id="viewId"
105+
:image-id="currentImageID"
106+
:view-direction="VIEW_DIRECTION"
107+
/>
108+
<select-tool />
109+
<svg class="overlay-no-events">
110+
<bounding-rectangle :points="selectionPoints" />
111+
</svg>
112+
<slot></slot>
113+
</vtk-slice-view>
114+
</div>
115+
</div>
116+
</div>
117+
</template>
118+
119+
<script setup lang="ts">
120+
import { ref, toRefs, computed } from 'vue';
121+
import { storeToRefs } from 'pinia';
122+
import { useCurrentImage } from '@/src/composables/useCurrentImage';
123+
import VtkSliceView from '@/src/components/vtk/VtkSliceView.vue';
124+
import { VtkViewApi } from '@/src/types/vtk-types';
125+
import { Tools } from '@/src/store/tools/types';
126+
import VtkBaseSliceRepresentation from '@/src/components/vtk/VtkBaseSliceRepresentation.vue';
127+
import { useViewAnimationListener } from '@/src/composables/useViewAnimationListener';
128+
import PolygonTool from '@/src/components/tools/polygon/PolygonTool.vue';
129+
import RulerTool from '@/src/components/tools/ruler/RulerTool.vue';
130+
import RectangleTool from '@/src/components/tools/rectangle/RectangleTool.vue';
131+
import SelectTool from '@/src/components/tools/SelectTool.vue';
132+
import BoundingRectangle from '@/src/components/tools/BoundingRectangle.vue';
133+
import SliceSlider from '@/src/components/SliceSlider.vue';
134+
import CineViewerOverlay from '@/src/components/CineViewerOverlay.vue';
135+
import { useToolSelectionStore } from '@/src/store/tools/toolSelection';
136+
import { useAnnotationToolStore, useToolStore } from '@/src/store/tools';
137+
import { useWebGLWatchdog } from '@/src/composables/useWebGLWatchdog';
138+
import { useCineFrame } from '@/src/composables/useCineFrame';
139+
import VtkCineScrubManipulator from '@/src/components/vtk/VtkCineScrubManipulator.vue';
140+
import VtkCineScrubKeyManipulator from '@/src/components/vtk/VtkCineScrubKeyManipulator.vue';
141+
import VtkMouseInteractionManipulator from '@/src/components/vtk/VtkMouseInteractionManipulator.vue';
142+
import vtkMouseCameraTrackballPanManipulator from '@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballPanManipulator';
143+
import vtkMouseCameraTrackballZoomToMouseManipulator from '@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballZoomToMouseManipulator';
144+
import { useResetViewsEvents } from '@/src/components/tools/ResetViews.vue';
145+
import { onVTKEvent } from '@/src/composables/onVTKEvent';
146+
import { get2DViewingVectors } from '@/src/utils/getViewingVectors';
147+
import type { LPSAxis } from '@/src/types/lps';
148+
149+
interface Props {
150+
viewId: string;
151+
}
152+
153+
const VIEW_AXIS: LPSAxis = 'Axial';
154+
const { viewDirection: VIEW_DIRECTION, viewUp: VIEW_UP } =
155+
get2DViewingVectors(VIEW_AXIS);
156+
157+
const vtkView = ref<VtkViewApi>();
158+
const baseSliceRep = ref();
159+
160+
const props = defineProps<Props>();
161+
const { viewId } = toRefs(props);
162+
163+
const { currentImageID, currentImageData, isImageLoading } = useCurrentImage();
164+
165+
const hover = ref(false);
166+
167+
function resetCamera() {
168+
vtkView.value?.resetCamera();
169+
}
170+
171+
useResetViewsEvents().onClick(resetCamera);
172+
173+
useWebGLWatchdog(vtkView);
174+
useViewAnimationListener(vtkView, viewId, '2D');
175+
176+
const { currentTool } = storeToRefs(useToolStore());
177+
178+
const { frame: currentFrame, frameRange } = useCineFrame(
179+
viewId,
180+
currentImageID
181+
);
182+
183+
onVTKEvent(currentImageData, 'onModified', () => {
184+
vtkView.value?.requestRender();
185+
});
186+
187+
const selectionStore = useToolSelectionStore();
188+
const selectionPoints = computed(() => {
189+
return selectionStore.selection
190+
.map((sel) => {
191+
const store = useAnnotationToolStore(sel.type);
192+
return { store, tool: store.toolByID[sel.id] };
193+
})
194+
.filter(
195+
({ tool }) =>
196+
tool.imageID === currentImageID.value &&
197+
tool.frame === currentFrame.value &&
198+
!tool.hidden
199+
)
200+
.flatMap(({ store, tool }) => store.getPoints(tool.id));
201+
});
202+
</script>
203+
204+
<style scoped src="@/src/components/styles/vtk-view.css"></style>
205+
<style scoped src="@/src/components/styles/utils.css"></style>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script setup lang="ts">
2+
import { toRefs, computed } from 'vue';
3+
import ViewOverlayGrid from '@/src/components/ViewOverlayGrid.vue';
4+
import { Maybe } from '@/src/types';
5+
import { useCineFrame } from '@/src/composables/useCineFrame';
6+
import DicomQuickInfoButton from '@/src/components/DicomQuickInfoButton.vue';
7+
import { useImage } from '@/src/composables/useCurrentImage';
8+
import PlayControls from '@/src/components/PlayControls.vue';
9+
10+
interface Props {
11+
viewId: string;
12+
imageId: Maybe<string>;
13+
}
14+
15+
const props = defineProps<Props>();
16+
const { viewId, imageId } = toRefs(props);
17+
18+
const { metadata } = useImage(imageId);
19+
const { frame, frameRange } = useCineFrame(viewId, imageId);
20+
const frameCount = computed(() => frameRange.value[1] + 1);
21+
</script>
22+
23+
<template>
24+
<view-overlay-grid class="overlay-no-events view-annotations">
25+
<template v-slot:top-left>
26+
<div class="annotation-cell">
27+
<span>{{ metadata.name }}</span>
28+
</div>
29+
</template>
30+
<template v-slot:bottom-left>
31+
<div class="annotation-cell">
32+
<div>
33+
<span class="frame-label">
34+
Frame: {{ frame + 1 }} / {{ frameCount }}
35+
</span>
36+
</div>
37+
</div>
38+
</template>
39+
<template v-slot:top-right>
40+
<div class="annotation-cell">
41+
<dicom-quick-info-button :image-id="imageId"></dicom-quick-info-button>
42+
</div>
43+
</template>
44+
<template #bottom-right>
45+
<div class="annotation-cell" @click.stop>
46+
<play-controls :view-id="viewId" :image-id="imageId" />
47+
</div>
48+
</template>
49+
</view-overlay-grid>
50+
</template>
51+
52+
<style scoped src="@/src/components/styles/vtk-view.css"></style>

src/components/ControlsStripTools.vue

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@
4545
icon="mdi-crosshairs"
4646
:name="`Crosshairs [${nameToShortcut['Crosshairs']}]`"
4747
:buttonClass="['tool-btn', active ? 'tool-btn-selected' : '']"
48-
:disabled="noCurrentImage || isObliqueLayout"
48+
:disabled="
49+
noCurrentImage ||
50+
isObliqueLayout ||
51+
isDisallowedOnCine(Tools.Crosshairs)
52+
"
4953
@click="toggle"
5054
/>
5155
</groupable-item>
@@ -64,7 +68,9 @@
6468
icon="mdi-brush"
6569
:name="`Paint [${nameToShortcut['Paint']}]`"
6670
:buttonClass="['tool-btn', active ? 'tool-btn-selected' : '']"
67-
:disabled="noCurrentImage || isObliqueLayout"
71+
:disabled="
72+
noCurrentImage || isObliqueLayout || isDisallowedOnCine(Tools.Paint)
73+
"
6874
@click="toggle"
6975
></control-button>
7076
</groupable-item>
@@ -114,7 +120,9 @@
114120
icon="mdi-crop"
115121
:name="`Crop [${nameToShortcut['Crop']}]`"
116122
:active="active"
117-
:disabled="noCurrentImage || isObliqueLayout"
123+
:disabled="
124+
noCurrentImage || isObliqueLayout || isDisallowedOnCine(Tools.Crop)
125+
"
118126
@click="toggle"
119127
>
120128
<crop-controls />
@@ -132,7 +140,9 @@ import { Tools } from '@/src/store/tools/types';
132140
import ControlButton from '@/src/components/ControlButton.vue';
133141
import ItemGroup from '@/src/components/ItemGroup.vue';
134142
import GroupableItem from '@/src/components/GroupableItem.vue';
135-
import { useToolStore } from '@/src/store/tools';
143+
import { useToolStore, isToolAllowedFor } from '@/src/store/tools';
144+
import { useEffectiveView } from '@/src/composables/useEffectiveView';
145+
import { toRef } from 'vue';
136146
import MenuControlButton from '@/src/components/MenuControlButton.vue';
137147
import CropControls from '@/src/components/tools/crop/CropControls.vue';
138148
import ResetViews from '@/src/components/tools/ResetViews.vue';
@@ -164,11 +174,20 @@ export default defineComponent({
164174
const { currentImageID } = useCurrentImage();
165175
const noCurrentImage = computed(() => !currentImageID.value);
166176
const currentTool = computed(() => toolStore.currentTool);
167-
const isObliqueLayout = computed(() => {
168-
if (!viewStore.activeView) return false;
169-
const view = viewStore.viewByID[viewStore.activeView];
170-
return view.type === 'Oblique';
171-
});
177+
178+
const activeViewRef = toRef(viewStore, 'activeView');
179+
const activeEffective = useEffectiveView(
180+
computed(() => activeViewRef.value ?? '')
181+
);
182+
// The rendered viewer is decided by effective kind, not stored slot type:
183+
// a cine clip dropped into an Oblique slot still renders as cine, so the
184+
// toolbar should treat it as cine, not Oblique.
185+
const isObliqueLayout = computed(
186+
() => activeEffective.value?.kind === 'oblique'
187+
);
188+
const isCineActive = computed(() => activeEffective.value?.kind === 'cine');
189+
const isDisallowedOnCine = (tool: Tools) =>
190+
isCineActive.value && !isToolAllowedFor(tool, activeEffective.value);
172191
173192
const paintMenu = ref(false);
174193
const cropMenu = ref(false);
@@ -211,6 +230,7 @@ export default defineComponent({
211230
setCurrentTool: toolStore.setCurrentTool,
212231
noCurrentImage,
213232
isObliqueLayout,
233+
isDisallowedOnCine,
214234
Tools,
215235
paintMenu,
216236
cropMenu,

src/components/LayoutGridItem.vue

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts" setup>
22
import CurrentImageProvider from '@/src/components/CurrentImageProvider.vue';
33
import { IMAGE_DRAG_MEDIA_TYPE } from '@/src/constants';
4-
import { getComponentFromViewInfo } from '@/src/core/viewTypes';
4+
import { resolveSlotRendering } from '@/src/core/viewTypes';
55
import { useViewStore } from '@/src/store/views';
66
import { computed, ref } from 'vue';
77
@@ -11,10 +11,9 @@ const viewStore = useViewStore();
1111
const dragCounter = ref(0);
1212
const showDropTarget = computed(() => dragCounter.value > 0);
1313
14-
const ItemComponent = computed(() => {
15-
const viewInfo = viewStore.viewByID[props.viewId];
16-
return getComponentFromViewInfo(viewInfo);
17-
});
14+
const rendering = computed(() =>
15+
resolveSlotRendering(viewStore.viewByID[props.viewId])
16+
);
1817
1918
const activeStyles = computed(() => {
2019
if (showDropTarget.value) {
@@ -32,11 +31,6 @@ const activeStyles = computed(() => {
3231
};
3332
});
3433
35-
const imageID = computed(() => {
36-
const viewInfo = viewStore.viewByID[props.viewId];
37-
return viewInfo.dataID;
38-
});
39-
4034
function isValidDragEvent(event: DragEvent) {
4135
return event.dataTransfer?.types.includes(IMAGE_DRAG_MEDIA_TYPE);
4236
}
@@ -74,11 +68,11 @@ function onDrop(event: DragEvent) {
7468
@dragleave="onDragLeave"
7569
@drop="onDrop"
7670
>
77-
<div v-show="!imageID" class="overlay">
71+
<div v-show="!rendering.renderImageID" class="overlay">
7872
<v-icon color="grey-darken-3" size="x-large">mdi-image-off</v-icon>
7973
</div>
80-
<CurrentImageProvider :image-id="imageID">
81-
<ItemComponent :view-id="viewId" />
74+
<CurrentImageProvider :image-id="rendering.renderImageID">
75+
<component :is="rendering.component" :view-id="viewId" />
8276
</CurrentImageProvider>
8377
</div>
8478
</template>

0 commit comments

Comments
 (0)