@geode/opengeodeweb-front 10.31.1-rc.1 → 10.32.0
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/app/components/HybridRenderingView.vue +8 -0
- package/app/components/Ruler.vue +144 -0
- package/app/components/ViewToolbar.vue +12 -0
- package/app/components/Viewer/Ui.vue +27 -1
- package/app/stores/hybrid_viewer.js +13 -9
- package/internal/stores/hybrid_viewer/ruler.js +121 -0
- package/package.json +3 -3
|
@@ -55,6 +55,14 @@ function debounce(func, wait) {
|
|
|
55
55
|
|
|
56
56
|
async function handleClick(event) {
|
|
57
57
|
const { offsetX, offsetY, clientX, clientY } = event;
|
|
58
|
+
if (hybridViewerStore.is_ruler_active) {
|
|
59
|
+
const rect = container.value.$el.getBoundingClientRect();
|
|
60
|
+
const x = event.clientX - rect.left;
|
|
61
|
+
const y = elementHeight.value - (event.clientY - rect.top);
|
|
62
|
+
await hybridViewerStore.handleRulerClick(x, y);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
58
66
|
if (viewerStore.picking_mode) {
|
|
59
67
|
const rect = container.value.$el.getBoundingClientRect();
|
|
60
68
|
const x = event.clientX - rect.left;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import ToolPanel from "@ogw_front/components/ToolPanel";
|
|
3
|
+
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
4
|
+
|
|
5
|
+
const show = defineModel("show", { type: Boolean, default: false });
|
|
6
|
+
const hybridViewerStore = useHybridViewerStore();
|
|
7
|
+
|
|
8
|
+
const localPoint1 = ref([0, 0, 0]);
|
|
9
|
+
const localPoint2 = ref([0, 0, 0]);
|
|
10
|
+
|
|
11
|
+
watch(
|
|
12
|
+
() => hybridViewerStore.ruler_point1,
|
|
13
|
+
(point) => {
|
|
14
|
+
if (point) {
|
|
15
|
+
localPoint1.value = [...point];
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
watch(
|
|
21
|
+
() => hybridViewerStore.ruler_point2,
|
|
22
|
+
(point) => {
|
|
23
|
+
if (point) {
|
|
24
|
+
localPoint2.value = [...point];
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
watch(show, (visible) => {
|
|
30
|
+
if (visible) {
|
|
31
|
+
hybridViewerStore.is_ruler_active = true;
|
|
32
|
+
} else if (hybridViewerStore.is_ruler_active) {
|
|
33
|
+
hybridViewerStore.deactivateRuler();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
watch(
|
|
38
|
+
() => hybridViewerStore.is_ruler_active,
|
|
39
|
+
(active) => {
|
|
40
|
+
if (!active) {
|
|
41
|
+
show.value = false;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
async function applyManualCoords() {
|
|
47
|
+
hybridViewerStore.ruler_point1 = [...localPoint1.value];
|
|
48
|
+
hybridViewerStore.ruler_point2 = [...localPoint2.value];
|
|
49
|
+
await hybridViewerStore.applyRuler();
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<ToolPanel
|
|
55
|
+
v-model="show"
|
|
56
|
+
title="Ruler"
|
|
57
|
+
:width="300"
|
|
58
|
+
close-label="Stop ruler"
|
|
59
|
+
:click-outside="false"
|
|
60
|
+
>
|
|
61
|
+
<v-card-text class="pa-3">
|
|
62
|
+
<v-switch
|
|
63
|
+
v-model="hybridViewerStore.ruler_snap"
|
|
64
|
+
data-testid="rulerSnapToggle"
|
|
65
|
+
label="Snap to vertex"
|
|
66
|
+
color="primary"
|
|
67
|
+
density="compact"
|
|
68
|
+
hide-details
|
|
69
|
+
class="mb-4"
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<v-divider class="mb-3" />
|
|
73
|
+
|
|
74
|
+
<div class="text-caption font-weight-bold mb-2 text-medium-emphasis">Point 1</div>
|
|
75
|
+
<v-row dense class="mb-1" data-testid="rulerPointCard">
|
|
76
|
+
<v-col v-for="(axis, index) in ['X', 'Y', 'Z']" :key="axis">
|
|
77
|
+
<v-text-field
|
|
78
|
+
v-model.number="localPoint1[index]"
|
|
79
|
+
data-testid="rulerPointCoordInput"
|
|
80
|
+
:label="axis"
|
|
81
|
+
type="number"
|
|
82
|
+
density="compact"
|
|
83
|
+
variant="outlined"
|
|
84
|
+
hide-details
|
|
85
|
+
/>
|
|
86
|
+
</v-col>
|
|
87
|
+
</v-row>
|
|
88
|
+
|
|
89
|
+
<div class="text-caption font-weight-bold mb-2 mt-3 text-medium-emphasis">Point 2</div>
|
|
90
|
+
<v-row dense class="mb-1" data-testid="rulerPointCard">
|
|
91
|
+
<v-col v-for="(axis, index) in ['X', 'Y', 'Z']" :key="axis">
|
|
92
|
+
<v-text-field
|
|
93
|
+
v-model.number="localPoint2[index]"
|
|
94
|
+
data-testid="rulerPointCoordInput"
|
|
95
|
+
:label="axis"
|
|
96
|
+
type="number"
|
|
97
|
+
density="compact"
|
|
98
|
+
variant="outlined"
|
|
99
|
+
hide-details
|
|
100
|
+
/>
|
|
101
|
+
</v-col>
|
|
102
|
+
</v-row>
|
|
103
|
+
|
|
104
|
+
<v-btn
|
|
105
|
+
data-testid="rulerApplyButton"
|
|
106
|
+
variant="tonal"
|
|
107
|
+
size="small"
|
|
108
|
+
color="primary"
|
|
109
|
+
block
|
|
110
|
+
class="mt-3 text-caption text-none"
|
|
111
|
+
@click="applyManualCoords"
|
|
112
|
+
>
|
|
113
|
+
Apply
|
|
114
|
+
</v-btn>
|
|
115
|
+
|
|
116
|
+
<v-divider class="my-3" />
|
|
117
|
+
|
|
118
|
+
<div v-if="hybridViewerStore.ruler_distance !== undefined" class="text-center">
|
|
119
|
+
<div class="text-caption text-medium-emphasis mb-1">Distance</div>
|
|
120
|
+
<div data-testid="rulerDistance" class="text-h6 font-weight-bold">
|
|
121
|
+
{{ hybridViewerStore.ruler_distance.toFixed(4) }}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div v-else class="text-caption text-medium-emphasis text-center">
|
|
125
|
+
Click in the scene to set both points
|
|
126
|
+
</div>
|
|
127
|
+
</v-card-text>
|
|
128
|
+
|
|
129
|
+
<template #actions>
|
|
130
|
+
<v-card-actions class="justify-center pb-3 pt-0">
|
|
131
|
+
<v-btn
|
|
132
|
+
data-testid="rulerClearButton"
|
|
133
|
+
variant="text"
|
|
134
|
+
size="small"
|
|
135
|
+
color="error"
|
|
136
|
+
class="text-caption text-none"
|
|
137
|
+
@click="hybridViewerStore.clearRuler()"
|
|
138
|
+
>
|
|
139
|
+
Clear
|
|
140
|
+
</v-btn>
|
|
141
|
+
</v-card-actions>
|
|
142
|
+
</template>
|
|
143
|
+
</ToolPanel>
|
|
144
|
+
</template>
|
|
@@ -4,6 +4,7 @@ import CameraBookmarkIcon from "@ogw_front/assets/viewer_svgs/camera-bookmark.sv
|
|
|
4
4
|
import CameraManager from "@ogw_front/components/CameraManager";
|
|
5
5
|
import CameraOrientation from "@ogw_front/components/CameraOrientation";
|
|
6
6
|
import ClippingPlanes from "@ogw_front/components/ClippingPlanes";
|
|
7
|
+
import Ruler from "@ogw_front/components/Ruler";
|
|
7
8
|
import Screenshot from "@ogw_front/components/Screenshot";
|
|
8
9
|
import ShrinkFilter from "@ogw_front/components/ShrinkFilter";
|
|
9
10
|
import ZScaling from "@ogw_front/components/ZScaling";
|
|
@@ -19,6 +20,7 @@ const showCameraOrientation = ref(false);
|
|
|
19
20
|
const showZScaling = ref(false);
|
|
20
21
|
const showClippingPlanes = ref(false);
|
|
21
22
|
const showShrinkFilter = ref(false);
|
|
23
|
+
const showRuler = ref(false);
|
|
22
24
|
const grid_scale = ref(false);
|
|
23
25
|
const zScale = ref(hybridViewerStore.zScale);
|
|
24
26
|
|
|
@@ -173,6 +175,15 @@ const camera_options = computed(() => [
|
|
|
173
175
|
showShrinkFilter.value = !showShrinkFilter.value;
|
|
174
176
|
},
|
|
175
177
|
},
|
|
178
|
+
{
|
|
179
|
+
testId: "rulerButton",
|
|
180
|
+
tooltip: "Ruler",
|
|
181
|
+
icon: "mdi-ruler",
|
|
182
|
+
color: showRuler.value ? "primary" : undefined,
|
|
183
|
+
action: () => {
|
|
184
|
+
showRuler.value = !showRuler.value;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
176
187
|
]);
|
|
177
188
|
</script>
|
|
178
189
|
|
|
@@ -247,6 +258,7 @@ const camera_options = computed(() => [
|
|
|
247
258
|
/>
|
|
248
259
|
<ClippingPlanes v-model:show="showClippingPlanes" />
|
|
249
260
|
<ShrinkFilter v-model:show="showShrinkFilter" />
|
|
261
|
+
<Ruler v-model:show="showRuler" />
|
|
250
262
|
</template>
|
|
251
263
|
|
|
252
264
|
<style module>
|
|
@@ -25,7 +25,9 @@ function stopHoverHighlight() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
onKeyStroke("Escape", () => {
|
|
28
|
-
if (hybridViewerStore.
|
|
28
|
+
if (hybridViewerStore.is_ruler_active) {
|
|
29
|
+
hybridViewerStore.deactivateRuler();
|
|
30
|
+
} else if (hybridViewerStore.is_hover_highlight) {
|
|
29
31
|
stopHoverHighlight();
|
|
30
32
|
}
|
|
31
33
|
});
|
|
@@ -129,6 +131,30 @@ defineExpose({ get_viewer_id });
|
|
|
129
131
|
</v-chip>
|
|
130
132
|
</div>
|
|
131
133
|
</v-fade-transition>
|
|
134
|
+
|
|
135
|
+
<v-fade-transition>
|
|
136
|
+
<div
|
|
137
|
+
v-if="hybridViewerStore.is_ruler_active"
|
|
138
|
+
class="picking-message-container d-flex justify-center w-100 pa-4"
|
|
139
|
+
>
|
|
140
|
+
<v-chip
|
|
141
|
+
data-testid="rulerActiveChip"
|
|
142
|
+
color="secondary"
|
|
143
|
+
elevation="8"
|
|
144
|
+
size="large"
|
|
145
|
+
variant="flat"
|
|
146
|
+
class="pick-pulse"
|
|
147
|
+
style="pointer-events: auto"
|
|
148
|
+
prepend-icon="mdi-ruler"
|
|
149
|
+
@click="hybridViewerStore.clearRuler()"
|
|
150
|
+
>
|
|
151
|
+
Ruler — click to set point {{ hybridViewerStore.ruler_awaiting_point }}
|
|
152
|
+
· Esc to stop
|
|
153
|
+
<v-divider vertical class="mx-2 my-1" opacity="0.3" />
|
|
154
|
+
<v-icon icon="mdi-close" size="small" />
|
|
155
|
+
</v-chip>
|
|
156
|
+
</div>
|
|
157
|
+
</v-fade-transition>
|
|
132
158
|
</template>
|
|
133
159
|
|
|
134
160
|
<style scoped>
|
|
@@ -7,6 +7,7 @@ import { BACKGROUND_COLOR } from "@ogw_internal/stores/hybrid_viewer/constants";
|
|
|
7
7
|
import { useHybridViewerBrightness } from "@ogw_internal/stores/hybrid_viewer/brightness";
|
|
8
8
|
import { useHybridViewerFilters } from "@ogw_internal/stores/hybrid_viewer/filters";
|
|
9
9
|
import { useHybridViewerHighlight } from "@ogw_internal/stores/hybrid_viewer/highlight";
|
|
10
|
+
import { useHybridViewerRuler } from "@ogw_internal/stores/hybrid_viewer/ruler";
|
|
10
11
|
import { useHybridViewerScene } from "@ogw_internal/stores/hybrid_viewer/scene";
|
|
11
12
|
import { useHybridViewerViewport } from "@ogw_internal/stores/hybrid_viewer/viewport";
|
|
12
13
|
import { newInstance as vtkGenericRenderWindow } from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
|
|
@@ -25,7 +26,17 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
|
|
|
25
26
|
const is_picking = ref(false);
|
|
26
27
|
let imageStyle = undefined;
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
const brightnessStore = useHybridViewerBrightness();
|
|
30
|
+
const sceneStore = useHybridViewerScene();
|
|
31
|
+
const filtersStore = useHybridViewerFilters();
|
|
32
|
+
const highlightStore = useHybridViewerHighlight();
|
|
33
|
+
const rulerStore = useHybridViewerRuler();
|
|
34
|
+
const cameraStore = useHybridViewerCamera();
|
|
35
|
+
const viewportStore = useHybridViewerViewport();
|
|
36
|
+
|
|
37
|
+
const is_cursor_crosshair = computed(() => is_picking.value || rulerStore.is_ruler_active.value);
|
|
38
|
+
|
|
39
|
+
watch(is_cursor_crosshair, (value) => {
|
|
29
40
|
if (!genericRenderWindow.value) {
|
|
30
41
|
return;
|
|
31
42
|
}
|
|
@@ -36,8 +47,6 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
|
|
|
36
47
|
}
|
|
37
48
|
});
|
|
38
49
|
|
|
39
|
-
const brightnessStore = useHybridViewerBrightness();
|
|
40
|
-
|
|
41
50
|
async function initHybridViewer() {
|
|
42
51
|
if (status.value !== Status.NOT_CREATED) {
|
|
43
52
|
return;
|
|
@@ -94,12 +103,6 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
|
|
|
94
103
|
return renderPromise;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
const sceneStore = useHybridViewerScene();
|
|
98
|
-
const filtersStore = useHybridViewerFilters();
|
|
99
|
-
const highlightStore = useHybridViewerHighlight();
|
|
100
|
-
const cameraStore = useHybridViewerCamera();
|
|
101
|
-
const viewportStore = useHybridViewerViewport();
|
|
102
|
-
|
|
103
106
|
function exportStores() {
|
|
104
107
|
const renderer = genericRenderWindow.value.getRenderer();
|
|
105
108
|
const camera = renderer.getActiveCamera();
|
|
@@ -123,6 +126,7 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
|
|
|
123
126
|
...viewportStore,
|
|
124
127
|
...filtersStore,
|
|
125
128
|
...highlightStore,
|
|
129
|
+
...rulerStore,
|
|
126
130
|
...cameraStore,
|
|
127
131
|
};
|
|
128
132
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
2
|
+
import { useViewerStore } from "@ogw_front/stores/viewer";
|
|
3
|
+
import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
|
|
4
|
+
|
|
5
|
+
function useHybridViewerRuler() {
|
|
6
|
+
const is_ruler_active = ref(false);
|
|
7
|
+
const ruler_snap = ref(false);
|
|
8
|
+
const ruler_point1 = ref(undefined);
|
|
9
|
+
const ruler_point2 = ref(undefined);
|
|
10
|
+
const ruler_distance = ref(undefined);
|
|
11
|
+
const ruler_awaiting_point = ref(1);
|
|
12
|
+
const ruler_previous_hover_state = ref({ active: false, fieldType: "CELL" });
|
|
13
|
+
|
|
14
|
+
function updateRulerSnapHighlight() {
|
|
15
|
+
const hybridViewerStore = useHybridViewerStore();
|
|
16
|
+
const { is_hover_highlight, hover_highlight_field_type } = storeToRefs(hybridViewerStore);
|
|
17
|
+
const { clearHoverHighlight } = hybridViewerStore;
|
|
18
|
+
|
|
19
|
+
if (is_ruler_active.value && ruler_snap.value) {
|
|
20
|
+
ruler_previous_hover_state.value = {
|
|
21
|
+
active: is_hover_highlight.value,
|
|
22
|
+
fieldType: hover_highlight_field_type.value,
|
|
23
|
+
};
|
|
24
|
+
is_hover_highlight.value = true;
|
|
25
|
+
hover_highlight_field_type.value = "POINT";
|
|
26
|
+
} else {
|
|
27
|
+
is_hover_highlight.value = ruler_previous_hover_state.value.active;
|
|
28
|
+
hover_highlight_field_type.value = ruler_previous_hover_state.value.fieldType;
|
|
29
|
+
if (!is_hover_highlight.value) {
|
|
30
|
+
clearHoverHighlight();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
watch([is_ruler_active, ruler_snap], updateRulerSnapHighlight);
|
|
36
|
+
|
|
37
|
+
async function applyRuler() {
|
|
38
|
+
if (!ruler_point1.value) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const points = ruler_point2.value
|
|
42
|
+
? [ruler_point1.value, ruler_point2.value]
|
|
43
|
+
: [ruler_point1.value];
|
|
44
|
+
const { remoteRender } = useHybridViewerStore();
|
|
45
|
+
const viewerStore = useViewerStore();
|
|
46
|
+
const schema = viewer_schemas.opengeodeweb_viewer.viewer.ruler;
|
|
47
|
+
const response = await viewerStore.request({ schema, params: { points } });
|
|
48
|
+
ruler_distance.value = response.distance;
|
|
49
|
+
await remoteRender();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function handleRulerClick(x, y) {
|
|
53
|
+
const { hybridDb } = useHybridViewerStore();
|
|
54
|
+
const viewerStore = useViewerStore();
|
|
55
|
+
let coords = undefined;
|
|
56
|
+
if (ruler_snap.value) {
|
|
57
|
+
const schema = viewer_schemas.opengeodeweb_viewer.viewer.highlight;
|
|
58
|
+
const params = {
|
|
59
|
+
x: Math.round(x),
|
|
60
|
+
y: Math.round(y),
|
|
61
|
+
field_type: "POINT",
|
|
62
|
+
ids: Object.keys(hybridDb),
|
|
63
|
+
};
|
|
64
|
+
const response = await viewerStore.request({ schema, params });
|
|
65
|
+
coords = response.attributes?.coordinates;
|
|
66
|
+
} else {
|
|
67
|
+
const schema = viewer_schemas.opengeodeweb_viewer.viewer.get_point_position;
|
|
68
|
+
const params = { x: Math.round(x), y: Math.round(y) };
|
|
69
|
+
const response = await viewerStore.request({ schema, params });
|
|
70
|
+
coords = [response.x, response.y, response.z];
|
|
71
|
+
}
|
|
72
|
+
if (!coords) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (ruler_awaiting_point.value === 1) {
|
|
77
|
+
ruler_point1.value = coords;
|
|
78
|
+
ruler_point2.value = undefined;
|
|
79
|
+
ruler_distance.value = undefined;
|
|
80
|
+
ruler_awaiting_point.value = 2;
|
|
81
|
+
} else {
|
|
82
|
+
ruler_point2.value = coords;
|
|
83
|
+
ruler_awaiting_point.value = 1;
|
|
84
|
+
}
|
|
85
|
+
await applyRuler();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deactivateRuler() {
|
|
89
|
+
const { clearHoverHighlight } = useHybridViewerStore();
|
|
90
|
+
is_ruler_active.value = false;
|
|
91
|
+
clearHoverHighlight();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function clearRuler() {
|
|
95
|
+
ruler_point1.value = undefined;
|
|
96
|
+
ruler_point2.value = undefined;
|
|
97
|
+
ruler_distance.value = undefined;
|
|
98
|
+
ruler_awaiting_point.value = 1;
|
|
99
|
+
deactivateRuler();
|
|
100
|
+
const { remoteRender } = useHybridViewerStore();
|
|
101
|
+
const viewerStore = useViewerStore();
|
|
102
|
+
const schema = viewer_schemas.opengeodeweb_viewer.viewer.reset_ruler;
|
|
103
|
+
await viewerStore.request({ schema });
|
|
104
|
+
await remoteRender();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
is_ruler_active,
|
|
109
|
+
ruler_snap,
|
|
110
|
+
ruler_point1,
|
|
111
|
+
ruler_point2,
|
|
112
|
+
ruler_distance,
|
|
113
|
+
ruler_awaiting_point,
|
|
114
|
+
handleRulerClick,
|
|
115
|
+
applyRuler,
|
|
116
|
+
clearRuler,
|
|
117
|
+
deactivateRuler,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { useHybridViewerRuler };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geode/opengeodeweb-front",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.32.0",
|
|
4
4
|
"description": "OpenSource Vue/Nuxt/Pinia/Vuetify framework for web applications",
|
|
5
5
|
"homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Front",
|
|
6
6
|
"bugs": {
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"build": ""
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@geode/opengeodeweb-back": "
|
|
43
|
-
"@geode/opengeodeweb-viewer": "
|
|
42
|
+
"@geode/opengeodeweb-back": "latest",
|
|
43
|
+
"@geode/opengeodeweb-viewer": "latest",
|
|
44
44
|
"@google-cloud/run": "3.2.0",
|
|
45
45
|
"@kitware/vtk.js": "33.3.0",
|
|
46
46
|
"@mdi/font": "7.4.47",
|