diff --git a/app/components/ExplodeFilter.vue b/app/components/ExplodeFilter.vue new file mode 100644 index 00000000..823bb79f --- /dev/null +++ b/app/components/ExplodeFilter.vue @@ -0,0 +1,34 @@ + + + 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/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)" /> + ( + 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 { + await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.explode, { + ids, + explode_factor, + }); } 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 { @@ -64,22 +61,15 @@ 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; setShrink: (ids: string[], shrink_factor: number) => Promise; + setExplode: (ids: string[], explode_factor: number) => Promise; setSlice: ( ids: string[], slices: { axis: SliceAxis; index: number }[], @@ -89,6 +79,7 @@ function useHybridViewerFilters(): { return { setClippingPlanes, setShrink, + setExplode, setSlice, setThreshold, }; diff --git a/tests/unit/components/FactorFilterPanel.nuxt.test.ts b/tests/unit/components/FactorFilterPanel.nuxt.test.ts new file mode 100644 index 00000000..0bf69e59 --- /dev/null +++ b/tests/unit/components/FactorFilterPanel.nuxt.test.ts @@ -0,0 +1,102 @@ +// 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, + ); +});