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

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>
@@ -1,4 +1,8 @@
1
1
  <script setup>
2
+ import { useDataStore } from "@ogw_front/stores/data";
3
+
4
+ const dataStore = useDataStore();
5
+
2
6
  const { item, itemProps, selection, isSelected, getIndeterminate } = defineProps({
3
7
  item: { type: Object, required: true },
4
8
  itemProps: { type: Object, required: true },
@@ -7,13 +11,34 @@ const { item, itemProps, selection, isSelected, getIndeterminate } = defineProps
7
11
  getIndeterminate: { type: Function, required: true },
8
12
  });
9
13
 
10
- defineEmits(["toggle-open", "toggle-select", "hover-eye-enter", "hover-eye-leave"]);
14
+ const emit = defineEmits(["toggle-open", "toggle-select", "hover-eye-enter", "hover-eye-leave"]);
11
15
 
12
16
  const INDENT_STEP = 10;
17
+
18
+ function triggerHorizonStackModal(rawItem) {
19
+ globalThis.dispatchEvent(new CustomEvent("open-horizon-stack-modal", { detail: rawItem }));
20
+ }
21
+ const isHorizonStack = computed(() => item.raw.geode_object_type === "HorizonStack3D");
22
+ const isViewable = computed(() => dataStore.isItemViewable(item.raw));
23
+ const showEyeButton = computed(
24
+ () => !isHorizonStack.value && item.raw.title !== "HorizonStack3D" && isViewable.value,
25
+ );
26
+
27
+ function handleRowClick(event) {
28
+ if (isHorizonStack.value) {
29
+ if (!item.isLeaf) {
30
+ return;
31
+ }
32
+
33
+ event.stopPropagation();
34
+ event.preventDefault();
35
+ triggerHorizonStackModal(item.raw);
36
+ }
37
+ }
13
38
  </script>
14
39
 
15
40
  <template>
16
- <div class="tree-row-content d-flex align-center px-2 ps-2 w-100">
41
+ <div class="tree-row-content d-flex align-center px-2 ps-2 w-100" @click="handleRowClick">
17
42
  <div
18
43
  v-if="item.depth > 0"
19
44
  class="flex-shrink-0"
@@ -30,24 +55,37 @@ const INDENT_STEP = 10;
30
55
  />
31
56
  <div v-else class="icon-placeholder" />
32
57
 
33
- <v-btn
34
- v-if="selection.selectable"
35
- :icon="
36
- getIndeterminate(item.raw)
37
- ? 'mdi-eye-minus-outline'
38
- : isSelected(item.raw)
39
- ? 'mdi-eye'
40
- : 'mdi-eye-off-outline'
41
- "
42
- variant="text"
43
- density="compact"
44
- color="black"
45
- class="flex-shrink-0"
46
- @click.stop="$emit('toggle-select', item.raw)"
47
- @mousedown.stop
48
- @mouseenter="$emit('hover-eye-enter', item.raw)"
49
- @mouseleave="$emit('hover-eye-leave', item.raw)"
50
- />
58
+ <template v-if="selection.selectable">
59
+ <v-btn
60
+ v-if="isHorizonStack && item.isLeaf"
61
+ icon="mdi-layers-triple"
62
+ variant="text"
63
+ density="compact"
64
+ color="black"
65
+ class="flex-shrink-0"
66
+ style="z-index: 4"
67
+ @click.stop="triggerHorizonStackModal(item.raw)"
68
+ @mousedown.stop
69
+ />
70
+ <v-btn
71
+ v-else-if="showEyeButton"
72
+ :icon="
73
+ getIndeterminate(item.raw)
74
+ ? 'mdi-eye-minus-outline'
75
+ : isSelected(item.raw)
76
+ ? 'mdi-eye'
77
+ : 'mdi-eye-off-outline'
78
+ "
79
+ variant="text"
80
+ density="compact"
81
+ color="black"
82
+ class="flex-shrink-0"
83
+ @click.stop="$emit('toggle-select', item.raw)"
84
+ @mousedown.stop
85
+ @mouseenter="$emit('hover-eye-enter', item.raw)"
86
+ @mouseleave="$emit('hover-eye-leave', item.raw)"
87
+ />
88
+ </template>
51
89
  </div>
52
90
 
53
91
  <div class="tree-title flex-grow-1 overflow-hidden d-flex align-center ms-1 pt-1">
@@ -179,46 +179,48 @@ function expandAll() {
179
179
  </template>
180
180
 
181
181
  <template #append="{ item }">
182
- <v-btn
183
- v-if="item.viewer_type"
184
- icon="mdi-target"
185
- size="medium"
186
- variant="text"
187
- v-tooltip="'Focus camera on object'"
188
- @click.stop="hybridViewerStore.focusCameraOnObject(item.id)"
189
- />
190
- <v-btn
191
- v-if="isModel(item)"
192
- icon="mdi-magnify-expand"
193
- size="medium"
194
- class="ml-2"
195
- variant="text"
196
- v-tooltip="'Model\'s mesh components'"
197
- @click.stop="
198
- treeviewStore.displayAdditionalTree(
199
- item.id,
200
- item.title,
201
- item.geode_object_type,
202
- 'model_components',
203
- )
204
- "
205
- />
206
- <v-btn
207
- v-if="isModel(item) && hasCollectionsMap[item.id]"
208
- icon="mdi-format-list-group"
209
- size="medium"
210
- class="ml-2"
211
- variant="text"
212
- v-tooltip="'Model\'s collections'"
213
- @click.stop="
214
- treeviewStore.displayAdditionalTree(
215
- item.id,
216
- item.title,
217
- item.geode_object_type,
218
- 'model_collections',
219
- )
220
- "
221
- />
182
+ <template v-if="item.geode_object_type !== 'HorizonStack3D'">
183
+ <v-btn
184
+ v-if="item.viewer_type"
185
+ icon="mdi-target"
186
+ size="medium"
187
+ variant="text"
188
+ v-tooltip="'Focus camera on object'"
189
+ @click.stop="hybridViewerStore.focusCameraOnObject(item.id)"
190
+ />
191
+ <v-btn
192
+ v-if="isModel(item)"
193
+ icon="mdi-magnify-expand"
194
+ size="medium"
195
+ class="ml-2"
196
+ variant="text"
197
+ v-tooltip="'Model\'s mesh components'"
198
+ @click.stop="
199
+ treeviewStore.displayAdditionalTree(
200
+ item.id,
201
+ item.title,
202
+ item.geode_object_type,
203
+ 'model_components',
204
+ )
205
+ "
206
+ />
207
+ <v-btn
208
+ v-if="isModel(item) && hasCollectionsMap[item.id]"
209
+ icon="mdi-format-list-group"
210
+ size="medium"
211
+ class="ml-2"
212
+ variant="text"
213
+ v-tooltip="'Model\'s collections'"
214
+ @click.stop="
215
+ treeviewStore.displayAdditionalTree(
216
+ item.id,
217
+ item.title,
218
+ item.geode_object_type,
219
+ 'model_collections',
220
+ )
221
+ "
222
+ />
223
+ </template>
222
224
  </template>
223
225
  </CommonTreeView>
224
226
  </div>