From 241cc18e7f2c5de17287984629459e60ac91419e Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 5 Oct 2026 15:27:24 +0200 Subject: [PATCH 1/7] feat(exploded_view): exploded view tool [skip ci] --- app/components/ExplodeFilter.vue | 188 +++++++++++++++++++++++ app/components/ViewToolbar.vue | 16 ++ internal/stores/hybrid_viewer/filters.ts | 16 ++ 3 files changed, 220 insertions(+) create mode 100644 app/components/ExplodeFilter.vue diff --git a/app/components/ExplodeFilter.vue b/app/components/ExplodeFilter.vue new file mode 100644 index 00000000..1047bd2b --- /dev/null +++ b/app/components/ExplodeFilter.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/app/components/ViewToolbar.vue b/app/components/ViewToolbar.vue index f86d5157..1d182bbc 100644 --- a/app/components/ViewToolbar.vue +++ b/app/components/ViewToolbar.vue @@ -7,6 +7,7 @@ import CameraBookmarkIcon from "@ogw_front/assets/viewer_svgs/camera-bookmark.sv import CameraManager from "@ogw_front/components/CameraManager.vue"; import CameraOrientation from "@ogw_front/components/CameraOrientation.vue"; import ClippingPlanes from "@ogw_front/components/ClippingPlanes.vue"; +import ExplodeFilter from "@ogw_front/components/ExplodeFilter.vue"; import Ruler from "@ogw_front/components/Ruler.vue"; import Screenshot from "@ogw_front/components/Screenshot.vue"; import ShrinkFilter from "@ogw_front/components/ShrinkFilter.vue"; @@ -26,6 +27,7 @@ const showCameraOrientation = ref(false); const showZScaling = ref(false); const showClippingPlanes = ref(false); const showShrinkFilter = ref(false); +const showExplodeFilter = ref(false); const showThresholdFilter = ref(false); const showRuler = ref(false); const gridScale = ref(false); @@ -77,6 +79,7 @@ function closeAllToolsExcept(toolRef: Ref): void { showZScaling, showClippingPlanes, showShrinkFilter, + showExplodeFilter, showThresholdFilter, showRuler, is_picking, @@ -236,6 +239,15 @@ const camera_options = computed(() => [ toggleTool(showShrinkFilter); }, }, + { + testId: "explodeFilterButton", + tooltip: "Exploded View", + icon: "mdi-arrow-expand-all", + color: showExplodeFilter.value ? "primary" : undefined, + action: (): void => { + toggleTool(showExplodeFilter); + }, + }, { testId: "thresholdFilterButton", tooltip: "Threshold Filter", @@ -334,6 +346,10 @@ const camera_options = computed(() => [ v-model:show="showShrinkFilter" :escapeFunction="() => (showShrinkFilter = false)" /> + { }); await remoteRender(); } +async function setExplode(ids: string[], explode_factor: number): Promise { + const viewerStore = useViewerStore(); + const { remoteRender } = useHybridViewerCore(); + const schema = viewer_schemas.opengeodeweb_viewer.viewer.explode; + const params = { + ids, + explode_factor, + }; + await viewerStore.request({ + schema, + params, + }); + await remoteRender(); +} async function setSlice( ids: string[], slices: { axis: SliceAxis; index: number }[], @@ -80,6 +94,7 @@ async function setThreshold(ids: string[], attribute?: ThresholdAttribute): Prom function useHybridViewerFilters(): { setClippingPlanes: (ids: string[], planes: ViewerClippingPlanesParams["planes"]) => Promise; setShrink: (ids: string[], shrink_factor: number) => Promise; + setExplode: (ids: string[], explode_factor: number) => Promise; setSlice: ( ids: string[], slices: { axis: SliceAxis; index: number }[], @@ -89,6 +104,7 @@ function useHybridViewerFilters(): { return { setClippingPlanes, setShrink, + setExplode, setSlice, setThreshold, }; From 1721411c58d7fcac1457a789966938f519020db0 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 5 Oct 2026 17:06:03 +0200 Subject: [PATCH 2/7] trigger From bd8b093586060659ae47811522aa5e913a70a526 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Tue, 6 Oct 2026 10:10:19 +0200 Subject: [PATCH 3/7] refacto shrink and explode components and filter store --- app/components/ExplodeFilter.vue | 180 ++------------- app/components/FactorFilterPanel.vue | 211 ++++++++++++++++++ app/components/ShrinkFilter.vue | 183 ++------------- internal/stores/hybrid_viewer/filters.ts | 69 ++---- .../components/FactorFilterPanel.nuxt.test.ts | 90 ++++++++ 5 files changed, 354 insertions(+), 379 deletions(-) create mode 100644 app/components/FactorFilterPanel.vue create mode 100644 tests/unit/components/FactorFilterPanel.nuxt.test.ts diff --git a/app/components/ExplodeFilter.vue b/app/components/ExplodeFilter.vue index 1047bd2b..d2fcf2a4 100644 --- a/app/components/ExplodeFilter.vue +++ b/app/components/ExplodeFilter.vue @@ -1,13 +1,10 @@ - - diff --git a/app/components/FactorFilterPanel.vue b/app/components/FactorFilterPanel.vue new file mode 100644 index 00000000..33385806 --- /dev/null +++ b/app/components/FactorFilterPanel.vue @@ -0,0 +1,211 @@ + + + + + diff --git a/app/components/ShrinkFilter.vue b/app/components/ShrinkFilter.vue index dd50754e..b4459d8f 100644 --- a/app/components/ShrinkFilter.vue +++ b/app/components/ShrinkFilter.vue @@ -1,12 +1,11 @@ - - diff --git a/internal/stores/hybrid_viewer/filters.ts b/internal/stores/hybrid_viewer/filters.ts index b40af3d2..6a106c53 100644 --- a/internal/stores/hybrid_viewer/filters.ts +++ b/internal/stores/hybrid_viewer/filters.ts @@ -1,3 +1,4 @@ +import type { JsonRpcSchema, ParamsOf, ResponseOf } from "@ogw_shared/utils/types.js"; import viewer_schemas, { type ViewerClippingPlanesParams, } from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js"; @@ -5,69 +6,51 @@ import type { SliceAxis } from "@ogw_front/utils/slice"; import { useHybridViewerCore } from "./core"; import { useViewerStore } from "@ogw_front/stores/viewer"; +async function requestAndRender( + schema: Schema, + params: ParamsOf, +): Promise> { + const viewerStore = useViewerStore(); + const { remoteRender } = useHybridViewerCore(); + const response = await viewerStore.request({ + schema, + params, + }); + await remoteRender(); + return response; +} + async function setClippingPlanes( ids: string[], planes: ViewerClippingPlanesParams["planes"], ): Promise { - const viewerStore = useViewerStore(); - const { remoteRender } = useHybridViewerCore(); - const schema = viewer_schemas.opengeodeweb_viewer.viewer.clipping_planes; - const params = { + await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.clipping_planes, { ids, planes, - }; - await viewerStore.request({ - schema, - params, }); - await remoteRender(); } async function setShrink(ids: string[], shrink_factor: number): Promise { - const viewerStore = useViewerStore(); - const { remoteRender } = useHybridViewerCore(); - const schema = viewer_schemas.opengeodeweb_viewer.viewer.shrink; - const params = { + await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.shrink, { ids, shrink_factor, - }; - await viewerStore.request({ - schema, - params, }); - await remoteRender(); } async function setExplode(ids: string[], explode_factor: number): Promise { - const viewerStore = useViewerStore(); - const { remoteRender } = useHybridViewerCore(); - const schema = viewer_schemas.opengeodeweb_viewer.viewer.explode; - const params = { + await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.explode, { ids, explode_factor, - }; - await viewerStore.request({ - schema, - params, }); - await remoteRender(); } async function setSlice( ids: string[], slices: { axis: SliceAxis; index: number }[], ): Promise<[number, number, number]> { - const viewerStore = useViewerStore(); - const { remoteRender } = useHybridViewerCore(); - const schema = viewer_schemas.opengeodeweb_viewer.viewer.slice; - const params = { - ids, - slices, - }; const { max_indices: [max_i = 0, max_j = 0, max_k = 0], - } = await viewerStore.request({ - schema, - params, + } = await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.slice, { + ids, + slices, }); - await remoteRender(); return [max_i, max_j, max_k]; } interface ThresholdAttribute { @@ -78,18 +61,10 @@ interface ThresholdAttribute { maximum: number; } async function setThreshold(ids: string[], attribute?: ThresholdAttribute): Promise { - const viewerStore = useViewerStore(); - const { remoteRender } = useHybridViewerCore(); - const schema = viewer_schemas.opengeodeweb_viewer.viewer.threshold; - const params = { + await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.threshold, { ids, attribute, - }; - await viewerStore.request({ - schema, - params, }); - await remoteRender(); } function useHybridViewerFilters(): { setClippingPlanes: (ids: string[], planes: ViewerClippingPlanesParams["planes"]) => Promise; diff --git a/tests/unit/components/FactorFilterPanel.nuxt.test.ts b/tests/unit/components/FactorFilterPanel.nuxt.test.ts new file mode 100644 index 00000000..e05b57fd --- /dev/null +++ b/tests/unit/components/FactorFilterPanel.nuxt.test.ts @@ -0,0 +1,90 @@ +// Node imports +import { setTimeout as wait } from "node:timers/promises"; + +// Third party imports +import { type Mock, describe, expect, test, vi } from "vitest"; +import { type VueWrapper, flushPromises } from "@vue/test-utils"; +import { mountSuspended } from "@nuxt/test-utils/runtime"; + +// Local imports +import FactorFilterPanel from "@ogw_front/components/FactorFilterPanel.vue"; +import { vuetify } from "@ogw_tests/utils"; + +const DATA_ID = "12345678901234567890123456789012"; +const DEFAULT_VALUE = 0.5; +const NEUTRAL_VALUE = 0; +const DEBOUNCE_WAIT = 300; +const TIMEOUT = 5000; + +type SetFactor = (ids: string[], factor: number) => Promise; + +vi.mock(import("@ogw_front/stores/data") as Promise, () => ({ + useDataStore: (): { refAllItems: () => Ref<{ id: string; name: string }[]> } => ({ + refAllItems: (): Ref<{ id: string; name: string }[]> => ref([{ id: DATA_ID, name: "model" }]), + }), +})); +vi.mock(import("@ogw_front/stores/hybrid_viewer") as Promise, () => ({ + useHybridViewerStore: (): { hybridDb: Record } => ({ hybridDb: {} }), +})); + +function mockSetFactor(): Mock { + return vi.fn().mockResolvedValue(); +} + +async function mountOpenedPanel(setFactor: SetFactor): Promise { + const wrapper = await mountSuspended(FactorFilterPanel, { + global: { plugins: [vuetify] }, + props: { + show: false, + title: "Exploded View", + label: "Explode Factor", + testIdPrefix: "explode", + setFactor, + factorRange: { + min: 0, + max: 2, + defaultValue: DEFAULT_VALUE, + neutralValue: NEUTRAL_VALUE, + }, + }, + }); + await wrapper.setProps({ show: true }); + await flushPromises(); + return wrapper; +} + +async function settle(): Promise { + await wait(DEBOUNCE_WAIT); + await flushPromises(); +} + +describe("factor filter panel", () => { + test("applies the default factor once when opened", async () => { + const setFactor = mockSetFactor(); + await mountOpenedPanel(setFactor); + await settle(); + expect(setFactor.mock.calls).toStrictEqual([[[DATA_ID], DEFAULT_VALUE]]); + }, TIMEOUT); + + test("remove sends a single request with the neutral factor", async () => { + const setFactor = mockSetFactor(); + const wrapper = await mountOpenedPanel(setFactor); + await settle(); + setFactor.mockClear(); + await wrapper.find('[data-testid="removeExplodeButton"]').trigger("click"); + await settle(); + expect(setFactor.mock.calls).toStrictEqual([[[DATA_ID], NEUTRAL_VALUE]]); + }, TIMEOUT); + + test("reset sends a single request with the default factor", async () => { + const setFactor = mockSetFactor(); + const wrapper = await mountOpenedPanel(setFactor); + await settle(); + await wrapper.find('[data-testid="removeExplodeButton"]').trigger("click"); + await settle(); + setFactor.mockClear(); + await wrapper.find('[data-testid="resetExplodeButton"]').trigger("click"); + await settle(); + expect(setFactor.mock.calls).toStrictEqual([[[DATA_ID], DEFAULT_VALUE]]); + }, TIMEOUT); +}); From 91c8559b32339b2058646a1feb5c2cc2ca3319fc Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:11:17 +0000 Subject: [PATCH 4/7] Apply prepare changes --- .../components/FactorFilterPanel.nuxt.test.ts | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/tests/unit/components/FactorFilterPanel.nuxt.test.ts b/tests/unit/components/FactorFilterPanel.nuxt.test.ts index e05b57fd..0bf69e59 100644 --- a/tests/unit/components/FactorFilterPanel.nuxt.test.ts +++ b/tests/unit/components/FactorFilterPanel.nuxt.test.ts @@ -59,32 +59,44 @@ async function settle(): Promise { } describe("factor filter panel", () => { - test("applies the default factor once when opened", async () => { - const setFactor = mockSetFactor(); - await mountOpenedPanel(setFactor); - await settle(); - expect(setFactor.mock.calls).toStrictEqual([[[DATA_ID], DEFAULT_VALUE]]); - }, TIMEOUT); + test( + "applies the default factor once when opened", + async () => { + const setFactor = mockSetFactor(); + await mountOpenedPanel(setFactor); + await settle(); + expect(setFactor.mock.calls).toStrictEqual([[[DATA_ID], DEFAULT_VALUE]]); + }, + TIMEOUT, + ); - test("remove sends a single request with the neutral factor", async () => { - const setFactor = mockSetFactor(); - const wrapper = await mountOpenedPanel(setFactor); - await settle(); - setFactor.mockClear(); - await wrapper.find('[data-testid="removeExplodeButton"]').trigger("click"); - await settle(); - expect(setFactor.mock.calls).toStrictEqual([[[DATA_ID], NEUTRAL_VALUE]]); - }, TIMEOUT); + test( + "remove sends a single request with the neutral factor", + async () => { + const setFactor = mockSetFactor(); + const wrapper = await mountOpenedPanel(setFactor); + await settle(); + setFactor.mockClear(); + await wrapper.find('[data-testid="removeExplodeButton"]').trigger("click"); + await settle(); + expect(setFactor.mock.calls).toStrictEqual([[[DATA_ID], NEUTRAL_VALUE]]); + }, + TIMEOUT, + ); - test("reset sends a single request with the default factor", async () => { - const setFactor = mockSetFactor(); - const wrapper = await mountOpenedPanel(setFactor); - await settle(); - await wrapper.find('[data-testid="removeExplodeButton"]').trigger("click"); - await settle(); - setFactor.mockClear(); - await wrapper.find('[data-testid="resetExplodeButton"]').trigger("click"); - await settle(); - expect(setFactor.mock.calls).toStrictEqual([[[DATA_ID], DEFAULT_VALUE]]); - }, TIMEOUT); + test( + "reset sends a single request with the default factor", + async () => { + const setFactor = mockSetFactor(); + const wrapper = await mountOpenedPanel(setFactor); + await settle(); + await wrapper.find('[data-testid="removeExplodeButton"]').trigger("click"); + await settle(); + setFactor.mockClear(); + await wrapper.find('[data-testid="resetExplodeButton"]').trigger("click"); + await settle(); + expect(setFactor.mock.calls).toStrictEqual([[[DATA_ID], DEFAULT_VALUE]]); + }, + TIMEOUT, + ); }); From 97438ae2627a9e614d58e3bd89af2c5fcb87d215 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Tue, 6 Oct 2026 13:38:36 +0200 Subject: [PATCH 5/7] new vizualisation of explode [skip ci] --- app/components/ExplodeFilter.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/ExplodeFilter.vue b/app/components/ExplodeFilter.vue index d2fcf2a4..823bb79f 100644 --- a/app/components/ExplodeFilter.vue +++ b/app/components/ExplodeFilter.vue @@ -2,9 +2,9 @@ import FactorFilterPanel from "@ogw_front/components/FactorFilterPanel.vue"; import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer"; -const DEFAULT_EXPLODE_VALUE = 0.5; +const DEFAULT_EXPLODE_VALUE = 0.2; const MIN_EXPLODE_VALUE = 0; -const MAX_EXPLODE_VALUE = 2; +const MAX_EXPLODE_VALUE = 1; interface Props { escapeFunction?: () => void; From 313a9c1c5424a4221e232a69e4c3025d6fd3e5e1 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Tue, 6 Oct 2026 15:03:29 +0200 Subject: [PATCH 6/7] trigger From db04b40575c12881d257d2ab3b30e8b773e7b23f Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Tue, 6 Oct 2026 15:56:55 +0200 Subject: [PATCH 7/7] trigger