From cbe8badfc2e32ce7c8bc44f7d979552a1b407573 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 27 Jan 2025 17:16:40 -0800 Subject: [PATCH] Viewer: fix camera-orbit and camera-target attributes (#16116) * Improve handling of for camera interpolation while loading * Add some initial automated tests --- packages/tools/viewer/src/viewer.ts | 24 ++-- packages/tools/viewer/test/apps/web/test.html | 26 +++++ packages/tools/viewer/test/viewer.test.ts | 108 ++++++++++++++++++ playwright.config.ts | 5 + tsconfig.build.json | 2 +- tsconfig.json | 2 +- 6 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 packages/tools/viewer/test/apps/web/test.html create mode 100644 packages/tools/viewer/test/viewer.test.ts diff --git a/packages/tools/viewer/src/viewer.ts b/packages/tools/viewer/src/viewer.ts index 30aafa5642d..50aa0a882fb 100644 --- a/packages/tools/viewer/src/viewer.ts +++ b/packages/tools/viewer/src/viewer.ts @@ -46,7 +46,7 @@ import { registerBuiltInLoaders } from "loaders/dynamic"; const toneMappingOptions = ["none", "standard", "aces", "neutral"] as const; export type ToneMapping = (typeof toneMappingOptions)[number]; -export type LoadModelOptions = LoadAssetContainerOptions & { +type UpdateModelOptions = { /** * The default animation index. */ @@ -58,6 +58,8 @@ export type LoadModelOptions = LoadAssetContainerOptions & { animationAutoPlay?: boolean; }; +export type LoadModelOptions = LoadAssetContainerOptions & UpdateModelOptions; + export type CameraAutoOrbit = { /** * Whether the camera should automatically orbit around the model when idle. @@ -705,15 +707,21 @@ export class Viewer implements IDisposable { return this._modelInfo; } - protected _setModel(...args: [model: null] | [model: Model, source?: string | File | ArrayBufferView]): void { - const [model, source] = args; + protected _setModel( + ...args: [model: null] | [model: Model, options?: UpdateModelOptions & Partial<{ source: string | File | ArrayBufferView; interpolateCamera: boolean }>] + ): void { + const [model, options] = args; if (model !== this._modelInfo) { this._modelInfo = model; - this._updateCamera(true); this._updateLight(); this._applyAnimationSpeed(); + this._selectAnimation(options?.defaultAnimation ?? 0, false); + if (options?.animationAutoPlay) { + this.playAnimation(); + } this.onSelectedMaterialVariantChanged.notifyObservers(); - this.onModelChanged.notifyObservers(source ?? null); + this._updateCamera(options?.interpolateCamera); + this.onModelChanged.notifyObservers(options?.source ?? null); } } @@ -973,11 +981,7 @@ export class Viewer implements IDisposable { this.selectedAnimation = -1; if (source) { - this._setModel(await this._loadModel(source, options, abortController.signal), source); - this._selectAnimation(options?.defaultAnimation ?? 0, false); - if (options?.animationAutoPlay) { - this.playAnimation(); - } + this._setModel(await this._loadModel(source, options, abortController.signal), Object.assign({ source, interpolateCamera: false }, options)); } }); } diff --git a/packages/tools/viewer/test/apps/web/test.html b/packages/tools/viewer/test/apps/web/test.html new file mode 100644 index 00000000000..55bc8097001 --- /dev/null +++ b/packages/tools/viewer/test/apps/web/test.html @@ -0,0 +1,26 @@ + + + + + + Viewer Local Development + + + + + + + \ No newline at end of file diff --git a/packages/tools/viewer/test/viewer.test.ts b/packages/tools/viewer/test/viewer.test.ts new file mode 100644 index 00000000000..a1433296355 --- /dev/null +++ b/packages/tools/viewer/test/viewer.test.ts @@ -0,0 +1,108 @@ +import { test, expect, Page } from "@playwright/test"; +import { getGlobalConfig } from "@tools/test-tools"; +import { ViewerElement } from "viewer/viewerElement"; + +// if running in the CI we need to use the babylon snapshot when loading the tools +const snapshot = process.env.SNAPSHOT ? "?snapshot=" + process.env.SNAPSHOT : ""; +const viewerUrl = + (process.env.VIEWER_BASE_URL || getGlobalConfig().baseUrl.replace(":1337", process.env.VIEWER_PORT || ":1342")) + "/packages/tools/viewer/test/apps/web/test.html" + snapshot; + +async function attachViewerElement(page: Page, viewerHtml: string) { + await page.goto(viewerUrl, { + waitUntil: "networkidle", + }); + + await page.evaluate((viewerHtml) => { + const container = document.createElement("div"); + container.innerHTML = viewerHtml; + document.body.appendChild(container); + }, viewerHtml); + + const viewerElementLocator = page.locator("babylon-viewer"); + await viewerElementLocator.waitFor(); + return await viewerElementLocator.elementHandle(); +} + +test("viewerDetails available", async ({ page }) => { + const viewerElementHandle = await attachViewerElement( + page, + ` + + + ` + ); + + // Wait for the viewerDetails property to become defined + const viewerDetails = await page.waitForFunction((viewerElement) => { + return (viewerElement as ViewerElement).viewerDetails; + }, viewerElementHandle); + + expect(viewerDetails).toBeDefined(); + expect(viewerDetails.getProperty("scene")).toBeDefined(); +}); + +test("animation-auto-play", async ({ page }) => { + const viewerElementHandle = await attachViewerElement( + page, + ` + + + ` + ); + + // Wait for the viewerDetails property to become defined + const isAnimationPlaying = await page.waitForFunction((viewerElement) => { + return (viewerElement as ViewerElement).viewerDetails?.viewer.isAnimationPlaying; + }, viewerElementHandle); + + expect(isAnimationPlaying).toBeTruthy(); +}); + +test("camera-orbit", async ({ page }) => { + const viewerElementHandle = await attachViewerElement( + page, + ` + + + ` + ); + + // Wait for the viewerDetails property to become defined + const cameraPose = await page.waitForFunction((viewerElement) => { + const viewerDetails = (viewerElement as ViewerElement).viewerDetails; + if (viewerDetails?.model) { + return [viewerDetails.camera.alpha, viewerDetails.camera.beta, viewerDetails.camera.radius]; + } + }, viewerElementHandle); + + expect(await cameraPose.jsonValue()).toEqual([1, 2, 3]); +}); + +test("camera-target", async ({ page }) => { + const viewerElementHandle = await attachViewerElement( + page, + ` + + + ` + ); + + // Wait for the viewerDetails property to become defined + const cameraPose = await page.waitForFunction((viewerElement) => { + const viewerDetails = (viewerElement as ViewerElement).viewerDetails; + if (viewerDetails?.model) { + return [viewerDetails.camera.target.x, viewerDetails.camera.target.y, viewerDetails.camera.target.z]; + } + }, viewerElementHandle); + + expect(await cameraPose.jsonValue()).toEqual([1, 2, 3]); +}); diff --git a/playwright.config.ts b/playwright.config.ts index 739032a5add..6491a370406 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -95,6 +95,11 @@ export default defineConfig({ testMatch: "**/*.tools.test.ts", use: getUseDefinition("Graph Tools"), }, + { + name: "viewer", + testMatch: "packages/tools/viewer/test/viewer.test.ts", + use: getUseDefinition("Viewer"), + }, ], snapshotPathTemplate: "packages/tools/tests/test/visualization/ReferenceImages/{arg}{ext}", diff --git a/tsconfig.build.json b/tsconfig.build.json index cb9515918f8..eaa778e110c 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -37,7 +37,7 @@ "node-render-graph-editor/*": ["tools/nodeRenderGraphEditor/dist/*"], "gui-editor/*": ["tools/guiEditor/dist/*"], "accessibility/*": ["tools/accessibility/dist/*"], - "viewer-legacy/*": ["tools/viewer-legacy/dist/*"], + "viewer/*": ["tools/viewer/dist/*"], "ktx2decoder/*": ["tools/ktx2Decoder/dist/*"], "vsm": ["tools/vsm/dist/*"] } diff --git a/tsconfig.json b/tsconfig.json index c69dd1ecf10..4567793862f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,7 +28,7 @@ "node-render-graph-editor/*": ["tools/nodeRenderGraphEditor/src/*"], "gui-editor/*": ["tools/guiEditor/src/*"], "accessibility/*": ["tools/accessibility/src/*"], - "viewer-legacy/*": ["tools/viewer-legacy/src/*"], + "viewer/*": ["tools/viewer/src/*"], "ktx2decoder/*": ["tools/ktx2Decoder/src/*"], "shared-ui-components/*": ["dev/sharedUiComponents/src/*"], "@tools/gui-editor/*": ["tools/guiEditor/src/*"],