diff --git a/app/components/Viewer/ObjectTree/Base/TreeRow.vue b/app/components/Viewer/ObjectTree/Base/TreeRow.vue index f9d24437..f4a2dc70 100644 --- a/app/components/Viewer/ObjectTree/Base/TreeRow.vue +++ b/app/components/Viewer/ObjectTree/Base/TreeRow.vue @@ -1,4 +1,8 @@ diff --git a/app/composables/hover_highlight.js b/app/composables/hover_highlight.js index 9dfe5c8d..2ab11f68 100644 --- a/app/composables/hover_highlight.js +++ b/app/composables/hover_highlight.js @@ -1,3 +1,4 @@ +import { useDataStore } from "@ogw_front/stores/data"; import { useViewerStore } from "@ogw_front/stores/viewer"; import vtk_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json"; @@ -5,6 +6,7 @@ const HOVER_DELAY = 200; export function useHoverhighlight() { const viewerStore = useViewerStore(); + const dataStore = useDataStore(); let timer = undefined; let currentId = undefined; let currentType = undefined; @@ -20,6 +22,11 @@ export function useHoverhighlight() { currentId = id; currentType = type; + const value = await dataStore.item(id); + if (value && !dataStore.isItemViewable(value)) { + return; + } + let block_ids = []; if (typeof block_ids_provider === "function") { block_ids = await block_ids_provider(); diff --git a/app/composables/use_overlapping_picker.js b/app/composables/use_overlapping_picker.js index cf72075f..d92ec6fb 100644 --- a/app/composables/use_overlapping_picker.js +++ b/app/composables/use_overlapping_picker.js @@ -64,8 +64,22 @@ export function useOverlappingPicker() { } async function get_viewer_id({ x, y, containerWidth, containerHeight, containerRect }) { - const activeIds = new Set(dataItems.value.map((item) => item.id)); - const ids = Object.keys(dataStyleStore.styles).filter((styleId) => activeIds.has(styleId)); + const activeIds = new Set(dataItems.value.map((i) => i.id)); + const visibleStyleIds = Object.keys(dataStyleStore.styles).filter( + (styleId) => activeIds.has(styleId) && dataStyleStore.objectVisibility(styleId), + ); + + const viewableChecks = await Promise.all( + visibleStyleIds.map(async (styleId) => { + try { + const dataItem = await dataStore.item(styleId); + return dataStore.isItemViewable(dataItem) ? styleId : undefined; + } catch { + return undefined; + } + }), + ); + const ids = viewableChecks.filter(Boolean); const result = { id: undefined, viewer_id: undefined }; let pickedResponse = undefined; diff --git a/app/stores/app.js b/app/stores/app.js index 4051efe6..f6e6c528 100644 --- a/app/stores/app.js +++ b/app/stores/app.js @@ -8,6 +8,23 @@ import { useInfraStore } from "@ogw_front/stores/infra"; // oxlint-disable-next-line max-lines-per-function, max-statements export const useAppStore = defineStore("app", () => { const stores = []; + const globalComponents = ref(new Map()); + + function registerGlobalComponent(extensionId, componentId, component) { + if (!globalComponents.value.has(extensionId)) { + globalComponents.value.set(extensionId, new Map()); + } + globalComponents.value.get(extensionId).set(componentId, component); + console.log( + `[AppStore] Registered global component ${componentId} for extension ${extensionId}`, + ); + } + + function unregisterGlobalComponent(extensionId, componentId) { + if (globalComponents.value.has(extensionId)) { + globalComponents.value.get(extensionId).delete(componentId); + } + } function registerStore(store) { const isAlreadyRegistered = stores.some((registeredStore) => registeredStore.$id === store.$id); @@ -285,5 +302,8 @@ export const useAppStore = defineStore("app", () => { createProjectFolder, start_request, stop_request, + globalComponents, + registerGlobalComponent, + unregisterGlobalComponent, }; }); diff --git a/app/stores/data.js b/app/stores/data.js index c62e3897..fbcdf122 100644 --- a/app/stores/data.js +++ b/app/stores/data.js @@ -11,6 +11,19 @@ import { useViewerStore } from "@ogw_front/stores/viewer"; const viewer_generic_schemas = viewer_schemas.opengeodeweb_viewer.generic; +function isItemViewable(item) { + if (!item || typeof item !== "object") { + return false; + } + if (item.is_viewable !== undefined) { + return Boolean(item.is_viewable); + } + if (item.binary_light_viewable !== undefined) { + return item.binary_light_viewable !== "not_viewable"; + } + return true; +} + // oxlint-disable-next-line max-lines-per-function, max-statements export const useDataStore = defineStore("data", () => { const viewerStore = useViewerStore(); @@ -93,8 +106,11 @@ export const useDataStore = defineStore("data", () => { geode_object_type: new_item.geode_object_type, visible: true, created_at: new Date().toISOString(), - binary_light_viewable: new_item.binary_light_viewable, + is_viewable: new_item.is_viewable, }; + if (new_item.binary_light_viewable !== undefined && new_item.binary_light_viewable !== null) { + itemData.binary_light_viewable = new_item.binary_light_viewable; + } return data_db.put(itemData); } @@ -213,6 +229,7 @@ export const useDataStore = defineStore("data", () => { item, allItems, refItem, + isItemViewable, meshComponentType, registerObject, deregisterObject, diff --git a/app/stores/data_style.js b/app/stores/data_style.js index 5008f565..6f9d02e2 100644 --- a/app/stores/data_style.js +++ b/app/stores/data_style.js @@ -21,6 +21,10 @@ export const useDataStyleStore = defineStore("dataStyle", () => { async function setVisibility(id, visibility) { const item = await dataStore.item(id); + if (!dataStore.isItemViewable(item)) { + return dataStyleState.mutateStyle(id, { visibility }); + } + const { viewer_type } = item; if (viewer_type === "mesh") { @@ -34,6 +38,10 @@ export const useDataStyleStore = defineStore("dataStyle", () => { async function applyDefaultStyle(id) { const item = await dataStore.item(id); + if (!dataStore.isItemViewable(item)) { + throw new Error(`applyDefaultStyle called for non-viewable item: ${id}`); + } + const { viewer_type } = item; if (viewer_type === "mesh") { @@ -81,6 +89,9 @@ export const useDataStyleStore = defineStore("dataStyle", () => { const ids = Object.keys(dataStyleState.styles.value); const promises = ids.map(async (id) => { const meta = await dataStore.item(id); + if (!dataStore.isItemViewable(meta)) { + return; + } const viewerType = meta.viewer_type; if (viewerType === "mesh") { return meshStyleStore.applyMeshStyle(id); diff --git a/app/stores/hybrid_viewer.js b/app/stores/hybrid_viewer.js index eee1fa49..068045d7 100644 --- a/app/stores/hybrid_viewer.js +++ b/app/stores/hybrid_viewer.js @@ -12,7 +12,10 @@ import { performSyncRemoteCamera, } from "@ogw_internal/stores/hybrid_viewer_camera"; import { - computeAverageBrightness, + createClearHoverData, + createHoverHighlight, +} from "@ogw_internal/stores/hybrid_viewer_highlight"; +import { performAddItem, performClear, performClearHoverHighlight, @@ -22,10 +25,7 @@ import { performSetVisibility, performSetZScaling, } from "@ogw_internal/stores/hybrid_viewer"; -import { - createClearHoverData, - createHoverHighlight, -} from "@ogw_internal/stores/hybrid_viewer_highlight"; +import { computeAverageBrightness } from "@ogw_internal/stores/hybrid_viewer_brightness"; import { newInstance as vtkActor } from "@kitware/vtk.js/Rendering/Core/Actor"; import { newInstance as vtkGenericRenderWindow } from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow"; import { newInstance as vtkMapper } from "@kitware/vtk.js/Rendering/Core/Mapper"; diff --git a/app/utils/import_workflow.js b/app/utils/import_workflow.js index 8631091b..9c5dc4e1 100644 --- a/app/utils/import_workflow.js +++ b/app/utils/import_workflow.js @@ -62,13 +62,18 @@ async function importItem(item) { treeviewStore.addItem(item.geode_object_type, item.name, item.id, item.viewer_type); const addDataStyleTask = dataStyleStore.addDataStyle(item.id, item.geode_object_type); const addViewerTask = addDataTask.then(() => { - if (item.nb_vertices === 0) { + if (!dataStore.isItemViewable(item)) { return; } return hybridViewerStore.addItem(item.id); }); const applyStyleTask = Promise.all([registerTask, addDataComponentsTask, addDataStyleTask]).then( - () => dataStyleStore.applyDefaultStyle(item.id), + () => { + if (dataStore.isItemViewable(item)) { + return dataStyleStore.applyDefaultStyle(item.id); + } + return undefined; + }, ); await Promise.all([ registerTask, diff --git a/internal/stores/hybrid_viewer.js b/internal/stores/hybrid_viewer.js index 80b5a85e..d50ccfc7 100644 --- a/internal/stores/hybrid_viewer.js +++ b/internal/stores/hybrid_viewer.js @@ -1,65 +1,5 @@ -import { BACKGROUND_GREY_VALUE, RGB_MAX } from "./hybrid_viewer_constants"; import { centerCameraOnPosition } from "./hybrid_viewer_camera"; -const RGBA_CHANNELS = 4; -const SAMPLE_SIZE = 10; -const TOTAL_CHANNELS = 400; - -function mapRect(rect, latestImage, canvasRect) { - const scaleX = latestImage.width / canvasRect.width; - const scaleY = latestImage.height / canvasRect.height; - return { - relX: (rect.x - canvasRect.left) * scaleX, - relY: (rect.y - canvasRect.top) * scaleY, - relW: rect.width * scaleX, - relH: rect.height * scaleY, - }; -} - -function computeAverageBrightness(rect, options) { - const { latestImage, offscreenCtx, offscreenCanvas, genericRenderWindow } = options; - if (!latestImage || !offscreenCtx || !offscreenCanvas || !genericRenderWindow) { - return BACKGROUND_GREY_VALUE / RGB_MAX; - } - const canvas = genericRenderWindow.getApiSpecificRenderWindow().getCanvas(); - if (!canvas) { - return BACKGROUND_GREY_VALUE / RGB_MAX; - } - if (rect.width <= 0 || rect.height <= 0) { - return BACKGROUND_GREY_VALUE / RGB_MAX; - } - const { relX, relY, relW, relH } = mapRect(rect, latestImage, canvas.getBoundingClientRect()); - if (relW <= 0 || relH <= 0) { - return BACKGROUND_GREY_VALUE / RGB_MAX; - } - offscreenCanvas.width = SAMPLE_SIZE; - offscreenCanvas.height = SAMPLE_SIZE; - try { - offscreenCtx.drawImage( - latestImage, - Math.max(0, relX), - Math.max(0, relY), - Math.min(latestImage.width, relW), - Math.min(latestImage.height, relH), - 0, - 0, - SAMPLE_SIZE, - SAMPLE_SIZE, - ); - const { data } = offscreenCtx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE); - let minBrightness = 1; - for (let i = 0; i < TOTAL_CHANNELS; i += RGBA_CHANNELS) { - const brightness = (data[i] + data[i + 1] + data[i + 2]) / (3 * RGB_MAX); - if (brightness < minBrightness) { - minBrightness = brightness; - } - } - return minBrightness; - } catch { - return BACKGROUND_GREY_VALUE / RGB_MAX; - } -} - function performClickPicking(event, options) { const { container, viewerStore, viewer_schemas, genericRenderWindow, syncRemoteCamera } = options; const rect = container.getBoundingClientRect(); @@ -148,8 +88,11 @@ async function performAddItem(id, options) { if (!genericRenderWindow) { return; } - const reader = vtkXMLPolyDataReader(), - value = await dataStore.item(id); + const value = await dataStore.item(id); + if (value && !dataStore.isItemViewable(value)) { + return; + } + const reader = vtkXMLPolyDataReader(); await reader.parseAsArrayBuffer(new TextEncoder().encode(value.binary_light_viewable)); const actor = vtkActor(), mapper = vtkMapper(), @@ -158,6 +101,9 @@ async function performAddItem(id, options) { actor.getProperty().setColor(actorColor); actor.setMapper(mapper); const renderer = genericRenderWindow.getRenderer(); + if (hybridDb[id] && hybridDb[id].actor) { + renderer.removeActor(hybridDb[id].actor); + } const isFirst = renderer.getActors().length === 0; renderer.addActor(actor); if (isFirst) { @@ -307,7 +253,6 @@ function performClear(options) { } export { - computeAverageBrightness, performAddItem, performClearHoverHighlight, performClickPicking, diff --git a/internal/stores/hybrid_viewer_brightness.js b/internal/stores/hybrid_viewer_brightness.js new file mode 100644 index 00000000..b4da1328 --- /dev/null +++ b/internal/stores/hybrid_viewer_brightness.js @@ -0,0 +1,62 @@ +import { BACKGROUND_GREY_VALUE, RGB_MAX } from "./hybrid_viewer_constants"; + +const RGBA_CHANNELS = 4; +const SAMPLE_SIZE = 10; +const TOTAL_CHANNELS = 400; + +function mapRect(rect, latestImage, canvasRect) { + const scaleX = latestImage.width / canvasRect.width; + const scaleY = latestImage.height / canvasRect.height; + return { + relX: (rect.x - canvasRect.left) * scaleX, + relY: (rect.y - canvasRect.top) * scaleY, + relW: rect.width * scaleX, + relH: rect.height * scaleY, + }; +} + +function computeAverageBrightness(rect, options) { + const { latestImage, offscreenCtx, offscreenCanvas, genericRenderWindow } = options; + if (!latestImage || !offscreenCtx || !offscreenCanvas || !genericRenderWindow) { + return BACKGROUND_GREY_VALUE / RGB_MAX; + } + const canvas = genericRenderWindow.getApiSpecificRenderWindow().getCanvas(); + if (!canvas) { + return BACKGROUND_GREY_VALUE / RGB_MAX; + } + if (rect.width <= 0 || rect.height <= 0) { + return BACKGROUND_GREY_VALUE / RGB_MAX; + } + const { relX, relY, relW, relH } = mapRect(rect, latestImage, canvas.getBoundingClientRect()); + if (relW <= 0 || relH <= 0) { + return BACKGROUND_GREY_VALUE / RGB_MAX; + } + offscreenCanvas.width = SAMPLE_SIZE; + offscreenCanvas.height = SAMPLE_SIZE; + try { + offscreenCtx.drawImage( + latestImage, + Math.max(0, relX), + Math.max(0, relY), + Math.min(latestImage.width, relW), + Math.min(latestImage.height, relH), + 0, + 0, + SAMPLE_SIZE, + SAMPLE_SIZE, + ); + const { data } = offscreenCtx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE); + let minBrightness = 1; + for (let i = 0; i < TOTAL_CHANNELS; i += RGBA_CHANNELS) { + const brightness = (data[i] + data[i + 1] + data[i + 2]) / (3 * RGB_MAX); + if (brightness < minBrightness) { + minBrightness = brightness; + } + } + return minBrightness; + } catch { + return BACKGROUND_GREY_VALUE / RGB_MAX; + } +} + +export { computeAverageBrightness }; diff --git a/tests/unit/composables/project_manager.nuxt.test.js b/tests/unit/composables/project_manager.nuxt.test.js index c35b5a7c..57f1736f 100644 --- a/tests/unit/composables/project_manager.nuxt.test.js +++ b/tests/unit/composables/project_manager.nuxt.test.js @@ -105,6 +105,7 @@ const dataStoreMock = { registerObject: vi.fn().mockResolvedValue(), addItem: vi.fn().mockResolvedValue(), importStores: vi.fn().mockResolvedValue(), + isItemViewable: vi.fn().mockReturnValue(true), }; const dataStyleStoreMock = { importStores: vi.fn().mockResolvedValue(),