feat: integrate the cinematic nightdrive scene

This commit is contained in:
2026-09-02 15:14:19 +03:00
parent e4c832356b
commit 8deb56ed0c
8 changed files with 257 additions and 55 deletions

View File

@@ -89,7 +89,7 @@ test('draws only the independently eligible next roadside sign during passive fo
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
const headingTop = profile.getBoundingClientRect().top + window.scrollY;
const destination = Math.max(1, headingTop - viewportHeight * (0.35 + 0.8));
return { destination, viewportHeight, expectedLabel: '01 / PROFILE' };
return { destination, viewportHeight, expectedLabel: 'Profile' };
});
await page.evaluate(() => window.dispatchEvent(new WheelEvent('wheel', { deltaY: 1 })));
for (let y = 1; y < oracle.destination; y += oracle.viewportHeight * 0.5) {

View File

@@ -78,13 +78,23 @@ export async function installSceneInstrumentation(page: Page): Promise<void> {
}, { once: true });
const nativeFillText = CanvasRenderingContext2D.prototype.fillText;
const routeLabels = new Set([
'Start',
'Profile',
'Experience',
'Infrastructure',
'Selected builds',
'Additional work',
'Toolkit',
'Contact',
]);
CanvasRenderingContext2D.prototype.fillText = function fillText(
text: string,
x: number,
y: number,
maxWidth?: number,
): void {
if (/^0[0-7] \/ /u.test(text)) state.routeLabelsDrawn.push(text);
if (routeLabels.has(text)) state.routeLabelsDrawn.push(text);
if (maxWidth === undefined) nativeFillText.call(this, text, x, y);
else nativeFillText.call(this, text, x, y, maxWidth);
};

View File

@@ -192,9 +192,10 @@ describe('scene render quality', () => {
});
it('keeps route sign labels canonical and compact', () => {
expect(routeLabelFor('start')).toBe('00 / START');
expect(routeLabelFor('infrastructure')).toBe('03 / INFRASTRUCTURE');
expect(routeLabelFor('contact')).toBe('07 / DESTINATION');
expect(routeLabelFor('start')).toBe('Start');
expect(routeLabelFor('infrastructure')).toBe('Infrastructure');
expect(routeLabelFor('selected-builds')).toBe('Selected builds');
expect(routeLabelFor('contact')).toBe('Contact');
});
it('keeps the chase car recognizably wide and fully inside the desktop view', () => {

View File

@@ -15,6 +15,7 @@ import { createAtmosphere } from '@/scene/world/atmosphere';
import { createPostProcessor } from '@/scene/post-processing';
import { createCar } from '@/scene/world/car';
import { createChaseCamera } from '@/scene/world/environment';
import { createSign, routeLabelFor } from '@/scene/world/sign';
describe('procedural world models', () => {
it('scales geometry and atmosphere work monotonically by live quality tier', () => {
@@ -299,6 +300,61 @@ describe('procedural world models', () => {
expect(resources.snapshot()).toEqual(allocation);
resources.dispose();
});
it('renders one physical roadside sign and updates its plain label once per target', () => {
const resources = new SceneResourceBag();
const sign = createSign(resources);
for (const name of [
'sign-backplate',
'sign-face',
'sign-frame',
'sign-post-left',
'sign-post-right',
]) {
expect(sign.group.getObjectByName(name), name).toBeDefined();
}
const face = sign.group.getObjectByName('sign-face') as {
userData: { textureUpdateCount: number };
material: { opacity: number };
};
const allocation = resources.snapshot();
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 desktopX = (sign.group as { position: { x: number } }).position.x;
sign.setViewportAspect(0.6);
sign.update({
targetId: 'selected-builds',
phase: 'dwell',
progress: 1,
opacity: 1,
});
expect(face.userData.textureUpdateCount).toBe(2);
expect(routeLabelFor('selected-builds')).toBe('Selected builds');
expect((sign.group as { position: { x: number } }).position.x).toBeLessThan(desktopX);
expect(resources.snapshot()).toEqual(allocation);
resources.dispose();
});
});
function paletteSnapshot(palette: ReturnType<typeof createMutableChapterPalette>) {