@3cr/viewer-browser 0.0.1

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.
Files changed (49) hide show
  1. package/.browserslistrc +4 -0
  2. package/.editorconfig +5 -0
  3. package/.idea/git_toolbox_prj.xml +15 -0
  4. package/.idea/vcs.xml +6 -0
  5. package/.idea/workspace.xml +183 -0
  6. package/.vite/deps/_metadata.json +8 -0
  7. package/.vite/deps/package.json +3 -0
  8. package/README.md +81 -0
  9. package/components.d.ts +19 -0
  10. package/dist/3cr-viewer-browser.mjs +18644 -0
  11. package/dist/3cr-viewer-browser.umd.js +18 -0
  12. package/dist/style.css +5 -0
  13. package/index.html +24 -0
  14. package/index.ts +37 -0
  15. package/package.json +41 -0
  16. package/src/App.vue +40 -0
  17. package/src/assets/images/MainBackdrop.svg +48 -0
  18. package/src/assets/images/dark/3DICOM.png +0 -0
  19. package/src/assets/images/dark/3dicom-logo.svg +1 -0
  20. package/src/assets/images/dark/Singular-Health-Disc-Mono.svg +9 -0
  21. package/src/assets/images/dark/Singular-Health-Trademark-mono.svg +23 -0
  22. package/src/assets/images/light/3DICOM.png +0 -0
  23. package/src/assets/images/light/3dicom-logo.svg +1 -0
  24. package/src/assets/images/light/Singular-Health-Disc-Mono.svg +9 -0
  25. package/src/assets/images/light/Singular-Health-Trademark-mono.svg +23 -0
  26. package/src/assets/logo.png +0 -0
  27. package/src/assets/logo.svg +6 -0
  28. package/src/components/DoubleSliderSelector.vue +112 -0
  29. package/src/components/ExpansionHeaderMiniMenu.vue +19 -0
  30. package/src/components/HelloWorld.vue +157 -0
  31. package/src/components/LoadingSpinner.vue +157 -0
  32. package/src/components/MftpWebGL3DRModal.vue +995 -0
  33. package/src/components/README.md +35 -0
  34. package/src/components/SliderSelector.vue +101 -0
  35. package/src/components/VerticalSliderSelector.vue +83 -0
  36. package/src/components/WebGL3DR.vue +107 -0
  37. package/src/helpers/layoutOverlayStyle.ts +86 -0
  38. package/src/helpers/modelHelper.ts +109 -0
  39. package/src/helpers/models.ts +69 -0
  40. package/src/helpers/utils.ts +16 -0
  41. package/src/main.ts +23 -0
  42. package/src/plugins/README.md +3 -0
  43. package/src/plugins/index.ts +15 -0
  44. package/src/plugins/vuetify.ts +27 -0
  45. package/src/types/window.shim.ts +24 -0
  46. package/src/vite-env.d.ts +7 -0
  47. package/tsconfig.json +34 -0
  48. package/tsconfig.node.json +9 -0
  49. package/vite.config.mts +74 -0
@@ -0,0 +1,995 @@
1
+ <template>
2
+ <v-dialog
3
+ class="pa-0 ma-0 overflow-y-auto overflow-x-hidden"
4
+ :modelValue="value"
5
+ style="max-height: unset"
6
+ :scrollable="false"
7
+ fullscreen
8
+ persistent
9
+ @click:outside.prevent.stop="alterValue(false)"
10
+ @input="alterValue"
11
+ >
12
+ <v-dialog v-model="m_closeDialog">
13
+ <v-card class="pa-1 ma-auto position-relative" max-width="450">
14
+ <v-card-title>Close Viewer?</v-card-title>
15
+ <v-card-text>Are you sure you want to close the Online Viewer?</v-card-text>
16
+ <v-card-actions>
17
+ <v-btn variant="text" color="red" @click="m_closeDialog = false"> Cancel </v-btn>
18
+ <v-spacer />
19
+ <v-btn color="primary" @click="closeModal"> Close without saving </v-btn>
20
+ <v-btn color="primary" @click="closeModal"> Save Session </v-btn>
21
+ </v-card-actions>
22
+ </v-card>
23
+ </v-dialog>
24
+ <v-card
25
+ id="invoice-table-modal"
26
+ class="pa-0 ma-0 position-relative motif-background overflow-y-hidden rounded-0"
27
+ height="100vh"
28
+ >
29
+ <v-toolbar dense>
30
+ <v-menu :close-on-content-click="false" :close-on-click="true" open-on-hover offset-y>
31
+ <template #activator="{ props, isActive }">
32
+ <v-btn
33
+ v-bind="props"
34
+ :color="isActive ? 'secondary' : 'primary'"
35
+ class="ma-1 ml-n1 pa-1"
36
+ >
37
+ <v-icon>description</v-icon> File
38
+ </v-btn>
39
+ </template>
40
+ <v-card class="">
41
+ <v-list>
42
+ <v-list-item-group>
43
+ <v-list-item disabled>
44
+ <v-list-item-icon><v-icon> upload </v-icon></v-list-item-icon>
45
+ <v-list-item-title
46
+ >Load New DICOM Series&nbsp;<v-chip x-small color="success">Coming Soon</v-chip></v-list-item-title
47
+ >
48
+ </v-list-item>
49
+ <!-- <v-list-item @click="downloadDcm">-->
50
+ <!-- <v-list-item-icon><v-icon> download </v-icon></v-list-item-icon>-->
51
+ <!-- <v-list-item-title>Download DICOM Series</v-list-item-title>-->
52
+ <!-- </v-list-item>-->
53
+ <v-list-item disabled>
54
+ <v-list-item-icon><v-icon> sync </v-icon></v-list-item-icon>
55
+ <v-list-item-title
56
+ >Load Saved Session&nbsp;<v-chip x-small color="success">Coming Soon</v-chip></v-list-item-title
57
+ >
58
+ </v-list-item>
59
+ <v-list-item disabled>
60
+ <v-list-item-icon><v-icon> share </v-icon></v-list-item-icon>
61
+ <v-list-item-title>Share&nbsp;<v-chip x-small color="success">Coming Soon</v-chip></v-list-item-title>
62
+ </v-list-item>
63
+ <v-list-item @click="alterValue(false)">
64
+ <v-list-item-icon><v-icon> close </v-icon></v-list-item-icon>
65
+ <v-list-item-title>Close Viewer</v-list-item-title>
66
+ </v-list-item>
67
+ </v-list-item-group>
68
+ </v-list>
69
+ </v-card>
70
+ </v-menu>
71
+ <v-menu :close-on-content-click="false" :close-on-click="true" open-on-hover offset-y>
72
+ <template #activator="{ props, isActive }">
73
+ <v-btn v-bind="props" :color="isActive ? 'secondary' : 'primary'" class="">
74
+ <v-icon>settings</v-icon> Settings
75
+ </v-btn>
76
+ </template>
77
+ <v-card min-width="400" class="pb-2">
78
+ <v-card-subtitle>Viewer Settings</v-card-subtitle>
79
+ <SliderSelector
80
+ v-model="scanState.InteractionSettings.PanSensivitity"
81
+ :min="0"
82
+ :max="100"
83
+ label="Pan Sensitivity"
84
+ />
85
+ <SliderSelector
86
+ v-model="scanState.InteractionSettings.ZoomSensitivity"
87
+ :min="0"
88
+ :max="100"
89
+ label="Zoom Sensitivity"
90
+ />
91
+ <SliderSelector
92
+ v-model="scanState.InteractionSettings.RotateSensitivity"
93
+ :min="0"
94
+ :max="100"
95
+ label="Rotate Sensitivity"
96
+ />
97
+ <SliderSelector
98
+ v-model="scanState.InteractionSettings.CameraRotateSensitivity"
99
+ :min="0"
100
+ :max="100"
101
+ label="Camera Rotate Sensitivity"
102
+ />
103
+ </v-card>
104
+ </v-menu>
105
+
106
+ <v-spacer />
107
+ <div class="font-weight-bold">Not for Diagnostic Use</div>
108
+ <v-spacer />
109
+ <!-- <v-btn-->
110
+ <!-- class="ma-1 mr-0 pa-1"-->
111
+ <!-- height="36"-->
112
+ <!-- style="min-width: 36px !important"-->
113
+ <!-- :color="scanState.Layout.PositionData.length === 1 ? 'secondary' : 'primary'"-->
114
+ <!-- @click="layouts('lo_01')"-->
115
+ <!-- ><v-icon>fullscreen</v-icon></v-btn-->
116
+ <!-- >-->
117
+ <v-btn
118
+ class="ma-1 mr-0 pa-1"
119
+ height="36"
120
+ style="min-width: 36px !important"
121
+ :color="layoutIs2x2() ? 'secondary' : 'primary'"
122
+ @click="layouts('lo_02')"
123
+ ><v-icon>grid_view</v-icon></v-btn
124
+ >
125
+ <v-btn
126
+ class="ma-1 mr-0 pa-1"
127
+ height="36"
128
+ style="min-width: 36px !important"
129
+ :color="layoutIs1x3() ? 'secondary' : 'primary'"
130
+ @click="layouts('lo_03')"
131
+ ><v-icon style="rotate: -90deg">view_comfy</v-icon></v-btn
132
+ >
133
+ <v-btn class="ma-1 ml-1 pa-1 white--text mr-n1" color="red" @click="alterValue(false)">Close Viewer</v-btn>
134
+ </v-toolbar>
135
+ <v-navigation-drawer
136
+ v-model="drawer"
137
+ permanent
138
+ :mini-variant="drawerCollapsed"
139
+ mini-variant-width="68"
140
+ style="opacity: 0.95; margin-top: 48px; height: calc(100vh - 40px)"
141
+ absolute
142
+ dark
143
+ class="rounded-0 motif-background"
144
+ width="300px"
145
+ >
146
+ <template v-slot:prepend>
147
+ <div class="d-flex align-center pb-1" :class="drawerCollapsed ? 'py-2' : 'pa-2'" @click="snap">
148
+ <img
149
+ v-if="!drawerCollapsed"
150
+ src="@/assets/images/dark/3dicom-logo.svg"
151
+ height="48"
152
+ alt="logo"
153
+ class="ma-auto"
154
+ style="opacity: 0.9"
155
+ />
156
+ <img
157
+ v-if="drawerCollapsed"
158
+ src="@/assets/images/dark/3DICOM.png"
159
+ height="64"
160
+ alt="logo"
161
+ class="ma-auto"
162
+ style="opacity: 0.9"
163
+ />
164
+ </div>
165
+ <div
166
+ class="text-center mx-auto pa-0 white--text sub-type"
167
+ :style="drawerCollapsed ? 'font-size: 100%' : 'font-size: 140%'"
168
+ @click="snap"
169
+ >
170
+ Online Viewer
171
+ </div>
172
+ </template>
173
+ <template v-slot:append>
174
+ <v-divider></v-divider>
175
+ <v-list>
176
+ <v-tooltip right v-for="(item, index) in footerItems" :key="index">
177
+ <template #activator="{ props }">
178
+ <v-list-item
179
+ class="px-2"
180
+ :class="[drawerCollapsed && 'py-1']"
181
+ target="_blank"
182
+ v-bind="props"
183
+ :disabled="!(instanceLoaded && !scanLoading)"
184
+ @click="item.click()"
185
+ >
186
+ <v-list-item-icon
187
+ :class="[drawerCollapsed && 'mx-auto']"
188
+ :style="drawerCollapsed && 'margin-left: 7px !important; margin-right: 7px !important;'"
189
+ >
190
+ <v-icon :large="drawerCollapsed" :color="item.color">
191
+ {{ item.icon || 'radio_button_checked' }}
192
+ </v-icon>
193
+ </v-list-item-icon>
194
+ <v-list-item-content v-if="!drawerCollapsed">
195
+ <v-list-item-title>
196
+ {{ item.text }}
197
+ </v-list-item-title>
198
+ </v-list-item-content>
199
+ </v-list-item>
200
+ </template>
201
+ {{ item.text }}
202
+ </v-tooltip>
203
+ </v-list>
204
+ </template>
205
+ <v-list v-if="drawerCollapsed">
206
+ <v-tooltip right v-for="(item, index) in miniMenu" :key="item.text">
207
+ <template #activator="{ props }">
208
+ <v-list-item
209
+ :disabled="!(instanceLoaded && !scanLoading)"
210
+ class="px-2 py-1"
211
+ v-bind="props"
212
+ @click="
213
+ openPanels = index;
214
+ drawerCollapsed = false;
215
+ "
216
+ >
217
+ <v-list-item-icon class="mx-auto" style="margin-left: 7px !important; margin-right: 7px !important">
218
+ <v-icon large>
219
+ {{ item.icon || 'radio_button_checked' }}
220
+ </v-icon>
221
+ </v-list-item-icon>
222
+ </v-list-item>
223
+ </template>
224
+ {{ item.tooltip }}
225
+ </v-tooltip>
226
+ </v-list>
227
+ <v-expansion-panels
228
+ v-else
229
+ v-model="openPanels"
230
+ style="max-height: 95vh"
231
+ dark
232
+ accordion
233
+ class="mt-1 overflow-y-scroll transparent"
234
+ >
235
+ <v-expansion-panel class="transparent">
236
+ <ExpansionHeaderMiniMenu :mini-menu="miniMenu[0]" />
237
+ <v-expansion-panel-text>
238
+ <DoubleSliderSelector
239
+ v-model="windowSlider"
240
+ label="Skin to Bone"
241
+ :min="huMinMax.min"
242
+ :max="huMinMax.max"
243
+ />
244
+ <DoubleSliderSelector v-model="thresholdSlider" label="Fine Adjustment" v-bind="huMinMax" />
245
+ <v-card-actions class="py-1">
246
+ <v-select
247
+ :value="getCurrentGreyscalePreset()"
248
+ :items="initialScanState.GreyscalePresets"
249
+ label="Greyscale Preset"
250
+ item-text="Name"
251
+ dense
252
+ outlined
253
+ persistent-placeholder
254
+ hide-details
255
+ return-object
256
+ :menu-props="{
257
+ closeOnContentClick: true,
258
+ }"
259
+ placeholder="Select a Density Preset"
260
+ @change="setPreset(PresetActions.pr01, $event)"
261
+ >
262
+ <template #item="{ props, item }">
263
+ <v-list-item
264
+ v-bind="props"
265
+ ripple
266
+ :title="item.raw.Name"
267
+ lines="three"
268
+
269
+ @mousedown.prevent
270
+ @click="setPreset(PresetActions.pr01, item.raw)"
271
+ >
272
+ <template v-slot:prepend>
273
+ <v-icon :icon="getIconForPreset(item.raw.Name)"></v-icon>
274
+ </template>
275
+ <template v-slot:subtitle>
276
+ Skin Density: <span class="text-mono">{{ item.raw.Lower }}</span>
277
+ <v-spacer />
278
+ Bone Density: <span class="text-mono">{{ item.raw.Upper }}</span>
279
+ </template>
280
+ </v-list-item>
281
+ </template>
282
+ <template v-slot:selection="{ item }">
283
+ <span v-if="item.raw === undefined || !item.raw.Name">None</span>
284
+ <span v-else>{{ item.raw.Name }}</span>
285
+ </template>
286
+ </v-select>
287
+ </v-card-actions>
288
+ <v-card-actions class="py-1">
289
+ <v-select
290
+ :value="currentColourPreset"
291
+ label="Colour Preset"
292
+ :items="initialScanState.ColourPresets"
293
+ item-text="Name"
294
+ dense
295
+ hide-details
296
+ outlined
297
+ :menu-props="{
298
+ closeOnContentClick: true,
299
+ }"
300
+ placeholder="Select a Colour Preset"
301
+ return-object
302
+ @change="setPreset(PresetActions.pr02, $event)"
303
+ ></v-select>
304
+ </v-card-actions>
305
+ </v-expansion-panel-text>
306
+ </v-expansion-panel>
307
+ <v-expansion-panel class="transparent">
308
+ <ExpansionHeaderMiniMenu :mini-menu="miniMenu[1]" />
309
+ <v-expansion-panel-text>
310
+ <SliderSelector v-model="scanState.Display.Brightness" label="Adjust Brightness" />
311
+ <SliderSelector v-model="scanState.Display.Contrast" label="Adjust Contrast" />
312
+ <SliderSelector v-model="scanState.Display.Opacity" label="Adjust Opacity" />
313
+ </v-expansion-panel-text>
314
+ </v-expansion-panel>
315
+ </v-expansion-panels>
316
+ </v-navigation-drawer>
317
+
318
+ <v-btn
319
+ height="84"
320
+ width="20"
321
+ min-width="20"
322
+ class="my-auto rounded-bl-0 rounded-tl-0 pa-0 slide-item-card"
323
+ style="
324
+ border-bottom-left-radius: 0 !important;
325
+ border-top-left-radius: 0 !important;
326
+ position: absolute;
327
+ top: 50%;
328
+ z-index: 1000;
329
+ transform: translateY(-50%);
330
+ "
331
+ :style="drawerCollapsed ? 'left: 68px' : 'left: 300px'"
332
+ @click="drawerCollapsed = !drawerCollapsed"
333
+ >
334
+ <v-icon>{{ drawerCollapsed ? 'chevron_right' : 'chevron_left' }}</v-icon>
335
+ </v-btn>
336
+
337
+ <div class="position-relative pa-0" :style="drawerCollapsed ? 'margin-left: 68px' : 'margin-left: 300px'">
338
+ <WebGL3DR
339
+ v-show="instanceLoaded && !scanLoading"
340
+ :class="!(instanceLoaded && !scanLoading) && 'no-pointer-events'"
341
+ v-if="value"
342
+ ref="web_gl"
343
+ @on_payload="handleOnPayload"
344
+ @instance_loaded="emit('instanceLoaded');"
345
+ @hover="hoverOverCanvas"
346
+ >
347
+ <div
348
+ class="bordered-event-window"
349
+ v-for="layout in scanState.Layout.PositionData"
350
+ :key="layout.Anchor"
351
+ :style="{
352
+ ...generateDivStyleForLayout(layout),
353
+ cursor: getCurrentActiveView(layout) === ScanView.Volume ? 'grab !important' : 'default',
354
+ }"
355
+ >
356
+ <v-hover>
357
+ <template v-slot:default="{ isHovering }">
358
+ <div style="width: 100%; height: 100%">
359
+ <div class="buttons-in-view" v-show="isHovering">
360
+ <div v-if="scanState.Layout.PositionData.length !== 1">
361
+ <v-tooltip top>
362
+ <template #activator="{ props }">
363
+ <v-btn
364
+ class="transparent icon"
365
+ v-bind="props"
366
+ icon
367
+ @click="fullscreenLayout(layout.DefaultView)"
368
+ ><v-icon color="white">fullscreen</v-icon></v-btn
369
+ >
370
+ </template>
371
+ Make {{ getViewName(getCurrentActiveView(layout)) }} fullscreen
372
+ </v-tooltip>
373
+ </div>
374
+ <div v-if="scanState.Layout.PositionData.length === 1">
375
+ <v-tooltip top>
376
+ <template #activator="{ props }">
377
+ <v-btn
378
+ v-bind="props"
379
+ icon
380
+ class="transparent icon"
381
+ @click="layouts(previousLayout)"
382
+ ><v-icon color="white">fullscreen_exit</v-icon></v-btn
383
+ >
384
+ </template>
385
+ Exit Fullscreen View
386
+ </v-tooltip>
387
+ </div>
388
+ <div v-if="getCurrentActiveView(layout) === ScanView.Volume">
389
+ <v-tooltip top>
390
+ <template #activator="{ props }">
391
+ <v-btn
392
+ v-bind="props"
393
+ icon
394
+ class="transparent icon"
395
+ @click="
396
+ viewSelection('vs_05');
397
+ viewSelection('vs_06');
398
+ "
399
+ ><v-icon color="white">home</v-icon></v-btn
400
+ >
401
+ </template>
402
+ Reset volume to default view
403
+ </v-tooltip>
404
+ </div>
405
+ <div v-if="getCurrentActiveView(layout) === ScanView.Volume">
406
+ <v-menu :close-on-content-click="false" :close-on-click="true">
407
+ <template v-slot:activator="{ props }">
408
+ <v-tooltip top>
409
+ <template #activator="{ props: ttprops,}">
410
+ <v-btn
411
+ v-bind="{ ...ttprops, ...props }"
412
+ icon
413
+ class="icon transparent"
414
+ >
415
+ <v-icon color="white">cut</v-icon>
416
+ </v-btn>
417
+ </template>
418
+ Slice the 3D Volume
419
+ </v-tooltip>
420
+ </template>
421
+ <v-card min-width="400" class="pb-2">
422
+ <v-card-title>Slice into the 3D Volume</v-card-title>
423
+ <DoubleSliderSelector v-model="tSlider" label="Transverse" v-bind="tMinMax" />
424
+ <DoubleSliderSelector v-model="sSlider" label="Sagittal" v-bind="sMinMax" />
425
+ <DoubleSliderSelector v-model="cSlider" label="Coronal" v-bind="cMinMax" />
426
+ </v-card>
427
+ </v-menu>
428
+ </div>
429
+ <div v-if="getCurrentActiveView(layout) !== ScanView.Volume">
430
+ <v-menu :close-on-content-click="false" :close-on-click="true" offset-overflow top>
431
+ <template v-slot:activator="{ props }">
432
+ <v-tooltip top>
433
+ <template #activator="{ props: ttprops}">
434
+ <v-btn
435
+ v-bind="{ ...props, ...ttprops }"
436
+ icon
437
+ class="icon transparent"
438
+ >
439
+ <v-icon color="white">360</v-icon>
440
+ </v-btn>
441
+ </template>
442
+ Rotate {{ getViewName(getCurrentActiveView(layout)) }} by an angle
443
+ </v-tooltip>
444
+ </template>
445
+ <v-card min-width="200" width="200" class="pb-2">
446
+ <v-card-subtitle
447
+ >Rotate {{ getViewName(getCurrentActiveView(layout)) }} by an angle</v-card-subtitle
448
+ >
449
+ <v-card-text class="py-0">
450
+ <v-text-field
451
+ hide-details
452
+ outlined
453
+ v-model="rotationDeg"
454
+ type="number"
455
+ suffix="deg"
456
+ ></v-text-field>
457
+ </v-card-text>
458
+ <v-card-actions>
459
+ <v-spacer />
460
+ <v-btn color="primary" @click="rotateByDeg(getCurrentActiveView(layout), rotationDeg)"
461
+ >Rotate</v-btn
462
+ >
463
+ </v-card-actions>
464
+ </v-card>
465
+ </v-menu>
466
+ </div>
467
+ </div>
468
+ <div class="slider-in-view" v-if="isHovering">
469
+ <VerticalSliderSelector
470
+ v-if="getCurrentActiveView(layout) === ScanView.Transverse"
471
+ v-model="scanState.Orientations.Transverse.Slice"
472
+ v-bind="tMinMax"
473
+ />
474
+ <VerticalSliderSelector
475
+ v-if="getCurrentActiveView(layout) === ScanView.Coronal"
476
+ v-model="scanState.Orientations.Coronal.Slice"
477
+ v-bind="cMinMax"
478
+ />
479
+ <VerticalSliderSelector
480
+ v-if="getCurrentActiveView(layout) === ScanView.Sagittal"
481
+ v-model="scanState.Orientations.Sagittal.Slice"
482
+ v-bind="sMinMax"
483
+ />
484
+ </div>
485
+
486
+ <div class="top-lhc" v-if="isHovering">
487
+ <div class="white--text">
488
+ {{ getViewName(getCurrentActiveView(layout)) }}
489
+ </div>
490
+ </div>
491
+ </div>
492
+ </template>
493
+ </v-hover>
494
+ </div>
495
+ </WebGL3DR>
496
+ </div>
497
+
498
+ <LoadingSpinner v-if="!instanceLoaded" />
499
+ <LoadingSpinner v-if="scanLoading" text="Rendering your scan in <span class='sub-type'>3DICOM</span>" />
500
+ <v-textarea
501
+ v-if="stateOverlay"
502
+ style="position: absolute; top: 0px; right: -0px; width: 240px; z-index: 10000"
503
+ class="text--white"
504
+ color="white"
505
+ dark
506
+ outlined
507
+ height="800"
508
+ :value="JSON.stringify(scanState, null, 2)"
509
+ disabled
510
+ />
511
+ </v-card>
512
+ </v-dialog>
513
+ </template>
514
+ <script setup lang="ts" >
515
+
516
+ import ExpansionHeaderMiniMenu from '@/components/ExpansionHeaderMiniMenu.vue';
517
+ import LoadingSpinner from '@/components/LoadingSpinner.vue';
518
+ import WebGL3DR from '@/components/WebGL3DR.vue';
519
+ import DoubleSliderSelector from '@/components/DoubleSliderSelector.vue';
520
+ import SliderSelector from '@/components/SliderSelector.vue';
521
+ import VerticalSliderSelector from '@/components/VerticalSliderSelector.vue';
522
+
523
+ import { generateDivStyleForLayout } from '@/helpers/layoutOverlayStyle';
524
+ import {
525
+ SliderActions,
526
+ PresetActions,
527
+ ScanMovementActions,
528
+ ScanOrientationActions,
529
+ FileManagementActions,
530
+ FrontEndInterfaces,
531
+ } from '@/helpers/models';
532
+ import {
533
+ inflateInitialScanState,
534
+ inflateScanState,
535
+ } from '../helpers/modelHelper';
536
+ import {
537
+ AnchorPoint,
538
+ ColourPresetData,
539
+ CurrentScanState,
540
+ InitialScanState,
541
+ PositionData,
542
+ ScanView,
543
+ } from '@3cr/types-ts';
544
+
545
+ import { toNumber } from '@/helpers/utils';
546
+ import {computed, ref, unref, watch, defineEmits, nextTick} from "vue";
547
+ import {LoadViewerPayload} from "../../index";
548
+
549
+ const emit = defineEmits<{
550
+ instanceLoaded: [void];
551
+ }>();
552
+
553
+ defineExpose({
554
+ alterValue,
555
+ load
556
+ });
557
+
558
+ const emptyPayload = JSON.stringify({ Version: '1.1.0' });
559
+
560
+ const value = ref<boolean>(true);
561
+ const drawer = ref<boolean>(true);
562
+ const drawerCollapsed = ref<boolean>(true);
563
+ const scanLoading = ref<boolean>(true);
564
+ const instanceLoaded = ref<boolean>(true);
565
+ const scanState = ref<CurrentScanState>(inflateScanState());
566
+ const initialScanState = ref<InitialScanState>(inflateInitialScanState());
567
+ const currentColourPreset = ref<ColourPresetData | null>(null);
568
+ const openPanels = ref<number>(0);
569
+ const previousLayout = ref<string>('lo_02');
570
+ const footerItems = ref([
571
+ {
572
+ text: 'Reset Scan',
573
+ icon: 'refresh',
574
+ color: 'red',
575
+ click: () => {
576
+ //@ts-ignore
577
+ viewSelection('vs_05');
578
+ //@ts-ignore
579
+ viewSelection('vs_06');
580
+ },
581
+ },
582
+ {
583
+ text: 'Send to 3rd Party',
584
+ icon: 'send',
585
+ color: 'blue',
586
+ //@ts-ignore
587
+ click: () => alterValue(false),
588
+ },
589
+ {
590
+ text: 'Share to Mobile / VR',
591
+ icon: 'share',
592
+ color: 'yellow',
593
+ //@ts-ignore
594
+ click: () => alterValue(false),
595
+ },
596
+ {
597
+ text: 'Screenshot View',
598
+ icon: 'screenshot_region',
599
+ color: 'green',
600
+ click: () => {
601
+ //@ts-ignore
602
+ snap();
603
+ },
604
+ },
605
+ ]);
606
+
607
+ const miniMenu = ref([
608
+ {
609
+ icon: 'display_settings',
610
+ text: 'Tissue Density',
611
+ tooltip:
612
+ 'Tissue Density - Change the view of density within the scan, allowing you to see through a certain density of particles',
613
+ },
614
+ { icon: 'display_settings', text: 'Display', tooltip: 'Display - Alter the view of the current scan' },
615
+ ])
616
+
617
+ const huMinMax = ref({
618
+ min: -999999,
619
+ max: 999999,
620
+ })
621
+
622
+ const tMinMax = ref({
623
+ min: 0,
624
+ max: 999999,
625
+ })
626
+ const sMinMax = ref({
627
+ min: 0,
628
+ max: 999999,
629
+ })
630
+ const cMinMax = ref({
631
+ min: 0,
632
+ max: 999999,
633
+ })
634
+ const rotationDeg = ref<number>(0)
635
+ const scanStateIncoming = ref<string>('')
636
+ const transactionStarted = ref<boolean>(false)
637
+ const stateOverlay = ref<boolean>(false)
638
+ const m_closeDialog = ref<boolean>(false)
639
+
640
+ const windowSlider = computed({
641
+ get() {
642
+ return [unref(scanState).Display.WindowLower, unref(scanState).Display.WindowUpper];
643
+ },
644
+ set(value: Array<number>) {
645
+ scanState.value.Display.WindowLower = value[0];
646
+ scanState.value.Display.WindowUpper = value[1];
647
+ }
648
+ })
649
+ const thresholdSlider = computed({
650
+ get() {
651
+ return [Math.trunc(unref(scanState).Display.ThresholdLower), Math.trunc(unref(scanState).Display.ThresholdUpper)];
652
+ },
653
+ set(value: Array<number>) {
654
+ scanState.value.Display.ThresholdLower = value[0];
655
+ scanState.value.Display.ThresholdUpper = value[1];
656
+ }
657
+ })
658
+ const tSlider = computed({
659
+ get() {
660
+ return [Math.trunc(unref(scanState).Slice.TransverseLower), Math.trunc(unref(scanState).Slice.TransverseUpper)];
661
+ },
662
+ set(value: Array<number>) {
663
+ scanState.value.Slice.TransverseLower = value[0];
664
+ scanState.value.Slice.TransverseLower = value[1];
665
+ }
666
+ })
667
+ const sSlider = computed({
668
+ get() {
669
+ return [Math.trunc(unref(scanState).Slice.SagittalLower), Math.trunc(unref(scanState).Slice.SagittalUpper)];
670
+ },
671
+ set(value: Array<number>) {
672
+ scanState.value.Slice.SagittalLower = value[0];
673
+ scanState.value.Slice.SagittalUpper = value[1];
674
+ }
675
+ })
676
+
677
+ const cSlider = computed({
678
+ get() {
679
+ return [Math.trunc(unref(scanState).Slice.CoronalLower), Math.trunc(unref(scanState).Slice.CoronalUpper)];
680
+ },
681
+ set(value: Array<number>) {
682
+ scanState.value.Slice.CoronalLower = value[0];
683
+ scanState.value.Slice.CoronalUpper = value[1];
684
+ }
685
+ })
686
+ const web_gl = ref<typeof WebGL3DR | null>(null);
687
+
688
+
689
+ watch(() => scanState.value.Display.Brightness, async (value: number) => {
690
+ await sliderHandler(SliderActions.sl01, value);
691
+ })
692
+ watch(() => scanState.value.Display.Contrast, async (value: number) => {
693
+ await sliderHandler(SliderActions.sl02, value);
694
+ })
695
+ watch(() => scanState.value.Display.Opacity, async (value: number) => {
696
+ await sliderHandler(SliderActions.sl03, value);
697
+ })
698
+ watch(() => scanState.value.Display.WindowLower, async (value: number) => {
699
+ await sliderHandler(SliderActions.sl04, value);
700
+ })
701
+ watch(() => scanState.value.Display.WindowUpper, async (value: number) => {
702
+ await sliderHandler(SliderActions.sl05, value);
703
+ })
704
+ watch(() => scanState.value.Display.ThresholdLower, async (value: number) => {
705
+ await sliderHandler(SliderActions.sl06, value);
706
+ })
707
+ watch(() => scanState.value.Display.ThresholdUpper, async (value: number) => {
708
+ await sliderHandler(SliderActions.sl07, value);
709
+ })
710
+ watch(() => scanState.value.Slice.TransverseLower, async (value: number) => {
711
+ await sliderHandler(SliderActions.sl08, value);
712
+ })
713
+ watch(() => scanState.value.Orientations.Transverse.Slice, async (value: number) => {
714
+ await sliderHandler(SliderActions.sl09, value);
715
+ })
716
+ watch(() => scanState.value.Slice.TransverseUpper, async (value: number) => {
717
+ await sliderHandler(SliderActions.sl10, value);
718
+ })
719
+ watch(() => scanState.value.Slice.SagittalLower, async (value: number) => {
720
+ await sliderHandler(SliderActions.sl11, value);
721
+ })
722
+ watch(() => scanState.value.Orientations.Sagittal.Slice, async (value: number) => {
723
+ await sliderHandler(SliderActions.sl12, value);
724
+ })
725
+ watch(() => scanState.value.Slice.SagittalUpper, async (value: number) => {
726
+ await sliderHandler(SliderActions.sl13, value);
727
+ })
728
+ watch(() => scanState.value.Slice.CoronalLower, async (value: number) => {
729
+ await sliderHandler(SliderActions.sl14, value);
730
+ })
731
+ watch(() => scanState.value.Orientations.Coronal.Slice, async (value: number) => {
732
+ await sliderHandler(SliderActions.sl15, value);
733
+ })
734
+ watch(() => scanState.value.Slice.CoronalUpper, async (value: number) => {
735
+ await sliderHandler(SliderActions.sl16, value);
736
+ })
737
+ watch(() => scanState.value.InteractionSettings.PanSensivitity, async (value: number) => {
738
+ await scanMovementHandler(ScanMovementActions.sm05, value);
739
+ })
740
+ watch(() => scanState.value.InteractionSettings.ZoomSensitivity, async (value: number) => {
741
+ await scanMovementHandler(ScanMovementActions.sm08, value);
742
+ })
743
+ watch(() => scanState.value.InteractionSettings.RotateSensitivity, async (value: number) => {
744
+ await scanMovementHandler(ScanMovementActions.sm10, value);
745
+ })
746
+ watch(() => scanState.value.InteractionSettings.CameraRotateSensitivity, async (value: number) => {
747
+ await scanMovementHandler(ScanMovementActions.sm12, value);
748
+ })
749
+
750
+ function getIconForPreset(presetName: string): string | undefined {
751
+ if (presetName === 'Bone') return 'fa-solid fa-bone';
752
+ if (presetName === 'Brain') return 'fa-solid fa-brain';
753
+ if (presetName === 'Liver') return '$liver_icon';
754
+ if (presetName === 'Lungs') return 'fa-solid fa-lungs';
755
+ if (presetName === 'Muscle') return '$muscle_icon';
756
+ if (presetName === 'Temporal Bones') return '$temporal_bones_icon';
757
+ if (presetName === 'Soft Tissue') return '$torso_icon';
758
+ if (presetName === 'Skin') return '$skin_icon';
759
+ return undefined
760
+ }
761
+ async function rotateByDeg(view: ScanView, deg: number) {
762
+ await sendPayload(FrontEndInterfaces.scan_orientation, ScanOrientationActions.so01, {
763
+ Version: '0.0.1',
764
+ View: view,
765
+ Angle: deg,
766
+ });
767
+ }
768
+ function getCurrentActiveView(position: PositionData): ScanView {
769
+ return unref(scanState).Layout.PositionData.length !== 1 ? position.DefaultView : unref(scanState).CurrentView;
770
+ }
771
+ function getViewName(index: number) {
772
+ return Object.values(ScanView)
773
+ .filter((value) => typeof value === 'string')
774
+ .map((x) => {
775
+ if (x === 'Volume') return '3D Volume';
776
+ return x;
777
+ })[index];
778
+ }
779
+ async function fullscreenLayout(view: ScanView) {
780
+ await layouts('lo_01');
781
+ await viewSelection(`vs_0${view + 1}`);
782
+ }
783
+ // generateDivStyleForLayout,
784
+ async function scanMovementHandler(action: string, value: number) {
785
+ if (unref(transactionStarted)) return;
786
+ await scanMovement(action, value);
787
+ }
788
+ async function sliderHandler(action: string, value: number) {
789
+ if (unref(transactionStarted)) return;
790
+ await slider(action, value);
791
+ }
792
+
793
+ function closeModal() {
794
+ m_closeDialog.value = false;
795
+ value.value = false;
796
+ }
797
+ function alterValue(val: boolean) {
798
+ if (!val) {
799
+ m_closeDialog.value = true;
800
+ return
801
+ }
802
+
803
+ value.value = val;
804
+ }
805
+ async function sendPayload(interfaceType: string, actionType: string, message: any) {
806
+ await unref(web_gl)?.sendPayload(
807
+ JSON.stringify({
808
+ Version: '0.0.1',
809
+ Interface: interfaceType,
810
+ Action: actionType,
811
+ Message: JSON.stringify(message),
812
+ }),
813
+ );
814
+ }
815
+ async function load(downloadResponse: LoadViewerPayload) {
816
+ instanceLoaded.value = true;
817
+ scanLoading.value = true;
818
+ await sendPayload('file_management', 'fm_01', downloadResponse);
819
+ }
820
+ async function viewSelection(action: string) {
821
+ await sendPayload('view_selection', action, emptyPayload)
822
+ }
823
+ async function layouts(action: string) {
824
+ if (action !== 'lo_01') previousLayout.value = action;
825
+ await sendPayload('layout', action, emptyPayload)
826
+ }
827
+ async function snap() {
828
+ // await unref(web_gl).snap();
829
+ }
830
+ async function setPreset(action: PresetActions, preset: any) {
831
+ await sendPayload('presets', action, preset)
832
+ if (action === PresetActions.pr02) {
833
+ currentColourPreset.value = preset;
834
+ }
835
+ }
836
+ async function slider(action: string, value: number) {
837
+ await sendPayload('sliders', action, { Version: '0.0.1', Value: toNumber(value) })
838
+ }
839
+ async function scanMovement(action: string, value: number) {
840
+ await sendPayload('scan_movement', action, { Version: '0.0.1', Value: toNumber(value) })
841
+ }
842
+ async function hoverOverCanvas(isHovering: boolean) {
843
+ await sendPayload('interactivity', 'in_01', { Version: '0.0.1', Value: isHovering })
844
+ await sendPayload('interactivity', 'in_02', { Version: '0.0.1', Value: isHovering })
845
+ }
846
+ async function i_scanState(_: string, message: string) {
847
+ transactionStarted.value = true;
848
+ scanStateIncoming.value = JSON.stringify(JSON.parse(message), null, 2);
849
+
850
+ setScanState(message);
851
+ await nextTick();
852
+ transactionStarted.value = false;
853
+ }
854
+ async function i_fileManagement(action: string, message: string) {
855
+ if (action !== FileManagementActions.fm02) return;
856
+ transactionStarted.value = true;
857
+
858
+ scanStateIncoming.value = JSON.stringify(JSON.parse(message), null, 2);
859
+ const obj = JSON.parse(unref(scanStateIncoming)) as InitialScanState;
860
+
861
+ setScanState(JSON.stringify(obj.DefaultDisplaySettings, null, 2));
862
+ initialScanState.value = obj;
863
+ huMinMax.value.max = obj.HuUpper;
864
+ huMinMax.value.min = obj.HuLower;
865
+ sMinMax.value.max = obj.XSlices;
866
+ cMinMax.value.max = obj.YSlices;
867
+ tMinMax.value.max = obj.ZSlices;
868
+ currentColourPreset.value = obj.ColourPresets[0];
869
+
870
+ await nextTick();
871
+ transactionStarted.value = false;
872
+ scanLoading.value = false;
873
+ drawerCollapsed.value = false;
874
+ await hoverOverCanvas(false);
875
+ }
876
+ async function i_notifications(action: string, message: string) {
877
+ // const obj = JSON.parse(message);
878
+ // console.log(obj);
879
+ //{\"Version\":\"1.0.0\",\"Interface\":\"interactivity\",\"Action\":\"in_01\",\"Code\":\"0000\",\"Description\":\"\"}"
880
+
881
+ // if (obj.Description) {
882
+ // if (action === NotificationActions.no01) {
883
+ // await NotificationService.Instantiate().notificationSuccess(obj.Description);
884
+ // }
885
+ // if (action === NotificationActions.no02) {
886
+ // await NotificationService.Instantiate().notificationError(obj.Description);
887
+ // }
888
+ // if (action === NotificationActions.no03) {
889
+ // await NotificationService.Instantiate().notificationWarning(obj.Description);
890
+ // }
891
+ // }
892
+ }
893
+ function layoutIs2x2() {
894
+ return (
895
+ unref(scanState).Layout.PositionData.length > 1 &&
896
+ unref(scanState).Layout.PositionData[0].Anchor === AnchorPoint.TOP_LEFT &&
897
+ unref(scanState).Layout.PositionData[1].Anchor === AnchorPoint.TOP_RIGHT &&
898
+ unref(scanState).Layout.PositionData[2].Anchor === AnchorPoint.BOTTOM_LEFT &&
899
+ unref(scanState).Layout.PositionData[3].Anchor === AnchorPoint.BOTTOM_RIGHT
900
+ );
901
+ }
902
+ function layoutIs1x3() {
903
+ return (
904
+ unref(scanState).Layout.PositionData.length > 1 &&
905
+ unref(scanState).Layout.PositionData[0].Anchor === AnchorPoint.CENTER &&
906
+ unref(scanState).Layout.PositionData[1].Anchor === AnchorPoint.TOP_RIGHT &&
907
+ unref(scanState).Layout.PositionData[2].Anchor === AnchorPoint.RIGHT &&
908
+ unref(scanState).Layout.PositionData[3].Anchor === AnchorPoint.BOTTOM_RIGHT
909
+ );
910
+ }
911
+
912
+ function getCurrentGreyscalePreset() {
913
+ for (const preset of unref(initialScanState).GreyscalePresets) {
914
+ if (
915
+ (unref(scanState).Display.WindowLower === preset.Lower ||
916
+ (preset.Lower > unref(initialScanState).HuLower &&
917
+ unref(scanState).Display.WindowLower === unref(initialScanState).HuLower)) &&
918
+ (unref(scanState).Display.WindowUpper === preset.Upper ||
919
+ (preset.Upper > unref(initialScanState).HuUpper &&
920
+ unref(scanState).Display.WindowUpper === unref(initialScanState).HuUpper))
921
+ ) {
922
+ return preset.Name;
923
+ }
924
+ }
925
+ return undefined;
926
+ }
927
+ function setScanState(message: string) {
928
+ scanState.value = JSON.parse(message) as CurrentScanState;
929
+ }
930
+
931
+ function handleOnPayload(interfaceSet: string | FrontEndInterfaces, actionSet: string, message: string) {
932
+ if (interfaceSet === FrontEndInterfaces.scan_loading) {
933
+ i_scanState(actionSet, message)
934
+ }
935
+ if (interfaceSet === FrontEndInterfaces.file_management) {
936
+ i_fileManagement(actionSet, message)
937
+ }
938
+ if (interfaceSet === 'notifications') {
939
+ i_notifications(actionSet, message)
940
+ }
941
+ }
942
+
943
+ </script>
944
+ <style>
945
+ .v-dialog:not(.v-dialog--fullscreen) {
946
+ max-height: unset !important;
947
+ }
948
+ .v-expansion-panel-content__wrap {
949
+ padding: 0 8px 4px;
950
+ flex: 1 1 auto;
951
+ max-width: 100%;
952
+ }
953
+ .bordered-event-window {
954
+ position: absolute;
955
+ border: 1px rgba(128, 128, 128, 0.56) solid;
956
+ pointer-events: auto;
957
+ }
958
+ .bordered-event-window:hover {
959
+ position: absolute;
960
+ border: 2px dashed rgb(0, 152, 253);
961
+ //pointer-events: none;
962
+ }
963
+ .v-expansion-panel--active > .v-expansion-panel-header {
964
+ min-height: 48px;
965
+ }
966
+
967
+ .btn-close-dense {
968
+ position: absolute;
969
+ right: 0;
970
+ top: 0;
971
+ }
972
+ .v-slider.v-slider--vertical {
973
+ height: 90%;
974
+ }
975
+
976
+ .slider-in-view {
977
+ position: absolute;
978
+ right: 16px;
979
+ top: 0;
980
+ height: 100%;
981
+ }
982
+ .buttons-in-view {
983
+ position: absolute;
984
+ left: 12px;
985
+ bottom: 12px;
986
+ }
987
+ .top-lhc {
988
+ position: absolute;
989
+ left: 16px;
990
+ top: 12px;
991
+ }
992
+ .buttons-in-view > div {
993
+ display: inline-block;
994
+ }
995
+ </style>