@geode/opengeodeweb-front 10.14.2-rc.3 → 10.15.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.
@@ -0,0 +1,273 @@
1
+ <script setup>
2
+ import ToolPanel from "@ogw_front/components/ToolPanel";
3
+ import { applyCameraOptions } from "@ogw_front/utils/hybrid_viewer";
4
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
5
+ import { newInstance as vtkAnnotatedCubeActor } from "@kitware/vtk.js/Rendering/Core/AnnotatedCubeActor";
6
+ import { newInstance as vtkGenericRenderWindow } from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
7
+
8
+ const { panel, width } = defineProps({
9
+ panel: { type: Boolean, default: false },
10
+ width: { type: Number, default: 400 },
11
+ });
12
+
13
+ const show = defineModel("show", { type: Boolean, default: false });
14
+ const emit = defineEmits(["select"]);
15
+
16
+ const orientations = [
17
+ {
18
+ label: "Z+",
19
+ value: "zplus",
20
+ face: "top",
21
+ vtkKey: "ZPlus",
22
+ rotation: 0,
23
+ position: { top: "15%", left: "50%" },
24
+ },
25
+ {
26
+ label: "Z-",
27
+ value: "zminus",
28
+ face: "bottom",
29
+ vtkKey: "ZMinus",
30
+ rotation: 0,
31
+ position: { top: "85%", left: "50%" },
32
+ },
33
+ {
34
+ label: "Y+",
35
+ value: "yplus",
36
+ face: "front",
37
+ vtkKey: "YPlus",
38
+ rotation: 180,
39
+ position: { top: "35%", left: "20%" },
40
+ },
41
+ {
42
+ label: "Y-",
43
+ value: "yminus",
44
+ face: "back",
45
+ vtkKey: "YMinus",
46
+ rotation: 0,
47
+ position: { top: "65%", left: "80%" },
48
+ },
49
+ {
50
+ label: "X+",
51
+ value: "xplus",
52
+ face: "right",
53
+ vtkKey: "XPlus",
54
+ rotation: 90,
55
+ position: { top: "35%", left: "80%" },
56
+ },
57
+ {
58
+ label: "X-",
59
+ value: "xminus",
60
+ face: "left",
61
+ vtkKey: "XMinus",
62
+ rotation: -90,
63
+ position: { top: "65%", left: "20%" },
64
+ },
65
+ ];
66
+
67
+ const hoveredFace = ref(undefined);
68
+ const hybridViewerStore = useHybridViewerStore();
69
+ const cubeContainer = useTemplateRef("cubeContainer");
70
+
71
+ let genericRenderWindow = undefined;
72
+ let cubeActor = undefined;
73
+ let isInteracting = false;
74
+
75
+ function initVTK() {
76
+ if (genericRenderWindow) {
77
+ return;
78
+ }
79
+ genericRenderWindow = vtkGenericRenderWindow({
80
+ background: [0, 0, 0, 0],
81
+ listenWindowResize: false,
82
+ });
83
+
84
+ const interactor = genericRenderWindow.getInteractor();
85
+ interactor.onStartAnimation(() => {
86
+ isInteracting = true;
87
+ });
88
+
89
+ interactor.onEndAnimation(() => {
90
+ isInteracting = false;
91
+ });
92
+
93
+ cubeActor = vtkAnnotatedCubeActor();
94
+ cubeActor.setDefaultStyle({
95
+ fontFamily: "sans-serif",
96
+ fontStyle: "bold",
97
+ faceColor: "rgba(60, 60, 60, 1)",
98
+ fontColor: "white",
99
+ edgeColor: "rgba(255, 255, 255, 0.4)",
100
+ edgeThickness: 0.1,
101
+ resolution: 400,
102
+ fontSizeScale: (resolution) => resolution / 4,
103
+ });
104
+
105
+ for (const orientation of orientations) {
106
+ cubeActor[`set${orientation.vtkKey}FaceProperty`]({
107
+ text: orientation.label,
108
+ faceRotation: orientation.rotation,
109
+ });
110
+ }
111
+
112
+ cubeActor.getProperty().setBackfaceCulling(true);
113
+
114
+ const renderer = genericRenderWindow.getRenderer();
115
+
116
+ renderer.addActor(cubeActor);
117
+ renderer.resetCamera();
118
+ }
119
+
120
+ function syncCubeCamera() {
121
+ const options = hybridViewerStore.camera_options;
122
+ if (!genericRenderWindow || isInteracting || !options.position) {
123
+ return;
124
+ }
125
+ const camera = genericRenderWindow.getRenderer().getActiveCamera();
126
+ applyCameraOptions(camera, options);
127
+ genericRenderWindow.getRenderer().resetCamera();
128
+ genericRenderWindow.getRenderWindow().render();
129
+ }
130
+
131
+ watch(cubeContainer, (newContainer) => {
132
+ if (!newContainer || !import.meta.client) {
133
+ return;
134
+ }
135
+ initVTK();
136
+ genericRenderWindow.setContainer(newContainer);
137
+ const canvas = genericRenderWindow.getApiSpecificRenderWindow().getCanvas();
138
+ canvas.style.width = "100%";
139
+ canvas.style.height = "100%";
140
+ canvas.style.background = "transparent";
141
+ genericRenderWindow.resize();
142
+ syncCubeCamera();
143
+ });
144
+
145
+ onBeforeUnmount(() => genericRenderWindow?.delete());
146
+
147
+ watch(() => hybridViewerStore.camera_options, syncCubeCamera, { deep: true });
148
+
149
+ watch(hoveredFace, (newFace, oldFace) => {
150
+ if (!cubeActor) {
151
+ return;
152
+ }
153
+ function updateFace(face, active) {
154
+ const config = orientations.find((orientation) => orientation.face === face);
155
+ if (config) {
156
+ cubeActor[`set${config.vtkKey}FaceProperty`]({
157
+ faceColor: active ? "rgba(255, 255, 255, 0.95)" : "rgba(60, 60, 60, 1)",
158
+ fontColor: active ? "black" : "white",
159
+ });
160
+ }
161
+ }
162
+
163
+ updateFace(oldFace, false);
164
+ updateFace(newFace, true);
165
+ genericRenderWindow.getRenderWindow().render();
166
+ });
167
+ </script>
168
+
169
+ <template>
170
+ <ToolPanel
171
+ v-if="panel"
172
+ v-model="show"
173
+ title="Camera Orientations"
174
+ :width="width"
175
+ style="z-index: 2; top: 90px; right: 55px"
176
+ >
177
+ <div
178
+ class="pa-0 overflow-hidden position-relative"
179
+ style="
180
+ height: 320px;
181
+ background: radial-gradient(circle at center, rgba(255, 255, 255, 0.05), transparent 70%);
182
+ "
183
+ >
184
+ <svg class="position-absolute fill-height w-100" style="pointer-events: none">
185
+ <line
186
+ v-for="orientation in orientations"
187
+ :key="orientation.value"
188
+ x1="50%"
189
+ y1="50%"
190
+ :x2="orientation.position.left"
191
+ :y2="orientation.position.top"
192
+ :stroke="hoveredFace === orientation.face ? 'white' : 'rgba(255,255,255,0.1)'"
193
+ :stroke-width="hoveredFace === orientation.face ? 2 : 1"
194
+ class="transition-all"
195
+ style="filter: drop-shadow(0 0 3px white)"
196
+ />
197
+ </svg>
198
+
199
+ <div
200
+ class="position-absolute d-flex align-center justify-center"
201
+ style="top: 50%; left: 50%; transform: translate(-50%, -50%)"
202
+ >
203
+ <div ref="cubeContainer" style="width: 100px; height: 100px; pointer-events: none" />
204
+ </div>
205
+
206
+ <v-btn
207
+ v-for="orientation in orientations"
208
+ :key="orientation.value"
209
+ icon
210
+ variant="tonal"
211
+ size="44"
212
+ class="satellite-node position-absolute"
213
+ :style="orientation.position"
214
+ @mouseenter="hoveredFace = orientation.face"
215
+ @mouseleave="hoveredFace = undefined"
216
+ @click.stop="emit('select', orientation.value)"
217
+ >
218
+ <v-tooltip activator="parent" location="top">{{ orientation.value }} View</v-tooltip>
219
+ <span class="text-caption font-weight-black">{{ orientation.label }}</span>
220
+ </v-btn>
221
+ </div>
222
+ </ToolPanel>
223
+
224
+ <v-list v-else density="compact" class="pa-4 orientation-menu rounded-lg" elevation="8">
225
+ <div class="d-flex flex-column align-center" style="gap: 16px">
226
+ <div
227
+ class="d-flex align-center justify-center"
228
+ style="width: 60px; height: 60px; border-radius: 8px; overflow: hidden"
229
+ >
230
+ <div ref="cubeContainer" class="w-100 h-100" style="pointer-events: none" />
231
+ </div>
232
+ <v-divider class="w-100" />
233
+ <div class="d-flex flex-wrap justify-center" style="max-width: 140px">
234
+ <v-btn
235
+ v-for="orientation in orientations"
236
+ :key="orientation.value"
237
+ icon
238
+ size="32"
239
+ variant="text"
240
+ class="ma-1"
241
+ @mouseenter="hoveredFace = orientation.face"
242
+ @mouseleave="hoveredFace = undefined"
243
+ @click.stop="emit('select', orientation.value)"
244
+ >
245
+ <v-tooltip activator="parent" location="top">{{ orientation.label }}</v-tooltip>
246
+ <span class="text-caption font-weight-black">{{ orientation.label }}</span>
247
+ </v-btn>
248
+ </div>
249
+ </div>
250
+ </v-list>
251
+ </template>
252
+
253
+ <style scoped>
254
+ .satellite-node {
255
+ transform: translate(-50%, -50%);
256
+ background: rgba(0, 0, 0, 0.2) !important;
257
+ backdrop-filter: blur(8px);
258
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
259
+ transition: all 0.2s ease;
260
+ }
261
+ .satellite-node:hover {
262
+ transform: translate(-50%, -50%) scale(1.1);
263
+ border-color: white !important;
264
+ background: rgba(255, 255, 255, 0.1) !important;
265
+ }
266
+ .transition-all {
267
+ transition: all 0.4s ease;
268
+ }
269
+ .orientation-menu {
270
+ min-width: 180px;
271
+ background: rgb(var(--v-theme-surface)) !important;
272
+ }
273
+ </style>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import VeaseViewToolbar from "@ogw_front/components/VeaseViewToolbar";
2
+ import ViewToolbar from "@ogw_front/components/ViewToolbar";
3
3
  import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
4
4
  import { useViewerStore } from "@ogw_front/stores/viewer";
5
5
 
@@ -47,7 +47,7 @@ function debounce(func, wait) {
47
47
  <template>
48
48
  <ClientOnly>
49
49
  <div data-testid="hybridViewer" class="fill-height" style="position: relative; height: 100%">
50
- <VeaseViewToolbar />
50
+ <ViewToolbar />
51
51
  <slot name="ui"></slot>
52
52
  <v-col
53
53
  class="pa-0"
@@ -1,15 +1,13 @@
1
1
  <script setup>
2
- import GlassCard from "@ogw_front/components/GlassCard";
2
+ import ToolPanel from "@ogw_front/components/ToolPanel";
3
3
  import fileDownload from "js-file-download";
4
- import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
5
-
6
4
  import { useViewerStore } from "@ogw_front/stores/viewer";
5
+ import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
7
6
 
8
- const emit = defineEmits(["close"]);
7
+ const show = defineModel({ type: Boolean, default: false });
9
8
 
10
- const { show_dialog, width } = defineProps({
11
- show_dialog: { type: Boolean, required: true },
12
- width: { type: Number, required: false, default: 400 },
9
+ const { width } = defineProps({
10
+ width: { type: Number, default: 400 },
13
11
  });
14
12
 
15
13
  const output_extensions =
@@ -33,7 +31,7 @@ async function takeScreenshot() {
33
31
  },
34
32
  },
35
33
  );
36
- emit("close");
34
+ show.value = false;
37
35
  }
38
36
 
39
37
  watch(output_extension, (value) => {
@@ -42,58 +40,41 @@ watch(output_extension, (value) => {
42
40
  }
43
41
  });
44
42
  </script>
43
+
45
44
  <template>
46
- <GlassCard
47
- v-if="show_dialog"
48
- @click.stop
45
+ <ToolPanel
46
+ v-model="show"
49
47
  title="Take a screenshot"
50
48
  :width="width"
51
- :ripple="false"
52
- variant="panel"
53
- padding="pa-0"
54
- class="position-absolute elevation-24"
55
- style="z-index: 2; top: 90px; right: 55px"
49
+ close-label="Cancel"
50
+ action-label="Screenshot"
51
+ @action="takeScreenshot"
56
52
  >
57
- <v-card-text class="pa-5">
58
- <v-container>
59
- <v-row>
60
- <v-col cols="8" class="py-0">
61
- <v-text-field v-model="filename" label="File name"></v-text-field>
62
- </v-col>
63
- <v-col cols="4" class="py-0">
64
- <v-select
65
- v-model="output_extension"
66
- :items="output_extensions"
67
- label="Extension"
68
- required
69
- />
70
- </v-col>
71
- </v-row>
72
-
73
- <v-row>
74
- <v-col cols="12" class="py-0">
75
- <v-switch
76
- v-model="include_background"
77
- :disabled="output_extension !== 'png'"
78
- label="Include background"
79
- inset
80
- ></v-switch>
81
- </v-col>
82
- </v-row>
83
- </v-container>
84
- </v-card-text>
53
+ <v-container class="pa-5">
54
+ <v-row>
55
+ <v-col cols="8" class="py-0">
56
+ <v-text-field v-model="filename" label="File name"></v-text-field>
57
+ </v-col>
58
+ <v-col cols="4" class="py-0">
59
+ <v-select
60
+ v-model="output_extension"
61
+ :items="output_extensions"
62
+ label="Extension"
63
+ required
64
+ />
65
+ </v-col>
66
+ </v-row>
85
67
 
86
- <template #actions>
87
- <v-card-actions class="justify-center pb-4">
88
- <v-btn variant="text" color="primary" @click="emit('close')">Close</v-btn>
89
- <v-btn
90
- variant="outlined"
91
- :disabled="!filename || !output_extension"
92
- color="primary"
93
- @click="takeScreenshot()"
94
- >Screenshot</v-btn
95
- >
96
- </v-card-actions>
97
- </template>
98
- </GlassCard>
68
+ <v-row>
69
+ <v-col cols="12" class="py-0">
70
+ <v-switch
71
+ v-model="include_background"
72
+ :disabled="output_extension !== 'png'"
73
+ label="Include background"
74
+ inset
75
+ ></v-switch>
76
+ </v-col>
77
+ </v-row>
78
+ </v-container>
79
+ </ToolPanel>
99
80
  </template>
@@ -0,0 +1,62 @@
1
+ <script setup>
2
+ import GlassCard from "@ogw_front/components/GlassCard";
3
+
4
+ const { title, width, closeLabel, actionLabel } = defineProps({
5
+ title: { type: String, default: "" },
6
+ width: { type: Number, default: 400 },
7
+ closeLabel: { type: String, default: "Close" },
8
+ actionLabel: { type: String, default: undefined },
9
+ });
10
+
11
+ const model = defineModel({ type: Boolean, default: false });
12
+ const emit = defineEmits(["action"]);
13
+
14
+ function close() {
15
+ model.value = false;
16
+ }
17
+ </script>
18
+
19
+ <template>
20
+ <GlassCard
21
+ v-if="model"
22
+ v-click-outside="close"
23
+ :title="title"
24
+ :width="width"
25
+ :ripple="false"
26
+ variant="panel"
27
+ padding="pa-0"
28
+ class="position-absolute rounded-xl elevation-24 tool-panel"
29
+ v-bind="$attrs"
30
+ >
31
+ <v-card-text class="pa-0 overflow-hidden position-relative">
32
+ <slot />
33
+ </v-card-text>
34
+
35
+ <template #actions>
36
+ <slot name="actions">
37
+ <v-card-actions class="justify-center pb-6" style="gap: 12px">
38
+ <v-btn variant="text" size="small" color="white" @click="close">
39
+ {{ closeLabel }}
40
+ </v-btn>
41
+ <v-btn
42
+ v-if="actionLabel"
43
+ variant="outlined"
44
+ size="small"
45
+ color="white"
46
+ @click="emit('action')"
47
+ >
48
+ {{ actionLabel }}
49
+ </v-btn>
50
+ </v-card-actions>
51
+ </slot>
52
+ </template>
53
+ </GlassCard>
54
+ </template>
55
+
56
+ <style scoped>
57
+ .tool-panel {
58
+ z-index: 2;
59
+ top: 90px;
60
+ right: 55px;
61
+ }
62
+ </style>
@@ -2,19 +2,46 @@
2
2
  import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
3
3
 
4
4
  import ActionButton from "@ogw_front/components/ActionButton.vue";
5
+ import CameraOrientation from "@ogw_front/components/CameraOrientation.vue";
5
6
  import Screenshot from "@ogw_front/components/Screenshot";
7
+ import ZScaling from "@ogw_front/components/ZScaling";
8
+
9
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
6
10
  import { useViewerStore } from "@ogw_front/stores/viewer";
7
11
 
12
+ const hybridViewerStore = useHybridViewerStore();
8
13
  const viewerStore = useViewerStore();
9
14
  const take_screenshot = ref(false);
15
+ const showCameraOrientation = ref(false);
16
+ const showZScaling = ref(false);
10
17
  const grid_scale = ref(false);
18
+ const zScale = ref(hybridViewerStore.zScale);
19
+
20
+ watch(
21
+ () => hybridViewerStore.zScale,
22
+ (newVal) => {
23
+ zScale.value = newVal;
24
+ },
25
+ );
26
+
27
+ async function handleZScalingClose() {
28
+ await hybridViewerStore.setZScaling(zScale.value);
29
+ showZScaling.value = false;
30
+ }
11
31
 
12
32
  const camera_options = [
13
33
  {
14
34
  tooltip: "Reset camera",
15
35
  icon: "mdi-cube-scan",
16
36
  action: () => {
17
- viewerStore.request(schemas.opengeodeweb_viewer.viewer.reset_camera);
37
+ hybridViewerStore.resetCamera();
38
+ },
39
+ },
40
+ {
41
+ tooltip: "Camera orientation",
42
+ icon: "mdi-rotate-3d",
43
+ action: () => {
44
+ showCameraOrientation.value = !showCameraOrientation.value;
18
45
  },
19
46
  },
20
47
  {
@@ -34,11 +61,19 @@ const camera_options = [
34
61
  {
35
62
  response_function: () => {
36
63
  grid_scale.value = !grid_scale.value;
64
+ hybridViewerStore.remoteRender();
37
65
  },
38
66
  },
39
67
  );
40
68
  },
41
69
  },
70
+ {
71
+ tooltip: "Z Scaling Control",
72
+ icon: "mdi-sort",
73
+ action: () => {
74
+ showZScaling.value = !showZScaling.value;
75
+ },
76
+ },
42
77
  ];
43
78
  </script>
44
79
 
@@ -47,15 +82,26 @@ const camera_options = [
47
82
  <v-row v-for="camera_option in camera_options" :key="camera_option.icon" dense>
48
83
  <v-col>
49
84
  <ActionButton
50
- :tooltip="camera_option.tooltip"
51
85
  :icon="camera_option.icon"
86
+ :tooltip="camera_option.tooltip"
52
87
  tooltip-location="left"
53
- @click="camera_option.action"
88
+ @click.stop="camera_option.action"
54
89
  />
55
90
  </v-col>
56
91
  </v-row>
57
92
  </v-container>
58
- <Screenshot :show_dialog="take_screenshot" @close="take_screenshot = false" />
93
+ <CameraOrientation
94
+ v-model:show="showCameraOrientation"
95
+ panel
96
+ @select="hybridViewerStore.setCameraOrientation"
97
+ />
98
+ <Screenshot v-model="take_screenshot" />
99
+ <ZScaling
100
+ v-model:show="showZScaling"
101
+ v-model="zScale"
102
+ :width="400"
103
+ @apply="handleZScalingClose"
104
+ />
59
105
  </template>
60
106
 
61
107
  <style module>
@@ -1,67 +1,49 @@
1
1
  <script setup>
2
- import GlassCard from "@ogw_front/components/GlassCard";
2
+ import ToolPanel from "@ogw_front/components/ToolPanel";
3
3
 
4
4
  const zScale = defineModel({ type: Number, default: 1 });
5
+ const show = defineModel("show", { type: Boolean, default: false });
6
+
5
7
  const { width } = defineProps({
6
8
  width: { type: Number, default: 400 },
7
9
  });
8
10
 
9
- const emit = defineEmits(["close"]);
11
+ const emit = defineEmits(["apply"]);
12
+
13
+ function apply() {
14
+ emit("apply");
15
+ show.value = false;
16
+ }
10
17
  </script>
18
+
11
19
  <template>
12
- <GlassCard
13
- @click.stop
20
+ <ToolPanel
21
+ v-model="show"
14
22
  title="Z Scaling Control"
15
23
  :width="width"
16
- :ripple="false"
17
- variant="panel"
18
- padding="pa-0"
19
- class="position-absolute rounded-xl elevation-24"
20
- style="z-index: 2; top: 90px; right: 55px"
24
+ action-label="Apply"
25
+ @action="apply"
21
26
  >
22
- <v-card-text class="pa-5">
23
- <v-container>
24
- <v-row>
25
- <v-col cols="12" class="py-2">
26
- <v-slider v-model="zScale" :min="1" :max="10" :step="0.2" label="Z Scale" thumb-label />
27
- </v-col>
28
- </v-row>
29
- <v-row>
30
- <v-col cols="12" class="py-2">
31
- <v-text-field
32
- v-model.number="zScale"
33
- type="number"
34
- label="Z Scale Value"
35
- outlined
36
- dense
37
- hide-details
38
- step="0.1"
39
- :min="1"
40
- />
41
- </v-col>
42
- </v-row>
43
- </v-container>
44
- </v-card-text>
45
-
46
- <template #actions>
47
- <v-card-actions class="justify-center pb-4">
48
- <v-btn variant="text" color="white" @click="emit('close')">Close</v-btn>
49
- <v-btn variant="outlined" color="white" @click="emit('close')">Apply</v-btn>
50
- </v-card-actions>
51
- </template>
52
- </GlassCard>
27
+ <v-container class="pa-5">
28
+ <v-row>
29
+ <v-col cols="12" class="py-2">
30
+ <v-slider v-model="zScale" :min="1" :max="10" :step="0.2" label="Z Scale" thumb-label />
31
+ </v-col>
32
+ </v-row>
33
+ <v-row>
34
+ <v-col cols="12" class="py-2">
35
+ <v-text-field
36
+ v-model.number="zScale"
37
+ type="number"
38
+ label="Z Scale Value"
39
+ outlined
40
+ dense
41
+ hide-details
42
+ step="0.1"
43
+ :min="1"
44
+ />
45
+ </v-col>
46
+ </v-row>
47
+ </v-container>
48
+ </ToolPanel>
53
49
  </template>
54
-
55
- <style scoped>
56
- .z-scaling-menu {
57
- position: absolute;
58
- z-index: 2;
59
- top: 90px;
60
- right: 55px;
61
- border-radius: 12px !important;
62
- }
63
-
64
- .custom-number-input :deep(.v-input__control) {
65
- min-height: 48px;
66
- }
67
- </style>
@@ -1,34 +1,32 @@
1
1
  import {
2
+ ORIENTATIONS,
3
+ animateCamera,
2
4
  applyCameraOptions,
3
5
  computeAverageBrightness,
4
6
  getCameraOptions,
5
7
  } from "@ogw_front/utils/hybrid_viewer";
8
+ import { dot } from "@kitware/vtk.js/Common/Core/Math";
6
9
  import { newInstance as vtkActor } from "@kitware/vtk.js/Rendering/Core/Actor";
7
10
  import { newInstance as vtkGenericRenderWindow } from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
8
11
  import { newInstance as vtkMapper } from "@kitware/vtk.js/Rendering/Core/Mapper";
9
12
  import { newInstance as vtkXMLPolyDataReader } from "@kitware/vtk.js/IO/XML/XMLPolyDataReader";
10
13
 
14
+ import {
15
+ ACTOR_COLOR,
16
+ ALIGNMENT_THRESHOLD,
17
+ BACKGROUND_COLOR,
18
+ BUMP_MULTIPLIER,
19
+ EASE_EXPONENT,
20
+ LONG_ANIMATION_DURATION,
21
+ SHORT_ANIMATION_DURATION,
22
+ WHEEL_TIME_OUT_MS,
23
+ } from "@ogw_front/utils/vtk/constants";
24
+ import { Status } from "@ogw_front/utils/status";
11
25
  import { useDataStore } from "@ogw_front/stores/data";
12
26
  import { useViewerStore } from "@ogw_front/stores/viewer";
13
27
 
14
- import { Status } from "@ogw_front/utils/status";
15
28
  import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
16
29
 
17
- const RGB_MAX = 255;
18
- const BACKGROUND_GREY_VALUE = 180;
19
- const ACTOR_DARK_VALUE = 20;
20
- const BACKGROUND_COLOR = [
21
- BACKGROUND_GREY_VALUE / RGB_MAX,
22
- BACKGROUND_GREY_VALUE / RGB_MAX,
23
- BACKGROUND_GREY_VALUE / RGB_MAX,
24
- ];
25
- const ACTOR_COLOR = [
26
- ACTOR_DARK_VALUE / RGB_MAX,
27
- ACTOR_DARK_VALUE / RGB_MAX,
28
- ACTOR_DARK_VALUE / RGB_MAX,
29
- ];
30
- const WHEEL_TIME_OUT_MS = 600;
31
-
32
30
  export const useHybridViewerStore = defineStore("hybridViewer", () => {
33
31
  const dataStore = useDataStore();
34
32
  const viewerStore = useViewerStore();
@@ -39,6 +37,7 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
39
37
  const is_moving = ref(false);
40
38
  const zScale = ref(1);
41
39
  let viewStream = undefined;
40
+ let imageStyle = undefined;
42
41
  const gridActor = undefined;
43
42
 
44
43
  const latestImage = ref(undefined);
@@ -59,7 +58,7 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
59
58
  });
60
59
 
61
60
  const webGLRenderWindow = genericRenderWindow.value.getApiSpecificRenderWindow();
62
- const imageStyle = webGLRenderWindow.getReferenceByName("bgImage").style;
61
+ imageStyle = webGLRenderWindow.getReferenceByName("bgImage").style;
63
62
  imageStyle.transition = "opacity 0.1s ease-in";
64
63
  imageStyle.zIndex = 1;
65
64
 
@@ -74,6 +73,12 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
74
73
  imageStyle.opacity = 1;
75
74
  });
76
75
 
76
+ const renderer = genericRenderWindow.value.getRenderer();
77
+ const camera = renderer.getActiveCamera();
78
+ camera.onModified(() => {
79
+ Object.assign(camera_options, getCameraOptions(camera));
80
+ });
81
+
77
82
  status.value = Status.CREATED;
78
83
  }
79
84
 
@@ -147,20 +152,50 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
147
152
  syncRemoteCamera();
148
153
  }
149
154
 
155
+ function setCameraOrientation(orientation) {
156
+ const config = ORIENTATIONS[orientation.toLowerCase()];
157
+ const renderer = genericRenderWindow.value.getRenderer();
158
+ const camera = renderer.getActiveCamera();
159
+ const startState = getCameraOptions(camera);
160
+
161
+ applyCameraOptions(camera, {
162
+ ...config,
163
+ focal_point: [0, 0, 0],
164
+ });
165
+ renderer.resetCamera();
166
+ const targetState = getCameraOptions(camera);
167
+
168
+ applyCameraOptions(camera, startState);
169
+
170
+ const alignment = dot(camera.getDirectionOfProjection(), config.position);
171
+ const duration =
172
+ alignment > ALIGNMENT_THRESHOLD ? LONG_ANIMATION_DURATION : SHORT_ANIMATION_DURATION;
173
+ is_moving.value = true;
174
+ imageStyle.opacity = 0;
175
+
176
+ animateCamera({
177
+ camera,
178
+ startState,
179
+ targetState,
180
+ duration,
181
+ bumpMultiplier: BUMP_MULTIPLIER,
182
+ easeExponent: EASE_EXPONENT,
183
+ onUpdate: () => genericRenderWindow.value.getRenderWindow().render(),
184
+ onEnd: () => {
185
+ is_moving.value = false;
186
+ syncRemoteCamera();
187
+ },
188
+ });
189
+ }
190
+
150
191
  function syncRemoteCamera() {
151
192
  const renderer = genericRenderWindow.value.getRenderer();
152
193
  const camera = renderer.getActiveCamera();
153
- const params = {
154
- camera_options: getCameraOptions(camera),
155
- };
194
+ const params = { camera_options: getCameraOptions(camera) };
156
195
  viewerStore.request(viewer_schemas.opengeodeweb_viewer.viewer.update_camera, params, {
157
196
  response_function: () => {
158
197
  remoteRender();
159
- for (const key in params.camera_options) {
160
- if (Object.hasOwn(params.camera_options, key)) {
161
- camera_options[key] = params.camera_options[key];
162
- }
163
- }
198
+ Object.assign(camera_options, params.camera_options);
164
199
  },
165
200
  });
166
201
  }
@@ -176,7 +211,7 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
176
211
  genericRenderWindow.value.setContainer(container.value.$el);
177
212
  const webGLRenderWindow = genericRenderWindow.value.getApiSpecificRenderWindow();
178
213
  webGLRenderWindow.setUseBackgroundImage(true);
179
- const imageStyle = webGLRenderWindow.getReferenceByName("bgImage").style;
214
+ imageStyle = webGLRenderWindow.getReferenceByName("bgImage").style;
180
215
  imageStyle.transition = "opacity 0.1s ease-in";
181
216
  imageStyle.zIndex = 1;
182
217
  resize(container.value.$el.offsetWidth, container.value.$el.offsetHeight);
@@ -248,36 +283,17 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
248
283
  return;
249
284
  }
250
285
  const z_scale = snapshot.zScale;
286
+ if (typeof z_scale === "number") {
287
+ await setZScaling(z_scale);
288
+ }
251
289
 
252
- function applyCamera() {
253
- const { camera_options: snapshot_camera_options } = snapshot;
254
- if (!snapshot_camera_options) {
255
- return;
256
- }
257
-
290
+ const { camera_options: snapshot_camera_options } = snapshot;
291
+ if (snapshot_camera_options) {
258
292
  const renderer = genericRenderWindow.value.getRenderer();
259
- const camera = renderer.getActiveCamera();
260
-
261
- applyCameraOptions(camera, snapshot_camera_options);
262
-
293
+ applyCameraOptions(renderer.getActiveCamera(), snapshot_camera_options);
263
294
  genericRenderWindow.value.getRenderWindow().render();
264
-
265
- const payload = {
266
- camera_options: getCameraOptions(snapshot_camera_options),
267
- };
268
- return viewerStore.request(viewer_schemas.opengeodeweb_viewer.viewer.update_camera, payload, {
269
- response_function: () => {
270
- remoteRender();
271
- Object.assign(camera_options, payload.camera_options);
272
- },
273
- });
274
- }
275
-
276
- if (typeof z_scale === "number") {
277
- await setZScaling(z_scale);
278
- return await applyCamera();
295
+ syncRemoteCamera();
279
296
  }
280
- return await applyCamera();
281
297
  }
282
298
 
283
299
  function clear() {
@@ -304,11 +320,13 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
304
320
  remoteRender,
305
321
  resize,
306
322
  resetCamera,
323
+ setCameraOrientation,
307
324
  setContainer,
308
325
  zScale,
309
326
  clear,
310
327
  exportStores,
311
328
  importStores,
329
+ camera_options,
312
330
  latestImage,
313
331
  getAverageBrightness,
314
332
  };
@@ -1,45 +1,42 @@
1
+ import { SHORT_ANIMATION_DURATION } from "@ogw_front/utils/vtk/constants";
1
2
  const RGB_MAX = 255;
2
3
  const BACKGROUND_GREY_VALUE = 180;
3
4
  const SAMPLE_SIZE = 10;
4
5
  const TOTAL_CHANNELS = 400;
5
6
  const RGBA_CHANNELS = 4;
6
7
 
7
- function getCameraOptions(camera) {
8
- if (!camera) {
9
- return undefined;
10
- }
8
+ const ORIENTATIONS = {
9
+ zplus: { position: [0, 0, 1], view_up: [0, 1, 0] },
10
+ zminus: { position: [0, 0, -1], view_up: [0, 1, 0] },
11
+ yplus: { position: [0, 1, 0], view_up: [0, 0, 1] },
12
+ yminus: { position: [0, -1, 0], view_up: [0, 0, 1] },
13
+ xplus: { position: [1, 0, 0], view_up: [0, 0, 1] },
14
+ xminus: { position: [-1, 0, 0], view_up: [0, 0, 1] },
15
+ };
11
16
 
12
- if (typeof camera.getFocalPoint !== "function") {
13
- return { ...camera };
17
+ function getCameraOptions(camera) {
18
+ if (!camera?.getFocalPoint) {
19
+ return camera;
14
20
  }
15
21
  return {
16
- focal_point: [...camera.getFocalPoint()],
17
- view_up: [...camera.getViewUp()],
18
- position: [...camera.getPosition()],
22
+ focal_point: [...(camera.getFocalPoint() ?? [])],
23
+ view_up: [...(camera.getViewUp() ?? [])],
24
+ position: [...(camera.getPosition() ?? [])],
19
25
  view_angle: camera.getViewAngle(),
20
- clipping_range: [...camera.getClippingRange()],
26
+ clipping_range: [...(camera.getClippingRange() ?? [])],
21
27
  distance: camera.getDistance(),
22
28
  };
23
29
  }
24
30
 
25
31
  function applyCameraOptions(camera, options) {
26
- if (!camera || !options) {
27
- return;
28
- }
29
- if (options.focal_point) {
30
- camera.setFocalPoint(...options.focal_point);
31
- }
32
- if (options.view_up) {
33
- camera.setViewUp(...options.view_up);
34
- }
35
- if (options.position) {
36
- camera.setPosition(...options.position);
37
- }
38
- if (options.view_angle) {
39
- camera.setViewAngle(options.view_angle);
40
- }
41
- if (options.clipping_range) {
42
- camera.setClippingRange(...options.clipping_range);
32
+ if (camera?.set && options) {
33
+ camera.set({
34
+ focalPoint: options.focal_point,
35
+ viewUp: options.view_up,
36
+ position: options.position,
37
+ viewAngle: options.view_angle,
38
+ clippingRange: options.clipping_range,
39
+ });
43
40
  }
44
41
  }
45
42
 
@@ -98,4 +95,55 @@ function computeAverageBrightness(rect, options) {
98
95
  }
99
96
  }
100
97
 
101
- export { applyCameraOptions, computeAverageBrightness, getCameraOptions };
98
+ function animateCamera(options) {
99
+ const {
100
+ camera,
101
+ startState,
102
+ targetState,
103
+ duration,
104
+ bumpMultiplier,
105
+ easeExponent,
106
+ onUpdate,
107
+ onEnd,
108
+ } = options;
109
+ const startTime = performance.now();
110
+
111
+ function animate(currentTime) {
112
+ const progress = Math.min((currentTime - startTime) / duration, 1);
113
+ const ease =
114
+ duration > SHORT_ANIMATION_DURATION
115
+ ? 1 - (1 - progress) ** easeExponent
116
+ : progress * (2 - progress);
117
+ const bump = bumpMultiplier * Math.sin(Math.PI * progress);
118
+
119
+ camera.set({
120
+ position: startState.position.map(
121
+ (startValue, index) =>
122
+ startValue + (targetState.position[index] - startValue) * ease + bump,
123
+ ),
124
+ viewUp: startState.view_up.map(
125
+ (startValue, index) => startValue + (targetState.view_up[index] - startValue) * ease,
126
+ ),
127
+ focalPoint: startState.focal_point.map(
128
+ (startValue, index) => startValue + (targetState.focal_point[index] - startValue) * ease,
129
+ ),
130
+ });
131
+
132
+ onUpdate();
133
+
134
+ if (progress < 1) {
135
+ requestAnimationFrame(animate);
136
+ } else {
137
+ onEnd();
138
+ }
139
+ }
140
+ requestAnimationFrame(animate);
141
+ }
142
+
143
+ export {
144
+ ORIENTATIONS,
145
+ animateCamera,
146
+ applyCameraOptions,
147
+ computeAverageBrightness,
148
+ getCameraOptions,
149
+ };
@@ -0,0 +1,31 @@
1
+ const RGB_MAX = 255;
2
+ const BACKGROUND_GREY_VALUE = 180;
3
+ const ACTOR_DARK_VALUE = 20;
4
+
5
+ const BACKGROUND_COLOR = [
6
+ BACKGROUND_GREY_VALUE / RGB_MAX,
7
+ BACKGROUND_GREY_VALUE / RGB_MAX,
8
+ BACKGROUND_GREY_VALUE / RGB_MAX,
9
+ ];
10
+ const ACTOR_COLOR = [
11
+ ACTOR_DARK_VALUE / RGB_MAX,
12
+ ACTOR_DARK_VALUE / RGB_MAX,
13
+ ACTOR_DARK_VALUE / RGB_MAX,
14
+ ];
15
+ const WHEEL_TIME_OUT_MS = 600;
16
+ const BUMP_MULTIPLIER = 0.2;
17
+ const ALIGNMENT_THRESHOLD = 0.9;
18
+ const LONG_ANIMATION_DURATION = 1000;
19
+ const SHORT_ANIMATION_DURATION = 500;
20
+ const EASE_EXPONENT = 1.1;
21
+
22
+ export {
23
+ BACKGROUND_COLOR,
24
+ ACTOR_COLOR,
25
+ WHEEL_TIME_OUT_MS,
26
+ BUMP_MULTIPLIER,
27
+ ALIGNMENT_THRESHOLD,
28
+ LONG_ANIMATION_DURATION,
29
+ SHORT_ANIMATION_DURATION,
30
+ EASE_EXPONENT,
31
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.14.2-rc.3",
3
+ "version": "10.15.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": {
@@ -34,8 +34,8 @@
34
34
  "build": ""
35
35
  },
36
36
  "dependencies": {
37
- "@geode/opengeodeweb-back": "next",
38
- "@geode/opengeodeweb-viewer": "next",
37
+ "@geode/opengeodeweb-back": "latest",
38
+ "@geode/opengeodeweb-viewer": "latest",
39
39
  "@google-cloud/run": "3.2.0",
40
40
  "@kitware/vtk.js": "33.3.0",
41
41
  "@mdi/font": "7.4.47",
@@ -1,95 +0,0 @@
1
- <script setup>
2
- import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
3
-
4
- import ActionButton from "@ogw_front/components/ActionButton.vue";
5
- import Screenshot from "@ogw_front/components/Screenshot";
6
- import ZScaling from "@ogw_front/components/ZScaling";
7
-
8
- import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
9
- import { useViewerStore } from "@ogw_front/stores/viewer";
10
-
11
- const hybridViewerStore = useHybridViewerStore();
12
- const viewerStore = useViewerStore();
13
- const take_screenshot = ref(false);
14
- const showZScaling = ref(false);
15
- const grid_scale = ref(false);
16
- const zScale = ref(hybridViewerStore.zScale);
17
-
18
- watch(
19
- () => hybridViewerStore.zScale,
20
- (newVal) => {
21
- zScale.value = newVal;
22
- },
23
- );
24
-
25
- async function handleZScalingClose() {
26
- await hybridViewerStore.setZScaling(zScale.value);
27
- showZScaling.value = false;
28
- }
29
-
30
- const camera_options = [
31
- {
32
- tooltip: "Reset camera",
33
- icon: "mdi-cube-scan",
34
- action: () => {
35
- hybridViewerStore.resetCamera();
36
- },
37
- },
38
- {
39
- tooltip: "Take a screenshot",
40
- icon: "mdi-camera",
41
- action: () => {
42
- take_screenshot.value = !take_screenshot.value;
43
- },
44
- },
45
- {
46
- tooltip: "Toggle grid scale",
47
- icon: "mdi-ruler-square",
48
- action: () => {
49
- viewerStore.request(
50
- schemas.opengeodeweb_viewer.viewer.grid_scale,
51
- { visibility: !grid_scale.value },
52
- {
53
- response_function: () => {
54
- grid_scale.value = !grid_scale.value;
55
- hybridViewerStore.remoteRender();
56
- },
57
- },
58
- );
59
- },
60
- },
61
- {
62
- tooltip: "Z Scaling Control",
63
- icon: "mdi-sort",
64
- action: () => {
65
- showZScaling.value = !showZScaling.value;
66
- },
67
- },
68
- ];
69
- </script>
70
-
71
- <template>
72
- <v-container :class="[$style.floatToolbar, 'pa-0']" width="auto">
73
- <v-row v-for="camera_option in camera_options" :key="camera_option.icon" dense>
74
- <v-col>
75
- <ActionButton
76
- :icon="camera_option.icon"
77
- :tooltip="camera_option.tooltip"
78
- @click.stop="camera_option.action"
79
- />
80
- </v-col>
81
- </v-row>
82
- </v-container>
83
- <Screenshot :show_dialog="take_screenshot" @close="take_screenshot = false" />
84
- <ZScaling v-if="showZScaling" v-model="zScale" :width="400" @close="handleZScalingClose" />
85
- </template>
86
-
87
- <style module>
88
- .floatToolbar {
89
- position: absolute;
90
- z-index: 2;
91
- right: 20px;
92
- top: 20px;
93
- background-color: rgba(0, 0, 0, 0);
94
- }
95
- </style>