@geode/opengeodeweb-front 10.29.1 → 10.30.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/app/components/ClippingPlaneCard.vue +112 -0
  2. package/app/components/ClippingPlanes.vue +238 -0
  3. package/app/components/Launcher.vue +1 -1
  4. package/app/components/ObjectSelector.vue +26 -76
  5. package/app/components/ToolPanel.vue +3 -2
  6. package/app/components/ViewToolbar.vue +12 -0
  7. package/app/components/Viewer/ObjectTree/Base/TreeRow.vue +58 -20
  8. package/app/components/Viewer/ObjectTree/Views/GlobalObjects.vue +42 -40
  9. package/app/composables/clipping_planes_widget.js +262 -0
  10. package/app/composables/hover_highlight.js +6 -0
  11. package/app/composables/project_manager.js +14 -11
  12. package/app/composables/use_overlapping_picker.js +15 -2
  13. package/app/stores/app.js +56 -11
  14. package/app/stores/back.js +10 -27
  15. package/app/stores/cloud.js +7 -1
  16. package/app/stores/data.js +32 -1
  17. package/app/stores/data_style.js +11 -0
  18. package/app/stores/hybrid_viewer.js +13 -5
  19. package/app/stores/infra.js +2 -7
  20. package/app/stores/viewer.js +9 -27
  21. package/app/utils/clipping_planes.js +110 -0
  22. package/app/utils/extension.js +30 -3
  23. package/app/utils/import_workflow.js +9 -12
  24. package/app/utils/log.js +18 -0
  25. package/app/utils/stores.js +32 -0
  26. package/internal/stores/hybrid_viewer.js +8 -63
  27. package/internal/stores/hybrid_viewer_brightness.js +62 -0
  28. package/internal/utils/api_fetch.js +27 -53
  29. package/internal/utils/upload_file.js +36 -29
  30. package/internal/utils/viewer_call.js +7 -2
  31. package/nuxt.config.js +2 -1
  32. package/package.json +4 -3
  33. package/server/api/local/{project_folder_path.post.js → app/project_folder_path.post.js} +1 -1
  34. package/server/api/local/{run_back.post.js → app/run_back.post.js} +1 -1
  35. package/server/api/local/{run_viewer.post.js → app/run_viewer.post.js} +1 -1
  36. package/server/api/{microservice → local}/extensions/upload.put.js +16 -37
  37. package/server/api/microservice/app/set_app_base_url.post.js +25 -0
  38. package/server/api/microservice/app/set_back_base_url.post.js +22 -0
  39. package/server/api/microservice/app/set_viewer_base_url.post.js +22 -0
  40. package/server/api/microservice/extensions/download.post.js +32 -0
  41. package/server/api/microservice/extensions/kill.post.js +3 -3
  42. package/server/api/microservice/extensions/run.post.js +5 -4
  43. package/server/api/serverless/{run_cloud.js → run_cloud.post.js} +1 -1
  44. package/{app/utils/config.js → server/utils/app_config.js} +28 -3
  45. package/{app/utils/local → server/utils}/path.js +1 -1
  46. package/{app/utils/local → server/utils}/scripts.js +5 -2
  47. package/server/utils/server_config.js +32 -0
  48. package/shared/scripts.js +56 -0
  49. package/shared/utils/fetch_raw.js +61 -0
  50. package/shared/utils/fetch_schema.js +46 -0
  51. package/shared/utils/file.js +16 -0
  52. package/shared/utils/response_handlers/load.js +74 -0
  53. package/tests/integration/setup.js +3 -3
  54. package/tests/integration/stores/data_style/mesh/cells.nuxt.test.js +1 -1
  55. package/tests/integration/stores/data_style/mesh/edges.nuxt.test.js +1 -1
  56. package/tests/integration/stores/data_style/mesh/index.nuxt.test.js +1 -1
  57. package/tests/integration/stores/data_style/mesh/points.nuxt.test.js +1 -1
  58. package/tests/integration/stores/data_style/mesh/polygons.nuxt.test.js +1 -1
  59. package/tests/integration/stores/data_style/mesh/polyhedra.nuxt.test.js +1 -1
  60. package/tests/integration/stores/data_style/model/blocks.nuxt.test.js +1 -1
  61. package/tests/integration/stores/data_style/model/corners.nuxt.test.js +1 -1
  62. package/tests/integration/stores/data_style/model/edges.nuxt.test.js +1 -1
  63. package/tests/integration/stores/data_style/model/index.nuxt.test.js +1 -1
  64. package/tests/integration/stores/data_style/model/lines.nuxt.test.js +1 -1
  65. package/tests/integration/stores/data_style/model/points.nuxt.test.js +1 -1
  66. package/tests/integration/stores/data_style/model/surfaces.nuxt.test.js +1 -1
  67. package/tests/integration/stores/viewer.nuxt.test.js +1 -1
  68. package/tests/unit/composables/project_manager.nuxt.test.js +14 -2
  69. package/tests/unit/composables/upload_file.nuxt.test.js +2 -2
  70. package/tests/unit/stores/back.nuxt.test.js +1 -1
  71. package/tests/unit/stores/cloud.nuxt.test.js +22 -20
  72. package/tests/unit/stores/data.nuxt.test.js +35 -0
  73. package/tests/unit/stores/infra.nuxt.test.js +13 -6
  74. package/tests/unit/stores/viewer.nuxt.test.js +1 -1
  75. package/tests/unit/utils/validate_schema.nuxt.test.js +1 -1
  76. package/tests/vitest.config.js +7 -5
  77. /package/server/api/local/{kill.post.js → app/kill.post.js} +0 -0
  78. /package/{app/utils/local → server/utils}/cleanup.js +0 -0
  79. /package/{app/utils/local → server/utils}/microservices.js +0 -0
  80. /package/{app → server}/utils/server.js +0 -0
  81. /package/{app/utils/local → shared}/app_mode.js +0 -0
  82. /package/{app → shared}/utils/validate_schema.js +0 -0
@@ -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>
@@ -2,7 +2,7 @@
2
2
  import Loading from "@ogw_front/components/Loading";
3
3
  import Recaptcha from "@ogw_front/components/Recaptcha";
4
4
  import { Status } from "@ogw_front/utils/status";
5
- import { appMode } from "@ogw_front/utils/local/app_mode";
5
+ import { appMode } from "@ogw_shared/app_mode";
6
6
  import { useInfraStore } from "@ogw_front/stores/infra";
7
7
 
8
8
  const { appName, email, isUserAuthenticated, logo } = defineProps({
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import FetchingData from "@ogw_front/components/FetchingData.vue";
3
3
  import { geode_objects } from "@ogw_front/assets/geode_objects";
4
+ import { resolveAllowedObjects } from "@ogw_shared/utils/response_handlers/load.js";
4
5
  import schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json";
5
6
  import { useBackStore } from "@ogw_front/stores/back";
6
7
 
@@ -15,100 +16,49 @@ const { filenames } = defineProps({
15
16
  const backStore = useBackStore();
16
17
 
17
18
  const loading = ref(false);
18
- const allowed_objects = ref({});
19
- const toggle_loading = useToggle(loading);
20
- const multiple_files_no_common = ref(false);
19
+ const allowedGeodeObjects = ref({});
20
+ const toggleLoading = useToggle(loading);
21
+ const multipleFilesNoCommon = ref(false);
21
22
 
22
- function select_geode_object(object_map) {
23
- const object_keys = Object.keys(object_map);
24
- if (object_keys.length === 0) {
25
- return undefined;
26
- }
27
- if (object_keys.length === 1 && object_map[object_keys[0]].is_loadable > 0) {
28
- return object_keys[0];
29
- }
30
- const highest_load_score = Math.max(...object_keys.map((key) => object_map[key].is_loadable));
31
- if (highest_load_score <= 0) {
32
- return undefined;
33
- }
34
- const best_score_objects = object_keys.filter(
35
- (key) => object_map[key].is_loadable === highest_load_score,
36
- );
37
- if (best_score_objects.length === 1) {
38
- return best_score_objects[0];
39
- }
40
- const highest_priority = Math.max(
41
- ...best_score_objects.map((key) => object_map[key].object_priority ?? -Infinity),
42
- );
43
- const best_priority_objects = best_score_objects.filter(
44
- (key) => object_map[key].object_priority === highest_priority,
45
- );
46
- if (highest_priority !== -Infinity && best_priority_objects.length === 1) {
47
- return best_priority_objects[0];
48
- }
49
- return undefined;
50
- }
51
-
52
- function isCsvFile(filename) {
53
- return filename.toLowerCase().endsWith(".csv") || filename.toLowerCase().endsWith(".csv.json");
54
- }
55
-
56
- async function get_allowed_objects() {
57
- toggle_loading();
58
- allowed_objects.value = {};
59
- multiple_files_no_common.value = false;
60
-
61
- const promise_array = filenames.map((filename) => {
23
+ async function fetchAllowedObjectsList() {
24
+ const promiseArray = filenames.map((filename) => {
62
25
  const params = { filename };
63
26
  return backStore.request({ schema, params });
64
27
  });
65
- const responses = await Promise.all(promise_array);
66
- const allowed_objects_list = responses.map((response) => response.allowed_objects);
67
- const all_keys = [...new Set(allowed_objects_list.flatMap((obj) => Object.keys(obj)))];
68
- const common_keys = all_keys.filter((key) => allowed_objects_list.every((obj) => key in obj));
28
+ const responses = await Promise.all(promiseArray);
29
+ return responses.map((response) => response.allowed_objects);
30
+ }
69
31
 
70
- if (filenames.length > 1 && all_keys.length > 0 && common_keys.length === 0) {
71
- multiple_files_no_common.value = true;
72
- }
32
+ async function getAllowedGeodeObjects() {
33
+ toggleLoading();
34
+ allowedGeodeObjects.value = {};
35
+ multipleFilesNoCommon.value = false;
73
36
 
74
- const final_object = {};
75
- for (const key of common_keys) {
76
- const load_scores = allowed_objects_list.map((obj) => obj[key].is_loadable);
77
- const priorities = allowed_objects_list
78
- .map((obj) => obj[key].object_priority)
79
- .filter((priority) => priority !== undefined);
80
- final_object[key] = { is_loadable: Math.min(...load_scores) };
81
- if (priorities.length > 0) {
82
- final_object[key].object_priority = Math.max(...priorities);
83
- }
84
- }
85
- const isCsv = filenames.some((filename) => isCsvFile(filename));
86
- if (isCsv && !final_object["PointSet3D"]) {
87
- final_object["PointSet3D"] = { is_loadable: 1, object_priority: 100 };
88
- }
37
+ const allowedGeodeObjectsList = await fetchAllowedObjectsList();
38
+ const resolved = resolveAllowedObjects(filenames, allowedGeodeObjectsList);
89
39
 
90
- allowed_objects.value = final_object;
91
- const selected_object = select_geode_object(final_object);
92
- if (selected_object) {
93
- set_geode_object(selected_object);
40
+ allowedGeodeObjects.value = resolved.mergedAllowedObjects;
41
+ multipleFilesNoCommon.value = resolved.multipleFilesNoCommon;
42
+ if (resolved.selectedGeodeObject) {
43
+ setGeodeObject(resolved.selectedGeodeObject);
94
44
  }
95
- toggle_loading();
45
+ toggleLoading();
96
46
  }
97
47
 
98
- function set_geode_object(geode_object_type) {
48
+ function setGeodeObject(geode_object_type) {
99
49
  if (geode_object_type) {
100
50
  emit("update_values", { geode_object_type });
101
51
  emit("increment_step");
102
52
  }
103
53
  }
104
54
  // oxlint-disable-next-line no-top-level-await
105
- await get_allowed_objects();
55
+ await getAllowedGeodeObjects();
106
56
  </script>
107
57
 
108
58
  <template>
109
59
  <FetchingData v-if="loading" />
110
- <v-row v-else-if="Object.keys(allowed_objects).length" class="justify-left">
111
- <v-col v-for="(value, key) in allowed_objects" :key="key" cols="3" md="4">
60
+ <v-row v-else-if="Object.keys(allowedGeodeObjects).length" class="justify-left">
61
+ <v-col v-for="(value, key) in allowedGeodeObjects" :key="key" cols="3" md="4">
112
62
  <v-tooltip
113
63
  :text="
114
64
  value['is_loadable']
@@ -124,7 +74,7 @@ await get_allowed_objects();
124
74
  class="card ma-2"
125
75
  hover
126
76
  rounded
127
- @click="set_geode_object(key)"
77
+ @click="setGeodeObject(key)"
128
78
  :disabled="!value['is_loadable']"
129
79
  :elevation="value['is_loadable'] ? 5 : 3"
130
80
  >
@@ -139,7 +89,7 @@ await get_allowed_objects();
139
89
  </v-tooltip>
140
90
  </v-col>
141
91
  </v-row>
142
- <v-row v-else-if="multiple_files_no_common" class="pa-5">
92
+ <v-row v-else-if="multipleFilesNoCommon" class="pa-5">
143
93
  <v-card class="card" variant="tonal" rounded>
144
94
  <v-card-text>
145
95
  These files cannot be loaded together because they don't share a common data type.
@@ -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">