diff --git a/components/VeaseViewToolbar.vue b/components/VeaseViewToolbar.vue index 0636e8b6..bbc18bc8 100644 --- a/components/VeaseViewToolbar.vue +++ b/components/VeaseViewToolbar.vue @@ -81,6 +81,7 @@ { response_function: () => { grid_scale.value = !grid_scale.value + hybridViewerStore.remoteRender() }, }, ) diff --git a/composables/project_manager.js b/composables/project_manager.js new file mode 100644 index 00000000..00060fbb --- /dev/null +++ b/composables/project_manager.js @@ -0,0 +1,146 @@ +import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json" +import fileDownload from "js-file-download" +import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json" +import { once } from "lodash" + +export function useProjectManager() { + const geode = useGeodeStore() + const appStore = useAppStore() + + const exportProject = function () { + geode.start_request() + const infraStore = useInfraStore() + const snapshot = appStore.exportStores() + const schema = back_schemas.opengeodeweb_back.export_project + const defaultName = "project.zip" + + return infraStore + .create_connection() + .then(function () { + return api_fetch( + { schema, params: { snapshot, filename: defaultName } }, + { + response_function: once(function (response) { + const data = response._data + const name = + (response.headers && + typeof response.headers.get === "function" && + response.headers.get("new-file-name")) || + defaultName + fileDownload(data, name) + }), + }, + ) + }) + .finally(function () { + geode.stop_request() + }) + } + + const importProjectFile = function (file) { + geode.start_request() + + const viewerStore = useViewerStore() + const dataBaseStore = useDataBaseStore() + const treeviewStore = useTreeviewStore() + const hybridViewerStore = useHybridViewerStore() + const infraStore = useInfraStore() + + return infraStore + .create_connection() + .then(function () { + return viewerStore.ws_connect() + }) + .then(function () { + return viewer_call({ + schema: viewer_schemas.opengeodeweb_viewer.import_project, + params: {}, + }) + }) + .then(function () { + return viewer_call({ + schema: viewer_schemas.opengeodeweb_viewer.viewer.reset_visualization, + params: {}, + }) + }) + .then(function () { + treeviewStore.clear() + dataBaseStore.clear() + hybridViewerStore.clear() + + const schemaImport = back_schemas.opengeodeweb_back.import_project + const form = new FormData() + const fileName = file && file.name ? file.name : "file" + form.append("file", file, fileName) + + return $fetch(schemaImport.$id, { + baseURL: geode.base_url, + method: "POST", + body: form, + }) + }) + .then(function (result) { + const snapshot = result && result.snapshot ? result.snapshot : {} + + treeviewStore.isImporting = true + + return Promise.resolve() + .then(function () { + return treeviewStore.importStores(snapshot.treeview) + }) + .then(function () { + return hybridViewerStore.initHybridViewer() + }) + .then(function () { + return hybridViewerStore.importStores(snapshot.hybridViewer) + }) + .then(function () { + const snapshotDataBase = + snapshot && snapshot.dataBase && snapshot.dataBase.db + ? snapshot.dataBase.db + : {} + const items = Object.entries(snapshotDataBase).map(function (pair) { + const id = pair[0] + const item = pair[1] + const binaryLightViewable = + item && item.vtk_js && item.vtk_js.binary_light_viewable + ? item.vtk_js.binary_light_viewable + : undefined + return { + id: id, + object_type: item.object_type, + geode_object: item.geode_object, + native_filename: item.native_filename, + viewable_filename: item.viewable_filename, + displayed_name: item.displayed_name, + vtk_js: { binary_light_viewable: binaryLightViewable }, + } + }) + + return importWorkflowFromSnapshot(items) + }) + .then(function () { + return hybridViewerStore.importStores(snapshot.hybridViewer) + }) + .then(function () { + const dataStyleStore = useDataStyleStore() + return dataStyleStore.importStores(snapshot.dataStyle) + }) + .then(function () { + const dataStyleStore = useDataStyleStore() + return dataStyleStore.applyAllStylesFromState() + }) + .then(function () { + treeviewStore.finalizeImportSelection() + treeviewStore.isImporting = false + }) + }) + .finally(function () { + geode.stop_request() + }) + } + + return { exportProject, importProjectFile } +} + +export default useProjectManager diff --git a/composables/viewer_call.js b/composables/viewer_call.js index 6a91d36a..6b5bc836 100644 --- a/composables/viewer_call.js +++ b/composables/viewer_call.js @@ -18,8 +18,9 @@ export function viewer_call( const client = viewer_store.client return new Promise((resolve, reject) => { - if (!client) { - reject() + if (!client.getConnection) { + resolve() + return } viewer_store.start_request() client @@ -37,7 +38,7 @@ export function viewer_call( if (request_error_function) { request_error_function(reason) } - reject() + reject(reason) }, ) .catch((error) => { @@ -50,7 +51,7 @@ export function viewer_call( if (response_error_function) { response_error_function(error) } - reject() + reject(error) }) .finally(() => { viewer_store.stop_request() diff --git a/plugins/autoStoreRegister.js b/plugins/auto_store_register.js similarity index 100% rename from plugins/autoStoreRegister.js rename to plugins/auto_store_register.js diff --git a/stores/app_store.js b/stores/app.js similarity index 55% rename from stores/app_store.js rename to stores/app.js index fde1a59a..4fb94817 100644 --- a/stores/app_store.js +++ b/stores/app.js @@ -5,81 +5,72 @@ export const useAppStore = defineStore("app", () => { const isAlreadyRegistered = stores.some( (registeredStore) => registeredStore.$id === store.$id, ) - if (isAlreadyRegistered) { console.log( `[AppStore] Store "${store.$id}" already registered, skipping`, ) return } - console.log("[AppStore] Registering store", store.$id) stores.push(store) } - function save() { + function exportStores() { const snapshot = {} - let savedCount = 0 + let exportCount = 0 for (const store of stores) { - if (!store.save) { - continue - } + if (!store.exportStores) continue const storeId = store.$id try { - snapshot[storeId] = store.save() - savedCount++ + snapshot[storeId] = store.exportStores() + exportCount++ } catch (error) { - console.error(`[AppStore] Error saving store "${storeId}":`, error) + console.error(`[AppStore] Error exporting store "${storeId}":`, error) } } - - console.log(`[AppStore] Saved ${savedCount} stores`) + console.log( + `[AppStore] Exported ${exportCount} stores; snapshot keys:`, + Object.keys(snapshot), + ) return snapshot } - function load(snapshot) { + async function importStores(snapshot) { if (!snapshot) { - console.warn("[AppStore] load called with invalid snapshot") + console.warn("[AppStore] import called with invalid snapshot") return } + console.log("[AppStore] Import snapshot keys:", Object.keys(snapshot || {})) - let loadedCount = 0 + let importedCount = 0 const notFoundStores = [] - for (const store of stores) { - if (!store.load) { - continue - } - + if (!store.importStores) continue const storeId = store.$id - if (!snapshot[storeId]) { notFoundStores.push(storeId) continue } - try { - store.load(snapshot[storeId]) - loadedCount++ + await store.importStores(snapshot[storeId]) + importedCount++ } catch (error) { - console.error(`[AppStore] Error loading store "${storeId}":`, error) + console.error(`[AppStore] Error importing store "${storeId}":`, error) } } - if (notFoundStores.length > 0) { console.warn( `[AppStore] Stores not found in snapshot: ${notFoundStores.join(", ")}`, ) } - - console.log(`[AppStore] Loaded ${loadedCount} stores`) + console.log(`[AppStore] Imported ${importedCount} stores`) } return { stores, registerStore, - save, - load, + exportStores, + importStores, } }) diff --git a/stores/data_base.js b/stores/data_base.js index cc4668d8..7db27343 100644 --- a/stores/data_base.js +++ b/stores/data_base.js @@ -120,6 +120,43 @@ export const useDataBaseStore = defineStore("dataBase", () => { return flat_indexes } + function exportStores() { + const snapshotDb = {} + for (const [id, item] of Object.entries(db)) { + if (!item) continue + snapshotDb[id] = { + object_type: item.object_type, + geode_object: item.geode_object, + native_filename: item.native_filename, + viewable_filename: item.viewable_filename, + displayed_name: item.displayed_name, + vtk_js: { + binary_light_viewable: item?.vtk_js?.binary_light_viewable, + }, + } + } + return { db: snapshotDb } + } + + async function importStores(snapshot) { + await hybridViewerStore.initHybridViewer() + hybridViewerStore.clear() + console.log( + "[DataBase] importStores entries:", + Object.keys(snapshot?.db || {}), + ) + for (const [id, item] of Object.entries(snapshot?.db || {})) { + await registerObject(id) + await addItem(id, item) + } + } + + function clear() { + for (const id of Object.keys(db)) { + delete db[id] + } + } + return { db, itemMetaDatas, @@ -134,5 +171,8 @@ export const useDataBaseStore = defineStore("dataBase", () => { getSurfacesUuids, getBlocksUuids, getFlatIndexes, + exportStores, + importStores, + clear, } }) diff --git a/stores/data_style.js b/stores/data_style.js index 994f306b..d1a91295 100644 --- a/stores/data_style.js +++ b/stores/data_style.js @@ -1,3 +1,4 @@ +import { getDefaultStyle } from "../utils/default_styles.js" import useDataStyleState from "../internal_stores/data_style_state.js" import useMeshStyle from "../internal_stores/mesh/index.js" import useModelStyle from "../internal_stores/model/index.js" @@ -7,9 +8,11 @@ export const useDataStyleStore = defineStore("dataStyle", () => { const meshStyleStore = useMeshStyle() const modelStyleStore = useModelStyle() const dataBaseStore = useDataBaseStore() + const hybridViewerStore = useHybridViewerStore() function addDataStyle(id, geode_object) { - dataStyleState.styles[id] = getDefaultStyle(geode_object) + const style = getDefaultStyle(geode_object) + dataStyleState.styles[id] = style } function setVisibility(id, visibility) { @@ -37,6 +40,44 @@ export const useDataStyleStore = defineStore("dataStyle", () => { } } + const setModelEdgesVisibility = (id, visibility) => { + return modelStyleStore.setModelEdgesVisibility(id, visibility) + } + + const modelEdgesVisibility = (id) => { + return modelStyleStore.modelEdgesVisibility(id) + } + + const exportStores = () => { + return { styles: dataStyleState.styles } + } + + const importStores = (snapshot) => { + const stylesSnapshot = snapshot.styles || {} + for (const id of Object.keys(dataStyleState.styles)) { + delete dataStyleState.styles[id] + } + for (const [id, style] of Object.entries(stylesSnapshot)) { + dataStyleState.styles[id] = style + } + } + + const applyAllStylesFromState = () => { + const ids = Object.keys(dataStyleState.styles || {}) + const promises = [] + for (const id of ids) { + const meta = dataBaseStore.itemMetaDatas(id) + const objectType = meta?.object_type + const style = dataStyleState.styles[id] + if (style && objectType === "mesh") { + promises.push(meshStyleStore.applyMeshStyle(id)) + } else if (style && objectType === "model") { + promises.push(modelStyleStore.applyModelStyle(id)) + } + } + return Promise.all(promises) + } + return { ...dataStyleState, ...meshStyleStore, @@ -44,5 +85,10 @@ export const useDataStyleStore = defineStore("dataStyle", () => { addDataStyle, applyDefaultStyle, setVisibility, + setModelEdgesVisibility, + modelEdgesVisibility, + exportStores, + importStores, + applyAllStylesFromState, } }) diff --git a/stores/hybrid_viewer.js b/stores/hybrid_viewer.js index 56cb35ee..b31f3975 100644 --- a/stores/hybrid_viewer.js +++ b/stores/hybrid_viewer.js @@ -97,6 +97,7 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => { z_scale: z_scale, }, }) + remoteRender() } function syncRemoteCamera() { @@ -198,6 +199,83 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => { remoteRender() } + const clear = () => { + const renderer = genericRenderWindow.value.getRenderer() + const actors = renderer.getActors() + for (const actor of actors) { + renderer.removeActor(actor) + } + genericRenderWindow.value.getRenderWindow().render() + for (const id of Object.keys(db)) { + delete db[id] + } + } + + const exportStores = () => { + const renderer = genericRenderWindow.value.getRenderer() + const camera = renderer.getActiveCamera() + const cameraSnapshot = camera + ? { + focal_point: [...camera.getFocalPoint()], + view_up: [...camera.getViewUp()], + position: [...camera.getPosition()], + view_angle: camera.getViewAngle(), + clipping_range: [...camera.getClippingRange()], + distance: camera.getDistance(), + } + : camera_options + return { zScale: zScale.value, camera_options: cameraSnapshot } + } + + const importStores = (snapshot) => { + const z_scale = snapshot.zScale + + const applyCamera = () => { + const { camera_options } = snapshot + if (!camera_options) { + return + } + + const renderer = genericRenderWindow.value.getRenderer() + const camera = renderer.getActiveCamera() + + camera.setFocalPoint(...camera_options.focal_point) + camera.setViewUp(...camera_options.view_up) + camera.setPosition(...camera_options.position) + camera.setViewAngle(camera_options.view_angle) + camera.setClippingRange(...camera_options.clipping_range) + + genericRenderWindow.value.getRenderWindow().render() + + const payload = { + camera_options: { + focal_point: camera_options.focal_point, + view_up: camera_options.view_up, + position: camera_options.position, + view_angle: camera_options.view_angle, + clipping_range: camera_options.clipping_range, + }, + } + return viewer_call( + { + schema: viewer_schemas.opengeodeweb_viewer.viewer.update_camera, + params: payload, + }, + { + response_function: () => { + remoteRender() + Object.assign(camera_options, payload.camera_options) + }, + }, + ) + } + + if (typeof z_scale === "number") { + return setZScaling(z_scale).then(() => applyCamera()) + } + return applyCamera() + } + return { db, genericRenderWindow, @@ -210,5 +288,8 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => { resize, setContainer, zScale, + clear, + exportStores, + importStores, } }) diff --git a/stores/treeview.js b/stores/treeview.js index f9a89caf..20512c50 100644 --- a/stores/treeview.js +++ b/stores/treeview.js @@ -1,7 +1,4 @@ export const useTreeviewStore = defineStore("treeview", () => { - const dataStyleStore = useDataStyleStore() - const dataBaseStore = useDataBaseStore() - const items = ref([]) const selection = ref([]) const components_selection = ref([]) @@ -10,6 +7,8 @@ export const useTreeviewStore = defineStore("treeview", () => { const model_id = ref("") const isTreeCollection = ref(false) const selectedTree = ref(null) + const isImporting = ref(false) + const pendingSelectionIds = ref([]) // /** Functions **/ function addItem(geodeObject, displayed_name, id, object_type) { @@ -24,12 +23,16 @@ export const useTreeviewStore = defineStore("treeview", () => { sensitivity: "base", }), ) - selection.value.push(child) + if (!isImporting.value) { + selection.value.push(child) + } return } } items.value.push({ title: geodeObject, children: [child] }) - selection.value.push(child) + if (!isImporting.value) { + selection.value.push(child) + } } function displayAdditionalTree(id) { @@ -53,6 +56,62 @@ export const useTreeviewStore = defineStore("treeview", () => { panelWidth.value = width } + function exportStores() { + return { + isAdditionnalTreeDisplayed: isAdditionnalTreeDisplayed.value, + panelWidth: panelWidth.value, + model_id: model_id.value, + isTreeCollection: isTreeCollection.value, + selectedTree: selectedTree.value, + selectionIds: selection.value.map((c) => c.id), + } + } + + async function importStores(snapshot) { + isAdditionnalTreeDisplayed.value = + snapshot?.isAdditionnalTreeDisplayed || false + panelWidth.value = snapshot?.panelWidth || 300 + model_id.value = snapshot?.model_id || "" + isTreeCollection.value = snapshot?.isTreeCollection || false + selectedTree.value = snapshot?.selectedTree || null + + pendingSelectionIds.value = + snapshot?.selectionIds || + (snapshot?.selection || []).map((c) => c.id) || + [] + } + + function finalizeImportSelection() { + const ids = pendingSelectionIds.value || [] + const rebuilt = [] + if (!ids.length) { + for (const group of items.value) { + for (const child of group.children) { + rebuilt.push(child) + } + } + } else { + for (const group of items.value) { + for (const child of group.children) { + if (ids.includes(child.id)) { + rebuilt.push(child) + } + } + } + } + selection.value = rebuilt + pendingSelectionIds.value = [] + } + + const clear = () => { + items.value = [] + selection.value = [] + components_selection.value = [] + pendingSelectionIds.value = [] + model_id.value = "" + selectedTree.value = undefined + } + return { items, selection, @@ -61,10 +120,15 @@ export const useTreeviewStore = defineStore("treeview", () => { panelWidth, model_id, selectedTree, + isImporting, addItem, displayAdditionalTree, displayFileTree, toggleTreeView, setPanelWidth, + exportStores, + importStores, + finalizeImportSelection, + clear, } }) diff --git a/tests/integration/microservices/back/requirements.txt b/tests/integration/microservices/back/requirements.txt index 0c0e8387..bd3a3ef5 100644 --- a/tests/integration/microservices/back/requirements.txt +++ b/tests/integration/microservices/back/requirements.txt @@ -5,4 +5,3 @@ # pip-compile --output-file=tests/integration/microservices/back/requirements.txt tests/integration/microservices/back/requirements.in # -opengeodeweb-back==5.*,>=5.12.0 diff --git a/tests/integration/microservices/viewer/requirements.txt b/tests/integration/microservices/viewer/requirements.txt index 397baec7..4d097394 100644 --- a/tests/integration/microservices/viewer/requirements.txt +++ b/tests/integration/microservices/viewer/requirements.txt @@ -5,4 +5,3 @@ # pip-compile --output-file=tests/integration/microservices/viewer/requirements.txt tests/integration/microservices/viewer/requirements.in # -opengeodeweb-viewer==1.*,>=1.11.9 diff --git a/tests/unit/composables/ProjectManager.nuxt.test.js b/tests/unit/composables/ProjectManager.nuxt.test.js new file mode 100644 index 00000000..318df3a3 --- /dev/null +++ b/tests/unit/composables/ProjectManager.nuxt.test.js @@ -0,0 +1,230 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" +import { setActivePinia } from "pinia" +import { createTestingPinia } from "@pinia/testing" +import { useProjectManager } from "@/composables/project_manager.js" + +// Snapshot +const snapshotMock = { + dataBase: { + db: { + abc123: { + object_type: "mesh", + geode_object: "PointSet2D", + native_filename: "native.ext", + viewable_filename: "viewable.ext", + displayed_name: "My Data", + vtk_js: { binary_light_viewable: "VGxpZ2h0RGF0YQ==" }, + }, + }, + }, + treeview: { + isAdditionnalTreeDisplayed: false, + panelWidth: 320, + model_id: "", + isTreeCollection: false, + selectedTree: null, + selectionIds: [], + }, + dataStyle: { + styles: { + abc123: { + points: { + visibility: true, + coloring: { + active: "color", + color: { r: 255, g: 255, b: 255 }, + vertex: null, + }, + size: 2, + }, + }, + }, + }, + hybridViewer: { + zScale: 1.5, + camera_options: { + focal_point: [1, 2, 3], + view_up: [0, 1, 0], + position: [10, 11, 12], + view_angle: 30, + clipping_range: [0.1, 1000], + }, + }, +} + +const geodeStoreMock = { + start_request: vi.fn(), + stop_request: vi.fn(), + base_url: "", + $reset: vi.fn(), +} +const infraStoreMock = { create_connection: vi.fn(() => Promise.resolve()) } +const viewerStoreMock = { ws_connect: vi.fn(() => Promise.resolve()) } +const treeviewStoreMock = { + clear: vi.fn(), + importStores: vi.fn(() => Promise.resolve()), + finalizeImportSelection: vi.fn(), + addItem: vi.fn(() => Promise.resolve()), +} +const dataBaseStoreMock = { + clear: vi.fn(), + registerObject: vi.fn(() => Promise.resolve()), + addItem: vi.fn(() => Promise.resolve()), +} +const dataStyleStoreMock = { + importStores: vi.fn(() => Promise.resolve()), + applyAllStylesFromState: vi.fn(() => Promise.resolve()), + addDataStyle: vi.fn(() => Promise.resolve()), + applyDefaultStyle: vi.fn(() => Promise.resolve()), +} +const hybridViewerStoreMock = { + clear: vi.fn(), + initHybridViewer: vi.fn(() => Promise.resolve()), + importStores: vi.fn(async (snapshot) => { + if (snapshot?.zScale != null) + hybridViewerStoreMock.setZScaling(snapshot.zScale) + if (snapshot?.camera_options) { + const { viewer_call } = await import("@/composables/viewer_call.js") + viewer_call({ + schema: { $id: "opengeodeweb_viewer/viewer.update_camera" }, + params: { camera_options: snapshot.camera_options }, + }) + hybridViewerStoreMock.remoteRender() + } + }), + addItem: vi.fn(() => Promise.resolve()), + remoteRender: vi.fn(), + setZScaling: vi.fn(), +} + +// Mocks +vi.stubGlobal( + "$fetch", + vi.fn(async () => ({ snapshot: snapshotMock })), +) +vi.mock("@/composables/viewer_call.js", () => ({ + viewer_call: vi.fn(() => Promise.resolve()), +})) +vi.mock("@/composables/api_fetch.js", () => ({ + api_fetch: vi.fn(async (_req, options = {}) => { + const response = { + _data: new Blob(["zipcontent"], { type: "application/zip" }), + headers: { + get: (k) => (k === "new-file-name" ? "project_123.zip" : null), + }, + } + if (options.response_function) await options.response_function(response) + return response + }), +})) +vi.mock("js-file-download", () => ({ default: vi.fn() })) +vi.mock("@/stores/infra.js", () => ({ useInfraStore: () => infraStoreMock })) +vi.mock("@/stores/viewer.js", () => ({ useViewerStore: () => viewerStoreMock })) +vi.mock("@/stores/treeview.js", () => ({ + useTreeviewStore: () => treeviewStoreMock, +})) +vi.mock("@/stores/data_base.js", () => ({ + useDataBaseStore: () => dataBaseStoreMock, +})) +vi.mock("@/stores/data_style.js", () => ({ + useDataStyleStore: () => dataStyleStoreMock, +})) +vi.mock("@/stores/hybrid_viewer.js", () => ({ + useHybridViewerStore: () => hybridViewerStoreMock, +})) +vi.mock("@/stores/geode.js", () => ({ useGeodeStore: () => geodeStoreMock })) +vi.mock("@/stores/app.js", () => ({ + useAppStore: () => ({ + exportStores: vi.fn(() => ({ projectName: "mockedProject" })), + }), +})) + +vi.stubGlobal("useAppStore", () => ({ + exportStores: vi.fn(() => ({ projectName: "mockedProject" })), +})) + +describe("ProjectManager composable (compact)", () => { + beforeEach(async () => { + const pinia = createTestingPinia({ stubActions: false, createSpy: vi.fn }) + setActivePinia(pinia) + + // reset spies + for (const store of [ + infraStoreMock, + viewerStoreMock, + treeviewStoreMock, + dataBaseStoreMock, + dataStyleStoreMock, + hybridViewerStoreMock, + ]) { + Object.values(store).forEach( + (v) => typeof v === "function" && v.mockClear && v.mockClear(), + ) + } + const { viewer_call } = await import("@/composables/viewer_call.js") + viewer_call.mockClear() + }) + + test("exportProject", async () => { + const { exportProject } = useProjectManager() + const { default: fileDownload } = await import("js-file-download") + + await exportProject() + + expect(infraStoreMock.create_connection).toHaveBeenCalled() + expect(fileDownload).toHaveBeenCalled() + }) + + test("importProjectFile with snapshot", async () => { + const { importProjectFile } = useProjectManager() + const file = new Blob(['{"dataBase":{"db":{}}}'], { + type: "application/json", + }) + + await importProjectFile(file) + + const { viewer_call } = await import("@/composables/viewer_call.js") + + expect(infraStoreMock.create_connection).toHaveBeenCalled() + expect(viewerStoreMock.ws_connect).toHaveBeenCalled() + expect(viewer_call).toHaveBeenCalledTimes(4) + + expect(treeviewStoreMock.importStores).toHaveBeenCalledWith( + snapshotMock.treeview, + ) + expect(hybridViewerStoreMock.initHybridViewer).toHaveBeenCalled() + expect(hybridViewerStoreMock.importStores).toHaveBeenCalledWith( + snapshotMock.hybridViewer, + ) + expect(hybridViewerStoreMock.setZScaling).toHaveBeenCalledWith(1.5) + + expect(dataStyleStoreMock.importStores).toHaveBeenCalledWith( + snapshotMock.dataStyle, + ) + expect(dataStyleStoreMock.applyAllStylesFromState).toHaveBeenCalled() + + expect(dataBaseStoreMock.registerObject).toHaveBeenCalledWith("abc123") + expect(dataBaseStoreMock.addItem).toHaveBeenCalledWith( + "abc123", + expect.objectContaining({ + object_type: "mesh", + geode_object: "PointSet2D", + displayed_name: "My Data", + }), + ) + expect(treeviewStoreMock.addItem).toHaveBeenCalledWith( + "PointSet2D", + "My Data", + "abc123", + "mesh", + ) + expect(hybridViewerStoreMock.addItem).toHaveBeenCalledWith("abc123") + expect(dataStyleStoreMock.addDataStyle).toHaveBeenCalledWith( + "abc123", + "PointSet2D", + ) + expect(dataStyleStoreMock.applyDefaultStyle).toHaveBeenCalledWith("abc123") + + expect(hybridViewerStoreMock.remoteRender).toHaveBeenCalled() + }) +}) diff --git a/tests/unit/plugins/project_load.nuxt.test.js b/tests/unit/plugins/project_load.nuxt.test.js new file mode 100644 index 00000000..69142652 --- /dev/null +++ b/tests/unit/plugins/project_load.nuxt.test.js @@ -0,0 +1,75 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" +import { createTestingPinia } from "@pinia/testing" +import { setActivePinia } from "pinia" + +vi.mock("@/composables/viewer_call.js", () => ({ + default: vi.fn(() => Promise.resolve({})), + viewer_call: vi.fn(() => Promise.resolve({})), +})) +vi.mock("@/stores/hybrid_viewer.js", () => ({ + useHybridViewerStore: () => ({ + $id: "hybridViewer", + initHybridViewer: vi.fn(), + clear: vi.fn(), + addItem: vi.fn(), + setZScaling: vi.fn(), + save: vi.fn(), + load: vi.fn(), + }), +})) + +beforeEach(() => { + setActivePinia( + createTestingPinia({ + stubActions: false, + createSpy: vi.fn, + }), + ) +}) + +describe("Project import", () => { + test("app.importStores restores stores", async () => { + const stores = { + app: useAppStore(), + dataBase: useDataBaseStore(), + treeview: useTreeviewStore(), + dataStyle: useDataStyleStore(), + hybrid: useHybridViewerStore(), + } + Object.values(stores) + .slice(1) + .forEach((store) => stores.app.registerStore(store)) + + const snapshot = { + dataBase: { + db: { + abc123: { + object_type: "mesh", + geode_object: "PointSet2D", + native_filename: "native.ext", + viewable_filename: "viewable.ext", + displayed_name: "My Data", + vtk_js: { binary_light_viewable: "VGxpZ2h0RGF0YQ==" }, + }, + }, + }, + treeview: { + items: [{ title: "PointSet2D", children: [] }], + selection: [], + components_selection: [], + isAdditionnalTreeDisplayed: false, + panelWidth: 320, + model_id: "", + isTreeCollection: false, + selectedTree: null, + }, + dataStyle: { styles: { abc123: { some: "style" } } }, + hybridViewer: { zScale: 1.5 }, + } + + await stores.app.importStores(snapshot) + + expect(stores.dataBase.db.abc123).toBeDefined() + expect(stores.dataStyle.styles.abc123).toBeDefined() + }) +}) diff --git a/tests/unit/stores/Appstore.nuxt.test.js b/tests/unit/stores/Appstore.nuxt.test.js index 4f4a7155..ee37ecda 100644 --- a/tests/unit/stores/Appstore.nuxt.test.js +++ b/tests/unit/stores/Appstore.nuxt.test.js @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, expectTypeOf, test, vi } from "vitest" import { createTestingPinia } from "@pinia/testing" -import { useAppStore } from "@/stores/app_store.js" +import { useAppStore } from "@/stores/app.js" import { setActivePinia } from "pinia" beforeEach(async () => { @@ -16,8 +16,8 @@ describe("App Store", () => { test("initial state", () => { const app_store = useAppStore() expectTypeOf(app_store.stores).toBeArray() - expectTypeOf(app_store.save).toBeFunction() - expectTypeOf(app_store.load).toBeFunction() + expectTypeOf(app_store.exportStores).toBeFunction() + expectTypeOf(app_store.importStores).toBeFunction() expectTypeOf(app_store.registerStore).toBeFunction() }) }) @@ -46,7 +46,7 @@ describe("App Store", () => { load: vi.fn().mockImplementation(() => {}), } const mock_store_2 = { - $id: "cartStore", + $id: "geodeStore", save: vi.fn().mockImplementation(() => {}), load: vi.fn().mockImplementation(() => {}), } @@ -56,58 +56,60 @@ describe("App Store", () => { expect(app_store.stores.length).toBe(2) expect(app_store.stores[0].$id).toBe("userStore") - expect(app_store.stores[1].$id).toBe("cartStore") + expect(app_store.stores[1].$id).toBe("geodeStore") }) }) - describe("save", () => { - test("save stores with save method", () => { + describe("Export", () => { + test("export stores with exportStores method", () => { const app_store = useAppStore() const mock_store_1 = { $id: "userStore", - save: vi.fn().mockImplementation(() => ({ + exportStores: vi.fn().mockImplementation(() => ({ name: "toto", email: "toto@titi.com", })), - load: vi.fn().mockImplementation(() => {}), + importStores: vi.fn().mockImplementation(() => {}), } const mock_store_2 = { - $id: "cartStore", - save: vi.fn().mockImplementation(() => ({ items: [], total: 0 })), - load: vi.fn().mockImplementation(() => {}), + $id: "geodeStore", + exportStores: vi + .fn() + .mockImplementation(() => ({ items: [], total: 0 })), + importStores: vi.fn().mockImplementation(() => {}), } app_store.registerStore(mock_store_1) app_store.registerStore(mock_store_2) - const snapshot = app_store.save() + const snapshot = app_store.exportStores() - expect(mock_store_1.save).toHaveBeenCalledTimes(1) - expect(mock_store_2.save).toHaveBeenCalledTimes(1) + expect(mock_store_1.exportStores).toHaveBeenCalledTimes(1) + expect(mock_store_2.exportStores).toHaveBeenCalledTimes(1) expect(snapshot).toEqual({ userStore: { name: "toto", email: "toto@titi.com" }, - cartStore: { items: [], total: 0 }, + geodeStore: { items: [], total: 0 }, }) }) - test("skip stores without save method", () => { + test("skip stores without exportSave method", () => { const app_store = useAppStore() const mock_store_1 = { $id: "withSave", - save: vi.fn().mockImplementation(() => ({ data: "test" })), - load: vi.fn().mockImplementation(() => {}), + exportStores: vi.fn().mockImplementation(() => ({ data: "test" })), + importStores: vi.fn().mockImplementation(() => {}), } const mock_store_2 = { $id: "withoutSave", - load: vi.fn().mockImplementation(() => {}), + importStores: vi.fn().mockImplementation(() => {}), } app_store.registerStore(mock_store_1) app_store.registerStore(mock_store_2) - const snapshot = app_store.save() + const snapshot = app_store.exportStores() - expect(mock_store_1.save).toHaveBeenCalledTimes(1) + expect(mock_store_1.exportStores).toHaveBeenCalledTimes(1) expect(snapshot).toEqual({ withSave: { data: "test" }, }) @@ -116,71 +118,53 @@ describe("App Store", () => { test("return empty snapshot when no stores registered", () => { const app_store = useAppStore() - const snapshot = app_store.save() + const snapshot = app_store.exportStores() expect(snapshot).toEqual({}) }) }) describe("load", () => { - test("load stores with load method", () => { - const app_store = useAppStore() - const mock_store_1 = { + test("App Store > actions > importStores > import stores with importStores method", async () => { + const appStore = useAppStore() + const userStore = { $id: "userStore", - save: vi.fn().mockImplementation(() => {}), - load: vi.fn().mockImplementation(() => {}), + importStores: vi.fn().mockResolvedValue(), } - const mock_store_2 = { - $id: "cartStore", - save: vi.fn().mockImplementation(() => {}), - load: vi.fn().mockImplementation(() => {}), + const geodeStore = { + $id: "geodeStore", + importStores: vi.fn().mockResolvedValue(), } - - app_store.registerStore(mock_store_1) - app_store.registerStore(mock_store_2) - + appStore.registerStore(userStore) + appStore.registerStore(geodeStore) const snapshot = { - userStore: { name: "tata", email: "tata@tutu.com" }, - cartStore: { items: [{ id: 1 }], total: 50 }, + userStore: { some: "data" }, + geodeStore: { other: "data" }, } - - app_store.load(snapshot) - - expect(mock_store_1.load).toHaveBeenCalledTimes(1) - expect(mock_store_1.load).toHaveBeenCalledWith({ - name: "tata", - email: "tata@tutu.com", - }) - expect(mock_store_2.load).toHaveBeenCalledTimes(1) - expect(mock_store_2.load).toHaveBeenCalledWith({ - items: [{ id: 1 }], - total: 50, - }) + await appStore.importStores(snapshot) + expect(userStore.importStores).toHaveBeenCalledTimes(1) + expect(geodeStore.importStores).toHaveBeenCalledTimes(1) }) - test("skip stores without load method", () => { + test("skip stores without importStores method", () => { const app_store = useAppStore() const mock_store_1 = { - $id: "withLoad", + $id: "withImport", save: vi.fn().mockImplementation(() => {}), - load: vi.fn().mockImplementation(() => {}), + importStores: vi.fn().mockImplementation(() => {}), } const mock_store_2 = { - $id: "withoutLoad", + $id: "withoutImport", save: vi.fn().mockImplementation(() => {}), } - app_store.registerStore(mock_store_1) app_store.registerStore(mock_store_2) - const snapshot = { - withLoad: { data: "test" }, - withoutLoad: { data: "ignored" }, + withImport: { data: "test" }, + withoutImport: { data: "ignored" }, } - - app_store.load(snapshot) - - expect(mock_store_1.load).toHaveBeenCalledTimes(1) - expect(mock_store_2.load).toBeUndefined() + app_store.importStores(snapshot) + expect(mock_store_1.importStores).toHaveBeenCalledTimes(1) + expect(mock_store_2.importStores).toBeUndefined() }) test("warn when store not found in snapshot", () => { @@ -190,12 +174,10 @@ describe("App Store", () => { .mockImplementation(() => {}) const mock_store = { $id: "testStore", - load: vi.fn().mockImplementation(() => {}), + importStores: vi.fn().mockImplementation(() => {}), } - app_store.registerStore(mock_store) - app_store.load({}) - + app_store.importStores({}) expect(console_warn_spy).toHaveBeenCalledWith( expect.stringContaining("Stores not found in snapshot: testStore"), ) diff --git a/utils/default_styles.js b/utils/default_styles.js index eede29e7..6f071350 100644 --- a/utils/default_styles.js +++ b/utils/default_styles.js @@ -16,9 +16,9 @@ const corners_defaultColor = { r: 20, g: 20, b: 20 } const lines_defaultVisibility = true const lines_defaultColor = { r: 20, g: 20, b: 20 } const surfaces_defaultVisibility = true -const surfaces_defaultColor = { r: 20, g: 20, b: 20 } +const surfaces_defaultColor = { r: 255, g: 255, b: 255 } const blocks_defaultVisibility = true -const blocks_defaultColor = { r: 20, g: 20, b: 20 } +const blocks_defaultColor = { r: 255, g: 255, b: 255 } // Mesh functions const meshPointsDefaultStyle = ( diff --git a/utils/file_import_workflow.js b/utils/file_import_workflow.js index 998d23d8..bbb31f4a 100644 --- a/utils/file_import_workflow.js +++ b/utils/file_import_workflow.js @@ -60,11 +60,7 @@ async function importFile(filename, geode_object) { await hybridViewerStore.addItem(id) console.log("after dataBaseStore.addItem") - await dataStyleStore.addDataStyle( - data._value.id, - data._value.geode_object, - data._value.object_type, - ) + await dataStyleStore.addDataStyle(data._value.id, data._value.geode_object) console.log("after dataStyleStore.addDataStyle") if (data._value.object_type === "model") { await Promise.all([ @@ -80,4 +76,65 @@ async function importFile(filename, geode_object) { return data._value.id } -export { importFile, importWorkflow } +async function importItemFromSnapshot(item) { + const dataBaseStore = useDataBaseStore() + const dataStyleStore = useDataStyleStore() + const hybridViewerStore = useHybridViewerStore() + const treeviewStore = useTreeviewStore() + await dataBaseStore.registerObject(item.id) + await dataBaseStore.addItem(item.id, { + object_type: item.object_type, + geode_object: item.geode_object, + native_filename: item.native_filename, + viewable_filename: item.viewable_filename, + displayed_name: item.displayed_name, + vtk_js: item.vtk_js, + }) + + await treeviewStore.addItem( + item.geode_object, + item.displayed_name, + item.id, + item.object_type, + ) + + await hybridViewerStore.addItem(item.id) + + await dataStyleStore.addDataStyle(item.id, item.geode_object) + + if (item.object_type === "model") { + await Promise.all([ + dataBaseStore.fetchMeshComponents(item.id), + dataBaseStore.fetchUuidToFlatIndexDict(item.id), + ]) + } + + await dataStyleStore.applyDefaultStyle(item.id) + hybridViewerStore.remoteRender() + return item.id +} + +async function importWorkflowFromSnapshot(items) { + console.log("[importWorkflowFromSnapshot] start", { count: items?.length }) + const dataBaseStore = useDataBaseStore() + const treeviewStore = useTreeviewStore() + const dataStyleStore = useDataStyleStore() + const hybridViewerStore = useHybridViewerStore() + + const ids = [] + for (const item of items) { + const id = await importItemFromSnapshot( + item, + dataBaseStore, + treeviewStore, + dataStyleStore, + hybridViewerStore, + ) + ids.push(id) + } + hybridViewerStore.remoteRender() + console.log("[importWorkflowFromSnapshot] done", { ids }) + return ids +} + +export { importFile, importWorkflow, importWorkflowFromSnapshot }