Skip to content

Commit c8e52d9

Browse files
authored
Merge pull request Kitware#714 from PaulHax/grab-slice
feat: change slice with key and mouse movement
2 parents 3d33e7a + 431df91 commit c8e52d9

File tree

8 files changed

+180
-16
lines changed

8 files changed

+180
-16
lines changed

docs/mouse_controls.md

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

1010
## Slice
1111

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

1920
## Mouse Controls
2021

2122
| Action | 2D | 3D |
2223
| ------------ | --------------- | ------ |
23-
| Left | grayscale | rotate |
24+
| Left | window level | rotate |
2425
| Mid | pan | pan |
2526
| Right | zoom | zoom |
2627
| Ctrl + Left | zoom | zoom |

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 } 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+
rangeManipulator.value.setupMouseMove(view.interactor);
52+
53+
const keys = useMagicKeys();
54+
const enableGrabSlice = computed(() => keys[actionToKey.value.grabSlice].value);
55+
watch(
56+
enableGrabSlice,
57+
(value) => {
58+
rangeManipulator.value.setGateEnabled(value);
59+
},
60+
{ immediate: true }
61+
);
62+
63+
const sliceConfig = useSliceConfig(viewId, imageId);
64+
useSliceConfigInitializer(viewId, imageId, viewDirection);
65+
66+
const scroll = useMouseRangeManipulatorListener(
67+
rangeManipulator,
68+
'vertical',
69+
sliceConfig.range,
70+
1,
71+
sliceConfig.slice.value
72+
);
73+
74+
syncRef(scroll, sliceConfig.slice, { immediate: true });
75+
76+
// set just scrolled view as active view
77+
watch(scroll, () => {
78+
const viewStore = useViewStore();
79+
if (unref(viewId) !== viewStore.activeViewID) {
80+
viewStore.setActiveViewID(unref(viewId));
81+
}
82+
});
83+
</script>
84+
85+
<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';
@@ -75,14 +75,15 @@ export const ACTION_TO_FUNC = {
7575

7676
nextSlice: changeSlice(1),
7777
previousSlice: changeSlice(-1),
78+
grabSlice: NOOP, // acts as a modifier key rather than immediate effect, so no-op
7879

7980
decrementLabel: applyLabelOffset(-1),
8081
incrementLabel: applyLabelOffset(1),
8182

8283
deleteCurrentImage: deleteCurrentImage(),
8384
clearScene: clearScene(),
8485

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

8788
showKeyboardShortcuts,
8889
} 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,17 +54,20 @@ 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
deleteCurrentImage: {
@@ -76,7 +79,8 @@ export const ACTIONS = {
7679
},
7780

7881
mergeNewPolygon: {
79-
readable: 'Hold to merge new polygons with overlapping polygons',
82+
readable:
83+
'Merge new polygons by holding key and finishing an overlapping polygon',
8084
},
8185

8286
showKeyboardShortcuts: {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
const cleanupListener = () => {
29+
model.mouseMoveListenerSubscription?.unsubscribe();
30+
model.mouseMoveListenerSubscription = null;
31+
};
32+
33+
publicAPI.setupMouseMove = (interactor) => {
34+
cleanupListener();
35+
model.mouseMoveListenerSubscription = interactor.onMouseMove((event) => {
36+
const invertY = {
37+
...event.position,
38+
y: interactor.getView().getSize()[1] - event.position.y,
39+
};
40+
if (model.newInteraction) {
41+
model.newInteraction = false;
42+
publicAPI.onButtonDown(interactor, event.pokedRenderer, invertY);
43+
}
44+
publicAPI.onMouseMove(interactor, event.pokedRenderer, invertY);
45+
});
46+
};
47+
48+
publicAPI.delete = macro.chain(publicAPI.delete, cleanupListener);
49+
}
50+
51+
function extend(publicAPI, model, initialValues = {}) {
52+
vtkMouseRangeManipulator.extend(publicAPI, model, initialValues);
53+
Object.assign(model, {
54+
gateEnabled: true,
55+
mouseMoveListener: null,
56+
viewContainer: null,
57+
});
58+
vtkGatedMouseRangeManipulator(publicAPI, model);
59+
}
60+
61+
export const newInstance = macro.newInstance(
62+
extend,
63+
'vtkGatedMouseRangeManipulator'
64+
);
65+
66+
export default { newInstance, extend };

0 commit comments

Comments
 (0)