@3cr/viewer-browser 0.0.220 → 0.0.247
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/components.d.ts +6 -2
- package/dist/Viewer3CR.js +32 -32
- package/dist/Viewer3CR.mjs +17808 -14315
- package/dist/Viewer3CR.umd.js +32 -32
- package/index.html +1 -1
- package/package.json +4 -3
- package/playground/index.html +4 -12
- package/src/App.vue +29 -14
- package/src/__tests__/main.spec.ts +4 -4
- package/src/assets/styles.scss +6 -2
- package/src/components/demo/DemoPatientModal.vue +12 -22
- package/src/components/demo/licence/DemoLicenceInfoModal.vue +11 -29
- package/src/components/demo/options.ts +42 -39
- package/src/components/demo/patient/DemoPatientInfoModal.vue +11 -29
- package/src/components/modal/CloseViewerModal.vue +1 -1
- package/src/components/modal/MftpWebGL3DRModal.vue +133 -120
- package/src/components/modal/ViewerActionRail.vue +0 -6
- package/src/components/modal/ViewerNavigationDrawerContent.vue +25 -16
- package/src/components/modal/ViewerNavigationDrawerFooter.vue +32 -9
- package/src/components/modal/ViewerNavigationDrawerHeader.vue +6 -1
- package/src/components/modal/WebGL3DR.vue +7 -0
- package/src/components/modal/__tests__/ViewerNavigationDrawerHeader.spec.ts +3 -2
- package/src/components/modal/buttons/AutoAnnotateBtn.vue +181 -13
- package/src/components/modal/menus/FileMenu.vue +2 -9
- package/src/components/modal/menus/SettingsMenu.vue +2 -7
- package/src/components/navigation/mcad/McadGlobalActions.vue +20 -0
- package/src/components/navigation/mcad/McadGlobalOpacitySlider.vue +103 -0
- package/src/components/navigation/mcad/McadGlobalScanViewBtn.vue +28 -0
- package/src/components/navigation/mcad/McadGlobalVisibilityBtn.vue +38 -0
- package/src/components/shared/LoadingSpinner.vue +27 -36
- package/src/components/views/AnnotationTreeView.vue +3 -1
- package/src/components/views/MarkupTreeView.vue +3 -1
- package/src/components/views/McadObjectTreeView.vue +46 -36
- package/src/components/views/modals/DataOverlayGeneralModal.vue +71 -0
- package/src/components/views/modals/DataOverlayMarkupModal.vue +60 -0
- package/src/components/views/modals/DataOverlayModal.vue +54 -55
- package/src/components/views/shared/Opacity.vue +0 -1
- package/src/composables/useDebounce.ts +11 -0
- package/src/composables/useIntroJs.ts +23 -6
- package/src/composables/useViewerOptions.ts +1 -0
- package/src/functions/guards/isDataOverlayAngle.ts +6 -0
- package/src/functions/guards/isDataOverlayLength.ts +6 -0
- package/src/functions/guards/isDataOverlayPolygon.ts +6 -0
- package/src/functions/modelHelper.ts +2 -0
- package/src/functions/notification.ts +63 -29
- package/src/main.ts +23 -15
- package/src/models/loadViewerOptions.ts +39 -0
- package/src/models/loadViewerPayload.ts +0 -7
- package/src/services/viewer-3cr.service.ts +13 -1
- package/src/tools/data-overlay.tool.ts +71 -0
- package/src/types/data-overlay-event.ts +5 -0
- package/src/types/data-overlay-info.ts +4 -0
- package/src/types/demo-type.ts +4 -0
- package/src/components/modal/actions/HideViewAction.vue +0 -38
- package/src/components/views/modals/DataOverlayModalManager.vue +0 -104
- package/src/components/views/modals/__tests__/DataOverlayModal.spec.ts +0 -33
- package/src/components/views/modals/__tests__/DataOverlayModalManager.spec.ts +0 -93
|
@@ -1,41 +1,45 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
<div>
|
|
3
|
+
<mcad-global-actions :items="items" />
|
|
4
|
+
<v-treeview class="py-0" :items="data" item-value="id" density="compact" open-strategy="single">
|
|
5
|
+
<template #prepend="{ item }">
|
|
6
|
+
<visibility-btn :is-visible="item.isVisible" @click.stop="toggleVisibility(item)" />
|
|
7
|
+
</template>
|
|
8
|
+
<template #title="{ item }">
|
|
9
|
+
<div class="d-flex align-center">
|
|
10
|
+
<object-label class="mr-1" :label="item.title">
|
|
11
|
+
<template #prepend>
|
|
12
|
+
<object-color class="ml-1 mr-2" :color="getColor(item)" />
|
|
13
|
+
</template>
|
|
14
|
+
</object-label>
|
|
15
|
+
<v-spacer />
|
|
16
|
+
<opacity
|
|
17
|
+
:model-value="getOpacity(item)"
|
|
18
|
+
max-width="72px"
|
|
19
|
+
@update:model-value="onOpacityUpdate(item, $event)"
|
|
20
|
+
@mousedown.stop
|
|
21
|
+
@click.stop
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
<template #append="{ item }">
|
|
26
|
+
<v-menu v-if="showMenu">
|
|
27
|
+
<template #activator="{ props }">
|
|
28
|
+
<v-btn v-bind="props" data-testid="menu" size="32" variant="text" icon="more_vert" />
|
|
11
29
|
</template>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
</template>
|
|
22
|
-
<template #append="{ item }">
|
|
23
|
-
<v-menu>
|
|
24
|
-
<template #activator="{ props }">
|
|
25
|
-
<v-btn v-bind="props" data-testid="menu" size="32" variant="text" icon="more_vert" />
|
|
26
|
-
</template>
|
|
27
|
-
<v-list>
|
|
28
|
-
<v-list-item data-testid="download" prepend-icon="download" @click="downloadMcad(item.key)">
|
|
29
|
-
<v-list-item-title>Download</v-list-item-title>
|
|
30
|
-
</v-list-item>
|
|
31
|
-
</v-list>
|
|
32
|
-
</v-menu>
|
|
33
|
-
</template>
|
|
34
|
-
</v-treeview>
|
|
30
|
+
<v-list>
|
|
31
|
+
<v-list-item data-testid="download" prepend-icon="download" @click="downloadMcad(item.key)">
|
|
32
|
+
<v-list-item-title>Download</v-list-item-title>
|
|
33
|
+
</v-list-item>
|
|
34
|
+
</v-list>
|
|
35
|
+
</v-menu>
|
|
36
|
+
</template>
|
|
37
|
+
</v-treeview>
|
|
38
|
+
</div>
|
|
35
39
|
</template>
|
|
36
40
|
|
|
37
41
|
<script setup lang="ts">
|
|
38
|
-
import { ref, watch } from 'vue';
|
|
42
|
+
import { computed, ref, watch } from 'vue';
|
|
39
43
|
import { useViewer3cr } from '@/composables/useViewer3cr';
|
|
40
44
|
import { clamp } from '@/functions/clamp';
|
|
41
45
|
import { rgbaNormalizedToCss } from '@/functions/rgbaToCss';
|
|
@@ -55,6 +59,10 @@ const viewer3cr = useViewer3cr();
|
|
|
55
59
|
const options = useViewerOptions();
|
|
56
60
|
const data = ref<McadObjectTreeViewItem[]>([]);
|
|
57
61
|
|
|
62
|
+
const showMenu = computed(() => {
|
|
63
|
+
return !!options.value.OnDownloadMcad;
|
|
64
|
+
});
|
|
65
|
+
|
|
58
66
|
watch(
|
|
59
67
|
() => props.items,
|
|
60
68
|
(items) => {
|
|
@@ -64,7 +72,9 @@ watch(
|
|
|
64
72
|
);
|
|
65
73
|
|
|
66
74
|
function setTreeViewData(mcadObjects: DataOverlayMcad[]): void {
|
|
67
|
-
data.value = mcadObjects
|
|
75
|
+
data.value = mcadObjects
|
|
76
|
+
.sort((a, b) => a.Title.localeCompare(b.Title))
|
|
77
|
+
.map((mcadObject) => mapToTreeViewItem(mcadObject));
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
async function downloadMcad(id: string): Promise<void> {
|
|
@@ -74,8 +84,8 @@ async function downloadMcad(id: string): Promise<void> {
|
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
function getColor(item: McadObjectTreeViewItem): string {
|
|
77
|
-
const { r, g, b
|
|
78
|
-
return rgbaNormalizedToCss(r
|
|
87
|
+
const { r, g, b } = item.colour;
|
|
88
|
+
return rgbaNormalizedToCss(r, g, b, 1);
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
function getOpacity(item: McadObjectTreeViewItem): number {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card density="compact">
|
|
3
|
+
<v-card-title>{{ dataOverlay.Title }}</v-card-title>
|
|
4
|
+
<v-list density="compact" :lines="false">
|
|
5
|
+
<v-list-item v-if="hasDescription" prepend-icon="description">
|
|
6
|
+
<v-list-item-title>Description</v-list-item-title>
|
|
7
|
+
<v-list-item-subtitle>{{ dataOverlay.Description }}</v-list-item-subtitle>
|
|
8
|
+
</v-list-item>
|
|
9
|
+
<v-list-item v-else prepend-icon="psychology">
|
|
10
|
+
<v-list-item-title>Smart Description</v-list-item-title>
|
|
11
|
+
<v-list-item-subtitle v-if="hasSmartDescription">
|
|
12
|
+
{{ smartDescriptions[dataOverlay.Title].GptResponse }}
|
|
13
|
+
</v-list-item-subtitle>
|
|
14
|
+
<v-list-item-subtitle v-else>
|
|
15
|
+
<v-progress-linear class="mt-2" indeterminate />
|
|
16
|
+
</v-list-item-subtitle>
|
|
17
|
+
</v-list-item>
|
|
18
|
+
<v-list-item prepend-icon="info">
|
|
19
|
+
<v-list-item-title>Resources</v-list-item-title>
|
|
20
|
+
<v-list-item-subtitle v-if="hasActions">
|
|
21
|
+
<template v-for="action in dataOverlay.CallToAction!.Actions">
|
|
22
|
+
<a @click.stop class="text-small" :href="action.ActionData.Url" target="_blank">
|
|
23
|
+
{{ action.ActionData.Description }}
|
|
24
|
+
</a>
|
|
25
|
+
<br />
|
|
26
|
+
</template>
|
|
27
|
+
</v-list-item-subtitle>
|
|
28
|
+
<v-list-item-subtitle v-else class="font-italic">No resources provided</v-list-item-subtitle>
|
|
29
|
+
</v-list-item>
|
|
30
|
+
</v-list>
|
|
31
|
+
</v-card>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { DataOverlay } from '@/types/data-overlay';
|
|
36
|
+
import { computed, ref, watch } from 'vue';
|
|
37
|
+
import { GptResponsePayload } from '@/types/gpt-response-payload';
|
|
38
|
+
import { GptService } from '@/services/gpt.service';
|
|
39
|
+
|
|
40
|
+
interface Props {
|
|
41
|
+
dataOverlay: DataOverlay;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const props = defineProps<Props>();
|
|
45
|
+
|
|
46
|
+
const smartDescriptions = ref<Record<string, GptResponsePayload>>({});
|
|
47
|
+
|
|
48
|
+
const hasDescription = computed(() => !!props.dataOverlay.Description);
|
|
49
|
+
|
|
50
|
+
const hasSmartDescription = computed(() => props.dataOverlay.Title in smartDescriptions.value);
|
|
51
|
+
|
|
52
|
+
const hasActions = computed(() => {
|
|
53
|
+
return props.dataOverlay.CallToAction?.Actions && props.dataOverlay.CallToAction.Actions.length > 0;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
watch(
|
|
57
|
+
() => props.dataOverlay,
|
|
58
|
+
async (value: DataOverlay) => {
|
|
59
|
+
await generateSmartDescriptions(value);
|
|
60
|
+
},
|
|
61
|
+
{ immediate: true }
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
async function generateSmartDescriptions(overlay: DataOverlay): Promise<void> {
|
|
65
|
+
const key = overlay.Title;
|
|
66
|
+
if (!(key in smartDescriptions.value)) {
|
|
67
|
+
const response = await GptService.Instantiate().GenerateAnnotations(key);
|
|
68
|
+
smartDescriptions.value[key] = response.data;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card density="compact">
|
|
3
|
+
<v-card-title>{{ markup.Title }}</v-card-title>
|
|
4
|
+
<v-list density="compact" :lines="false">
|
|
5
|
+
<v-list-item prepend-icon="analytics">
|
|
6
|
+
<v-list-item-title>Data</v-list-item-title>
|
|
7
|
+
<v-list-item-subtitle>
|
|
8
|
+
<template v-if="'SegmentLengths' in markup">
|
|
9
|
+
<v-row no-gutters v-for="(segment, idx) in markup.SegmentLengths">
|
|
10
|
+
<v-col style="width: 200px">Segment {{ idx + 1 }}</v-col>
|
|
11
|
+
<v-col class="text-mono">{{ segment }}{{ markup.Units }}</v-col>
|
|
12
|
+
</v-row>
|
|
13
|
+
<v-row no-gutters>
|
|
14
|
+
<v-col style="width: 200px">Total Length</v-col>
|
|
15
|
+
<v-col class="text-mono">{{ markup.TotalLength }}{{ markup.Units }}</v-col>
|
|
16
|
+
</v-row>
|
|
17
|
+
</template>
|
|
18
|
+
<template v-if="'Area' in markup">
|
|
19
|
+
<v-row no-gutters>
|
|
20
|
+
<v-col style="width: 200px">Area</v-col>
|
|
21
|
+
<v-col class="text-mono">{{ markup.Area }}</v-col>
|
|
22
|
+
</v-row>
|
|
23
|
+
<v-row no-gutters>
|
|
24
|
+
<v-col style="width: 200px">Perimeter</v-col>
|
|
25
|
+
<v-col class="text-mono">{{ markup.Perimeter }}</v-col>
|
|
26
|
+
</v-row>
|
|
27
|
+
<v-row no-gutters>
|
|
28
|
+
<v-col style="width: 200px">Average HU</v-col>
|
|
29
|
+
<v-col class="text-mono">{{ markup.AverageHU }}</v-col>
|
|
30
|
+
</v-row>
|
|
31
|
+
<v-row no-gutters>
|
|
32
|
+
<v-col style="width: 200px">Lowest HU</v-col>
|
|
33
|
+
<v-col class="text-mono">{{ markup.LowestHU }}</v-col>
|
|
34
|
+
</v-row>
|
|
35
|
+
<v-row no-gutters>
|
|
36
|
+
<v-col style="width: 200px">Highest HU</v-col>
|
|
37
|
+
<v-col class="text-mono">{{ markup.HighestHU }}</v-col>
|
|
38
|
+
</v-row>
|
|
39
|
+
</template>
|
|
40
|
+
<template v-if="'SegmentAngles' in markup">
|
|
41
|
+
<v-row no-gutters v-for="(angle, idx) in markup.SegmentAngles">
|
|
42
|
+
<v-col style="width: 200px">Angle {{ idx + 1 }}</v-col>
|
|
43
|
+
<v-col class="text-mono">{{ angle.Acute }}{{ markup.Units }}</v-col>
|
|
44
|
+
</v-row>
|
|
45
|
+
</template>
|
|
46
|
+
</v-list-item-subtitle>
|
|
47
|
+
</v-list-item>
|
|
48
|
+
</v-list>
|
|
49
|
+
</v-card>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
import { DataOverlayMarkup } from '@/types/data-overlay-markup';
|
|
54
|
+
|
|
55
|
+
interface Props {
|
|
56
|
+
markup: DataOverlayMarkup;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
defineProps<Props>();
|
|
60
|
+
</script>
|
|
@@ -1,89 +1,88 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-menu
|
|
3
|
-
v-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
:no-click-animation="true"
|
|
8
|
-
:target="item.target"
|
|
9
|
-
>
|
|
10
|
-
<v-card density="compact">
|
|
11
|
-
<v-card-title>{{ item.title }}</v-card-title>
|
|
12
|
-
<v-list density="compact" :lines="false">
|
|
13
|
-
<v-list-item v-for="data of item.data">
|
|
14
|
-
<template #prepend>
|
|
15
|
-
<v-icon class="mr-n4" :icon="data.icon" />
|
|
16
|
-
</template>
|
|
17
|
-
<v-list-item-title>{{ data.title }}</v-list-item-title>
|
|
18
|
-
|
|
19
|
-
<v-list-item-subtitle v-if="data.subtitle">{{ data.subtitle }}</v-list-item-subtitle>
|
|
20
|
-
|
|
21
|
-
<v-list-item-subtitle v-if="data.actions && data.actions.length > 0">
|
|
22
|
-
<template v-for="action in data.actions">
|
|
23
|
-
<a @click.stop class="text-small" :href="action.url" target="_blank">
|
|
24
|
-
{{ action.description }}
|
|
25
|
-
</a>
|
|
26
|
-
<br />
|
|
27
|
-
</template>
|
|
28
|
-
</v-list-item-subtitle>
|
|
29
|
-
</v-list-item>
|
|
30
|
-
</v-list>
|
|
31
|
-
</v-card>
|
|
2
|
+
<v-menu v-model="menu" max-width="500" persistent :no-click-animation="true" :target="menuTarget">
|
|
3
|
+
<template v-if="selected">
|
|
4
|
+
<data-overlay-markup-modal v-if="isMarkup" :markup="selected as DataOverlayMarkup" />
|
|
5
|
+
<data-overlay-general-modal v-else :data-overlay="selected" />
|
|
6
|
+
</template>
|
|
32
7
|
</v-menu>
|
|
33
8
|
</template>
|
|
34
9
|
|
|
35
10
|
<script setup lang="ts">
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
11
|
+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
12
|
+
import { useEventListener } from '@/composables/useEventListener';
|
|
13
|
+
import { DataOverlay } from '@/types/data-overlay';
|
|
14
|
+
import { DataOverlayMarkup } from '@/types/data-overlay-markup';
|
|
15
|
+
import { isDataOverlayLength } from '@/functions/guards/isDataOverlayLength';
|
|
16
|
+
import { isDataOverlayPolygon } from '@/functions/guards/isDataOverlayPolygon';
|
|
17
|
+
import { isDataOverlayAngle } from '@/functions/guards/isDataOverlayAngle';
|
|
18
|
+
import { useDataOverlayTool } from '@/tools/data-overlay.tool';
|
|
38
19
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
item: DataOverlayInfo;
|
|
42
|
-
}
|
|
20
|
+
useEventListener(document, 'mousemove', onMouseMove);
|
|
21
|
+
useEventListener(document, 'mousedown', onMouseDown);
|
|
43
22
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
23
|
+
const dataOverlayTool = useDataOverlayTool();
|
|
24
|
+
const menu = ref<boolean>(false);
|
|
25
|
+
const currentTarget = ref<[number, number]>([0, 0]);
|
|
26
|
+
const menuTarget = ref<[number, number]>([0, 0]);
|
|
47
27
|
|
|
48
|
-
const
|
|
49
|
-
|
|
28
|
+
const selected = computed(() => dataOverlayTool.selected.value);
|
|
29
|
+
|
|
30
|
+
const isMarkup = computed(() => {
|
|
31
|
+
const val = selected.value;
|
|
32
|
+
return val && (isDataOverlayLength(val) || isDataOverlayPolygon(val) || isDataOverlayAngle(val));
|
|
50
33
|
});
|
|
51
34
|
|
|
52
|
-
|
|
35
|
+
onMounted(() => {
|
|
36
|
+
dataOverlayTool.activate();
|
|
37
|
+
});
|
|
53
38
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return props.modal;
|
|
57
|
-
},
|
|
58
|
-
set(value: boolean): void {
|
|
59
|
-
emit('update:modal', value);
|
|
60
|
-
}
|
|
39
|
+
onUnmounted(() => {
|
|
40
|
+
dataOverlayTool.deactivate();
|
|
61
41
|
});
|
|
42
|
+
|
|
43
|
+
watch(
|
|
44
|
+
selected,
|
|
45
|
+
async (value: DataOverlay | null) => {
|
|
46
|
+
if (value !== null) {
|
|
47
|
+
menuTarget.value = currentTarget.value;
|
|
48
|
+
menu.value = true;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{ immediate: true }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
function onMouseMove(event: MouseEvent): void {
|
|
55
|
+
currentTarget.value = [event.x, event.y];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onMouseDown(): void {
|
|
59
|
+
menu.value = false;
|
|
60
|
+
}
|
|
62
61
|
</script>
|
|
63
62
|
|
|
64
63
|
<style scoped lang="scss">
|
|
65
|
-
.v-card {
|
|
64
|
+
:deep(.v-card) {
|
|
66
65
|
background: rgba(var(--v-theme-surface), 0.5) !important;
|
|
67
|
-
backdrop-filter: blur(
|
|
66
|
+
backdrop-filter: blur(24px);
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
.v-card-title {
|
|
69
|
+
:deep(.v-card-title) {
|
|
71
70
|
background: none;
|
|
72
71
|
font-size: 14px;
|
|
73
72
|
padding-bottom: 0;
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
.v-list {
|
|
75
|
+
:deep(.v-list) {
|
|
77
76
|
background: none;
|
|
78
77
|
padding-top: 4px;
|
|
79
78
|
padding-bottom: 4px;
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
.v-list-item-title {
|
|
81
|
+
:deep(.v-list-item-title) {
|
|
83
82
|
font-size: 13px;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
.v-list-item-subtitle {
|
|
85
|
+
:deep(.v-list-item-subtitle) {
|
|
87
86
|
font-size: 12px;
|
|
88
87
|
}
|
|
89
88
|
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Debouncable<Args extends unknown[]> = (...args: Args) => any;
|
|
2
|
+
|
|
3
|
+
export type Debounced<Args extends unknown[]> = (...args: Args) => void;
|
|
4
|
+
|
|
5
|
+
export function useDebounce<Args extends unknown[]>(callback: Debouncable<Args>, delay: number): Debounced<Args> {
|
|
6
|
+
let timeout: NodeJS.Timeout;
|
|
7
|
+
return function (...args: Args): void {
|
|
8
|
+
clearTimeout(timeout);
|
|
9
|
+
timeout = setTimeout(callback, delay, ...args);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -5,13 +5,17 @@ import { computed, nextTick, ref, watch, WatchStopHandle } from 'vue';
|
|
|
5
5
|
import { ScanView } from '@3cr/types-ts';
|
|
6
6
|
import { useViewer3cr } from '@/composables/useViewer3cr';
|
|
7
7
|
import { isFullscreen, previousLayout, scanState } from '@/models/scanState';
|
|
8
|
+
import { DemoType } from '@/types/demo-type';
|
|
8
9
|
import introJs from 'intro.js';
|
|
9
|
-
|
|
10
|
-
const viewer3cr = useViewer3cr();
|
|
10
|
+
import { useViewerOptions } from '@/composables/useViewerOptions';
|
|
11
11
|
|
|
12
12
|
const demo = computed<string>(() => {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
switch (demoType.value) {
|
|
14
|
+
case DemoType.Licence:
|
|
15
|
+
return 'Licence';
|
|
16
|
+
default:
|
|
17
|
+
return 'Patient';
|
|
18
|
+
}
|
|
15
19
|
});
|
|
16
20
|
|
|
17
21
|
const options = computed<Partial<TourOptions>>(() => ({
|
|
@@ -71,11 +75,16 @@ function blurActiveElement(): void {
|
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
export function useIntroJs() {
|
|
78
|
+
const viewer3cr = useViewer3cr();
|
|
79
|
+
const viewerOptions = useViewerOptions();
|
|
80
|
+
|
|
74
81
|
let scanStateHandle: WatchStopHandle;
|
|
75
82
|
|
|
76
83
|
function onStart(this: Tour): void {
|
|
77
84
|
scanStateHandle = watch(scanState, () => {
|
|
78
|
-
this.
|
|
85
|
+
if (this.isActive()) {
|
|
86
|
+
this.refresh(true);
|
|
87
|
+
}
|
|
79
88
|
});
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -106,9 +115,16 @@ export function useIntroJs() {
|
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
/* istanbul ignore next: does not work with jsdom -- @preserve */
|
|
109
|
-
function
|
|
118
|
+
function onBeforeExit(this: Tour): boolean {
|
|
110
119
|
scanStateHandle();
|
|
111
120
|
setTimeout(() => blurActiveElement(), 500);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function onExit(this: Tour): Promise<void> {
|
|
125
|
+
if (viewerOptions.value.OnInteractionReady) {
|
|
126
|
+
await viewerOptions.value.OnInteractionReady();
|
|
127
|
+
}
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
const tour = introJs
|
|
@@ -117,6 +133,7 @@ export function useIntroJs() {
|
|
|
117
133
|
.onStart(onStart)
|
|
118
134
|
.onBeforeChange(onBeforeChange)
|
|
119
135
|
.onChange(onChange)
|
|
136
|
+
.onBeforeExit(onBeforeExit)
|
|
120
137
|
.onExit(onExit);
|
|
121
138
|
|
|
122
139
|
const intro = ref<Tour>(tour);
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CurrentDataOverlayState,
|
|
4
4
|
CurrentMcadState,
|
|
5
5
|
CurrentScanState,
|
|
6
|
+
GraphicType,
|
|
6
7
|
InitialDataOverlayState,
|
|
7
8
|
InitialMcadState,
|
|
8
9
|
InitialScanState,
|
|
@@ -97,6 +98,7 @@ export function inflateScanState(): CurrentScanState {
|
|
|
97
98
|
},
|
|
98
99
|
NavigationCube: {
|
|
99
100
|
Version: '1.0.0',
|
|
101
|
+
NavCubeGraphicType: GraphicType.BODY_SYMBOL,
|
|
100
102
|
Transform: {
|
|
101
103
|
Version: '1.0.0',
|
|
102
104
|
AnchorPoint: AnchorPoint.DEFAULT,
|
|
@@ -1,48 +1,82 @@
|
|
|
1
1
|
import { InteractivityActions, NavigationCubeActions, NotificationPayload, NotificationsActions } from '@3cr/types-ts';
|
|
2
2
|
import { useNotification } from '@kyvg/vue3-notification';
|
|
3
|
+
import { t } from '@3cr/translations-ts';
|
|
4
|
+
|
|
3
5
|
const { notify } = useNotification();
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
const notification = JSON.parse(message) as NotificationPayload;
|
|
7
|
+
type NotificationType = 'success' | 'info' | 'warn' | 'error';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
case
|
|
11
|
-
|
|
12
|
-
case InteractivityActions.in04:
|
|
13
|
-
case NavigationCubeActions.nc01:
|
|
14
|
-
return;
|
|
15
|
-
default:
|
|
16
|
-
}
|
|
9
|
+
function getNotificationType(action: NotificationsActions): NotificationType {
|
|
10
|
+
switch (action) {
|
|
11
|
+
case NotificationsActions.no01:
|
|
12
|
+
return 'success';
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
case NotificationsActions.no02:
|
|
15
|
+
return 'error';
|
|
16
|
+
|
|
17
|
+
case NotificationsActions.no03:
|
|
18
|
+
return 'warn';
|
|
19
|
+
|
|
20
|
+
default:
|
|
21
|
+
case NotificationsActions.no04:
|
|
22
|
+
return 'info';
|
|
20
23
|
}
|
|
24
|
+
}
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
function getNotificationTitle(action: NotificationsActions): string {
|
|
23
27
|
switch (action) {
|
|
24
|
-
// Muting no01
|
|
25
28
|
case NotificationsActions.no01:
|
|
26
|
-
|
|
27
|
-
return;
|
|
29
|
+
return 'Success';
|
|
28
30
|
|
|
29
31
|
case NotificationsActions.no02:
|
|
30
|
-
|
|
31
|
-
break;
|
|
32
|
+
return 'Error';
|
|
32
33
|
|
|
33
34
|
case NotificationsActions.no03:
|
|
34
|
-
|
|
35
|
-
break;
|
|
35
|
+
return 'Warning';
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
default:
|
|
38
38
|
case NotificationsActions.no04:
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
return 'Information';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getNotificationText(notification: NotificationPayload): string {
|
|
44
|
+
return t(notification.Code, {});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function shouldDisplayNotification(action: NotificationsActions, notification: NotificationPayload): boolean {
|
|
48
|
+
// Hide type specific messages
|
|
49
|
+
const hiddenTypes = [NotificationsActions.no01, NotificationsActions.no04];
|
|
50
|
+
if (hiddenTypes.includes(action)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Hide all slider actions
|
|
55
|
+
if (notification.Action.startsWith('sl')) {
|
|
56
|
+
return false;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
// Hide action specific messages
|
|
60
|
+
const hiddenActions = [
|
|
61
|
+
InteractivityActions.in01,
|
|
62
|
+
InteractivityActions.in02,
|
|
63
|
+
InteractivityActions.in03,
|
|
64
|
+
InteractivityActions.in04,
|
|
65
|
+
NavigationCubeActions.nc01
|
|
66
|
+
] as string[];
|
|
67
|
+
if (hiddenActions.includes(notification.Action)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function handleNotification(action: NotificationsActions, message: string) {
|
|
75
|
+
const notification = JSON.parse(message) as NotificationPayload;
|
|
76
|
+
if (shouldDisplayNotification(action, notification)) {
|
|
77
|
+
const title = getNotificationTitle(action);
|
|
78
|
+
const text = getNotificationText(notification);
|
|
79
|
+
const type = getNotificationType(action);
|
|
80
|
+
notify({ title, text, type });
|
|
81
|
+
}
|
|
48
82
|
}
|