fix: make nightdrive signage reliable and legible

This commit is contained in:
2026-09-03 02:34:15 +03:00
parent a03d4d8dba
commit cce018d3cb
39 changed files with 4635 additions and 196 deletions

View File

@@ -576,8 +576,15 @@ describe("procedural world models", () => {
}
const face = sign.group.getObjectByName("sign-face") as {
userData: { textureUpdateCount: number };
material: { opacity: number };
geometry: { getAttribute(name: string): { array: ArrayLike<number> } };
material: {
opacity: number;
map: { anisotropy: number; image: { width: number; height: number } };
};
geometry: {
boundingBox: { min: { x: number; y: number }; max: { x: number; y: number } } | null;
computeBoundingBox(): void;
getAttribute(name: string): { array: ArrayLike<number> };
};
position: { z: number };
};
const backplate = sign.group.getObjectByName("sign-backplate") as {
@@ -593,6 +600,12 @@ describe("procedural world models", () => {
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);
const allocation = resources.snapshot();
sign.update(null);
@@ -667,6 +680,94 @@ describe("procedural world models", () => {
expect(sign.group.scale.x).toBeCloseTo(approachScale, 6);
resources.dispose();
});
it("defers responsive pose changes for the complete lifecycle, including concealed anticipation", () => {
const resources = new SceneResourceBag();
const sign = createSign(resources);
const camera = createChaseCamera(16 / 9);
sign.setViewportSize(1_440, 900);
sign.update({ targetId: "profile", phase: "anticipate", progress: 0, opacity: 0 });
sign.setViewportSize(844, 390);
const concealed = sign.inspect(camera, 844, 390);
expect(concealed?.viewportProfile).toBe("wide");
sign.update({ targetId: "profile", phase: "approach", progress: 0.5, opacity: 0.7 });
expect(sign.inspect(camera, 844, 390)?.viewportProfile).toBe("wide");
sign.update(null);
sign.update({ targetId: "experience", phase: "anticipate", progress: 0, opacity: 0 });
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("wide");
rebuiltResources.dispose();
});
it("departs laterally without crossing the chase camera plane or growing toward the car", () => {
const resources = new SceneResourceBag();
const sign = createSign(resources);
sign.setViewportSize(1_440, 900);
sign.update({ targetId: "profile", phase: "dwell", progress: 1, opacity: 1 });
const dwellX = sign.group.position.x;
const dwellScale = sign.group.scale.x;
sign.update({ targetId: "profile", phase: "depart", progress: 0.9, opacity: 0.1 });
expect(sign.group.position.x).toBeLessThan(dwellX);
expect(sign.group.position.z).toBeLessThanOrEqual(0);
expect(sign.group.scale.x).toBeLessThan(dwellScale);
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("places route signs in the corridor opposite their destination content", () => {
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);
expect(experienceBounds.minX).toBeGreaterThan(0);
resources.dispose();
});
});
function paletteSnapshot(