@3cr/viewer-browser 0.0.162 → 0.0.195
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.circleci/config.yml +8 -6
- package/__tests__/index.spec.ts +31 -24
- package/components.d.ts +32 -0
- package/dist/Viewer3CR.js +44 -27
- package/dist/Viewer3CR.mjs +22731 -17742
- package/dist/Viewer3CR.umd.js +44 -27
- package/index.html +5 -8
- package/index.ts +7 -2
- package/package.json +4 -2
- package/playground/index.html +6 -4
- package/src/App.vue +12 -4
- package/src/__tests__/app.spec.ts +2 -13
- package/src/assets/magic_wand.svg +24 -0
- package/src/components/WebGL3DR.vue +53 -92
- package/src/components/__tests__/webgl3dr.spec.ts +29 -48
- package/src/{demo → components/demo}/DemoModal.vue +1 -1
- package/src/{demo → components/demo}/DemoPatientModal.vue +4 -1
- package/src/components/demo/__tests__/DemoModal.spec.ts +25 -0
- package/src/components/demo/__tests__/DemoPatientModal.spec.ts +37 -0
- package/src/components/demo/__tests__/options.spec.ts +25 -0
- package/src/components/demo/licence/DemoLicenceEnableCloudStorageModal.vue +64 -0
- package/src/{demo → components/demo}/licence/DemoLicenceInfoModal.vue +5 -4
- package/src/{demo → components/demo}/licence/DemoLicenceSendToPartyModal.vue +6 -4
- package/src/{demo → components/demo}/licence/DemoLicenceShareToMobileModal.vue +8 -6
- package/src/components/demo/licence/__tests__/DemoLicenceEnableCloudStorageModal.spec.ts +37 -0
- package/src/components/demo/licence/__tests__/DemoLicenceInfoModal.spec.ts +37 -0
- package/src/components/demo/licence/__tests__/DemoLicenceSendToPartyModal.spec.ts +36 -0
- package/src/components/demo/licence/__tests__/DemoLicenceShareToMobileModal.spec.ts +51 -0
- package/src/{demo → components/demo}/options.ts +18 -20
- package/src/components/demo/patient/DemoPatientEnableCloudStorageModal.vue +64 -0
- package/src/{demo → components/demo}/patient/DemoPatientInfoModal.vue +5 -4
- package/src/{demo → components/demo}/patient/DemoPatientSendToPartyModal.vue +4 -3
- package/src/{demo → components/demo}/patient/DemoPatientShareToMobileModal.vue +8 -6
- package/src/components/demo/patient/__tests__/DemoPatientEnableCloudStorageModal.spec.ts +37 -0
- package/src/components/demo/patient/__tests__/DemoPatientInfoModal.spec.ts +37 -0
- package/src/components/demo/patient/__tests__/DemoPatientSendToPartyModal.spec.ts +36 -0
- package/src/components/demo/patient/__tests__/DemoPatientShareToMobileModal.spec.ts +51 -0
- package/src/components/loading/LoadingSpinner.vue +1 -1
- package/src/components/modal/ActionRail.vue +96 -0
- package/src/components/modal/AskAI.vue +250 -0
- package/src/components/modal/CloseViewerModal.vue +104 -0
- package/src/components/modal/MftpWebGL3DRModal.vue +415 -834
- package/src/components/modal/ViewerActionRail.vue +123 -0
- package/src/components/modal/ViewerAnnotationModal.vue +115 -0
- package/src/components/modal/ViewerAnnotations.vue +283 -0
- package/src/components/modal/ViewerDisplaySettings.vue +102 -0
- package/src/components/modal/ViewerNavigationDrawer.vue +90 -0
- package/src/components/modal/ViewerNavigationDrawerContent.vue +126 -0
- package/src/components/modal/ViewerNavigationDrawerFooter.vue +111 -0
- package/src/components/modal/ViewerNavigationDrawerHeader.vue +63 -0
- package/src/components/modal/__tests__/CloseViewerModal.spec.ts +60 -0
- package/src/components/modal/__tests__/{mftp-webgl-3dr-modal.spec.ts → MftpWebGL3DRModal.spec.ts} +47 -298
- package/src/components/modal/__tests__/ViewerAnnotationModal.spec.ts +79 -0
- package/src/components/modal/__tests__/ViewerDisplaySettings.spec.ts +61 -0
- package/src/components/modal/__tests__/ViewerNavigationDrawer.spec.ts +32 -0
- package/src/components/modal/__tests__/ViewerNavigationDrawerContent.spec.ts +29 -0
- package/src/components/modal/__tests__/ViewerNavigationDrawerFooter.spec.ts +43 -0
- package/src/components/modal/__tests__/ViewerNavigationDrawerHeader.spec.ts +37 -0
- package/src/components/modal/actions/Action.vue +40 -0
- package/src/components/modal/actions/Flip3dAction.vue +79 -0
- package/src/components/modal/actions/FlipHorizontalAction.vue +36 -0
- package/src/components/modal/actions/FlipVerticalAction.vue +36 -0
- package/src/components/modal/actions/FullscreenAction.vue +47 -0
- package/src/components/modal/actions/NavigationCubeAction.vue +33 -0
- package/src/components/modal/actions/PanAction.vue +78 -0
- package/src/components/modal/actions/ResetViewAction.vue +29 -0
- package/src/components/modal/actions/Rotate2dAction.vue +78 -0
- package/src/components/modal/actions/Slice3dAction.vue +71 -0
- package/src/components/modal/actions/ZoomAction.vue +70 -0
- package/src/components/modal/actions/__tests__/Action.spec.ts +29 -0
- package/src/components/modal/actions/__tests__/Flip3dAction.spec.ts +48 -0
- package/src/components/modal/actions/__tests__/FlipHorizontalAction.spec.ts +17 -0
- package/src/components/modal/actions/__tests__/FlipVerticalAction.spec.ts +17 -0
- package/src/components/modal/actions/__tests__/FullscreenAction.spec.ts +28 -0
- package/src/components/modal/actions/__tests__/NavigationCubeAction.spec.ts +25 -0
- package/src/components/modal/actions/__tests__/PanAction.spec.ts +46 -0
- package/src/components/modal/actions/__tests__/ResetViewAction.spec.ts +17 -0
- package/src/components/modal/actions/__tests__/Rotate2dAction.spec.ts +23 -0
- package/src/components/modal/actions/__tests__/Slice3dAction.spec.ts +14 -0
- package/src/components/modal/actions/__tests__/ZoomAction.spec.ts +34 -0
- package/src/components/modal/composables/__tests__/useNavigationCubeObserver.spec.ts +56 -0
- package/src/components/modal/composables/useEventListener.ts +22 -0
- package/src/components/modal/composables/useNavigationCubeObserver.ts +104 -0
- package/src/components/selectors/ValueSelector.vue +30 -33
- package/src/components/selectors/__tests__/value-selector.spec.ts +1 -1
- package/src/components/sliders/DoubleSliderSelector.vue +79 -71
- package/src/components/sliders/VerticalSliderSelector.vue +12 -17
- package/src/components/sliders/__tests__/double-slider-selector.spec.ts +1 -1
- package/src/components/sliders/__tests__/vertical-slider-selector.spec.ts +1 -1
- package/src/dataLayer/__tests__/clamp.spec.ts +16 -0
- package/src/dataLayer/__tests__/eventHandlers.spec.ts +38 -0
- package/src/dataLayer/__tests__/getIconForPreset.spec.ts +40 -0
- package/src/dataLayer/__tests__/patchDataOverlay.spec.ts +88 -0
- package/src/dataLayer/__tests__/scanState.spec.ts +93 -0
- package/src/dataLayer/__tests__/useViewer3cr.spec.ts +10 -0
- package/src/dataLayer/__tests__/viewer3cr.spec.ts +331 -0
- package/src/dataLayer/clamp.ts +9 -0
- package/src/dataLayer/eventHandlers.ts +26 -0
- package/src/dataLayer/patchDataOverlay.ts +101 -0
- package/src/dataLayer/scanState.ts +105 -26
- package/src/dataLayer/useViewer3cr.ts +7 -0
- package/src/dataLayer/viewer3cr.ts +410 -0
- package/src/helpers/__tests__/layout-overlay-style.spec.ts +24 -22
- package/src/helpers/__tests__/model-helper.spec.ts +44 -13
- package/src/helpers/layoutOverlayStyle.ts +16 -27
- package/src/helpers/modelHelper.ts +62 -10
- package/src/models/Callbacks.ts +2 -2
- package/src/models/LoadViewerOptions.ts +2 -0
- package/src/models/LoadViewerPayload.ts +1 -0
- package/src/notifications/notification.ts +3 -4
- package/src/plugins/vuetify.ts +5 -0
- package/src/services/gpt/__tests__/gpt.service.spec.ts +27 -0
- package/src/services/gpt/gpt.service.ts +27 -0
- package/static/3cr-types-browser/index.ts +74 -0
- package/static/3cr-types-browser/types/Action.ts +6 -0
- package/static/3cr-types-browser/types/ActionData.ts +4 -0
- package/static/3cr-types-browser/types/AlphaKeys.ts +5 -0
- package/static/3cr-types-browser/types/AnchorPoint.ts +12 -0
- package/static/3cr-types-browser/types/CallToAction.ts +5 -0
- package/static/3cr-types-browser/types/ColourData.ts +7 -0
- package/static/3cr-types-browser/types/ColourPresetData.ts +9 -0
- package/static/3cr-types-browser/types/CurrentDataOverlayState.ts +6 -0
- package/static/3cr-types-browser/types/CurrentScanState.ts +22 -0
- package/static/3cr-types-browser/types/DataOverlay.ts +22 -0
- package/static/3cr-types-browser/types/DataOverlayActions.ts +14 -0
- package/static/3cr-types-browser/types/DataOverlayData.ts +8 -0
- package/static/3cr-types-browser/types/DataOverlayEvent.ts +8 -0
- package/static/3cr-types-browser/types/DecryptionKey.ts +4 -0
- package/static/3cr-types-browser/types/DisplaySettings.ts +10 -0
- package/static/3cr-types-browser/types/EmptyPayload.ts +3 -0
- package/static/3cr-types-browser/types/EnumPayload.ts +4 -0
- package/static/3cr-types-browser/types/FileManagementActions.ts +11 -0
- package/static/3cr-types-browser/types/FlipValue.ts +7 -0
- package/static/3cr-types-browser/types/FrontEndInterfaces.ts +14 -0
- package/static/3cr-types-browser/types/GradientKeys.ts +7 -0
- package/static/3cr-types-browser/types/GreyscalePresetData.ts +6 -0
- package/static/3cr-types-browser/types/InitialDataOverlayState.ts +6 -0
- package/static/3cr-types-browser/types/InitialScanState.ts +19 -0
- package/static/3cr-types-browser/types/InteractionType.ts +8 -0
- package/static/3cr-types-browser/types/InteractivityActions.ts +6 -0
- package/static/3cr-types-browser/types/InteractivityState.ts +4 -0
- package/static/3cr-types-browser/types/InvertTransformData.ts +6 -0
- package/static/3cr-types-browser/types/LayoutActions.ts +6 -0
- package/static/3cr-types-browser/types/LayoutData.ts +7 -0
- package/static/3cr-types-browser/types/LoadDataSet.ts +6 -0
- package/static/3cr-types-browser/types/LoadSessionState.ts +4 -0
- package/static/3cr-types-browser/types/LocalLoadDataset.ts +3 -0
- package/static/3cr-types-browser/types/MovementData.ts +7 -0
- package/static/3cr-types-browser/types/NavigationCubeActions.ts +8 -0
- package/static/3cr-types-browser/types/NavigationCubeData.ts +12 -0
- package/static/3cr-types-browser/types/NavigationCubeTransform.ts +9 -0
- package/static/3cr-types-browser/types/NotificationPayload.ts +7 -0
- package/static/3cr-types-browser/types/NotificationsActions.ts +6 -0
- package/static/3cr-types-browser/types/Object.ts +1 -0
- package/static/3cr-types-browser/types/ObjectColour.ts +7 -0
- package/static/3cr-types-browser/types/ObjectIcon.ts +5 -0
- package/static/3cr-types-browser/types/ObjectInvert.ts +7 -0
- package/static/3cr-types-browser/types/ObjectSize.ts +7 -0
- package/static/3cr-types-browser/types/ObjectSize2D.ts +7 -0
- package/static/3cr-types-browser/types/ObjectVisible.ts +5 -0
- package/static/3cr-types-browser/types/PositionData.ts +14 -0
- package/static/3cr-types-browser/types/PresetsActions.ts +4 -0
- package/static/3cr-types-browser/types/RotationValue.ts +7 -0
- package/static/3cr-types-browser/types/ScanMovementActions.ts +27 -0
- package/static/3cr-types-browser/types/ScanMovementData.ts +3 -0
- package/static/3cr-types-browser/types/ScanOrientationActions.ts +6 -0
- package/static/3cr-types-browser/types/ScanStateActions.ts +4 -0
- package/static/3cr-types-browser/types/ScanView.ts +6 -0
- package/static/3cr-types-browser/types/SettingsData.ts +12 -0
- package/static/3cr-types-browser/types/SlicerData.ts +9 -0
- package/static/3cr-types-browser/types/SliderValue.ts +4 -0
- package/static/3cr-types-browser/types/SlidersActions.ts +18 -0
- package/static/3cr-types-browser/types/Vector2Data.ts +5 -0
- package/static/3cr-types-browser/types/Vector3Data.ts +6 -0
- package/static/3cr-types-browser/types/VectorMovementData.ts +8 -0
- package/static/3cr-types-browser/types/ViewInteractiveMode.ts +5 -0
- package/static/3cr-types-browser/types/ViewOrientation.ts +8 -0
- package/static/3cr-types-browser/types/ViewOrientations.ts +10 -0
- package/static/3cr-types-browser/types/ViewSelectionActions.ts +9 -0
- package/static/3cr-types-browser/types/ViewToggleData.ts +7 -0
- package/static/3cr-types-browser/types/VolumeOrientation.ts +7 -0
- package/test/helper.ts +10 -1
- package/test/setup.ts +13 -0
- package/tsconfig.json +1 -0
- package/vite.config.mts +1 -0
- package/src/dataLayer/__tests__/payload-handler.spec.ts +0 -214
- package/src/dataLayer/payloadHandler.ts +0 -138
- /package/src/dataLayer/{iconData.ts → getIconForPreset.ts} +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ActionRail
|
|
3
|
+
:actions="viewActions"
|
|
4
|
+
:menu-props="menuProps"
|
|
5
|
+
:menu-activator-props="menuActivatorProps"
|
|
6
|
+
@menu-active="onActionModal($event, view)">
|
|
7
|
+
<template #visible="{ action }">
|
|
8
|
+
<Component
|
|
9
|
+
:is="action.component"
|
|
10
|
+
:view="view"
|
|
11
|
+
:element="element"
|
|
12
|
+
variant="button"
|
|
13
|
+
@update:modal="onActionModal($event, view)"
|
|
14
|
+
/>
|
|
15
|
+
</template>
|
|
16
|
+
<template #hidden="{ action }">
|
|
17
|
+
<Component
|
|
18
|
+
:is="action.component"
|
|
19
|
+
:view="view"
|
|
20
|
+
:element="element"
|
|
21
|
+
variant="menu-item"
|
|
22
|
+
@update:modal="onActionModal($event, view)"
|
|
23
|
+
/>
|
|
24
|
+
</template>
|
|
25
|
+
</ActionRail>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
import { computed, shallowRef } from "vue";
|
|
30
|
+
import { ScanView } from "@3cr/types-ts";
|
|
31
|
+
import FlipVerticalAction from "@/components/modal/actions/FlipVerticalAction.vue";
|
|
32
|
+
import FlipHorizontalAction from "@/components/modal/actions/FlipHorizontalAction.vue";
|
|
33
|
+
import FullscreenAction from "@/components/modal/actions/FullscreenAction.vue";
|
|
34
|
+
import PanAction from "@/components/modal/actions/PanAction.vue";
|
|
35
|
+
import ResetViewAction from "@/components/modal/actions/ResetViewAction.vue";
|
|
36
|
+
import Rotate2dAction from "@/components/modal/actions/Rotate2dAction.vue";
|
|
37
|
+
import Slice3dAction from "@/components/modal/actions/Slice3dAction.vue";
|
|
38
|
+
import ZoomAction from "@/components/modal/actions/ZoomAction.vue";
|
|
39
|
+
import { useViewer3cr } from "@/dataLayer/useViewer3cr";
|
|
40
|
+
|
|
41
|
+
interface Props {
|
|
42
|
+
view: ScanView;
|
|
43
|
+
element: any;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Emits = {
|
|
47
|
+
modal: [boolean, number];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type ActionItem = {
|
|
51
|
+
component: any;
|
|
52
|
+
width: number;
|
|
53
|
+
condition: () => boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const menuProps = {
|
|
57
|
+
closeOnContentClick: false
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const menuActivatorProps = {
|
|
61
|
+
icon: 'more_vert',
|
|
62
|
+
variant: 'text',
|
|
63
|
+
color: 'white'
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const props = defineProps<Props>();
|
|
67
|
+
|
|
68
|
+
const emit = defineEmits<Emits>();
|
|
69
|
+
|
|
70
|
+
const viewer3cr = useViewer3cr();
|
|
71
|
+
|
|
72
|
+
const viewActions = computed(() => {
|
|
73
|
+
return actions.value.filter(action => action.condition());
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const actions = shallowRef<ActionItem[]>([
|
|
77
|
+
{
|
|
78
|
+
component: FullscreenAction,
|
|
79
|
+
width: 48,
|
|
80
|
+
condition: () => true,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
component: ResetViewAction,
|
|
84
|
+
width: 48,
|
|
85
|
+
condition: () => true,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
component: PanAction,
|
|
89
|
+
width: 48,
|
|
90
|
+
condition: () => true,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
component: ZoomAction,
|
|
94
|
+
width: 48,
|
|
95
|
+
condition: () => true,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
component: Slice3dAction,
|
|
99
|
+
width: 48,
|
|
100
|
+
condition: () => props.view === ScanView.Volume,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
component: Rotate2dAction,
|
|
104
|
+
width: 48,
|
|
105
|
+
condition: () => props.view !== ScanView.Volume,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
component: FlipHorizontalAction,
|
|
109
|
+
width: 48,
|
|
110
|
+
condition: () => props.view !== ScanView.Volume,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
component: FlipVerticalAction,
|
|
114
|
+
width: 48,
|
|
115
|
+
condition: () => props.view !== ScanView.Volume,
|
|
116
|
+
}
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
async function onActionModal(value: boolean, view: ScanView): Promise<void> {
|
|
120
|
+
await viewer3cr.hoverOverCanvas(!value); // disable unity inputs when interacting with modals
|
|
121
|
+
emit('modal', value, view);
|
|
122
|
+
}
|
|
123
|
+
</script>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-menu
|
|
3
|
+
v-model="menuState"
|
|
4
|
+
min-width="0"
|
|
5
|
+
max-width="300"
|
|
6
|
+
persistent
|
|
7
|
+
:no-click-animation="true"
|
|
8
|
+
:target="target"
|
|
9
|
+
:offset="[15, 0]"
|
|
10
|
+
>
|
|
11
|
+
<v-card>
|
|
12
|
+
<v-card-title class="text-body-1 d-flex align-center">
|
|
13
|
+
<span>{{ title }}</span>
|
|
14
|
+
</v-card-title>
|
|
15
|
+
<v-card-text>
|
|
16
|
+
<v-row no-gutters>
|
|
17
|
+
<v-col cols="4">
|
|
18
|
+
<b style="font-size: 12px">Description</b>
|
|
19
|
+
</v-col>
|
|
20
|
+
<v-col>
|
|
21
|
+
<span style="font-size: 12px">{{ description }}</span>
|
|
22
|
+
</v-col>
|
|
23
|
+
</v-row>
|
|
24
|
+
<v-row no-gutters v-if="smartResponses">
|
|
25
|
+
<v-col cols="4">
|
|
26
|
+
<b style="font-size: 12px">Smart Description</b>
|
|
27
|
+
</v-col>
|
|
28
|
+
<v-col>
|
|
29
|
+
<span style="font-size: 12px">{{
|
|
30
|
+
smartResponses.GptResponse
|
|
31
|
+
}}</span>
|
|
32
|
+
</v-col>
|
|
33
|
+
</v-row>
|
|
34
|
+
<v-row no-gutters>
|
|
35
|
+
<v-col cols="4">
|
|
36
|
+
<b style="font-size: 12px">Resources</b>
|
|
37
|
+
</v-col>
|
|
38
|
+
<v-col>
|
|
39
|
+
<template v-for="action in actions">
|
|
40
|
+
<a style="font-size: 12px" :href="action.ActionData.Url">{{
|
|
41
|
+
action.ActionData.Description
|
|
42
|
+
}}</a>
|
|
43
|
+
<br />
|
|
44
|
+
</template>
|
|
45
|
+
</v-col>
|
|
46
|
+
</v-row>
|
|
47
|
+
</v-card-text>
|
|
48
|
+
</v-card>
|
|
49
|
+
</v-menu>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
import { computed, ref, watch } from 'vue';
|
|
54
|
+
import { GptResponsePayload, GptService } from '@/services/gpt/gpt.service';
|
|
55
|
+
import { DataOverlay, DataOverlayEvent, InteractionType } from '@3cr/types-ts';
|
|
56
|
+
import { dataOverlayEvent, scanState } from '@/dataLayer/scanState';
|
|
57
|
+
import { useEventListener } from "@/components/modal/composables/useEventListener";
|
|
58
|
+
|
|
59
|
+
useEventListener(document, 'click', onClick);
|
|
60
|
+
const annotation = ref<DataOverlay | null>(null);
|
|
61
|
+
const target = ref<[number, number]>([0, 0]);
|
|
62
|
+
const smartResponses = ref<GptResponsePayload | null>(null);
|
|
63
|
+
|
|
64
|
+
const menuState = computed({
|
|
65
|
+
get(): boolean {
|
|
66
|
+
return !!annotation.value;
|
|
67
|
+
},
|
|
68
|
+
set(value: boolean): void {
|
|
69
|
+
if (!value) {
|
|
70
|
+
annotation.value = null;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const title = computed(() => {
|
|
76
|
+
return annotation.value?.Title ?? '';
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const description = computed(() => {
|
|
80
|
+
return annotation.value?.Description ?? '';
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const actions = computed(() => {
|
|
84
|
+
return annotation.value?.CallToAction?.Actions || [];
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
watch([
|
|
88
|
+
() => scanState.value.Orientations.Transverse.Slice,
|
|
89
|
+
() => scanState.value.Orientations.Sagittal.Slice,
|
|
90
|
+
() => scanState.value.Orientations.Coronal.Slice,
|
|
91
|
+
], () => {
|
|
92
|
+
menuState.value = false;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
watch(dataOverlayEvent, async (event: DataOverlayEvent | null) => {
|
|
96
|
+
if (event && event.Interaction === InteractionType.PointerUp) {
|
|
97
|
+
annotation.value = event.Annotation;
|
|
98
|
+
await getSmartResponses();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
async function getSmartResponses(): Promise<void> {
|
|
103
|
+
const response = await GptService.Instantiate().GenerateAnnotations(title.value);
|
|
104
|
+
smartResponses.value = response.data;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onClick(event: Event): void {
|
|
108
|
+
const e = event as MouseEvent;
|
|
109
|
+
if (dataOverlayEvent.value?.Interaction === InteractionType.PointerDown) {
|
|
110
|
+
target.value = [e.x, e.y];
|
|
111
|
+
} else {
|
|
112
|
+
menuState.value = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-treeview
|
|
3
|
+
class="annotation-treeview"
|
|
4
|
+
:items="data"
|
|
5
|
+
density="compact"
|
|
6
|
+
item-value="id"
|
|
7
|
+
open-strategy="single"
|
|
8
|
+
@update:opened="focusSlices($event)"
|
|
9
|
+
>
|
|
10
|
+
<template #prepend="{ item }">
|
|
11
|
+
<div v-if="hasChildren(item)">
|
|
12
|
+
<v-btn
|
|
13
|
+
size="xx-small"
|
|
14
|
+
:icon="getVisibilityIcon(item)"
|
|
15
|
+
variant="text"
|
|
16
|
+
@click.stop="toggleVisibility(item)"
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
<template #title="{ item }">
|
|
21
|
+
<div class="d-flex align-center ml-1">
|
|
22
|
+
<div :style="getMaskStyles(item)">
|
|
23
|
+
<div :style="getBackgroundStyles(item)"></div>
|
|
24
|
+
</div>
|
|
25
|
+
<b class="mx-2">{{ item.id }}</b>
|
|
26
|
+
<span class="text-subtitle-2">{{ item.title }}</span>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
<template #item="{ props }">
|
|
30
|
+
<v-row no-gutters class="px-6 py-2">
|
|
31
|
+
<v-col cols="4">
|
|
32
|
+
<b>{{ findNode(props.value)?.title }}</b>
|
|
33
|
+
</v-col>
|
|
34
|
+
<v-col>
|
|
35
|
+
<span v-html="findNode(props.value)?.description"></span>
|
|
36
|
+
<template v-for="action in findNode(props.value)?.actions">
|
|
37
|
+
<v-chip
|
|
38
|
+
v-if="action.isBtn"
|
|
39
|
+
size="small"
|
|
40
|
+
label
|
|
41
|
+
variant="tonal"
|
|
42
|
+
class="action text-wrap mb-2"
|
|
43
|
+
@click="selectAction(action)"
|
|
44
|
+
>
|
|
45
|
+
{{ action.description }}
|
|
46
|
+
</v-chip>
|
|
47
|
+
<a v-else :href="action.url">{{ action.description }}</a>
|
|
48
|
+
<br />
|
|
49
|
+
</template>
|
|
50
|
+
</v-col>
|
|
51
|
+
</v-row>
|
|
52
|
+
</template>
|
|
53
|
+
</v-treeview>
|
|
54
|
+
<AskAI
|
|
55
|
+
v-model:modal="m_askAi"
|
|
56
|
+
:title="title"
|
|
57
|
+
:question-key="questionKey"
|
|
58
|
+
:question-text="questionText"
|
|
59
|
+
@askFollowup="askFollowUpQuestion"
|
|
60
|
+
>
|
|
61
|
+
</AskAI>
|
|
62
|
+
<span
|
|
63
|
+
class="text-caption"
|
|
64
|
+
style="font-size: 8px !important; line-height: 8px !important"
|
|
65
|
+
>* Powered by ChatGPT, Smart Items are NOT for diagnostic use. Please
|
|
66
|
+
consult a medical professional for a diagnosis or treatment plan
|
|
67
|
+
</span>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script setup lang="ts">
|
|
71
|
+
import { dataOverlayState, initialScanState } from "@/dataLayer/scanState";
|
|
72
|
+
import { nextTick, onMounted, ref } from "vue";
|
|
73
|
+
import { ColourData, SlidersActions, Vector3Data } from "@3cr/types-ts";
|
|
74
|
+
import { GptQuestion, GptService } from "@/services/gpt/gpt.service";
|
|
75
|
+
import {useViewer3cr} from "@/dataLayer/useViewer3cr";
|
|
76
|
+
|
|
77
|
+
interface Annotation {
|
|
78
|
+
id: string;
|
|
79
|
+
index?: number | undefined;
|
|
80
|
+
title: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
icon?: string;
|
|
83
|
+
position?: Vector3Data;
|
|
84
|
+
colour?: ColourData;
|
|
85
|
+
children?: Annotation[];
|
|
86
|
+
actions?: AnnotationAction[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface AnnotationAction {
|
|
90
|
+
description: string;
|
|
91
|
+
url: string;
|
|
92
|
+
title?: string | undefined;
|
|
93
|
+
isBtn: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const viewer3cr = useViewer3cr();
|
|
97
|
+
const data = ref<Annotation[]>([]);
|
|
98
|
+
const m_askAi = ref<boolean>(false);
|
|
99
|
+
const questionKey = ref<number>(0);
|
|
100
|
+
const questionText = ref<string>("");
|
|
101
|
+
const title = ref<string>("");
|
|
102
|
+
|
|
103
|
+
onMounted(async () => {
|
|
104
|
+
data.value = await generateTreeViewData();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
async function selectAction(action: AnnotationAction): Promise<void> {
|
|
108
|
+
questionKey.value = Number(action.url);
|
|
109
|
+
questionText.value = action.description || "";
|
|
110
|
+
title.value = action.title || "";
|
|
111
|
+
m_askAi.value = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function askFollowUpQuestion(question: GptQuestion): Promise<void> {
|
|
115
|
+
m_askAi.value = false;
|
|
116
|
+
await nextTick();
|
|
117
|
+
questionKey.value = question.ApiPreFilledRequestKey;
|
|
118
|
+
questionText.value = question.Question;
|
|
119
|
+
m_askAi.value = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function toggleVisibility(item: any): Promise<void> {
|
|
123
|
+
const visibility = getVisibility(item);
|
|
124
|
+
await viewer3cr.toggle2dAnnotation(item.id, !visibility);
|
|
125
|
+
await viewer3cr.toggle3dAnnotation(item.id, !visibility);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getVisibilityIcon(item: Annotation): string {
|
|
129
|
+
return getVisibility(item) ? "visibility" : "visibility_off";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getVisibility(item: Annotation): boolean {
|
|
133
|
+
const overlay = dataOverlayState.value.DataOverlay.DataOverlay.find(
|
|
134
|
+
(overlay) => item.id === overlay.Id
|
|
135
|
+
);
|
|
136
|
+
return overlay ? overlay.Data.Visibility2d : false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function hasChildren(item: Annotation): boolean {
|
|
140
|
+
return "children" in item;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function findNode(
|
|
144
|
+
id: string,
|
|
145
|
+
items: Annotation | Annotation[] = data.value
|
|
146
|
+
): Annotation | null {
|
|
147
|
+
if (Array.isArray(items)) {
|
|
148
|
+
const values = items
|
|
149
|
+
.map((item) => findNode(id, item))
|
|
150
|
+
.filter((item) => item);
|
|
151
|
+
return values.length > 0 ? values[0] : null;
|
|
152
|
+
} else {
|
|
153
|
+
return items.id === id
|
|
154
|
+
? items
|
|
155
|
+
: hasChildren(items)
|
|
156
|
+
? findNode(id, items.children)
|
|
157
|
+
: null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function generateTreeViewData(): Promise<Annotation[]> {
|
|
162
|
+
return await Promise.all(
|
|
163
|
+
dataOverlayState.value.DataOverlay.DataOverlay.map(async (overlay) => {
|
|
164
|
+
const gpt = await GptService.Instantiate()
|
|
165
|
+
.GenerateAnnotations(overlay.Data.Title)
|
|
166
|
+
.then((data) => data.data)
|
|
167
|
+
.catch((err) => ({
|
|
168
|
+
GptResponse: "failed to execute",
|
|
169
|
+
FollowupQuestions: [],
|
|
170
|
+
}));
|
|
171
|
+
return {
|
|
172
|
+
id: overlay.Id,
|
|
173
|
+
title: overlay.Data.Title,
|
|
174
|
+
icon: overlay.Data.Icon2d,
|
|
175
|
+
colour: overlay.Data.Colour2d,
|
|
176
|
+
position: overlay.Data.Position,
|
|
177
|
+
children: [
|
|
178
|
+
overlay.Data.Description
|
|
179
|
+
? {
|
|
180
|
+
id: `${overlay.Id}-0`,
|
|
181
|
+
title: "Description",
|
|
182
|
+
description: overlay.Data.Description,
|
|
183
|
+
}
|
|
184
|
+
: {
|
|
185
|
+
id: `${overlay.Id}-2`,
|
|
186
|
+
title: "Smart Description",
|
|
187
|
+
description: gpt.GptResponse,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: `${overlay.Id}-1`,
|
|
191
|
+
title: "Resources",
|
|
192
|
+
actions: overlay.Data.CallToAction?.Actions.map((action) => ({
|
|
193
|
+
description: action.ActionData.Description,
|
|
194
|
+
url: action.ActionData.Url,
|
|
195
|
+
isBtn: false,
|
|
196
|
+
})),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: `${overlay.Id}-3`,
|
|
200
|
+
title: "Annotiva AI",
|
|
201
|
+
actions: gpt.FollowupQuestions.map((question) => ({
|
|
202
|
+
description: question.Question,
|
|
203
|
+
url: question.ApiPreFilledRequestKey.toString(),
|
|
204
|
+
title: overlay.Data.Title,
|
|
205
|
+
isBtn: true,
|
|
206
|
+
})),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getMaskStyles(item: Annotation): Record<string, any> {
|
|
215
|
+
const src = `data:image/png;base64, ${item.icon}`;
|
|
216
|
+
return {
|
|
217
|
+
"mask-size": "12px 12px",
|
|
218
|
+
"mask-image": `url('${src}')`,
|
|
219
|
+
"min-width": "12px",
|
|
220
|
+
"min-height": "12px",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getBackgroundStyles(item: Annotation): Record<string, any> {
|
|
225
|
+
const { R, G, B, A } = item.colour!;
|
|
226
|
+
const colour = `rgb(${R * 255}, ${G * 255}, ${B * 255}, ${A * 255})`;
|
|
227
|
+
return {
|
|
228
|
+
"background-color": colour,
|
|
229
|
+
"min-width": "12px",
|
|
230
|
+
"min-height": "12px",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function focusSlices(ids: unknown[]): Promise<void> {
|
|
235
|
+
const id = ids.length > 0 ? ids[0] : null;
|
|
236
|
+
if (!id) { return; }
|
|
237
|
+
|
|
238
|
+
const item = findNode(id as string);
|
|
239
|
+
if (!item) { return; }
|
|
240
|
+
|
|
241
|
+
const x = item.position?.X ?? 0;
|
|
242
|
+
const y = item.position?.Y ?? 0;
|
|
243
|
+
const z = item.position?.Z ?? 0;
|
|
244
|
+
|
|
245
|
+
const xSpace = initialScanState.value.XSpacing || 1;
|
|
246
|
+
const ySpace = initialScanState.value.YSpacing || 1;
|
|
247
|
+
const zSpace = initialScanState.value.ZSpacing || 1;
|
|
248
|
+
|
|
249
|
+
const sagittal = Math.round(x / xSpace);
|
|
250
|
+
const transverse = Math.round(y / ySpace);
|
|
251
|
+
const coronal = Math.round(z / zSpace);
|
|
252
|
+
|
|
253
|
+
await viewer3cr.sliderHandler(SlidersActions.sl12, sagittal);
|
|
254
|
+
await viewer3cr.sliderHandler(SlidersActions.sl09, coronal);
|
|
255
|
+
await viewer3cr.sliderHandler(SlidersActions.sl15, transverse);
|
|
256
|
+
}
|
|
257
|
+
</script>
|
|
258
|
+
|
|
259
|
+
<style lang="scss">
|
|
260
|
+
.annotation-treeview {
|
|
261
|
+
font-size: 12px;
|
|
262
|
+
padding-top: 0;
|
|
263
|
+
|
|
264
|
+
.v-list-group {
|
|
265
|
+
.v-list-item {
|
|
266
|
+
padding: 0 2px;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.v-list-group ~ .v-list-group {
|
|
271
|
+
border-top: thin solid rgb(100%, 100%, 100%, var(--v-border-opacity));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.v-chip.action {
|
|
275
|
+
text-wrap: wrap;
|
|
276
|
+
word-wrap: break-word;
|
|
277
|
+
min-height: 42px;
|
|
278
|
+
line-height: 14px !important;
|
|
279
|
+
height: unset;
|
|
280
|
+
padding: 4px 8px;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
</style>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row class="flex-column" no-gutters>
|
|
3
|
+
<v-row no-gutters>
|
|
4
|
+
<v-col>
|
|
5
|
+
<DoubleSliderSelector
|
|
6
|
+
id="skin-to-bone"
|
|
7
|
+
v-model:value="windowSlider"
|
|
8
|
+
v-bind="huMinMax"
|
|
9
|
+
label="Skin to Bone"
|
|
10
|
+
/>
|
|
11
|
+
</v-col>
|
|
12
|
+
</v-row>
|
|
13
|
+
<v-row no-gutters>
|
|
14
|
+
<v-col>
|
|
15
|
+
<DoubleSliderSelector
|
|
16
|
+
v-model:value="thresholdSlider"
|
|
17
|
+
v-bind="huMinMax"
|
|
18
|
+
label="Fine Adjustment"
|
|
19
|
+
/>
|
|
20
|
+
</v-col>
|
|
21
|
+
</v-row>
|
|
22
|
+
<v-row no-gutters>
|
|
23
|
+
<v-col>
|
|
24
|
+
<v-select
|
|
25
|
+
class="pt-4"
|
|
26
|
+
data-testid="greyscale"
|
|
27
|
+
:model-value="currentGreyscalePreset"
|
|
28
|
+
:items="initialScanState.GreyscalePresets"
|
|
29
|
+
label="Greyscale Preset"
|
|
30
|
+
item-text="Name"
|
|
31
|
+
density="compact"
|
|
32
|
+
variant="outlined"
|
|
33
|
+
persistent-placeholder
|
|
34
|
+
hide-details
|
|
35
|
+
return-object
|
|
36
|
+
placeholder="None"
|
|
37
|
+
@update:model-value="onGreyscalePresetChange"
|
|
38
|
+
>
|
|
39
|
+
<template #item="{ props, item }">
|
|
40
|
+
<v-list-item
|
|
41
|
+
v-bind="props"
|
|
42
|
+
:title="item.raw.Name"
|
|
43
|
+
lines="three"
|
|
44
|
+
>
|
|
45
|
+
<template v-slot:subtitle>
|
|
46
|
+
Skin Density:
|
|
47
|
+
<span class="text-mono">{{ item.raw.Lower }}</span>
|
|
48
|
+
<v-spacer />
|
|
49
|
+
Bone Density:
|
|
50
|
+
<span class="text-mono">{{ item.raw.Upper }}</span>
|
|
51
|
+
</template>
|
|
52
|
+
</v-list-item>
|
|
53
|
+
</template>
|
|
54
|
+
<template v-slot:selection="{ item }">
|
|
55
|
+
<span>{{ item.raw.Name }}</span>
|
|
56
|
+
</template>
|
|
57
|
+
</v-select>
|
|
58
|
+
</v-col>
|
|
59
|
+
</v-row>
|
|
60
|
+
<v-row no-gutters>
|
|
61
|
+
<v-col>
|
|
62
|
+
<v-select
|
|
63
|
+
class="pt-4"
|
|
64
|
+
data-testid="colour"
|
|
65
|
+
:model-value="currentColourPreset"
|
|
66
|
+
:items="initialScanState.ColourPresets"
|
|
67
|
+
label="Colour Preset"
|
|
68
|
+
variant="outlined"
|
|
69
|
+
density="compact"
|
|
70
|
+
item-title="Name"
|
|
71
|
+
hide-details
|
|
72
|
+
placeholder="Select a Colour Preset"
|
|
73
|
+
return-object
|
|
74
|
+
@update:model-value="onColourPresetChange"
|
|
75
|
+
></v-select>
|
|
76
|
+
</v-col>
|
|
77
|
+
</v-row>
|
|
78
|
+
</v-row>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<script setup lang="ts">
|
|
82
|
+
import {
|
|
83
|
+
currentColourPreset,
|
|
84
|
+
currentGreyscalePreset,
|
|
85
|
+
huMinMax,
|
|
86
|
+
initialScanState,
|
|
87
|
+
thresholdSlider,
|
|
88
|
+
windowSlider
|
|
89
|
+
} from "@/dataLayer/scanState";
|
|
90
|
+
import { ColourPresetData, GreyscalePresetData, PresetsActions } from "@3cr/types-ts";
|
|
91
|
+
import { useViewer3cr } from "@/dataLayer/useViewer3cr";
|
|
92
|
+
|
|
93
|
+
const viewer3cr = useViewer3cr();
|
|
94
|
+
|
|
95
|
+
async function onGreyscalePresetChange(preset: GreyscalePresetData): Promise<void> {
|
|
96
|
+
await viewer3cr.setPreset(PresetsActions.pr01, preset);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function onColourPresetChange(preset: ColourPresetData): Promise<void> {
|
|
100
|
+
await viewer3cr.setPreset(PresetsActions.pr02, preset);
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-navigation-drawer
|
|
3
|
+
class="motif-background"
|
|
4
|
+
:model-value="true"
|
|
5
|
+
:rail="!drawer"
|
|
6
|
+
rail-width="72"
|
|
7
|
+
width="350"
|
|
8
|
+
permanent
|
|
9
|
+
touchless
|
|
10
|
+
>
|
|
11
|
+
<template #prepend>
|
|
12
|
+
<ViewerNavigationDrawerHeader :drawer="drawer" />
|
|
13
|
+
</template>
|
|
14
|
+
<template #default>
|
|
15
|
+
<ViewerNavigationDrawerContent v-model:drawer="drawer" :options="options" @update:expanded="emit('update:expanded', $event)" />
|
|
16
|
+
</template>
|
|
17
|
+
<template #append>
|
|
18
|
+
<v-divider />
|
|
19
|
+
<ViewerNavigationDrawerFooter :drawer="drawer" :options="options" />
|
|
20
|
+
</template>
|
|
21
|
+
</v-navigation-drawer>
|
|
22
|
+
<v-btn
|
|
23
|
+
data-testid="toggle"
|
|
24
|
+
height="84"
|
|
25
|
+
color="grey"
|
|
26
|
+
variant="flat"
|
|
27
|
+
width="20"
|
|
28
|
+
min-width="20"
|
|
29
|
+
class="toggle"
|
|
30
|
+
:style="!drawer ? 'left: 72px' : 'left: 350px'"
|
|
31
|
+
@click="drawer = !drawer"
|
|
32
|
+
>
|
|
33
|
+
<v-icon color="black">
|
|
34
|
+
{{ !drawer ? "chevron_right" : "chevron_left" }}
|
|
35
|
+
</v-icon>
|
|
36
|
+
</v-btn>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { computed, ref, watch } from 'vue';
|
|
41
|
+
import { LoadViewerOptions } from "@/models/LoadViewerOptions";
|
|
42
|
+
|
|
43
|
+
interface Props {
|
|
44
|
+
options: LoadViewerOptions;
|
|
45
|
+
drawer?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Emits = {
|
|
49
|
+
'update:drawer': [boolean];
|
|
50
|
+
'update:expanded': [number | undefined];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
54
|
+
drawer: false
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const emit = defineEmits<Emits>();
|
|
58
|
+
|
|
59
|
+
const _drawer = ref<boolean>(props.drawer);
|
|
60
|
+
|
|
61
|
+
const drawer = computed({
|
|
62
|
+
get(): boolean {
|
|
63
|
+
return _drawer.value;
|
|
64
|
+
},
|
|
65
|
+
set(value: boolean): void {
|
|
66
|
+
_drawer.value = value;
|
|
67
|
+
emit('update:drawer', value);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
watch(() => props.drawer, (value: boolean) => {
|
|
72
|
+
drawer.value = value;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
defineExpose({ drawer });
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<style scoped>
|
|
79
|
+
.toggle {
|
|
80
|
+
border-bottom-left-radius: 0 !important;
|
|
81
|
+
border-top-left-radius: 0 !important;
|
|
82
|
+
padding: 0;
|
|
83
|
+
position: absolute;
|
|
84
|
+
top: 50%;
|
|
85
|
+
opacity: 0.7;
|
|
86
|
+
z-index: 1;
|
|
87
|
+
transform: translateY(-50%);
|
|
88
|
+
transition: 0.25s left;
|
|
89
|
+
}
|
|
90
|
+
</style>
|