From 08f3ac18c27153bc29c24c2994316507e01564dc Mon Sep 17 00:00:00 2001 From: LucaP Date: Sat, 5 Sep 2026 22:33:35 +0300 Subject: [PATCH] test: align scene regressions with the current portfolio Exercise sign state updates and composition together, covering responsive placement, grounding, label reuse, resource ownership, and departure motion. Update poster and visual-model expectations while retaining behavioral checks. Validation: 46 focused tests passed before commit. The preceding full build:preview run passed all 673 tests plus typecheck, builds, prerender, artifact verification, and transfer budgets. --- tests/unit/SceneCanvas.test.ts | 16 +- tests/unit/sign-rendering.test.ts | 216 ++++++++++++ tests/unit/world-models.test.ts | 530 +++--------------------------- 3 files changed, 263 insertions(+), 499 deletions(-) create mode 100644 tests/unit/sign-rendering.test.ts diff --git a/tests/unit/SceneCanvas.test.ts b/tests/unit/SceneCanvas.test.ts index 3b6e21b..3b1f519 100644 --- a/tests/unit/SceneCanvas.test.ts +++ b/tests/unit/SceneCanvas.test.ts @@ -4,6 +4,8 @@ import { nextTick } from "vue"; import SceneCanvas from "@/components/SceneCanvas.vue"; import DriveControls from "@/components/DriveControls.vue"; import JourneyHud from "@/components/JourneyHud.vue"; +import posterUrl from "@/assets/hero-chromatic.jpg"; +import mobilePosterUrl from "@/assets/hero-chromatic-mobile.jpg"; import type { AudioUiState } from "@/audio/types"; import type { MotionPolicy, QualityTier } from "@/journey/motion-policy"; import type { JourneySnapshot } from "@/journey/types"; @@ -87,15 +89,15 @@ describe("SceneCanvas", () => { expect(wrapper.findAll("canvas")).toHaveLength(0); expect(wrapper.get("img").attributes()).toMatchObject({ alt: "", - width: "1600", + src: posterUrl, + width: "1440", height: "900", }); - expect( - wrapper.findAll(".journey-poster-composition > span"), - ).toHaveLength(7); - expect(wrapper.find(".journey-poster__sun").exists()).toBe(true); - expect(wrapper.find(".journey-poster__road").exists()).toBe(true); - expect(wrapper.find(".journey-poster__car").exists()).toBe(true); + expect(wrapper.get('picture source').attributes()).toMatchObject({ + media: '(max-width: 767px)', + srcset: mobilePosterUrl, + }); + expect(wrapper.get('.journey-world').attributes('data-static-fallback')).toBe('true'); expect(wrapper.text()).toContain( "A decorative night road behind the portfolio content.", ); diff --git a/tests/unit/sign-rendering.test.ts b/tests/unit/sign-rendering.test.ts new file mode 100644 index 0000000..8a6200a --- /dev/null +++ b/tests/unit/sign-rendering.test.ts @@ -0,0 +1,216 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import type { SignSnapshot } from '@/journey/types'; +import { SceneResourceBag } from '@/scene/resources'; +import { createChaseCamera } from '@/scene/world/environment'; +import { createSign, routeLabelFor, type SignPresentation, type SignRenderOptions } from '@/scene/world/sign'; + +interface SignMesh { + position: { z: number }; + userData: { textureUpdateCount: number }; + geometry: { + computeBoundingBox(): void; + boundingBox: { min: { x: number; y: number }; max: { x: number; y: number; z: number } } | null; + getAttribute(name: string): { array: ArrayLike }; + }; + material: { + opacity: number; + depthWrite: boolean; + toneMapped: boolean; + map: { image: { width: number; height: number }; anisotropy: number; premultiplyAlpha: boolean }; + }; +} + +const owned: SceneResourceBag[] = []; +afterEach(() => { owned.splice(0).forEach(resources => resources.dispose()); }); +const profile: SignSnapshot = { targetId: 'profile', phase: 'dwell', progress: 1, opacity: 1 }; + +// Follow the live sequence: update journey state, then compose with the camera, +// viewport and navigation clearance. update() alone does not position a sign. +function setup(width = 1440, height = 900, options: SignRenderOptions = {}) { + const resources = new SceneResourceBag(); + owned.push(resources); + const sign = createSign(resources, options); + const camera = createChaseCamera(width / height); + let now = 0; + let navigationBottom = 68; + sign.setViewportSize(width, height); + return { + resources, sign, camera, + render(snapshot: SignSnapshot | null, frames = 1) { + for (let frame = 0; frame < frames; frame++) { + now += 1000 / 60; + sign.update(snapshot, now); + sign.compose(camera, width, height, navigationBottom, now); + } + return sign.inspect(camera, width, height); + }, + resize(nextWidth: number, nextHeight: number) { + width = nextWidth; + height = nextHeight; + camera.aspect = width / height; + camera.updateProjectionMatrix(); + sign.setViewportSize(width, height); + }, + setNavigationBottom(bottom: number) { navigationBottom = bottom; }, + }; +} + +function expectInViewport(bounds: SignPresentation['bounds'], width: number, height: number, navigationBottom = 68) { + expect(bounds.left).toBeGreaterThanOrEqual(8); + expect(bounds.right).toBeLessThanOrEqual(width - 8); + expect(bounds.top).toBeGreaterThanOrEqual(navigationBottom); + expect(bounds.bottom).toBeLessThanOrEqual(height - 8); + expect(bounds.width).toBeGreaterThan(80); +} + +describe('composed route signs', () => { + it('keeps the readable face in front of its housing with capped, undistorted sampling', () => { + const { sign } = setup(); + const face = sign.group.getObjectByName('sign-face') as SignMesh; + const backplate = sign.group.getObjectByName('sign-backplate') as SignMesh; + backplate.geometry.computeBoundingBox(); + expect(face.position.z - (backplate.position.z + backplate.geometry.boundingBox!.max.z)).toBeGreaterThan(0.05); + face.geometry.computeBoundingBox(); + const { min, max } = face.geometry.boundingBox!; + expect((max.x - min.x) / (max.y - min.y)).toBeCloseTo(4, 6); + const uvs = Array.from(face.geometry.getAttribute('uv').array); + expect(Math.min(...uvs)).toBeGreaterThanOrEqual(0); + expect(Math.max(...uvs)).toBeLessThanOrEqual(1); + expect(face.material.map!.image).toMatchObject({ width: 2048, height: 512 }); + expect(face.material.map!.anisotropy).toBeGreaterThanOrEqual(8); + expect(face.material.map!.premultiplyAlpha).toBe(true); + expect(face.material.depthWrite).toBe(false); + expect(face.material.toneMapped).toBe(false); + }); + + it('fades into view, reuses the active label and resources, and hides when cancelled', () => { + const h = setup(); + const face = h.sign.group.getObjectByName('sign-face') as SignMesh; + const allocation = h.resources.snapshot(); + h.render(null); + expect(h.sign.group.visible).toBe(false); + h.render({ ...profile, phase: 'anticipate', progress: 0, opacity: 0 }); + expect(h.sign.group.visible).toBe(false); + expect(face.userData.textureUpdateCount).toBe(1); + h.render({ ...profile, phase: 'approach', progress: 0.5, opacity: 0.7 }); + expect(h.sign.group.visible).toBe(true); + expect(face.material.opacity).toBeGreaterThan(0); + expect(face.material.opacity).toBeLessThanOrEqual(0.7); + h.render(profile, 120); + expect(face.material.opacity).toBe(1); + expect(face.userData.textureUpdateCount).toBe(1); + expect(h.resources.snapshot()).toEqual(allocation); + h.render(null); + expect(h.sign.group.visible).toBe(false); + h.render({ ...profile, targetId: 'selected-builds', phase: 'anticipate', progress: 0, opacity: 0 }); + expect(face.userData.textureUpdateCount).toBe(2); + expect(routeLabelFor('selected-builds')).toBe('Selected builds'); + expect(h.resources.snapshot()).toEqual(allocation); + h.resources.dispose(); + expect(h.resources.snapshot().owned).toMatchObject({ geometry: 0, material: 0, texture: 0 }); + }); + + it('caps label texture size and anisotropy to the renderer limits', () => { + const h = setup(1440, 900, { maximumAnisotropy: 4, maximumTextureSize: 1024 }); + expect(h.render(profile)!.texture).toMatchObject({ width: 1024, height: 256, maximumSize: 1024, anisotropy: 4 }); + }); + + it('accepts a concealed new target without reallocating its label texture', () => { + const h = setup(); + h.render({ ...profile, phase: 'approach', progress: 0.03, opacity: 0.01 }); + const allocation = h.resources.snapshot(); + const face = h.sign.group.getObjectByName('sign-face') as SignMesh; + const presentation = h.render({ ...profile, targetId: 'experience', phase: 'anticipate', progress: 0, opacity: 0 }); + expect(face.userData.textureUpdateCount).toBe(2); + expect(presentation!.corridor).toBe('right'); + expect(presentation!.visible).toBe(false); + expect(h.resources.snapshot()).toEqual(allocation); + }); + + it('eases between roadside and compact layouts over successive rendered frames', () => { + const h = setup(); + h.render(profile, 120); + const origin = h.sign.group.position.clone(); + h.resize(844, 390); + h.render(profile); + const first = h.sign.group.position.clone(); + h.render(profile, 10); + const midpoint = h.sign.group.position.clone(); + const final = h.render(profile, 120)!; + const destination = h.sign.group.position.clone(); + expect(first.distanceTo(origin)).toBeGreaterThan(0); + expect(first.distanceTo(origin)).toBeLessThan(destination.distanceTo(origin) / 2); + expect(midpoint.distanceTo(destination)).toBeLessThan(first.distanceTo(destination)); + expect(h.sign.group.userData.anchoring).toBe('compact-shelf'); + expectInViewport(final.bounds, 844, 390); + h.resize(1440, 900); + h.render(profile, 120); + expect(h.sign.group.userData.anchoring).toBe('roadside'); + expect(h.sign.group.position.x).toBeCloseTo(origin.x, 2); + }); + + it('moves the compact shelf below a taller navigator without a one-frame teleport', () => { + const h = setup(390, 844); + const original = h.render(profile, 60)!.bounds.top; + h.setNavigationBottom(180); + const first = h.render({ ...profile, navigationArrival: true })!.bounds.top; + const settled = h.render({ ...profile, navigationArrival: true }, 120)!.bounds; + expect(first).toBeGreaterThan(original); + expect(first - original).toBeLessThan((settled.top - original) / 2); + expectInViewport(settled, 390, 844, 180); + }); + + it('drives past a roadside sign along the road axis at constant physical scale', () => { + const h = setup(); + const dwell = h.render(profile, 120)!; + const position = h.sign.group.position.clone(); + const scale = h.sign.group.scale.x; + const passing = h.render({ ...profile, phase: 'depart', progress: 0.5 })!; + expect(h.sign.group.position.x).toBeCloseTo(position.x, 6); + expect(h.sign.group.position.y).toBeCloseTo(position.y, 6); + expect(h.sign.group.position.z).toBeGreaterThan(position.z); + expect(h.sign.group.scale.x).toBeCloseTo(scale, 6); + expect(passing.bounds.width).toBeGreaterThan(dwell.bounds.width); + const passingZ = h.sign.group.position.z; + h.render({ ...profile, phase: 'depart', progress: 0.9, opacity: 0.1 }); + expect(h.sign.group.position.z).toBeGreaterThan(passingZ); + expect(h.sign.group.scale.x).toBeCloseTo(scale, 6); + h.render(null); + expect(h.sign.group.visible).toBe(false); + }); + + it.each([[390, 844], [844, 390], [1440, 900]])('keeps both sign supports on the ground at %ix%i', (width, height) => { + const h = setup(width, height); + h.render(profile); + const footing = h.sign.group.getObjectByName('sign-footing') as { position: { y: number }; children: unknown[] }; + expect(h.sign.group.position.y + footing.position.y * h.sign.group.scale.x).toBeCloseTo(0, 6); + expect(footing.children).toHaveLength(2); + }); + + it.each([[780, 493], [844, 390]])('keeps short-landscape signs in the right-side shelf at %ix%i', (width, height) => { + const h = setup(width, height); + const presentation = h.render(profile, 60)!; + expectInViewport(presentation.bounds, width, height); + expect(presentation.bounds.left).toBeGreaterThan(width / 2); + expect(h.sign.group.userData.anchoring).toBe('compact-shelf'); + }); + + it.each([[320, 640], [390, 844], [692, 793], [1024, 768]])('keeps a readable compact navigator sign inside %ix%i', (width, height) => { + const h = setup(width, height); + const presentation = h.render({ ...profile, navigationArrival: true }, 60)!; + expectInViewport(presentation.bounds, width, height); + expect(presentation.bounds.width).toBeGreaterThan(200); + expect(presentation.bounds.width).toBeLessThan(380); + expect(h.sign.group.userData.anchoring).toBe('compact-shelf'); + }); + + it('anchors opposite chapter signs on opposite roadsides and clears the car envelope', () => { + const h = setup(); + h.render(profile, 120); + expect(h.sign.group.position.x).toBeLessThan(-11.6); + h.render(null); + h.render({ ...profile, targetId: 'experience' }, 120); + expect(h.sign.group.position.x).toBeGreaterThan(11.6); + expect(h.sign.group.userData.anchoring).toBe('roadside'); + }); +}); diff --git a/tests/unit/world-models.test.ts b/tests/unit/world-models.test.ts index ab9fc2e..c3f99a3 100644 --- a/tests/unit/world-models.test.ts +++ b/tests/unit/world-models.test.ts @@ -19,7 +19,6 @@ import { HORIZON_FOG_END, HORIZON_FOG_START, } from "@/scene/world/metrics"; -import { createSign, routeLabelFor } from "@/scene/world/sign"; import { createDriveDynamics } from "@/scene/drive-dynamics"; import { createTravelDistance } from "@/scene/world/travel-distance"; @@ -195,7 +194,7 @@ describe("procedural world models", () => { expect(target.warmth).toBeLessThan(CHAPTER_PALETTES.destination.warmth); }); - it("renders the sun as a scan-banded disc with a separately owned halo", () => { + it("updates the sun's palette and motion inputs without reallocating its disc and halo", () => { const resources = new SceneResourceBag(); const sun = createSun(resources); const disc = sun.group.getObjectByName("horizon-sun") as ShaderRenderable; @@ -214,18 +213,24 @@ describe("procedural world models", () => { uTime: { value: 0 }, uDriveEnergy: { value: 0 }, }); - expect(disc.material.fragmentShader).toContain("scanBand"); expect(resources.snapshot().owned.texture).toBe(1); + const allocation = resources.snapshot(); + const color = disc.material.uniforms.uColor!.value as { getHex(): number }; + const initialColor = color.getHex(); const palette = createMutableChapterPalette("destination"); sun.update(palette, 1, 3.2, 1); expect(disc.material.uniforms.uWarmth?.value).toBe(1); expect(disc.material.uniforms.uRouteProgress?.value).toBe(1); expect(disc.material.uniforms.uDriveEnergy?.value).toBe(1); + expect(disc.material.uniforms.uTime?.value).toBe(3.2); + expect(disc.material.uniforms.uColor?.value).toBe(color); + expect(color.getHex()).not.toBe(initialColor); + expect(resources.snapshot()).toEqual(allocation); resources.dispose(); }); - it("builds deterministic stars, haze layers, and quality-capped speed streaks", () => { + it("builds deterministic stars, a palette-driven sky, and quality-capped speed streaks", () => { const highResources = new SceneResourceBag(); const lowResources = new SceneResourceBag(); const highQuality = renderQualityFor("high", 1, false); @@ -261,33 +266,20 @@ describe("procedural world models", () => { expect(lowStreaks.geometry.getAttribute("position").count).toBe( lowQuality.streakCount * 2, ); - expect(high.group.getObjectByName("horizon-haze")).toBeInstanceOf( - THREE.Group, - ); const sky = high.group.getObjectByName("atmosphere-sky") as ShaderRenderable & { frustumCulled: boolean; }; sky.geometry.computeBoundingBox(); expect(sky.geometry.boundingBox?.min.x).toBeCloseTo(-1, 6); expect(sky.geometry.boundingBox?.max.x).toBeCloseTo(1, 6); - expect(sky.material.vertexShader).toContain( - "gl_Position = vec4(position.xy, 1.0, 1.0)", - ); expect(sky.frustumCulled).toBe(false); expect(sky.material.depthTest).toBe(false); - const haze = high.group.getObjectByName( - "horizon-haze-near", - ) as ShaderRenderable; - expect(haze.material.vertexShader).toContain( - "gl_Position = vec4(position.xy, 1.0, 1.0)", - ); + const hazeColor = sky.material.uniforms.uHazeColor!.value as { getHex(): number }; + const initialHaze = hazeColor.getHex(); expect( (high.group.getObjectByName("distant-silhouettes") as { children: unknown[] }) .children, ).toHaveLength(3); - expect(high.group.getObjectByName("roadside-anchors")).toBeInstanceOf( - THREE.InstancedMesh, - ); const highStarPositions = Array.from( highStars.geometry.getAttribute("position").array, @@ -307,6 +299,9 @@ describe("procedural world models", () => { high.update(index, 0.7, 0.8, palette); } expect(highResources.snapshot()).toEqual(before); + expect(sky.material.uniforms.uHazeColor!.value).toBe(hazeColor); + expect(hazeColor.getHex()).toBe(palette.haze.getHex()); + expect(hazeColor.getHex()).not.toBe(initialHaze); expect(highStreaks.material.uniforms.uAcceleration?.value).toBe(0.8); expect(highStreaks.material.uniforms.uVisibility?.value).toBeGreaterThan(0); @@ -400,7 +395,14 @@ describe("procedural world models", () => { ); processor.resize(1_200, 800, 1.25); + processor.setDynamics(0); + const restingBloom = { strength: bloomPass.strength, threshold: bloomPass.threshold }; processor.setDynamics(0.75); + expect(bloomPass.strength).toBeGreaterThan(restingBloom.strength); + expect(bloomPass.threshold).toBeLessThan(restingBloom.threshold); + processor.setDynamics(0); + expect(bloomPass.strength).toBe(restingBloom.strength); + expect(bloomPass.threshold).toBe(restingBloom.threshold); processor.render({}, {}); const allocations = resources.gpuBudgetSnapshot({ tier: "high", @@ -436,7 +438,6 @@ describe("procedural world models", () => { expect(bloomPass.dispose).toHaveBeenCalledOnce(); expect(outputPass.dispose).toHaveBeenCalledOnce(); expect(composer.dispose).toHaveBeenCalledOnce(); - expect(bloomPass.strength).toBeGreaterThan(0.54); }); it("keeps the grand tourer anatomy readable in desktop and portrait chase views", () => { @@ -458,7 +459,6 @@ describe("procedural world models", () => { } const desktopCamera = createChaseCamera(16 / 9); - expect((desktopCamera as { fov: number }).fov).toBe(66); const desktop = projectedBounds(car.group, desktopCamera); expect(desktop.maxX - desktop.minX).toBeGreaterThan(0.28); expect(desktop.minX).toBeGreaterThan(-0.96); @@ -467,7 +467,13 @@ describe("procedural world models", () => { expect(desktop.maxY).toBeLessThan(-0.05); car.setDetail("reduced"); - const portrait = projectedBounds(car.group, createChaseCamera(390 / 844)); + // The live scene recenters and pulls the lens back on portrait displays. + // Use that settled framing fixture, not the initial desktop chase pose. + const portraitCamera = new THREE.PerspectiveCamera(61, 390 / 844, 0.1, 1200); + portraitCamera.position.set(0, 6.5, 31); + portraitCamera.lookAt(0, 3.5, -88); + portraitCamera.setViewOffset(390, 844, 0, -844 * 0.16, 390, 844); + const portrait = projectedBounds(car.group, portraitCamera); expect(portrait.minX).toBeGreaterThan(-0.99); expect(portrait.maxX).toBeLessThan(0.99); expect(portrait.minY).toBeGreaterThan(-0.99); @@ -505,11 +511,14 @@ describe("procedural world models", () => { .rotation.y, ).toBeCloseTo(Math.PI, 6); expect(car.group.getObjectByName("car-body")).toBeDefined(); - expect( - (car.group.getObjectByName("car-body") as { - material: { emissive: { getHex(): number } }; - }).material.emissive.getHex(), - ).toBe(0x0b2738); + const body = car.group.getObjectByName("car-body") as { + material: { emissive: { getHex(): number }; emissiveIntensity: number }; + }; + expect(body.material.emissive.getHex()).not.toBe(0); + expect(body.material.emissiveIntensity).toBeGreaterThan(0); + expect(body.material.emissiveIntensity).toBeLessThan(1); + const material = body.material; + const allocation = resources.snapshot(); expect(car.group.getObjectByName("rear-glass")).toBeDefined(); expect(car.group.getObjectByName("rear-light-bar")).toBeDefined(); expect(car.group.getObjectByName("car-underglow")).toBeDefined(); @@ -519,6 +528,8 @@ describe("procedural world models", () => { car.update(0.8, 0.7, frame / 60); } expect((car.group as { children: unknown[] }).children).toHaveLength(childCount); + expect(body.material).toBe(material); + expect(resources.snapshot()).toEqual(allocation); expect(resources.snapshot().owned.geometry).toBeGreaterThanOrEqual(6); resources.dispose(); }); @@ -560,471 +571,6 @@ describe("procedural world models", () => { resources.dispose(); }); - it("renders one mounted roadside sign without mutating a visible label", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - for (const name of [ - "sign-backplate", - "sign-face-panel", - "sign-face", - "sign-housing", - "sign-edge-light", - "sign-post", - "sign-post-secondary", - "sign-bracket", - "sign-back-rails", - "sign-fasteners", - "sign-footing", - ]) { - expect(sign.group.getObjectByName(name), name).toBeDefined(); - } - const face = sign.group.getObjectByName("sign-face") as { - userData: { textureUpdateCount: number }; - material: { - depthWrite: boolean; - opacity: number; - toneMapped: boolean; - map: { - anisotropy: number; - image: { width: number; height: number }; - premultiplyAlpha: boolean; - }; - }; - geometry: { - boundingBox: { min: { x: number; y: number }; max: { x: number; y: number } } | null; - computeBoundingBox(): void; - getAttribute(name: string): { array: ArrayLike }; - }; - position: { z: number }; - }; - const backplate = sign.group.getObjectByName("sign-backplate") as { - geometry: { - boundingBox: { max: { z: number } } | null; - computeBoundingBox(): void; - }; - position: { z: number }; - }; - backplate.geometry.computeBoundingBox(); - const backplateFront = backplate.position.z + (backplate.geometry.boundingBox?.max.z ?? 0); - expect(face.position.z - backplateFront).toBeGreaterThan(0.05); - const faceUvs = Array.from(face.geometry.getAttribute("uv").array); - expect(Math.min(...faceUvs)).toBeGreaterThanOrEqual(0); - expect(Math.max(...faceUvs)).toBeLessThanOrEqual(1); - face.geometry.computeBoundingBox(); - const faceWidth = (face.geometry.boundingBox?.max.x ?? 0) - (face.geometry.boundingBox?.min.x ?? 0); - const faceHeight = (face.geometry.boundingBox?.max.y ?? 0) - (face.geometry.boundingBox?.min.y ?? 0); - expect(faceWidth / faceHeight).toBeCloseTo(4, 6); - expect(face.material.map.image).toMatchObject({ width: 2_048, height: 512 }); - expect(face.material.map.anisotropy).toBeGreaterThanOrEqual(8); - expect(face.material.map.premultiplyAlpha).toBe(true); - expect(face.material.depthWrite).toBe(false); - expect(face.material.toneMapped).toBe(false); - expect(face.material).toBeInstanceOf(THREE.MeshBasicMaterial); - const facePanel = sign.group.getObjectByName("sign-face-panel") as { material: unknown }; - expect(facePanel.material).toBeInstanceOf(THREE.MeshStandardMaterial); - const allocation = resources.snapshot(); - let renderablePartCount = 0; - (sign.group as unknown as { - traverse(callback: (object: { isMesh?: boolean }) => void): void; - }).traverse((object) => { - if (object.isMesh === true) renderablePartCount += 1; - }); - expect(renderablePartCount).toBe(20); - expect(allocation.owned).toMatchObject({ - geometry: 13, - material: 7, - texture: 1, - }); - - sign.update(null); - expect((sign.group as { visible: boolean }).visible).toBe(false); - sign.update({ - targetId: "infrastructure", - phase: "anticipate", - progress: 0, - opacity: 0, - }); - expect((sign.group as { visible: boolean }).visible).toBe(false); - expect(face.userData.textureUpdateCount).toBe(1); - expect(routeLabelFor("infrastructure")).toBe("Infrastructure"); - - sign.update({ - targetId: "infrastructure", - phase: "approach", - progress: 0.5, - opacity: 0.7, - }); - expect((sign.group as { visible: boolean }).visible).toBe(true); - expect(face.userData.textureUpdateCount).toBe(1); - expect(face.material.opacity).toBeCloseTo(0.7, 6); - - const visiblePosition = sign.group.position.clone(); - const visibleScale = sign.group.scale.x; - sign.setViewportAspect(0.6); - sign.update({ - targetId: "selected-builds", - phase: "dwell", - progress: 1, - opacity: 1, - }); - expect(face.userData.textureUpdateCount).toBe(1); - expect(routeLabelFor("selected-builds")).toBe("Selected builds"); - expect(sign.group.position).toEqual(visiblePosition); - expect(sign.group.scale.x).toBeCloseTo(visibleScale, 6); - - sign.update(null); - sign.update({ - targetId: "selected-builds", - phase: "anticipate", - progress: 0, - opacity: 0, - }); - expect(face.userData.textureUpdateCount).toBe(2); - expect(resources.snapshot()).toEqual(allocation); - resources.dispose(); - }); - - it("caps the sign texture to the renderer limit without changing its four-to-one sampling ratio", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources, { - maximumAnisotropy: 4, - maximumTextureSize: 1_024, - }); - sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }); - - expect(sign.inspect(createChaseCamera(16 / 9), 1_440, 900)?.texture).toMatchObject({ - width: 1_024, - height: 256, - maximumSize: 1_024, - anisotropy: 4, - }); - resources.dispose(); - }); - - it("accepts a new target when the previous face is below the concealed threshold", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - const face = sign.group.getObjectByName("sign-face") as { - userData: { textureUpdateCount: number }; - }; - - sign.update({ - targetId: "profile", - phase: "approach", - progress: 0.03, - opacity: 0.01, - }); - expect(face.userData.textureUpdateCount).toBe(1); - - sign.update({ - targetId: "experience", - phase: "anticipate", - progress: 0, - opacity: 0, - }); - - expect(face.userData.textureUpdateCount).toBe(2); - expect(sign.inspect(createChaseCamera(16 / 9), 1_440, 900)?.corridor).toBe("right"); - resources.dispose(); - }); - - it("begins sign departure from the rendered approach pose without a teleport", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - sign.setViewportAspect(16 / 9); - sign.update({ - targetId: "profile", - phase: "approach", - progress: 0.62, - opacity: 1, - }); - const approachPosition = sign.group.position.clone(); - const approachScale = sign.group.scale.x; - - sign.update({ - targetId: "profile", - phase: "depart", - progress: 0, - opacity: 1, - }); - - expect(sign.group.position.distanceTo(approachPosition)).toBeLessThan(0.001); - expect(sign.group.scale.x).toBeCloseTo(approachScale, 6); - resources.dispose(); - }); - - it("reflows a live sign to a new responsive pose without a resize teleport", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - sign.setViewportSize(1_440, 900); - const dwell = { targetId: "profile", phase: "dwell", progress: 1, opacity: 1 } as const; - sign.update(dwell, 0); - const resizeOrigin = sign.group.position.clone(); - - sign.setViewportSize(844, 390); - sign.update(dwell, 100); - expect(sign.group.position.distanceTo(resizeOrigin)).toBeLessThan(0.001); - sign.update(dwell, 260); - const midpoint = sign.group.position.clone(); - expect(midpoint.distanceTo(resizeOrigin)).toBeGreaterThan(0.1); - sign.update(dwell, 450); - expect(sign.group.position.distanceTo(midpoint)).toBeGreaterThan(0.1); - - sign.update(null); - sign.update({ targetId: "experience", phase: "anticipate", progress: 0, opacity: 0 }); - const camera = createChaseCamera(844 / 390); - expect(sign.inspect(camera, 844, 390)?.viewportProfile).toBe("short"); - resources.dispose(); - - const rebuiltResources = new SceneResourceBag(); - const rebuilt = createSign(rebuiltResources); - rebuilt.setViewportSize(844, 390); - rebuilt.update({ - targetId: "profile", - phase: "dwell", - progress: 1, - opacity: 1, - viewportProfile: "wide", - }); - expect(rebuilt.inspect(camera, 844, 390)?.viewportProfile).toBe("short"); - rebuiltResources.dispose(); - }); - - it("reflows a visible portrait sign into its navigator-safe pose without a teleport", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - const camera = createChaseCamera(390 / 844); - sign.setViewportSize(390, 844); - const passiveDwell = { - targetId: "profile", - phase: "dwell", - progress: 1, - opacity: 1, - } as const; - sign.update(passiveDwell, 0); - const passivePosition = sign.group.position.clone(); - - const navigationDwell = { ...passiveDwell, navigationArrival: true as const }; - sign.update(navigationDwell, 100); - expect(sign.group.position.distanceTo(passivePosition)).toBeLessThan(0.001); - sign.update(navigationDwell, 260); - const midpoint = sign.group.position.clone(); - expect(midpoint.distanceTo(passivePosition)).toBeGreaterThan(0.1); - sign.update(navigationDwell, 450); - - const presentation = sign.inspect(camera, 390, 844); - if (presentation === null) throw new Error("Missing navigator-safe Profile sign"); - expect(sign.group.position.distanceTo(midpoint)).toBeGreaterThan(0.1); - expect(presentation.bounds.top).toBeGreaterThanOrEqual(626); - expect(presentation.bounds.bottom).toBeLessThanOrEqual(690); - resources.dispose(); - }); - - it("drives past a sign at constant world scale before carrying it beyond the roadside edge", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - const camera = createChaseCamera(16 / 9); - sign.setViewportSize(1_440, 900); - sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }); - const dwell = sign.inspect(camera, 1_440, 900); - if (dwell === null) throw new Error("Missing Profile dwell presentation"); - const dwellPosition = sign.group.position.clone(); - const dwellScale = sign.group.scale.x; - - sign.update({ targetId: "profile", phase: "depart", progress: 0.5, opacity: 1 }); - const passing = sign.inspect(camera, 1_440, 900); - if (passing === null) throw new Error("Missing Profile pass-by presentation"); - - expect(sign.group.position.x).toBeLessThan(dwellPosition.x); - expect(sign.group.position.y).toBeCloseTo(dwellPosition.y, 6); - expect(sign.group.position.z).toBeGreaterThan(dwellPosition.z); - expect(sign.group.scale.x).toBeCloseTo(dwellScale, 6); - expect(passing.bounds.width).toBeGreaterThan(dwell.bounds.width); - expect(passing.bounds.left).toBeLessThan(dwell.bounds.left); - - const passingZ = sign.group.position.z; - sign.update({ targetId: "profile", phase: "depart", progress: 0.9, opacity: 0.1 }); - const leaving = sign.inspect(camera, 1_440, 900); - if (leaving === null) throw new Error("Missing Profile departure presentation"); - expect(sign.group.position.z).toBeGreaterThan(passingZ); - expect(sign.group.position.z).toBeLessThan(camera.position.z - 2); - expect(sign.group.scale.x).toBeCloseTo(dwellScale, 6); - expect(leaving.bounds.right).toBeLessThan(passing.bounds.right); - resources.dispose(); - }); - - it.each([ - [390, 844], - [844, 390], - [1_440, 900], - ])("keeps the responsive sign footing planted at %ix%i", (width, height) => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - sign.setViewportSize(width, height); - sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }); - const footing = sign.group.getObjectByName("sign-footing") as { position: { y: number } }; - const footingWorldY = sign.group.position.y + footing.position.y * sign.group.scale.x; - - expect(footingWorldY).toBeGreaterThanOrEqual(-0.5); - expect(footingWorldY).toBeLessThanOrEqual(0.5); - resources.dispose(); - }); - - it("keeps a short-landscape left sign inside the narrow desktop roadside gutter", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - const camera = createChaseCamera(780 / 493); - camera.position.set(-1.35, 8.6, 22); - camera.lookAt(1.6, 4.8, -128); - sign.setViewportSize(780, 493); - sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }); - - const presentation = sign.inspect(camera, 780, 493); - if (presentation === null) throw new Error("Missing short-landscape Profile sign"); - expect(presentation.bounds.left).toBeGreaterThanOrEqual(8); - expect(presentation.bounds.right).toBeLessThanOrEqual(265); - resources.dispose(); - }); - - it("keeps a short-landscape left sign horizontally clear at 844 by 390", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - const camera = createChaseCamera(844 / 390); - camera.position.set(-1.35, 8.6, 22); - camera.lookAt(1.6, 4.8, -128); - sign.setViewportSize(844, 390); - sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }); - - const presentation = sign.inspect(camera, 844, 390); - if (presentation === null) throw new Error("Missing 844 by 390 Profile sign"); - expect(presentation.bounds.left).toBeGreaterThanOrEqual(8); - expect(presentation.bounds.right).toBeLessThanOrEqual(288); - resources.dispose(); - }); - - it("places a portrait navigator arrival below its full-width destination panel", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - const camera = createChaseCamera(390 / 844); - sign.setViewportSize(390, 844); - sign.update({ - targetId: "profile", - phase: "dwell", - progress: 1, - opacity: 1, - navigationArrival: true, - }); - - const presentation = sign.inspect(camera, 390, 844); - if (presentation === null) throw new Error("Missing portrait navigator sign"); - expect(presentation.bounds.left).toBeGreaterThanOrEqual(8); - expect(presentation.bounds.top).toBeGreaterThanOrEqual(626); - expect(presentation.bounds.right).toBeLessThanOrEqual(382); - expect(presentation.bounds.bottom).toBeLessThanOrEqual(690); - expect(presentation.bounds.width).toBeGreaterThanOrEqual(85); - resources.dispose(); - }); - - it("grows a medium portrait navigator sign without enlarging the phone pose", () => { - const phoneResources = new SceneResourceBag(); - const phoneSign = createSign(phoneResources); - const phoneCamera = createChaseCamera(390 / 844); - phoneSign.setViewportSize(390, 844); - phoneSign.update({ - targetId: "profile", - phase: "dwell", - progress: 1, - opacity: 1, - navigationArrival: true, - }); - const phone = phoneSign.inspect(phoneCamera, 390, 844); - if (phone === null) throw new Error("Missing phone navigator sign"); - - const mediumResources = new SceneResourceBag(); - const mediumSign = createSign(mediumResources); - const mediumCamera = createChaseCamera(692 / 793); - mediumSign.setViewportSize(692, 793); - mediumSign.update({ - targetId: "profile", - phase: "dwell", - progress: 1, - opacity: 1, - navigationArrival: true, - }); - const medium = mediumSign.inspect(mediumCamera, 692, 793); - if (medium === null) throw new Error("Missing medium navigator sign"); - - expect(phone.bounds.width).toBeGreaterThanOrEqual(85); - expect(phone.bounds.width).toBeLessThanOrEqual(110); - expect(medium.viewportProfile).toBe("tall"); - expect(medium.bounds.width).toBeGreaterThanOrEqual(145); - expect(medium.bounds.width).toBeLessThanOrEqual(210); - expect(medium.bounds.width).toBeGreaterThanOrEqual(phone.bounds.width * 1.45); - expect(medium.bounds.top).toBeLessThanOrEqual(555); - expect(medium.bounds.bottom).toBeLessThanOrEqual(590); - - phoneResources.dispose(); - mediumResources.dispose(); - }); - - it("lands a resized Profile lifecycle in the short-landscape safe corridor", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - sign.setViewportSize(1_440, 900); - sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }); - - sign.setViewportSize(844, 390); - sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }, 100); - sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }, 450); - const camera = createChaseCamera(844 / 390); - camera.position.set(-1.35, 8.6, 22); - camera.lookAt(1.6, 4.8, -128); - const presentation = sign.inspect(camera, 844, 390); - if (presentation === null) throw new Error("Missing resized Profile sign"); - expect(presentation.viewportProfile).toBe("short"); - expect(presentation.bounds.left).toBeGreaterThanOrEqual(8); - expect(presentation.bounds.right).toBeLessThanOrEqual(288); - resources.dispose(); - }); - - it("places route signs in their authored transition-safe corridors", () => { - const resources = new SceneResourceBag(); - const sign = createSign(resources); - sign.setViewportAspect(16 / 9); - - sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 }); - expect(sign.group.position.x).toBeLessThan(0); - const profileFace = sign.group.getObjectByName("sign-face"); - const camera = createChaseCamera(16 / 9); - camera.position.set(-1.35, 8.6, 22); - camera.lookAt(1.6, 4.8, -128); - sign.group.updateMatrixWorld(true); - const profileBounds = projectedBounds(profileFace, camera); - expect(profileBounds.maxX).toBeLessThan(0.08); - - sign.update(null); - sign.update({ targetId: "experience", phase: "dwell", progress: 1, opacity: 1 }); - sign.group.updateMatrixWorld(true); - const experienceBounds = projectedBounds(profileFace, camera); - const experiencePresentation = sign.inspect(camera, 1_440, 900); - expect(experienceBounds.minX).toBeGreaterThan(0); - expect(experiencePresentation?.corridor).toBe("right"); - expect(experiencePresentation?.bounds.top).toBeGreaterThanOrEqual(540); - expect(experiencePresentation?.bounds.bottom).toBeLessThanOrEqual(630); - - sign.update(null); - sign.setViewportSize(844, 390); - sign.update({ targetId: "experience", phase: "dwell", progress: 1, opacity: 1 }); - const shortCamera = createChaseCamera(844 / 390); - shortCamera.position.set(-1.35, 8.6, 22); - shortCamera.lookAt(1.6, 4.8, -128); - const shortExperience = sign.inspect(shortCamera, 844, 390); - expect(shortExperience?.corridor).toBe("right"); - expect(shortExperience?.bounds.left).toBeGreaterThan(422); - expect(shortExperience?.bounds.top).toBeGreaterThanOrEqual(210); - expect(shortExperience?.bounds.bottom).toBeLessThanOrEqual(300); - resources.dispose(); - }); }); function paletteSnapshot(