Skip to content

Commit d865eed

Browse files
committed
feat: change slice with key and mouse movement
1 parent 55f993a commit d865eed

File tree

7 files changed

+208
-14
lines changed

7 files changed

+208
-14
lines changed

docs/mouse_controls.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@
77

88
## Slice
99

10-
| Shortcut | Action |
11-
| ----------- | -------------- |
12-
| Up arrow | Next Slice |
13-
| Right arrow | Next Slice |
14-
| Down array | Previous Slice |
15-
| Left array | Previous Slice |
10+
| Shortcut | Action |
11+
| ------------------- | --------------- |
12+
| Up arrow | Next Slice |
13+
| Right arrow | Next Slice |
14+
| Down array | Previous Slice |
15+
| Left array | Previous Slice |
16+
| Alt + mouse up/down | navigate slices |
1617

1718
## Mouse Controls
1819

1920
| Action | 2D | 3D |
2021
| ------------ | --------------- | ------ |
21-
| Left | grayscale | rotate |
22+
| Left | window level | rotate |
2223
| Mid | pan | pan |
2324
| Right | zoom | zoom |
2425
| Ctrl + Left | zoom | zoom |

src/components/SliceViewer.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@
7070
:image-id="currentImageID"
7171
:view-direction="viewDirection"
7272
></vtk-slice-view-slicing-manipulator>
73+
<vtk-slice-view-slicing-key-manipulator
74+
:view-id="id"
75+
:image-id="currentImageID"
76+
:view-direction="viewDirection"
77+
></vtk-slice-view-slicing-key-manipulator>
7378
<vtk-slice-view-window-manipulator
7479
:view-id="id"
7580
:image-id="currentImageID"
@@ -179,6 +184,7 @@ import { useWebGLWatchdog } from '@/src/composables/useWebGLWatchdog';
179184
import { useSliceConfig } from '@/src/composables/useSliceConfig';
180185
import VtkSliceViewWindowManipulator from '@/src/components/vtk/VtkSliceViewWindowManipulator.vue';
181186
import VtkSliceViewSlicingManipulator from '@/src/components/vtk/VtkSliceViewSlicingManipulator.vue';
187+
import VtkSliceViewSlicingKeyManipulator from '@/src/components/vtk/VtkSliceViewSlicingKeyManipulator.vue';
182188
import VtkMouseInteractionManipulator from '@/src/components/vtk/VtkMouseInteractionManipulator.vue';
183189
import vtkMouseCameraTrackballPanManipulator from '@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballPanManipulator';
184190
import vtkMouseCameraTrackballZoomToMouseManipulator from '@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballZoomToMouseManipulator';
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<script setup lang="ts">
2+
import { VtkViewContext } from '@/src/components/vtk/context';
3+
import { useSliceConfig } from '@/src/composables/useSliceConfig';
4+
import { useSliceConfigInitializer } from '@/src/composables/useSliceConfigInitializer';
5+
import { useMouseRangeManipulatorListener } from '@/src/core/vtk/useMouseRangeManipulatorListener';
6+
import { useVtkInteractionManipulator } from '@/src/core/vtk/useVtkInteractionManipulator';
7+
import { Maybe } from '@/src/types';
8+
import { LPSAxisDir } from '@/src/types/lps';
9+
import vtkGatedMouseRangeManipulator from '@/src/vtk/GatedMouseRangeManipulator';
10+
import { IMouseRangeManipulatorInitialValues } from '@kitware/vtk.js/Interaction/Manipulators/MouseRangeManipulator';
11+
import vtkInteractorStyleManipulator from '@kitware/vtk.js/Interaction/Style/InteractorStyleManipulator';
12+
import { syncRef, useMagicKeys } from '@vueuse/core';
13+
import { inject, toRefs, unref, watch, computed, watchPostEffect } from 'vue';
14+
import { useViewStore } from '@/src/store/views';
15+
import { actionToKey } from '@/src/composables/useKeyboardShortcuts';
16+
17+
interface Props {
18+
viewId: string;
19+
imageId: Maybe<string>;
20+
viewDirection: LPSAxisDir;
21+
manipulatorConfig?: IMouseRangeManipulatorInitialValues;
22+
}
23+
24+
const props = defineProps<Props>();
25+
const { viewId, imageId, viewDirection, manipulatorConfig } = toRefs(props);
26+
27+
const view = inject(VtkViewContext);
28+
if (!view) throw new Error('No VtkView');
29+
30+
const interactorStyle =
31+
view.interactorStyle as Maybe<vtkInteractorStyleManipulator>;
32+
if (!interactorStyle?.isA('vtkInteractorStyleManipulator')) {
33+
throw new Error('No vtkInteractorStyleManipulator');
34+
}
35+
36+
const config = computed(() => {
37+
return {
38+
button: -1,
39+
dragEnabled: false,
40+
scrollEnabled: false,
41+
...manipulatorConfig?.value,
42+
};
43+
});
44+
45+
const { instance: rangeManipulator } = useVtkInteractionManipulator(
46+
interactorStyle,
47+
vtkGatedMouseRangeManipulator,
48+
config
49+
);
50+
51+
// run after useWebGLRenderWindow assigns the container
52+
watchPostEffect(() => {
53+
const container = view.renderWindowView.getContainer();
54+
rangeManipulator.value.initializeGlobalMouseMove(
55+
view.renderWindow.getInteractor(),
56+
view.renderer,
57+
container
58+
);
59+
});
60+
61+
const keys = useMagicKeys();
62+
const enableGrabSlice = computed(() => keys[actionToKey.value.grabSlice].value);
63+
watch(
64+
enableGrabSlice,
65+
(value) => {
66+
rangeManipulator.value.setGateEnabled(value);
67+
},
68+
{ immediate: true }
69+
);
70+
71+
const sliceConfig = useSliceConfig(viewId, imageId);
72+
useSliceConfigInitializer(viewId, imageId, viewDirection);
73+
74+
const scroll = useMouseRangeManipulatorListener(
75+
rangeManipulator,
76+
'vertical',
77+
sliceConfig.range,
78+
1,
79+
sliceConfig.slice.value
80+
);
81+
82+
watch(scroll, () => {
83+
const viewStore = useViewStore();
84+
if (unref(viewId) !== viewStore.activeViewID) {
85+
viewStore.setActiveViewID(unref(viewId));
86+
}
87+
});
88+
89+
syncRef(scroll, sliceConfig.slice, { immediate: true });
90+
</script>
91+
92+
<template><slot></slot></template>

src/composables/actions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useRectangleStore } from '../store/tools/rectangles';
44
import { useRulerStore } from '../store/tools/rulers';
55
import { usePolygonStore } from '../store/tools/polygons';
66
import { useViewStore } from '../store/views';
7-
import { Action } from '../constants';
7+
import { Action, NOOP } from '../constants';
88
import { useKeyboardShortcutsStore } from '../store/keyboard-shortcuts';
99
import { useCurrentImage } from './useCurrentImage';
1010
import { useSliceConfig } from './useSliceConfig';
@@ -61,11 +61,12 @@ export const ACTION_TO_FUNC = {
6161

6262
nextSlice: changeSlice(1),
6363
previousSlice: changeSlice(-1),
64+
grabSlice: NOOP, // acts as a modifier key rather than immediate effect, so no-op
6465

6566
decrementLabel: applyLabelOffset(-1),
6667
incrementLabel: applyLabelOffset(1),
6768

68-
mergeNewPolygon: () => {}, // acts as a modifier key rather than immediate effect, so no-op
69+
mergeNewPolygon: NOOP, // acts as a modifier key rather than immediate effect, so no-op
6970

7071
showKeyboardShortcuts,
7172
} as const satisfies Record<Action, () => void>;

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ export const ACTION_TO_KEY = {
283283

284284
nextSlice: 'arrowdown',
285285
previousSlice: 'arrowup',
286+
grabSlice: 'Alt',
286287

287288
decrementLabel: 'q',
288289
incrementLabel: 'w',

src/constants.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,25 @@ export const ACTIONS = {
5454
},
5555

5656
nextSlice: {
57-
readable: 'Next Slice',
57+
readable: 'Next slice',
5858
},
5959
previousSlice: {
60-
readable: 'Previous Slice',
60+
readable: 'Previous slice',
61+
},
62+
grabSlice: {
63+
readable: 'Change slice by holding key and moving mouse up or down',
6164
},
6265

6366
decrementLabel: {
64-
readable: 'Activate previous Label',
67+
readable: 'Activate previous label',
6568
},
6669
incrementLabel: {
67-
readable: 'Activate next Label',
70+
readable: 'Activate next label',
6871
},
6972

7073
mergeNewPolygon: {
71-
readable: 'Hold to merge new polygons with overlapping polygons',
74+
readable:
75+
'Merge new polygons by holding key and finishing an overlapping polygon',
7276
},
7377

7478
showKeyboardShortcuts: {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import macro from '@kitware/vtk.js/macro';
2+
import vtkMouseRangeManipulator from '@kitware/vtk.js/Interaction/Manipulators/MouseRangeManipulator';
3+
4+
function vtkGatedMouseRangeManipulator(publicAPI, model) {
5+
model.classHierarchy.push('vtkGatedMouseRangeManipulator');
6+
7+
model.gateEnabled = true;
8+
9+
model.mouseMoveListener = null;
10+
model.viewContainer = null;
11+
model.newInteraction = true;
12+
13+
const superOnMouseMove = publicAPI.onMouseMove;
14+
15+
publicAPI.onMouseMove = (interactor, renderer, position) => {
16+
if (!model.gateEnabled) {
17+
return;
18+
}
19+
superOnMouseMove(interactor, renderer, position);
20+
};
21+
22+
publicAPI.setGateEnabled = (enabled) => {
23+
model.newInteraction = !model.gateEnabled && enabled;
24+
model.gateEnabled = enabled;
25+
publicAPI.modified();
26+
};
27+
28+
// Define the mouse move listener function outside initializeGlobalMouseMove
29+
const createMouseMoveListener = (interactor, renderer) => {
30+
return (event) => {
31+
// Proceed if the mouse is inside the container.
32+
if (!model.viewContainer.contains(event.target)) {
33+
return;
34+
}
35+
// Map the event client coordinates to the container's coordinate system.
36+
const rect = model.viewContainer.getBoundingClientRect();
37+
const position = {
38+
x: event.clientX - rect.left,
39+
y: event.clientY - rect.top,
40+
};
41+
42+
if (model.newInteraction) {
43+
model.newInteraction = false;
44+
publicAPI.onButtonDown(interactor, renderer, position);
45+
}
46+
publicAPI.onMouseMove(interactor, renderer, position);
47+
};
48+
};
49+
50+
// Initializes a global mousemove listener on the view container.
51+
// Only events occurring over the container will trigger onMouseMove.
52+
publicAPI.initializeGlobalMouseMove = (interactor, renderer, container) => {
53+
model.viewContainer = container;
54+
if (!model.mouseMoveListener) {
55+
model.mouseMoveListener = createMouseMoveListener(interactor, renderer);
56+
model.viewContainer.addEventListener(
57+
'mousemove',
58+
model.mouseMoveListener
59+
);
60+
}
61+
};
62+
63+
publicAPI.delete = macro.chain(publicAPI.delete, () => {
64+
if (model.mouseMoveListener && model.viewContainer) {
65+
model.viewContainer.removeEventListener(
66+
'mousemove',
67+
model.mouseMoveListener
68+
);
69+
model.mouseMoveListener = null;
70+
}
71+
});
72+
}
73+
74+
function extend(publicAPI, model, initialValues = {}) {
75+
vtkMouseRangeManipulator.extend(publicAPI, model, initialValues);
76+
Object.assign(model, {
77+
gateEnabled: true,
78+
mouseMoveListener: null,
79+
viewContainer: null,
80+
});
81+
vtkGatedMouseRangeManipulator(publicAPI, model);
82+
}
83+
84+
export const newInstance = macro.newInstance(
85+
extend,
86+
'vtkGatedMouseRangeManipulator'
87+
);
88+
89+
export default { newInstance, extend };

0 commit comments

Comments
 (0)