diff --git a/app/components/csv-preview/CsvPreviewer.vue b/app/components/csv-preview/CsvPreviewer.vue index c62b6fd1..34e644e6 100644 --- a/app/components/csv-preview/CsvPreviewer.vue +++ b/app/components/csv-preview/CsvPreviewer.vue @@ -77,34 +77,33 @@ function autoDetectSeparator(content: string): string { return best; } +function splitLine(line: string): string[] { + if (!separator.value) { + return [line]; + } + const result = []; + let current = ""; + let inQuotes = false; + for (const char of line) { + if (char === '"') { + inQuotes = !inQuotes; + } else if (char === separator.value && !inQuotes) { + result.push(current.trim()); + current = ""; + } else { + current += char; + } + } + result.push(current.trim()); + return result; +} + function parseContent(): string[] { if (!rawContent.value) { return []; } const allLines = rawContent.value.split(/\r?\n/u).filter((line) => line.trim() !== ""); - - function splitLine(line: string): string[] { - if (!separator.value) { - return [line]; - } - const result = []; - let current = ""; - let inQuotes = false; - for (const char of line) { - if (char === '"') { - inQuotes = !inQuotes; - } else if (char === separator.value && !inQuotes) { - result.push(current.trim()); - current = ""; - } else { - current += char; - } - } - result.push(current.trim()); - return result; - } - const headerLine = allLines[headerRow.value]; const rawHeaders = headerLine ? splitLine(headerLine) : []; diff --git a/app/stores/app.ts b/app/stores/app.ts index 910d06b8..7c686abc 100644 --- a/app/stores/app.ts +++ b/app/stores/app.ts @@ -142,6 +142,7 @@ export const useAppStore = defineStore("app", () => { getExtensionEnabled, } = useAppExtensions(); + // oxlint-disable-next-line unicorn/consistent-function-scoping async function upload(file: Readonly, callbacks: RequestHandlers = {}): Promise { const store = useAppStore(); const schema = opengeodeweb_front_schemas.api.local.extensions.upload; @@ -163,6 +164,7 @@ export const useAppStore = defineStore("app", () => { } // `TResult` is asserted, not verified, at the single `return result as TResult` boundary below: the backend response is only checked against `schema` at runtime, so callers' `TResult` is a contract with the schema, not something this function can prove. + // oxlint-disable-next-line unicorn/consistent-function-scoping async function request( { schema, diff --git a/app/stores/viewer.ts b/app/stores/viewer.ts index 51dcbf73..52f82a7b 100644 --- a/app/stores/viewer.ts +++ b/app/stores/viewer.ts @@ -62,6 +62,7 @@ export const useViewerStore = defineStore( function toggle_picking_mode(value: boolean): void { picking_mode.value = value; } + // oxlint-disable-next-line unicorn/consistent-function-scoping async function request( { schema, diff --git a/internal/stores/hybrid_viewer/camera.ts b/internal/stores/hybrid_viewer/camera.ts index f4168cee..837d14b9 100644 --- a/internal/stores/hybrid_viewer/camera.ts +++ b/internal/stores/hybrid_viewer/camera.ts @@ -110,13 +110,21 @@ function centerCameraOnPosition( if (!camera || !pickedPosition) { return; } - const focalPoint = camera.getFocalPoint(); const position = camera.getPosition(); + const directionOfProjection = camera.getDirectionOfProjection(); + const distance = dot( + [ + pickedPosition[0] - position[0], + pickedPosition[1] - position[1], + pickedPosition[2] - position[2], + ], + directionOfProjection, + ); camera.setFocalPoint(...pickedPosition); camera.setPosition( - position[0] + pickedPosition[0] - focalPoint[0], - position[1] + pickedPosition[1] - focalPoint[1], - position[2] + pickedPosition[2] - focalPoint[2], + pickedPosition[0] - distance * directionOfProjection[0], + pickedPosition[1] - distance * directionOfProjection[1], + pickedPosition[2] - distance * directionOfProjection[2], ); } diff --git a/internal/stores/hybrid_viewer/filters.ts b/internal/stores/hybrid_viewer/filters.ts index d820ef69..4683d013 100644 --- a/internal/stores/hybrid_viewer/filters.ts +++ b/internal/stores/hybrid_viewer/filters.ts @@ -2,7 +2,7 @@ import { useHybridViewerCore } from "./core"; import { useViewerStore } from "@ogw_front/stores/viewer"; import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json"; -async function performSetClippingPlanes(ids: string[], planes: unknown): Promise { +async function setClippingPlanes(ids: string[], planes: unknown): Promise { const viewerStore = useViewerStore(); const { remoteRender } = useHybridViewerCore(); const schema = viewer_schemas.opengeodeweb_viewer.viewer.clipping_planes; @@ -16,7 +16,7 @@ async function performSetClippingPlanes(ids: string[], planes: unknown): Promise }); await remoteRender(); } -async function performSetShrink(ids: string[], shrink_factor: number): Promise { +async function setShrink(ids: string[], shrink_factor: number): Promise { const viewerStore = useViewerStore(); const { remoteRender } = useHybridViewerCore(); const schema = viewer_schemas.opengeodeweb_viewer.viewer.shrink; @@ -34,15 +34,9 @@ function useHybridViewerFilters(): { setClippingPlanes: (ids: string[], planes: unknown) => Promise; setShrink: (ids: string[], shrink_factor: number) => Promise; } { - async function setClippingPlanes(ids: string[], planes: unknown): Promise { - await performSetClippingPlanes(ids, planes); - } - async function setShrink(ids: string[], shrink_factor: number): Promise { - await performSetShrink(ids, shrink_factor); - } return { setClippingPlanes, setShrink, }; } -export { performSetClippingPlanes, performSetShrink, useHybridViewerFilters }; +export { useHybridViewerFilters }; diff --git a/internal/stores/hybrid_viewer/viewport.ts b/internal/stores/hybrid_viewer/viewport.ts index e02e44a0..696f84d4 100644 --- a/internal/stores/hybrid_viewer/viewport.ts +++ b/internal/stores/hybrid_viewer/viewport.ts @@ -15,7 +15,7 @@ interface HybridViewerViewport { viewStream: Ref; } -async function performResize(width: number, height: number): Promise { +async function resize(width: number, height: number): Promise { const { genericRenderWindow, remoteRender, status, viewStream } = useHybridViewerCore(); const viewerStore = useViewerStore(); if (viewerStore.status !== Status.CONNECTED || status.value !== Status.CREATED) { @@ -77,7 +77,7 @@ function performClickPicking(event: MouseEvent, containerElement: HTMLElement): ); } -function performSetContainer(container: ContainerRef | undefined): void { +function setContainer(container: ContainerRef | undefined): void { if (!container || !container.value) { return; } @@ -102,7 +102,7 @@ function performSetContainer(container: ContainerRef | undefined): void { transition: "opacity 0.1s ease-in", zIndex: 1, }); - void performResize(containerElement.offsetWidth, containerElement.offsetHeight); + void resize(containerElement.offsetWidth, containerElement.offsetHeight); let has_dragged = false; useMousePressed({ target: containerElement, @@ -167,17 +167,11 @@ function performSetContainer(container: ContainerRef | undefined): void { } function useHybridViewerViewport(): HybridViewerViewport { const { viewStream } = useHybridViewerCore(); - function setContainer(container: ContainerRef | undefined): void { - performSetContainer(container); - } - async function resize(width: number, height: number): Promise { - await performResize(width, height); - } return { setContainer, resize, viewStream, }; } -export { performClickPicking, performResize, performSetContainer, useHybridViewerViewport }; +export { performClickPicking, useHybridViewerViewport }; export type { ViewStreamLike } from "./vtk_types"; diff --git a/tests/unit/stores/hybrid_viewer_camera.nuxt.test.ts b/tests/unit/stores/hybrid_viewer_camera.nuxt.test.ts new file mode 100644 index 00000000..e7490fbf --- /dev/null +++ b/tests/unit/stores/hybrid_viewer_camera.nuxt.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "vitest"; + +import type { Vector3 } from "@ogw_internal/stores/hybrid_viewer/vtk_types"; +import { centerCameraOnPosition } from "@ogw_internal/stores/hybrid_viewer/camera"; +import { newInstance as vtkCamera } from "@kitware/vtk.js/Rendering/Core/Camera"; + +const INITIAL_CAMERA_Z = 100; +const PICKED_X = 20; +const PICKED_Y = 30; +const PICKED_Z = 80; + +describe("centerCameraOnPosition helper", () => { + test("preserves distance along view direction and centers on picked position", () => { + const camera = vtkCamera(); + camera.setPosition(0, 0, INITIAL_CAMERA_Z); + camera.setFocalPoint(0, 0, 0); + + const pickedPosition: Vector3 = [PICKED_X, PICKED_Y, PICKED_Z]; + centerCameraOnPosition(camera, pickedPosition); + + expect(camera.getFocalPoint()).toStrictEqual(pickedPosition); + expect(camera.getPosition()).toStrictEqual([PICKED_X, PICKED_Y, INITIAL_CAMERA_Z]); + expect(camera.getDirectionOfProjection()).toStrictEqual([0, 0, -1]); + }); + + test("handles undefined inputs gracefully", () => { + const camera = vtkCamera(); + camera.setPosition(0, 0, INITIAL_CAMERA_Z); + camera.setFocalPoint(0, 0, 0); + + centerCameraOnPosition(undefined, [1, 2, 3]); + centerCameraOnPosition(camera, undefined); + + expect(camera.getPosition()).toStrictEqual([0, 0, INITIAL_CAMERA_Z]); + expect(camera.getFocalPoint()).toStrictEqual([0, 0, 0]); + }); +});