@geode/opengeodeweb-front 10.30.0-rc.3 → 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.
@@ -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>
@@ -1,3 +1,4 @@
1
+ import { useDataStore } from "@ogw_front/stores/data";
1
2
  import { useViewerStore } from "@ogw_front/stores/viewer";
2
3
  import vtk_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
3
4
 
@@ -5,6 +6,7 @@ const HOVER_DELAY = 200;
5
6
 
6
7
  export function useHoverhighlight() {
7
8
  const viewerStore = useViewerStore();
9
+ const dataStore = useDataStore();
8
10
  let timer = undefined;
9
11
  let currentId = undefined;
10
12
  let currentType = undefined;
@@ -20,6 +22,10 @@ export function useHoverhighlight() {
20
22
  currentId = id;
21
23
  currentType = type;
22
24
 
25
+ if (!(await dataStore.isItemViewable(id))) {
26
+ return;
27
+ }
28
+
23
29
  let block_ids = [];
24
30
  if (typeof block_ids_provider === "function") {
25
31
  block_ids = await block_ids_provider();
@@ -64,8 +64,21 @@ export function useOverlappingPicker() {
64
64
  }
65
65
 
66
66
  async function get_viewer_id({ x, y, containerWidth, containerHeight, containerRect }) {
67
- const activeIds = new Set(dataItems.value.map((item) => item.id));
68
- const ids = Object.keys(dataStyleStore.styles).filter((styleId) => activeIds.has(styleId));
67
+ const activeIds = new Set(dataItems.value.map((i) => i.id));
68
+ const visibleStyleIds = Object.keys(dataStyleStore.styles).filter(
69
+ (styleId) => activeIds.has(styleId) && dataStyleStore.objectVisibility(styleId),
70
+ );
71
+
72
+ const viewableChecks = await Promise.all(
73
+ visibleStyleIds.map(async (styleId) => {
74
+ try {
75
+ return (await dataStore.isItemViewable(styleId)) ? styleId : undefined;
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }),
80
+ );
81
+ const ids = viewableChecks.filter(Boolean);
69
82
 
70
83
  const result = { id: undefined, viewer_id: undefined };
71
84
  let pickedResponse = undefined;
package/app/stores/app.js CHANGED
@@ -9,11 +9,10 @@ import { useInfraStore } from "@ogw_front/stores/infra";
9
9
  // oxlint-disable-next-line max-lines-per-function, max-statements
10
10
  export const useAppStore = defineStore("app", () => {
11
11
  const stores = [];
12
+ const globalComponents = ref(new Map());
12
13
  const default_local_port = ref(globalThis.location.port);
13
14
  const status = ref(Status.NOT_CONNECTED);
14
-
15
15
  const protocol = computed(() => getRestApiProtocol());
16
-
17
16
  const port = computed(() => getRestApiPort(default_local_port.value));
18
17
 
19
18
  const base_url = computed(() => {
@@ -25,6 +24,22 @@ export const useAppStore = defineStore("app", () => {
25
24
  return app_url;
26
25
  });
27
26
 
27
+ function registerGlobalComponent(extensionId, componentId, component) {
28
+ if (!globalComponents.value.has(extensionId)) {
29
+ globalComponents.value.set(extensionId, new Map());
30
+ }
31
+ globalComponents.value.get(extensionId).set(componentId, component);
32
+ console.log(
33
+ `[AppStore] Registered global component ${componentId} for extension ${extensionId}`,
34
+ );
35
+ }
36
+
37
+ function unregisterGlobalComponent(extensionId, componentId) {
38
+ if (globalComponents.value.has(extensionId)) {
39
+ globalComponents.value.get(extensionId).delete(componentId);
40
+ }
41
+ }
42
+
28
43
  function registerStore(store) {
29
44
  const isAlreadyRegistered = stores.some((registeredStore) => registeredStore.$id === store.$id);
30
45
  if (isAlreadyRegistered) {
@@ -311,5 +326,8 @@ export const useAppStore = defineStore("app", () => {
311
326
  createProjectFolder,
312
327
  start_request,
313
328
  stop_request,
329
+ globalComponents,
330
+ registerGlobalComponent,
331
+ unregisterGlobalComponent,
314
332
  };
315
333
  });
@@ -11,6 +11,33 @@ import { useViewerStore } from "@ogw_front/stores/viewer";
11
11
 
12
12
  const viewer_generic_schemas = viewer_schemas.opengeodeweb_viewer.generic;
13
13
 
14
+ function checkItemViewable(item) {
15
+ if (!item || typeof item !== "object") {
16
+ return false;
17
+ }
18
+ if (item.is_viewable !== undefined) {
19
+ return Boolean(item.is_viewable);
20
+ }
21
+ if (item.binary_light_viewable !== undefined) {
22
+ return item.binary_light_viewable !== "not_viewable";
23
+ }
24
+ if (
25
+ item.geode_object_type === "HorizonStack3D" ||
26
+ item.id === "HorizonStack3D" ||
27
+ item.title === "HorizonStack3D"
28
+ ) {
29
+ return false;
30
+ }
31
+ return true;
32
+ }
33
+
34
+ function isItemViewable(itemOrId) {
35
+ if (typeof itemOrId === "string") {
36
+ return database.data.get(itemOrId).then(checkItemViewable);
37
+ }
38
+ return checkItemViewable(itemOrId);
39
+ }
40
+
14
41
  // oxlint-disable-next-line max-lines-per-function, max-statements
15
42
  export const useDataStore = defineStore("data", () => {
16
43
  const viewerStore = useViewerStore();
@@ -93,8 +120,11 @@ export const useDataStore = defineStore("data", () => {
93
120
  geode_object_type: new_item.geode_object_type,
94
121
  visible: true,
95
122
  created_at: new Date().toISOString(),
96
- binary_light_viewable: new_item.binary_light_viewable,
123
+ is_viewable: new_item.is_viewable,
97
124
  };
125
+ if (new_item.binary_light_viewable !== undefined && new_item.binary_light_viewable !== null) {
126
+ itemData.binary_light_viewable = new_item.binary_light_viewable;
127
+ }
98
128
  return data_db.put(itemData);
99
129
  }
100
130
 
@@ -213,6 +243,7 @@ export const useDataStore = defineStore("data", () => {
213
243
  item,
214
244
  allItems,
215
245
  refItem,
246
+ isItemViewable,
216
247
  meshComponentType,
217
248
  registerObject,
218
249
  deregisterObject,
@@ -21,6 +21,10 @@ export const useDataStyleStore = defineStore("dataStyle", () => {
21
21
 
22
22
  async function setVisibility(id, visibility) {
23
23
  const item = await dataStore.item(id);
24
+ if (!(await dataStore.isItemViewable(item))) {
25
+ return dataStyleState.mutateStyle(id, { visibility });
26
+ }
27
+
24
28
  const { viewer_type } = item;
25
29
 
26
30
  if (viewer_type === "mesh") {
@@ -34,6 +38,10 @@ export const useDataStyleStore = defineStore("dataStyle", () => {
34
38
 
35
39
  async function applyDefaultStyle(id) {
36
40
  const item = await dataStore.item(id);
41
+ if (!(await dataStore.isItemViewable(item))) {
42
+ throw new Error(`applyDefaultStyle called for non-viewable item: ${id}`);
43
+ }
44
+
37
45
  const { viewer_type } = item;
38
46
 
39
47
  if (viewer_type === "mesh") {
@@ -81,6 +89,9 @@ export const useDataStyleStore = defineStore("dataStyle", () => {
81
89
  const ids = Object.keys(dataStyleState.styles.value);
82
90
  const promises = ids.map(async (id) => {
83
91
  const meta = await dataStore.item(id);
92
+ if (!(await dataStore.isItemViewable(meta))) {
93
+ return;
94
+ }
84
95
  const viewerType = meta.viewer_type;
85
96
  if (viewerType === "mesh") {
86
97
  return meshStyleStore.applyMeshStyle(id);
@@ -12,7 +12,10 @@ import {
12
12
  performSyncRemoteCamera,
13
13
  } from "@ogw_internal/stores/hybrid_viewer_camera";
14
14
  import {
15
- computeAverageBrightness,
15
+ createClearHoverData,
16
+ createHoverHighlight,
17
+ } from "@ogw_internal/stores/hybrid_viewer_highlight";
18
+ import {
16
19
  performAddItem,
17
20
  performClear,
18
21
  performClearHoverHighlight,
@@ -22,10 +25,7 @@ import {
22
25
  performSetVisibility,
23
26
  performSetZScaling,
24
27
  } from "@ogw_internal/stores/hybrid_viewer";
25
- import {
26
- createClearHoverData,
27
- createHoverHighlight,
28
- } from "@ogw_internal/stores/hybrid_viewer_highlight";
28
+ import { computeAverageBrightness } from "@ogw_internal/stores/hybrid_viewer_brightness";
29
29
  import { newInstance as vtkActor } from "@kitware/vtk.js/Rendering/Core/Actor";
30
30
  import { newInstance as vtkGenericRenderWindow } from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
31
31
  import { newInstance as vtkMapper } from "@kitware/vtk.js/Rendering/Core/Mapper";
@@ -61,14 +61,19 @@ async function importItem(item) {
61
61
  item.viewer_type === "model" ? dataStore.addComponentRelations(item) : Promise.resolve();
62
62
  treeviewStore.addItem(item.geode_object_type, item.name, item.id, item.viewer_type);
63
63
  const addDataStyleTask = dataStyleStore.addDataStyle(item.id, item.geode_object_type);
64
- const addViewerTask = addDataTask.then(() => {
65
- if (item.nb_vertices === 0) {
64
+ const addViewerTask = addDataTask.then(async () => {
65
+ if (!(await dataStore.isItemViewable(item))) {
66
66
  return;
67
67
  }
68
68
  return hybridViewerStore.addItem(item.id);
69
69
  });
70
70
  const applyStyleTask = Promise.all([registerTask, addDataComponentsTask, addDataStyleTask]).then(
71
- () => dataStyleStore.applyDefaultStyle(item.id),
71
+ async () => {
72
+ if (await dataStore.isItemViewable(item)) {
73
+ return dataStyleStore.applyDefaultStyle(item.id);
74
+ }
75
+ return undefined;
76
+ },
72
77
  );
73
78
  await Promise.all([
74
79
  registerTask,
@@ -1,65 +1,5 @@
1
- import { BACKGROUND_GREY_VALUE, RGB_MAX } from "./hybrid_viewer_constants";
2
1
  import { centerCameraOnPosition } from "./hybrid_viewer_camera";
3
2
 
4
- const RGBA_CHANNELS = 4;
5
- const SAMPLE_SIZE = 10;
6
- const TOTAL_CHANNELS = 400;
7
-
8
- function mapRect(rect, latestImage, canvasRect) {
9
- const scaleX = latestImage.width / canvasRect.width;
10
- const scaleY = latestImage.height / canvasRect.height;
11
- return {
12
- relX: (rect.x - canvasRect.left) * scaleX,
13
- relY: (rect.y - canvasRect.top) * scaleY,
14
- relW: rect.width * scaleX,
15
- relH: rect.height * scaleY,
16
- };
17
- }
18
-
19
- function computeAverageBrightness(rect, options) {
20
- const { latestImage, offscreenCtx, offscreenCanvas, genericRenderWindow } = options;
21
- if (!latestImage || !offscreenCtx || !offscreenCanvas || !genericRenderWindow) {
22
- return BACKGROUND_GREY_VALUE / RGB_MAX;
23
- }
24
- const canvas = genericRenderWindow.getApiSpecificRenderWindow().getCanvas();
25
- if (!canvas) {
26
- return BACKGROUND_GREY_VALUE / RGB_MAX;
27
- }
28
- if (rect.width <= 0 || rect.height <= 0) {
29
- return BACKGROUND_GREY_VALUE / RGB_MAX;
30
- }
31
- const { relX, relY, relW, relH } = mapRect(rect, latestImage, canvas.getBoundingClientRect());
32
- if (relW <= 0 || relH <= 0) {
33
- return BACKGROUND_GREY_VALUE / RGB_MAX;
34
- }
35
- offscreenCanvas.width = SAMPLE_SIZE;
36
- offscreenCanvas.height = SAMPLE_SIZE;
37
- try {
38
- offscreenCtx.drawImage(
39
- latestImage,
40
- Math.max(0, relX),
41
- Math.max(0, relY),
42
- Math.min(latestImage.width, relW),
43
- Math.min(latestImage.height, relH),
44
- 0,
45
- 0,
46
- SAMPLE_SIZE,
47
- SAMPLE_SIZE,
48
- );
49
- const { data } = offscreenCtx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
50
- let minBrightness = 1;
51
- for (let i = 0; i < TOTAL_CHANNELS; i += RGBA_CHANNELS) {
52
- const brightness = (data[i] + data[i + 1] + data[i + 2]) / (3 * RGB_MAX);
53
- if (brightness < minBrightness) {
54
- minBrightness = brightness;
55
- }
56
- }
57
- return minBrightness;
58
- } catch {
59
- return BACKGROUND_GREY_VALUE / RGB_MAX;
60
- }
61
- }
62
-
63
3
  function performClickPicking(event, options) {
64
4
  const { container, viewerStore, viewer_schemas, genericRenderWindow, syncRemoteCamera } = options;
65
5
  const rect = container.getBoundingClientRect();
@@ -148,8 +88,11 @@ async function performAddItem(id, options) {
148
88
  if (!genericRenderWindow) {
149
89
  return;
150
90
  }
151
- const reader = vtkXMLPolyDataReader(),
152
- value = await dataStore.item(id);
91
+ const value = await dataStore.item(id);
92
+ if (value && !dataStore.isItemViewable(value)) {
93
+ return;
94
+ }
95
+ const reader = vtkXMLPolyDataReader();
153
96
  await reader.parseAsArrayBuffer(new TextEncoder().encode(value.binary_light_viewable));
154
97
  const actor = vtkActor(),
155
98
  mapper = vtkMapper(),
@@ -158,6 +101,9 @@ async function performAddItem(id, options) {
158
101
  actor.getProperty().setColor(actorColor);
159
102
  actor.setMapper(mapper);
160
103
  const renderer = genericRenderWindow.getRenderer();
104
+ if (hybridDb[id] && hybridDb[id].actor) {
105
+ renderer.removeActor(hybridDb[id].actor);
106
+ }
161
107
  const isFirst = renderer.getActors().length === 0;
162
108
  renderer.addActor(actor);
163
109
  if (isFirst) {
@@ -307,7 +253,6 @@ function performClear(options) {
307
253
  }
308
254
 
309
255
  export {
310
- computeAverageBrightness,
311
256
  performAddItem,
312
257
  performClearHoverHighlight,
313
258
  performClickPicking,
@@ -0,0 +1,62 @@
1
+ import { BACKGROUND_GREY_VALUE, RGB_MAX } from "./hybrid_viewer_constants";
2
+
3
+ const RGBA_CHANNELS = 4;
4
+ const SAMPLE_SIZE = 10;
5
+ const TOTAL_CHANNELS = 400;
6
+
7
+ function mapRect(rect, latestImage, canvasRect) {
8
+ const scaleX = latestImage.width / canvasRect.width;
9
+ const scaleY = latestImage.height / canvasRect.height;
10
+ return {
11
+ relX: (rect.x - canvasRect.left) * scaleX,
12
+ relY: (rect.y - canvasRect.top) * scaleY,
13
+ relW: rect.width * scaleX,
14
+ relH: rect.height * scaleY,
15
+ };
16
+ }
17
+
18
+ function computeAverageBrightness(rect, options) {
19
+ const { latestImage, offscreenCtx, offscreenCanvas, genericRenderWindow } = options;
20
+ if (!latestImage || !offscreenCtx || !offscreenCanvas || !genericRenderWindow) {
21
+ return BACKGROUND_GREY_VALUE / RGB_MAX;
22
+ }
23
+ const canvas = genericRenderWindow.getApiSpecificRenderWindow().getCanvas();
24
+ if (!canvas) {
25
+ return BACKGROUND_GREY_VALUE / RGB_MAX;
26
+ }
27
+ if (rect.width <= 0 || rect.height <= 0) {
28
+ return BACKGROUND_GREY_VALUE / RGB_MAX;
29
+ }
30
+ const { relX, relY, relW, relH } = mapRect(rect, latestImage, canvas.getBoundingClientRect());
31
+ if (relW <= 0 || relH <= 0) {
32
+ return BACKGROUND_GREY_VALUE / RGB_MAX;
33
+ }
34
+ offscreenCanvas.width = SAMPLE_SIZE;
35
+ offscreenCanvas.height = SAMPLE_SIZE;
36
+ try {
37
+ offscreenCtx.drawImage(
38
+ latestImage,
39
+ Math.max(0, relX),
40
+ Math.max(0, relY),
41
+ Math.min(latestImage.width, relW),
42
+ Math.min(latestImage.height, relH),
43
+ 0,
44
+ 0,
45
+ SAMPLE_SIZE,
46
+ SAMPLE_SIZE,
47
+ );
48
+ const { data } = offscreenCtx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
49
+ let minBrightness = 1;
50
+ for (let i = 0; i < TOTAL_CHANNELS; i += RGBA_CHANNELS) {
51
+ const brightness = (data[i] + data[i + 1] + data[i + 2]) / (3 * RGB_MAX);
52
+ if (brightness < minBrightness) {
53
+ minBrightness = brightness;
54
+ }
55
+ }
56
+ return minBrightness;
57
+ } catch {
58
+ return BACKGROUND_GREY_VALUE / RGB_MAX;
59
+ }
60
+ }
61
+
62
+ export { computeAverageBrightness };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.30.0-rc.3",
3
+ "version": "10.30.0-rc.4",
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": {
@@ -105,6 +105,7 @@ const dataStoreMock = {
105
105
  registerObject: vi.fn().mockResolvedValue(),
106
106
  addItem: vi.fn().mockResolvedValue(),
107
107
  importStores: vi.fn().mockResolvedValue(),
108
+ isItemViewable: vi.fn().mockReturnValue(true),
108
109
  };
109
110
  const dataStyleStoreMock = {
110
111
  importStores: vi.fn().mockResolvedValue(),
@@ -124,4 +124,39 @@ describe("useDataStore - collections", () => {
124
124
  },
125
125
  ]);
126
126
  });
127
+
128
+ describe("isItemViewable", () => {
129
+ test("correctly identifies viewable and HorizonStack3D items", () => {
130
+ const dataStore = useDataStore();
131
+
132
+ // Standard viewable item (BRep)
133
+ expect(
134
+ dataStore.isItemViewable({ id: "1", title: "BRep 1", geode_object_type: "BRep" }),
135
+ ).toBe(true);
136
+
137
+ // Group node for viewable type
138
+ expect(dataStore.isItemViewable({ id: "BRep", title: "BRep" })).toBe(true);
139
+
140
+ // HorizonStack3D leaf item
141
+ expect(
142
+ dataStore.isItemViewable({
143
+ id: "2",
144
+ title: "HS 1",
145
+ geode_object_type: "HorizonStack3D",
146
+ }),
147
+ ).toBe(false);
148
+
149
+ // HorizonStack3D group node
150
+ expect(dataStore.isItemViewable({ id: "HorizonStack3D", title: "HorizonStack3D" })).toBe(
151
+ false,
152
+ );
153
+ });
154
+
155
+ test("respects explicit is_viewable overrides", () => {
156
+ const dataStore = useDataStore();
157
+
158
+ expect(dataStore.isItemViewable({ is_viewable: false })).toBe(false);
159
+ expect(dataStore.isItemViewable({ is_viewable: true })).toBe(true);
160
+ });
161
+ });
127
162
  });