@geode/opengeodeweb-front 10.30.0-rc.2 → 10.30.0-rc.3

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,112 @@
1
+ <script setup>
2
+ import { getPlaneCssColor } from "@ogw_front/utils/clipping_planes";
3
+
4
+ const { plane, index } = defineProps({
5
+ plane: {
6
+ type: Object,
7
+ required: true,
8
+ },
9
+ index: {
10
+ type: Number,
11
+ required: true,
12
+ },
13
+ });
14
+
15
+ const emit = defineEmits(["remove", "flipNormal"]);
16
+ </script>
17
+
18
+ <template>
19
+ <v-card
20
+ data-testid="planeCard"
21
+ variant="outlined"
22
+ class="pa-2 mb-3 rounded-lg border-opacity-50"
23
+ :style="{ borderColor: getPlaneCssColor(index) }"
24
+ >
25
+ <v-row align="center" justify="space-between" no-gutters class="mb-1">
26
+ <v-col cols="auto" class="d-flex align-center">
27
+ <v-chip
28
+ size="x-small"
29
+ variant="flat"
30
+ :style="{
31
+ backgroundColor: getPlaneCssColor(index),
32
+ color: 'white',
33
+ }"
34
+ class="font-weight-bold"
35
+ >
36
+ Plane #{{ index + 1 }}
37
+ </v-chip>
38
+ </v-col>
39
+ <v-col cols="auto">
40
+ <v-btn
41
+ data-testid="removePlaneButton"
42
+ icon="mdi-trash-can-outline"
43
+ size="x-small"
44
+ variant="text"
45
+ color="error"
46
+ @click="emit('remove')"
47
+ />
48
+ </v-col>
49
+ </v-row>
50
+
51
+ <v-row no-gutters class="mb-1">
52
+ <v-col class="text-caption text-medium-emphasis">Origin [X, Y, Z]</v-col>
53
+ </v-row>
54
+ <v-row v-if="plane.origin" dense class="mb-2">
55
+ <v-col v-for="axis in 3" :key="'orig-' + axis" cols="4">
56
+ <v-text-field
57
+ v-model.number="plane.origin[axis - 1]"
58
+ data-testid="planeOriginInput"
59
+ type="number"
60
+ variant="outlined"
61
+ density="compact"
62
+ hide-details
63
+ step="any"
64
+ class="text-caption"
65
+ />
66
+ </v-col>
67
+ </v-row>
68
+ <v-row v-else dense class="mb-2">
69
+ <v-col v-for="axis in 3" :key="'orig-placeholder-' + axis" cols="4">
70
+ <v-text-field
71
+ placeholder="—"
72
+ variant="outlined"
73
+ density="compact"
74
+ hide-details
75
+ disabled
76
+ class="text-caption"
77
+ />
78
+ </v-col>
79
+ </v-row>
80
+
81
+ <v-row align="center" justify="space-between" no-gutters class="mb-1">
82
+ <v-col class="text-caption text-medium-emphasis">Normal [X, Y, Z]</v-col>
83
+ <v-col cols="auto">
84
+ <v-btn
85
+ data-testid="invertNormalButton"
86
+ size="x-small"
87
+ variant="text"
88
+ color="primary"
89
+ prepend-icon="mdi-swap-horizontal"
90
+ class="text-none text-caption px-1"
91
+ @click="emit('flipNormal')"
92
+ >
93
+ Invert Normal
94
+ </v-btn>
95
+ </v-col>
96
+ </v-row>
97
+ <v-row dense>
98
+ <v-col v-for="axis in 3" :key="'norm-' + axis" cols="4">
99
+ <v-text-field
100
+ v-model.number="plane.normal[axis - 1]"
101
+ data-testid="planeNormalInput"
102
+ type="number"
103
+ variant="outlined"
104
+ density="compact"
105
+ hide-details
106
+ step="any"
107
+ class="text-caption"
108
+ />
109
+ </v-col>
110
+ </v-row>
111
+ </v-card>
112
+ </template>
@@ -0,0 +1,238 @@
1
+ <script setup>
2
+ import { DEBOUNCE_DELAY, DEFAULT_NORMALS } from "@ogw_front/utils/clipping_planes";
3
+ import ClippingPlaneCard from "@ogw_front/components/ClippingPlaneCard";
4
+ import ToolPanel from "@ogw_front/components/ToolPanel";
5
+ import { useClippingPlanesWidget } from "@ogw_front/composables/clipping_planes_widget";
6
+ import { useDataStore } from "@ogw_front/stores/data";
7
+ import { useDebounceFn } from "@vueuse/core";
8
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
9
+
10
+ const show = defineModel("show", { type: Boolean, default: false });
11
+ const dataStore = useDataStore();
12
+ const hybridViewerStore = useHybridViewerStore();
13
+ const targetAllVisible = ref(true);
14
+ const selectedDatasetIds = ref([]);
15
+ const planes = ref([{ origin: undefined, normal: [1, 0, 0] }]);
16
+ const allItems = dataStore.refAllItems();
17
+ const availableDatasets = computed(() =>
18
+ allItems.value.map((item) => ({ title: item.name || item.id, value: item.id })),
19
+ );
20
+ const widgetContainer = useTemplateRef("widgetContainer");
21
+ const debouncedApply = useDebounceFn(() => applyClippingPlanes(), DEBOUNCE_DELAY);
22
+ const {
23
+ getSceneCenter,
24
+ syncWidgets,
25
+ syncLocalCamera,
26
+ cleanupLocalWidget,
27
+ initLocalWidget,
28
+ updateWidgetPlacement,
29
+ isFromWidget,
30
+ setFromWidget,
31
+ } = useClippingPlanesWidget({
32
+ planes,
33
+ targetAllVisible,
34
+ selectedDatasetIds,
35
+ allItems,
36
+ hybridViewerStore,
37
+ debouncedApply,
38
+ });
39
+
40
+ function addPlane() {
41
+ const normal = DEFAULT_NORMALS[planes.value.length % DEFAULT_NORMALS.length];
42
+ planes.value.push({ origin: getSceneCenter(), normal });
43
+ }
44
+
45
+ function removePlane(index) {
46
+ planes.value.splice(index, 1);
47
+ }
48
+
49
+ function flipNormal(plane) {
50
+ plane.normal = plane.normal.map((component) => -component);
51
+ syncWidgets();
52
+ applyClippingPlanes();
53
+ }
54
+
55
+ async function applyClippingPlanes() {
56
+ const allIds = allItems.value.map((item) => item.id);
57
+ if (allIds.length === 0) {
58
+ return;
59
+ }
60
+ const center = getSceneCenter();
61
+ const targetIds = targetAllVisible.value ? allIds : selectedDatasetIds.value;
62
+ const untargetedIds = allIds.filter((id) => !targetIds.includes(id));
63
+ const planesData = planes.value.map((plane) => ({
64
+ origin: (plane.origin || center).map(Number),
65
+ normal: plane.normal.map(Number),
66
+ }));
67
+
68
+ if (targetIds.length > 0) {
69
+ await hybridViewerStore.setClippingPlanes(targetIds, planesData);
70
+ }
71
+ if (untargetedIds.length > 0) {
72
+ await hybridViewerStore.setClippingPlanes(untargetedIds, []);
73
+ }
74
+ }
75
+
76
+ async function resetClippingPlanes() {
77
+ setFromWidget(true);
78
+ planes.value = [{ origin: undefined, normal: [1, 0, 0] }];
79
+ updateWidgetPlacement({ isReset: true });
80
+ setFromWidget(false);
81
+ await applyClippingPlanes();
82
+ }
83
+
84
+ async function removeClippingPlanes() {
85
+ const allIds = allItems.value.map((item) => item.id);
86
+ await hybridViewerStore.setClippingPlanes(allIds, []);
87
+ }
88
+
89
+ watch(widgetContainer, (container) => {
90
+ if (container) {
91
+ initLocalWidget(container.$el || container);
92
+ }
93
+ });
94
+
95
+ watch(
96
+ planes,
97
+ () => {
98
+ if (isFromWidget()) {
99
+ return;
100
+ }
101
+ syncWidgets();
102
+ debouncedApply();
103
+ },
104
+ { deep: true },
105
+ );
106
+
107
+ watch(show, (visible) => {
108
+ if (visible) {
109
+ updateWidgetPlacement({ isReset: true });
110
+ applyClippingPlanes();
111
+ }
112
+ });
113
+
114
+ watch(
115
+ [targetAllVisible, selectedDatasetIds],
116
+ () => {
117
+ if (show.value) {
118
+ updateWidgetPlacement({ isReset: true });
119
+ applyClippingPlanes();
120
+ }
121
+ },
122
+ { deep: true },
123
+ );
124
+
125
+ watch(allItems, () => {
126
+ if (show.value) {
127
+ updateWidgetPlacement({ isReset: true });
128
+ applyClippingPlanes();
129
+ }
130
+ });
131
+
132
+ watch(
133
+ () => Object.values(hybridViewerStore.hybridDb).filter((entry) => entry && entry.actor).length,
134
+ (actorCount) => {
135
+ if (show.value && actorCount > 0) {
136
+ updateWidgetPlacement({ isReset: true });
137
+ applyClippingPlanes();
138
+ }
139
+ },
140
+ );
141
+
142
+ watch(() => hybridViewerStore.camera_options, syncLocalCamera, { deep: true });
143
+
144
+ onBeforeUnmount(cleanupLocalWidget);
145
+ </script>
146
+
147
+ <template>
148
+ <ToolPanel v-model="show" title="Clipping Planes" :width="360" :click-outside="false">
149
+ <v-card-text class="pa-3 max-panel-height overflow-y-auto">
150
+ <v-sheet
151
+ ref="widgetContainer"
152
+ height="180"
153
+ color="transparent"
154
+ class="rounded-lg mb-3 overflow-hidden"
155
+ />
156
+ <v-switch
157
+ v-model="targetAllVisible"
158
+ data-testid="targetAllVisibleSwitch"
159
+ label="Apply to all visible datasets"
160
+ color="primary"
161
+ density="compact"
162
+ hide-details
163
+ class="mb-2 text-caption"
164
+ />
165
+
166
+ <v-select
167
+ v-if="!targetAllVisible"
168
+ v-model="selectedDatasetIds"
169
+ data-testid="selectedDatasetsSelect"
170
+ :items="availableDatasets"
171
+ label="Select datasets"
172
+ multiple
173
+ chips
174
+ closable-chips
175
+ variant="outlined"
176
+ density="compact"
177
+ hide-details
178
+ class="mb-3 text-caption"
179
+ />
180
+
181
+ <v-divider class="my-2" />
182
+
183
+ <v-row align="center" justify="space-between" no-gutters class="mb-2">
184
+ <v-col class="text-caption font-weight-bold">Planes ({{ planes.length }})</v-col>
185
+ <v-col cols="auto">
186
+ <v-btn
187
+ data-testid="addPlaneButton"
188
+ size="x-small"
189
+ variant="tonal"
190
+ color="primary"
191
+ icon="mdi-plus"
192
+ @click="addPlane"
193
+ />
194
+ </v-col>
195
+ </v-row>
196
+
197
+ <ClippingPlaneCard
198
+ v-for="(plane, idx) in planes"
199
+ :key="idx"
200
+ :plane="plane"
201
+ :index="idx"
202
+ @remove="removePlane(idx)"
203
+ @flip-normal="flipNormal(plane)"
204
+ />
205
+ </v-card-text>
206
+
207
+ <template #actions>
208
+ <v-card-actions class="justify-space-between px-3 pb-3 pt-0">
209
+ <v-btn
210
+ data-testid="removeClippingPlanesButton"
211
+ variant="text"
212
+ size="small"
213
+ color="error"
214
+ class="text-caption text-none"
215
+ @click="removeClippingPlanes"
216
+ >
217
+ Remove Clipping
218
+ </v-btn>
219
+ <v-btn
220
+ data-testid="resetClippingPlanesButton"
221
+ variant="tonal"
222
+ size="small"
223
+ color="secondary"
224
+ class="text-caption text-none"
225
+ @click="resetClippingPlanes"
226
+ >
227
+ Reset
228
+ </v-btn>
229
+ </v-card-actions>
230
+ </template>
231
+ </ToolPanel>
232
+ </template>
233
+
234
+ <style scoped>
235
+ .max-panel-height {
236
+ max-height: 520px;
237
+ }
238
+ </style>
@@ -1,11 +1,12 @@
1
1
  <script setup>
2
2
  import GlassCard from "@ogw_front/components/GlassCard";
3
3
 
4
- const { title, width, closeLabel, actionLabel } = defineProps({
4
+ const { title, width, closeLabel, actionLabel, clickOutside } = defineProps({
5
5
  title: { type: String, default: "" },
6
6
  width: { type: Number, default: 260 },
7
7
  closeLabel: { type: String, default: "Close" },
8
8
  actionLabel: { type: String, default: undefined },
9
+ clickOutside: { type: Boolean, default: true },
9
10
  });
10
11
 
11
12
  const model = defineModel({ type: Boolean, default: false });
@@ -19,7 +20,7 @@ function close() {
19
20
  <template>
20
21
  <GlassCard
21
22
  v-if="model"
22
- v-click-outside="close"
23
+ v-click-outside="{ handler: close, closeConditional: () => clickOutside }"
23
24
  :title="title"
24
25
  :width="width"
25
26
  :ripple="false"
@@ -3,6 +3,7 @@ import ActionButton from "@ogw_front/components/ActionButton";
3
3
  import CameraBookmarkIcon from "@ogw_front/assets/viewer_svgs/camera-bookmark.svg";
4
4
  import CameraManager from "@ogw_front/components/CameraManager";
5
5
  import CameraOrientation from "@ogw_front/components/CameraOrientation";
6
+ import ClippingPlanes from "@ogw_front/components/ClippingPlanes";
6
7
  import Screenshot from "@ogw_front/components/Screenshot";
7
8
  import ZScaling from "@ogw_front/components/ZScaling";
8
9
  import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
@@ -15,6 +16,7 @@ const take_screenshot = ref(false);
15
16
  const show_camera_manager = ref(false);
16
17
  const showCameraOrientation = ref(false);
17
18
  const showZScaling = ref(false);
19
+ const showClippingPlanes = ref(false);
18
20
  const grid_scale = ref(false);
19
21
  const zScale = ref(hybridViewerStore.zScale);
20
22
 
@@ -151,6 +153,15 @@ const camera_options = computed(() => [
151
153
  showZScaling.value = !showZScaling.value;
152
154
  },
153
155
  },
156
+ {
157
+ testId: "clippingPlanesButton",
158
+ tooltip: "Clipping Planes",
159
+ icon: "mdi-content-cut",
160
+ color: showClippingPlanes.value ? "primary" : undefined,
161
+ action: () => {
162
+ showClippingPlanes.value = !showClippingPlanes.value;
163
+ },
164
+ },
154
165
  ]);
155
166
  </script>
156
167
 
@@ -223,6 +234,7 @@ const camera_options = computed(() => [
223
234
  :width="260"
224
235
  @apply="handleZScalingClose"
225
236
  />
237
+ <ClippingPlanes v-model:show="showClippingPlanes" />
226
238
  </template>
227
239
 
228
240
  <style module>
@@ -0,0 +1,262 @@
1
+ import {
2
+ AXIS_SCALE,
3
+ CHANGE_THRESHOLD,
4
+ PLANE_COLORS,
5
+ SIZE_RATIO,
6
+ computeSceneBoundsInfo,
7
+ getPlaneStyle,
8
+ hasPlaneChanged,
9
+ } from "@ogw_front/utils/clipping_planes";
10
+ import { newInstance as vtkGenericRenderWindow } from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
11
+ import { newInstance as vtkImplicitPlaneWidget } from "@kitware/vtk.js/Widgets/Widgets3D/ImplicitPlaneWidget";
12
+ import { newInstance as vtkWidgetManager } from "@kitware/vtk.js/Widgets/Core/WidgetManager";
13
+
14
+ // oxlint-disable-next-line max-params max-lines-per-function
15
+ function useClippingPlanesWidget({
16
+ planes,
17
+ targetAllVisible,
18
+ selectedDatasetIds,
19
+ allItems,
20
+ hybridViewerStore,
21
+ debouncedApply,
22
+ }) {
23
+ let localRenderWindow = undefined;
24
+ let widgetManager = undefined;
25
+ let widgetEntries = [];
26
+ let fromWidget = false;
27
+ let maxDistance = 0;
28
+ let isLimitingCameraZoom = false;
29
+
30
+ function resolveActiveActors() {
31
+ const targetIds = targetAllVisible.value
32
+ ? allItems.value.map((item) => item.id)
33
+ : selectedDatasetIds.value;
34
+ const targeted = targetIds
35
+ .map((id) => {
36
+ const item = hybridViewerStore.hybridDb[id];
37
+ return item ? item.actor : undefined;
38
+ })
39
+ .filter(Boolean);
40
+ if (targeted.length > 0) {
41
+ return targeted;
42
+ }
43
+ return Object.values(hybridViewerStore.hybridDb)
44
+ .map((entry) => entry && entry.actor)
45
+ .filter(Boolean);
46
+ }
47
+
48
+ function getSceneBoundsInfo() {
49
+ return computeSceneBoundsInfo(resolveActiveActors());
50
+ }
51
+
52
+ function getSceneCenter() {
53
+ return getSceneBoundsInfo().center;
54
+ }
55
+
56
+ function createWidgetEntry(planeWidget, widgetHandle, planeIndex) {
57
+ const widgetState = planeWidget.getWidgetState();
58
+ const plane = planes.value[planeIndex];
59
+ if (plane.origin) {
60
+ widgetState.setOrigin(plane.origin);
61
+ }
62
+ widgetState.setNormal(plane.normal);
63
+ const subscription = widgetState.onModified(() => {
64
+ if (fromWidget) {
65
+ return;
66
+ }
67
+ const origin = widgetState.getOrigin().map((val) => Number(val.toFixed(4)));
68
+ const normal = widgetState.getNormal().map((val) => Number(val.toFixed(4)));
69
+ if (
70
+ !hasPlaneChanged(
71
+ origin,
72
+ normal,
73
+ planes.value[planeIndex].origin,
74
+ planes.value[planeIndex].normal,
75
+ )
76
+ ) {
77
+ return;
78
+ }
79
+ fromWidget = true;
80
+ planes.value[planeIndex].origin = origin;
81
+ planes.value[planeIndex].normal = normal;
82
+ nextTick(() => {
83
+ fromWidget = false;
84
+ });
85
+ debouncedApply();
86
+ });
87
+
88
+ return { planeWidget, widgetHandle, subscription };
89
+ }
90
+
91
+ function syncWidgets() {
92
+ if (!widgetManager || !localRenderWindow) {
93
+ return;
94
+ }
95
+ while (widgetEntries.length > planes.value.length) {
96
+ const entry = widgetEntries.pop();
97
+ entry.subscription.unsubscribe();
98
+ widgetManager.removeWidget(entry.planeWidget);
99
+ entry.planeWidget.delete();
100
+ }
101
+ const { cubicBounds } = getSceneBoundsInfo();
102
+ for (const [idx, plane] of planes.value.entries()) {
103
+ const rgb = PLANE_COLORS[idx % PLANE_COLORS.length];
104
+ if (!widgetEntries[idx]) {
105
+ const planeWidget = vtkImplicitPlaneWidget();
106
+ const widgetHandle = widgetManager.addWidget(planeWidget);
107
+ widgetHandle.setAxisScale(AXIS_SCALE);
108
+ widgetHandle.setHandleSizeRatio(SIZE_RATIO);
109
+ widgetEntries.push(createWidgetEntry(planeWidget, widgetHandle, idx));
110
+ }
111
+ const entry = widgetEntries[idx];
112
+ entry.widgetHandle.setRepresentationStyle(getPlaneStyle(rgb));
113
+ fromWidget = true;
114
+ entry.widgetHandle.placeWidget(cubicBounds);
115
+ fromWidget = false;
116
+ entry.widgetHandle.setAxisScale(AXIS_SCALE);
117
+ entry.widgetHandle.setHandleSizeRatio(SIZE_RATIO);
118
+ const widgetState = entry.planeWidget.getWidgetState();
119
+ if (plane.origin) {
120
+ widgetState.setOrigin(plane.origin);
121
+ }
122
+ widgetState.setNormal(plane.normal);
123
+ }
124
+ localRenderWindow.getRenderWindow().render();
125
+ }
126
+
127
+ function limitCameraZoomOut(camera) {
128
+ if (maxDistance <= 0 || isLimitingCameraZoom) {
129
+ return;
130
+ }
131
+
132
+ const currentDist = camera.getDistance();
133
+ if (currentDist <= maxDistance + CHANGE_THRESHOLD) {
134
+ return;
135
+ }
136
+
137
+ isLimitingCameraZoom = true;
138
+ const focal = camera.getFocalPoint();
139
+ const pos = camera.getPosition();
140
+ const ratio = maxDistance / currentDist;
141
+
142
+ camera.setPosition(
143
+ focal[0] + (pos[0] - focal[0]) * ratio,
144
+ focal[1] + (pos[1] - focal[1]) * ratio,
145
+ focal[2] + (pos[2] - focal[2]) * ratio,
146
+ );
147
+ localRenderWindow.getRenderWindow().render();
148
+ isLimitingCameraZoom = false;
149
+ }
150
+
151
+ function syncLocalCamera() {
152
+ if (!localRenderWindow) {
153
+ return;
154
+ }
155
+ const renderer = localRenderWindow.getRenderer();
156
+ const camera = renderer.getActiveCamera();
157
+ const mainCam = hybridViewerStore.camera_options;
158
+ const { center, cubicBounds } = getSceneBoundsInfo();
159
+ renderer.resetCamera(cubicBounds);
160
+ if (mainCam && mainCam.focal_point && mainCam.position) {
161
+ const dir = [
162
+ mainCam.position[0] - mainCam.focal_point[0],
163
+ mainCam.position[1] - mainCam.focal_point[1],
164
+ mainCam.position[2] - mainCam.focal_point[2],
165
+ ];
166
+ const dirLen = Math.hypot(...dir);
167
+ if (dirLen > 0) {
168
+ const distance = camera.getDistance();
169
+ const normDir = dir.map((component) => component / dirLen);
170
+ camera.setFocalPoint(...center);
171
+ camera.setPosition(
172
+ center[0] + normDir[0] * distance,
173
+ center[1] + normDir[1] * distance,
174
+ center[2] + normDir[2] * distance,
175
+ );
176
+ if (mainCam.view_up) {
177
+ camera.setViewUp(...mainCam.view_up);
178
+ }
179
+ }
180
+ }
181
+
182
+ limitCameraZoomOut(camera);
183
+ localRenderWindow.getRenderWindow().render();
184
+ }
185
+
186
+ function cleanupLocalWidget() {
187
+ maxDistance = 0;
188
+ isLimitingCameraZoom = false;
189
+ for (const entry of widgetEntries) {
190
+ entry.subscription.unsubscribe();
191
+ if (widgetManager) {
192
+ widgetManager.removeWidget(entry.planeWidget);
193
+ }
194
+ entry.planeWidget.delete();
195
+ }
196
+ widgetEntries = [];
197
+ if (localRenderWindow) {
198
+ localRenderWindow.delete();
199
+ localRenderWindow = undefined;
200
+ widgetManager = undefined;
201
+ }
202
+ }
203
+
204
+ function initLocalWidget(container) {
205
+ cleanupLocalWidget();
206
+ container.addEventListener("wheel", (event) => event.stopPropagation(), { passive: true });
207
+ localRenderWindow = vtkGenericRenderWindow({
208
+ background: [0, 0, 0, 0],
209
+ listenWindowResize: false,
210
+ });
211
+ localRenderWindow.setContainer(container);
212
+ const camera = localRenderWindow.getRenderer().getActiveCamera();
213
+ camera.onModified(() => limitCameraZoomOut(camera));
214
+ const canvas = localRenderWindow.getApiSpecificRenderWindow().getCanvas();
215
+ Object.assign(canvas.style, { width: "100%", height: "100%", background: "transparent" });
216
+ localRenderWindow.resize();
217
+ widgetManager = vtkWidgetManager();
218
+ widgetManager.setRenderer(localRenderWindow.getRenderer());
219
+ updateWidgetPlacement();
220
+ }
221
+
222
+ function updateWidgetPlacement({ isReset = false } = {}) {
223
+ if (!widgetManager || !localRenderWindow) {
224
+ return;
225
+ }
226
+ if (isReset) {
227
+ maxDistance = 0;
228
+ }
229
+ const center = getSceneCenter();
230
+ for (const plane of planes.value) {
231
+ if (!plane.origin || isReset) {
232
+ plane.origin = [...center];
233
+ }
234
+ }
235
+ syncWidgets();
236
+ syncLocalCamera();
237
+ if (maxDistance <= 0) {
238
+ maxDistance = localRenderWindow.getRenderer().getActiveCamera().getDistance();
239
+ }
240
+ }
241
+
242
+ function isFromWidget() {
243
+ return fromWidget;
244
+ }
245
+
246
+ function setFromWidget(value) {
247
+ fromWidget = value;
248
+ }
249
+
250
+ return {
251
+ getSceneCenter,
252
+ syncWidgets,
253
+ syncLocalCamera,
254
+ cleanupLocalWidget,
255
+ initLocalWidget,
256
+ updateWidgetPlacement,
257
+ isFromWidget,
258
+ setFromWidget,
259
+ };
260
+ }
261
+
262
+ export { useClippingPlanesWidget };
@@ -137,6 +137,13 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
137
137
  });
138
138
  }
139
139
 
140
+ async function setClippingPlanes(ids, planes) {
141
+ const schema = viewer_schemas.opengeodeweb_viewer.viewer.clipping_planes;
142
+ const params = { ids, planes };
143
+ await viewerStore.request({ schema, params });
144
+ await remoteRender();
145
+ }
146
+
140
147
  function resetCamera() {
141
148
  genericRenderWindow.value.getRenderer().resetCamera();
142
149
  genericRenderWindow.value.getRenderWindow().render();
@@ -307,6 +314,7 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
307
314
  removeItem,
308
315
  setVisibility,
309
316
  setZScaling,
317
+ setClippingPlanes,
310
318
  syncRemoteCamera,
311
319
  setCamera,
312
320
  initHybridViewer,
@@ -0,0 +1,110 @@
1
+ const AXIS_SCALE = 0.45;
2
+ const SIZE_RATIO = 0.1;
3
+ const DEBOUNCE_DELAY = 200;
4
+ const CHANGE_THRESHOLD = 1e-4;
5
+ // oxlint-disable no-magic-numbers
6
+ const COLOR_BLUE = [0.12, 0.53, 0.9];
7
+ const COLOR_GREEN = [0.26, 0.63, 0.28];
8
+ const COLOR_ORANGE = [0.98, 0.55, 0];
9
+ const COLOR_PURPLE = [0.55, 0.14, 0.67];
10
+ const COLOR_RED = [0.9, 0.22, 0.21];
11
+ // oxlint-enable no-magic-numbers
12
+ const PLANE_COLORS = [COLOR_BLUE, COLOR_GREEN, COLOR_ORANGE, COLOR_PURPLE, COLOR_RED];
13
+ const NORMAL_X = [1, 0, 0];
14
+ const NORMAL_Y = [0, 1, 0];
15
+ const NORMAL_Z = [0, 0, 1];
16
+ const DEFAULT_NORMALS = [NORMAL_X, NORMAL_Y, NORMAL_Z];
17
+ const RGB_MAX_VALUE = 255;
18
+
19
+ function getPlaneCssColor(index) {
20
+ const rgb = PLANE_COLORS[index % PLANE_COLORS.length];
21
+ return `rgb(${rgb.map((channel) => Math.round(channel * RGB_MAX_VALUE)).join(",")})`;
22
+ }
23
+
24
+ function hasPlaneChanged(origin, normal, currentOrigin, currentNormal) {
25
+ if (!currentOrigin) {
26
+ return true;
27
+ }
28
+ return (
29
+ origin.some((val, idx) => Math.abs(val - currentOrigin[idx]) > CHANGE_THRESHOLD) ||
30
+ normal.some((val, idx) => Math.abs(val - currentNormal[idx]) > CHANGE_THRESHOLD)
31
+ );
32
+ }
33
+
34
+ function getPlaneStyle(rgb) {
35
+ return {
36
+ active: {
37
+ plane: { opacity: 1, color: rgb },
38
+ normal: { opacity: 1, color: rgb },
39
+ origin: { opacity: 1, color: rgb },
40
+ },
41
+ inactive: {
42
+ plane: { opacity: 0.5, color: rgb },
43
+ normal: { opacity: 1, color: rgb },
44
+ origin: { opacity: 1, color: rgb },
45
+ },
46
+ static: {
47
+ display2D: { representation: 0 },
48
+ outline: { color: [1, 1, 1], opacity: 1, representation: 1, interpolation: 0 },
49
+ },
50
+ };
51
+ }
52
+
53
+ function computeSceneBounds(actors) {
54
+ let bounds = [Infinity, -Infinity, Infinity, -Infinity, Infinity, -Infinity];
55
+ for (const actor of actors) {
56
+ const boundsOfActor = actor.getBounds();
57
+ bounds = [
58
+ Math.min(bounds[0], boundsOfActor[0]),
59
+ Math.max(bounds[1], boundsOfActor[1]),
60
+ Math.min(bounds[2], boundsOfActor[2]),
61
+ Math.max(bounds[3], boundsOfActor[3]),
62
+ Math.min(bounds[4], boundsOfActor[4]),
63
+ Math.max(bounds[5], boundsOfActor[5]),
64
+ ];
65
+ }
66
+ return bounds;
67
+ }
68
+
69
+ function computeSceneBoundsInfo(actors) {
70
+ if (!actors || actors.length === 0) {
71
+ return { center: [0, 0, 0], cubicBounds: [-1, 1, -1, 1, -1, 1] };
72
+ }
73
+ const [xmin, xmax, ymin, ymax, zmin, zmax] = computeSceneBounds(actors);
74
+ const center = [
75
+ Number(((xmin + xmax) / 2).toFixed(4)),
76
+ Number(((ymin + ymax) / 2).toFixed(4)),
77
+ Number(((zmin + zmax) / 2).toFixed(4)),
78
+ ];
79
+
80
+ const maxActorExtent = Math.max(
81
+ ...actors.map((actor) => {
82
+ const bounds = actor.getBounds();
83
+ return Math.max(bounds[1] - bounds[0], bounds[3] - bounds[2], bounds[5] - bounds[4]);
84
+ }),
85
+ );
86
+ const halfExtent = maxActorExtent / 2;
87
+ const cubicBounds = [
88
+ center[0] - halfExtent,
89
+ center[0] + halfExtent,
90
+ center[1] - halfExtent,
91
+ center[1] + halfExtent,
92
+ center[2] - halfExtent,
93
+ center[2] + halfExtent,
94
+ ];
95
+ return { center, cubicBounds };
96
+ }
97
+
98
+ export {
99
+ AXIS_SCALE,
100
+ SIZE_RATIO,
101
+ DEBOUNCE_DELAY,
102
+ CHANGE_THRESHOLD,
103
+ PLANE_COLORS,
104
+ DEFAULT_NORMALS,
105
+ getPlaneCssColor,
106
+ hasPlaneChanged,
107
+ getPlaneStyle,
108
+ computeSceneBounds,
109
+ computeSceneBoundsInfo,
110
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.30.0-rc.2",
3
+ "version": "10.30.0-rc.3",
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": {