fix: make nightdrive signage reliable and legible

This commit is contained in:
2026-09-03 17:07:00 +03:00
parent 4404c07f93
commit b075041918
50 changed files with 4435 additions and 200 deletions

View File

@@ -198,10 +198,18 @@ test("explicit keyboard navigation focuses a section heading without a fixed-sur
await page.keyboard.press("Enter");
await expect(page.locator("#contact-heading")).toBeFocused();
const geometry = await currentFocusGeometry(page);
expect(geometry.focusVisible).toBe(true);
expect(geometry.insideSafeBounds).toBe(true);
expect(geometry.overlapArea).toBe(0);
await expect.poll(async () => {
const geometry = await currentFocusGeometry(page);
return {
focusVisible: geometry.focusVisible,
insideSafeBounds: geometry.insideSafeBounds,
overlapArea: geometry.overlapArea,
};
}).toEqual({
focusVisible: true,
insideSafeBounds: true,
overlapArea: 0,
});
});
test("responsive navigation exposes the exact visible label as each link's accessible name", async ({

View File

@@ -83,14 +83,19 @@ test('draws only the independently eligible next roadside sign during passive fo
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
const oracle = await page.evaluate(() => {
const viewport = page.viewportSize();
if (viewport === null) throw new Error('Missing browser viewport');
const oracle = await page.evaluate((candidateDistanceVh) => {
const profile = document.getElementById('profile-heading');
if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading');
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
const headingTop = profile.getBoundingClientRect().top + window.scrollY;
const destination = Math.max(1, headingTop - viewportHeight * (0.35 + 0.45));
const destination = Math.max(
1,
headingTop - viewportHeight * (0.35 + candidateDistanceVh),
);
return { destination, viewportHeight, expectedLabel: 'Profile' };
});
}, triggerProbeDistanceVh(viewport.width, viewport.height));
await page.evaluate(() => window.dispatchEvent(new WheelEvent('wheel', { deltaY: 1 })));
for (let y = 1; y < oracle.destination; y += oracle.viewportHeight * 0.5) {
await page.evaluate((nextY) => window.scrollTo(0, nextY), y);
@@ -134,6 +139,21 @@ test('keeps a readable sign immutable through reversal, resize, and a multi-sect
await page.mouse.wheel(0, -120);
await page.setViewportSize({ width: 844, height: 390 });
await page.waitForTimeout(500);
const resizedFrame = (await readSceneInstrumentation(page)).signFrames.findLast((frame) => (
frame.targetId === 'profile'
&& frame.viewport.width === 844
&& frame.viewport.height === 390
));
if (resizedFrame === undefined) throw new Error('Missing post-resize Profile frame');
expect(resizedFrame.phase).toBe('dwell');
expect(resizedFrame.viewportProfile).toBe('short');
expect(resizedFrame.bounds.left).toBeGreaterThanOrEqual(8);
expect(resizedFrame.bounds.right).toBeLessThanOrEqual(288);
expect(countSignPresentations(
(await readSceneInstrumentation(page)).signFrames,
'profile',
)).toBe(1);
await page.evaluate(() => {
const target = document.getElementById('selected-builds-heading');
if (!(target instanceof HTMLElement)) throw new Error('Missing selected builds heading');
@@ -146,7 +166,57 @@ test('keeps a readable sign immutable through reversal, resize, and a multi-sect
expect((await readSceneInstrumentation(page)).maximumCanvasCount).toBeLessThanOrEqual(1);
});
test('suppresses roadside signs for explicit navigation and history restoration', async ({
test('carries a departed sign past the car before its restrained edge fade', async ({
page,
distUrl,
}) => {
await page.setViewportSize({ width: 1_440, height: 900 });
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
const viewport = page.viewportSize();
if (viewport === null) throw new Error('Missing browser viewport');
const destination = await page.evaluate((candidateDistanceVh) => {
const profile = document.getElementById('profile-heading');
if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading');
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
return Math.max(
1,
profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh),
);
}, triggerProbeDistanceVh(viewport.width, viewport.height));
await page.mouse.wheel(0, 1);
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination);
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.some((frame) => frame.targetId === 'profile' && frame.phase === 'dwell')).toBe(true);
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.some((frame) => (
frame.targetId === 'profile'
&& frame.phase === 'depart'
&& frame.progress >= 0.52
)), { intervals: [16], timeout: 5_000 }).toBe(true);
const frames = (await readSceneInstrumentation(page)).signFrames;
const dwell = frames.findLast((frame) => frame.targetId === 'profile' && frame.phase === 'dwell');
const passing = frames.findLast((frame) => (
frame.targetId === 'profile'
&& frame.phase === 'depart'
&& frame.progress >= 0.3
&& frame.opacity >= 0.98
));
if (dwell === undefined || passing === undefined) {
throw new Error('Missing comparable Profile dwell and pass-by frames');
}
expect(passing.opacity).toBeGreaterThanOrEqual(0.98);
expect(passing.bounds.width).toBeGreaterThan(dwell.bounds.width * 1.18);
expect(passing.bounds.left).toBeLessThan(dwell.bounds.left - 24);
expect(passing.targetId).toBe(dwell.targetId);
});
test('shows exactly one destination sign for a navigator click and suppresses history restoration', async ({
page,
distUrl,
}) => {
@@ -155,15 +225,168 @@ test('suppresses roadside signs for explicit navigation and history restoration'
await clickNavigationLink(page, '#selected-builds');
await expect(page.locator('#selected-builds-heading')).toBeFocused();
await page.waitForTimeout(400);
expect((await readSceneInstrumentation(page)).routeLabelsDrawn).toEqual([]);
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.findLast((frame) => (
frame.targetId === 'selected-builds'
))?.phase).toBe('dwell');
await clickNavigationLink(page, '#selected-builds');
await expect(page.locator('#selected-builds-heading')).toBeFocused();
await page.waitForTimeout(2_400);
const afterRepeat = await readSceneInstrumentation(page);
expect(afterRepeat.routeLabelsDrawn).toEqual(['Selected builds']);
expect(countSignPresentations(afterRepeat.signFrames, 'selected-builds')).toBe(1);
await page.goBack({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(400);
expect((await readSceneInstrumentation(page)).routeLabelsDrawn).toEqual([]);
expect((await readSceneInstrumentation(page)).routeLabelsDrawn).toEqual(['Selected builds']);
});
test('restores the next sign after a navigator round trip', async ({
test('keeps the non-navigator Explore work section jump sign-suppressed', async ({
page,
distUrl,
}) => {
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
await page.locator('.hero-journey__action').click();
await expect(page.locator('#selected-builds-heading')).toBeFocused();
await page.waitForTimeout(400);
expect((await readSceneInstrumentation(page)).routeLabelsDrawn).toEqual([]);
const viewport = page.viewportSize();
if (viewport === null) throw new Error('Missing browser viewport');
const destination = await page.evaluate((candidateDistanceVh) => {
const heading = document.getElementById('additional-work-heading');
if (!(heading instanceof HTMLElement)) throw new Error('Missing Additional work heading');
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
return Math.max(
1,
heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh),
);
}, triggerProbeDistanceVh(viewport.width, viewport.height));
await page.mouse.wheel(0, 1);
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination);
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.findLast((frame) => frame.targetId === 'additional-work')?.phase).toBe('dwell');
expect((await readSceneInstrumentation(page)).routeLabelsDrawn).toEqual(['Additional work']);
});
test('keeps a navigator-triggered Profile sign fully beside its visible destination panel', async ({
page,
distUrl,
}) => {
await page.setViewportSize({ width: 780, height: 493 });
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
await clickNavigationLink(page, '#profile');
await expect(page.locator('#profile-heading')).toBeFocused();
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.some((frame) => (
frame.targetId === 'profile' && frame.phase === 'dwell'
))).toBe(true);
const frame = (await readSceneInstrumentation(page)).signFrames.findLast((candidate) => (
candidate.targetId === 'profile' && candidate.phase === 'dwell'
));
if (frame === undefined) throw new Error('Missing navigator-triggered Profile dwell frame');
const panel = await page.locator('#profile').evaluate((element) => {
const bounds = element.getBoundingClientRect();
return { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom };
});
expect(frame.bounds.right).toBeLessThanOrEqual(panel.left - 16);
expect(frame.bounds.left).toBeGreaterThanOrEqual(8);
});
test('keeps a portrait navigator sign clear of its full-width destination panel', async ({
page,
distUrl,
}, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'One exact portrait composition is sufficient.');
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
await clickNavigationLink(page, '#profile');
await expect(page.locator('#profile-heading')).toBeFocused();
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.some((frame) => frame.targetId === 'profile' && frame.phase === 'dwell')).toBe(true);
const frame = (await readSceneInstrumentation(page)).signFrames.findLast((candidate) => (
candidate.targetId === 'profile' && candidate.phase === 'dwell'
));
if (frame === undefined) throw new Error('Missing portrait navigator Profile dwell frame');
const panels = await page.locator('#profile, #experience').evaluateAll((elements) => (
elements.map((element) => {
const bounds = element.getBoundingClientRect();
return {
id: element.id,
left: bounds.left,
top: bounds.top,
right: bounds.right,
bottom: bounds.bottom,
};
})
));
for (const panel of panels) {
expect(
rectanglesOverlap(frame.bounds, panel, 16),
JSON.stringify({ frame: frame.bounds, panel }),
).toBe(false);
}
});
test('moves an already-readable portrait sign into navigator-safe placement without replaying it', async ({
page,
distUrl,
}, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'One exact portrait composition is sufficient.');
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
const destination = await page.evaluate(() => {
const profile = document.getElementById('profile-heading');
if (!(profile instanceof HTMLElement)) throw new Error('Missing Profile heading');
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
return Math.max(
1,
profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.22),
);
});
await page.mouse.wheel(0, 1);
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination);
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.findLast((frame) => frame.targetId === 'profile')?.phase).toBe('dwell');
await clickNavigationLink(page, '#profile');
await expect(page.locator('#profile-heading')).toBeFocused();
const panel = await page.locator('#profile').evaluate((element) => {
const bounds = element.getBoundingClientRect();
return { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom };
});
await expect.poll(async () => {
const frame = (await readSceneInstrumentation(page)).signFrames.findLast((candidate) => (
candidate.targetId === 'profile'
&& candidate.phase === 'dwell'
&& candidate.navigationArrival === true
));
return frame === undefined ? true : rectanglesOverlap(frame.bounds, panel, 16);
}, { intervals: [16], timeout: 5_000 }).toBe(false);
const instrumentation = await readSceneInstrumentation(page);
expect(countSignPresentations(instrumentation.signFrames, 'profile')).toBe(1);
expect(instrumentation.routeLabelsDrawn).toEqual(['Profile']);
});
test('re-arms passive signage after a navigator round trip to Start', async ({
page,
distUrl,
}) => {
@@ -172,31 +395,39 @@ test('restores the next sign after a navigator round trip', async ({
await clickNavigationLink(page, '#profile');
await expect(page.locator('#profile-heading')).toBeFocused();
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.some((frame) => frame.targetId === 'profile' && frame.phase === 'dwell')).toBe(true);
await page.waitForTimeout(2_500);
await clickNavigationLink(page, '#start');
await expect(page.locator('#start-heading')).toBeFocused();
const destination = await page.evaluate(() => {
const viewport = page.viewportSize();
if (viewport === null) throw new Error('Missing browser viewport');
const destination = await page.evaluate((candidateDistanceVh) => {
const profile = document.getElementById('profile-heading');
if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading');
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
return Math.max(
1,
profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.45),
- viewportHeight * (0.35 + candidateDistanceVh),
);
});
}, triggerProbeDistanceVh(viewport.width, viewport.height));
await page.mouse.wheel(0, 1);
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination);
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.some((frame) => frame.targetId === 'profile')).toBe(true);
await expect.poll(async () => countSignPresentations(
(await readSceneInstrumentation(page)).signFrames,
'profile',
)).toBe(2);
});
test('reconciles an idle rapid jump to the latest active route without flashing intermediates', async ({
page,
distUrl,
}) => {
await page.setViewportSize({ width: 1_440, height: 900 });
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
@@ -211,8 +442,20 @@ test('reconciles an idle rapid jump to the latest active route without flashing
await expect.poll(async () => (await readSceneInstrumentation(page)).routeLabelsDrawn)
.toContain('Selected builds');
expect([...new Set((await readSceneInstrumentation(page)).routeLabelsDrawn)])
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.findLast((frame) => (
frame.targetId === 'selected-builds'
))?.phase, { intervals: [16], timeout: 5_000 }).toBe('dwell');
const instrumentation = await readSceneInstrumentation(page);
expect([...new Set(instrumentation.routeLabelsDrawn)])
.toEqual(['Selected builds']);
const dwell = instrumentation.signFrames.findLast((frame) => (
frame.targetId === 'selected-builds' && frame.phase === 'dwell'
));
if (dwell === undefined) throw new Error('Missing reconciled Selected builds dwell frame');
expect(dwell.bounds.left).toBeGreaterThanOrEqual(8);
expect(dwell.bounds.right).toBeLessThanOrEqual(1_432);
});
test('keeps the readable Profile face outside its destination panel with verified texel density', async ({
@@ -267,6 +510,7 @@ test('keeps a fresh readable sign inside responsive world-safe corridors', async
for (const viewport of [
{ width: 390, height: 844, profile: 'tall', minimumFaceWidth: 120 },
{ width: 780, height: 493, profile: 'short', minimumFaceWidth: 120 },
{ width: 844, height: 390, profile: 'short', minimumFaceWidth: 130 },
{ width: 768, height: 1_024, profile: 'tall', minimumFaceWidth: 150 },
{ width: 1_440, height: 900, profile: 'wide', minimumFaceWidth: 180 },
@@ -275,16 +519,16 @@ test('keeps a fresh readable sign inside responsive world-safe corridors', async
await page.setViewportSize(viewport);
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
const destination = await page.evaluate(() => {
const destination = await page.evaluate((candidateDistanceVh) => {
const profile = document.getElementById('profile-heading');
if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading');
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
return Math.max(
1,
profile.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.45),
- viewportHeight * (0.35 + candidateDistanceVh),
);
});
}, triggerProbeDistanceVh(viewport.width, viewport.height));
await page.mouse.wheel(0, 1);
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination);
await expect.poll(async () => (
@@ -332,6 +576,159 @@ test('keeps a fresh readable sign inside responsive world-safe corridors', async
}
});
test('keeps the upcoming Experience sign clear of both outgoing and incoming panels', async ({
page,
distUrl,
}, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'One exact desktop composition is sufficient.');
await page.setViewportSize({ width: 1_440, height: 900 });
await page.goto(`${distUrl}/#profile`, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
await expect(page.locator('nav a[href="#profile"]')).toHaveAttribute('aria-current', 'page');
const destination = await page.evaluate(() => {
const heading = document.getElementById('experience-heading');
if (!(heading instanceof HTMLElement)) throw new Error('Missing Experience heading');
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
return Math.max(
1,
heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + 0.45),
);
});
await page.mouse.wheel(0, 1);
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination);
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.at(-1)?.phase, { intervals: [16], timeout: 5_000 }).toBe('dwell');
const frame = (await readSceneInstrumentation(page)).signFrames.findLast((candidate) => (
candidate.targetId === 'experience' && candidate.phase === 'dwell'
));
if (frame === undefined) throw new Error('Missing readable Experience sign frame');
const panels = await page.locator('#profile, #experience').evaluateAll((elements) => (
elements.map((element) => {
const bounds = element.getBoundingClientRect();
return {
id: element.id,
left: bounds.left,
top: bounds.top,
right: bounds.right,
bottom: bounds.bottom,
};
}).filter((bounds) => bounds.bottom > 0 && bounds.top < window.innerHeight)
));
expect(panels.map(({ id }) => id)).toEqual(['profile', 'experience']);
for (const panel of panels) {
expect(rectanglesOverlap(frame.bounds, panel, 16), panel.id).toBe(false);
}
});
test('keeps every passive transition sign clear of visible opaque panels and fixed controls', async ({
page,
distUrl,
}, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'One exact responsive transition matrix is sufficient.');
test.setTimeout(180_000);
for (const viewport of [
{ width: 1_440, height: 900 },
{ width: 390, height: 844 },
{ width: 844, height: 390 },
] as const) {
await page.setViewportSize(viewport);
for (let index = 1; index < sections.length; index += 1) {
const previous = sections[index - 1];
const next = sections[index];
if (previous === undefined || next === undefined) throw new Error('Incomplete route transition');
const startUrl = previous.id === 'start' ? distUrl : `${distUrl}/#${previous.id}`;
await page.goto(startUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
await expect(page.locator(`nav a[href="#${previous.id}"]`)).toHaveAttribute('aria-current', 'page');
const transition = `${viewport.width}x${viewport.height} ${previous.id} -> ${next.id}`;
const destination = await page.evaluate(({ targetId, candidateDistanceVh }) => {
const heading = document.getElementById(`${targetId}-heading`);
if (!(heading instanceof HTMLElement)) throw new Error(`Missing ${targetId} heading`);
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
return Math.max(
1,
heading.getBoundingClientRect().top + window.scrollY
- viewportHeight * (0.35 + candidateDistanceVh),
);
}, {
targetId: next.id,
candidateDistanceVh: viewport.width < viewport.height
? 0.225
: viewport.height < 560 ? 0.38 : 0.45,
});
await page.mouse.wheel(0, 1);
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), destination);
await expect.poll(async () => (
await readSceneInstrumentation(page)
).signFrames.findLast((frame) => frame.targetId === next.id)?.phase, {
intervals: [16],
message: `${transition} should reach readable dwell`,
timeout: 5_000,
}).toBe('dwell');
const frame = (await readSceneInstrumentation(page)).signFrames.findLast((candidate) => (
candidate.targetId === next.id && candidate.phase === 'dwell'
));
if (frame === undefined) throw new Error(`Missing readable ${next.id} sign frame`);
const occupied = await page.evaluate(() => {
const alpha = (color: string): number => {
const match = color.match(/rgba?\([^)]*(?:,|\s\/\s)([\d.]+)\)$/u);
return match?.[1] === undefined ? 1 : Number.parseFloat(match[1]);
};
const boundsFor = (element: Element) => {
const bounds = element.getBoundingClientRect();
return {
id: element.id || element.closest('[id]')?.id || element.className,
left: bounds.left,
top: bounds.top,
right: bounds.right,
bottom: bounds.bottom,
};
};
const panels = [...document.querySelectorAll(
[
'.hero-journey__content',
'.portfolio-section:not([data-layout="full-diagram"]):not([data-layout="project-stage"])',
'.portfolio-section[data-layout="full-diagram"] > h2',
'.infrastructure-case > h3',
'.infrastructure-case > p',
'.infrastructure-flow-figure',
'.portfolio-section[data-layout="project-stage"] > h2',
'.portfolio-section[data-layout="project-stage"] > .section-eyebrow',
'.project-case-study',
'.project-card',
].join(', '),
)].filter((element) => {
const bounds = element.getBoundingClientRect();
const style = getComputedStyle(element);
const painted = style.backgroundImage !== 'none' || alpha(style.backgroundColor) > 0.08;
return painted && bounds.bottom > 0 && bounds.top < window.innerHeight;
}).map(boundsFor);
const fixed = [...document.querySelectorAll(
'.site-navigation, .drive-controls, .journey-hud',
)].map(boundsFor);
return { panels, fixed };
});
const panelOverlaps = occupied.panels
.filter((panel) => rectanglesOverlap(frame.bounds, panel, 16))
.map((panel) => ({ panel, sign: frame.bounds }));
const fixedOverlaps = occupied.fixed
.filter((zone) => rectanglesOverlap(frame.bounds, zone, 8));
expect(frame.bounds.left, `${next.id} left edge`).toBeGreaterThanOrEqual(8);
expect(frame.bounds.right, `${next.id} right edge`).toBeLessThanOrEqual(viewport.width - 8);
expect(frame.bounds.top, `${next.id} top edge`).toBeGreaterThanOrEqual(8);
expect(frame.bounds.bottom, `${next.id} bottom edge`).toBeLessThanOrEqual(viewport.height - 8);
expect(panelOverlaps, transition).toEqual([]);
expect(fixedOverlaps, transition).toEqual([]);
}
}
});
function colorEnergy(color: string): number {
const value = Number.parseInt(color.slice(1), 16);
return ((value >> 16) & 0xff) + ((value >> 8) & 0xff) + (value & 0xff);
@@ -349,3 +746,24 @@ function rectanglesOverlap(
|| first.top >= second.bottom + gap
);
}
function countSignPresentations(
frames: ReadonlyArray<{ targetId: string; phase: string }>,
targetId: string,
): number {
let presentations = 0;
let previousPhase: string | null = null;
for (const frame of frames) {
if (frame.targetId !== targetId) continue;
if (frame.phase === 'anticipate' && previousPhase !== 'anticipate') presentations += 1;
previousPhase = frame.phase;
}
return presentations;
}
function triggerProbeDistanceVh(width: number, height: number): number {
const aspect = width / height;
if (height < 560 && aspect >= 1) return 0.37;
if (aspect < 1.05) return 0.22;
return 0.45;
}

View File

@@ -18,6 +18,7 @@ export interface SignFrameInstrumentation {
visible: boolean;
targetId: string;
phase: string;
navigationArrival?: true;
progress: number;
opacity: number;
activeSectionId: string;
@@ -25,8 +26,15 @@ export interface SignFrameInstrumentation {
corridor: string;
viewportProfile: string;
bounds: { left: number; top: number; right: number; bottom: number; width: number; height: number };
texture: { width: number; height: number; anisotropy: number; fontReadyWhenDrawn: boolean };
texture: {
width: number;
height: number;
maximumSize: number;
anisotropy: number;
fontReadyWhenDrawn: boolean;
};
rendererPixelRatio: number;
viewport: { width: number; height: number };
}
type InstrumentedWindow = Window & {

View File

@@ -206,6 +206,7 @@ describe("navigation epochs and effects", () => {
epoch: 2,
source: "passive-scroll",
destinationId: null,
signDestinationId: null,
});
});
@@ -283,6 +284,23 @@ describe("navigation epochs and effects", () => {
epoch: 2,
source: "history-restoration",
destinationId: "profile",
signDestinationId: null,
});
});
it("marks only an explicitly requested navigator epoch for roadside signage", () => {
const harness = createRuntime(true);
const controller = createNavigationController(sectionIds, harness.runtime);
controller.navigate("profile", target("profile"));
expect(controller.state().signDestinationId).toBeNull();
controller.navigate("experience", target("experience"), { requestSign: true });
expect(controller.state()).toMatchObject({
epoch: 2,
source: "explicit-navigation",
destinationId: "experience",
signDestinationId: "experience",
});
});

View File

@@ -466,6 +466,7 @@ describe('useScrollDrive', () => {
epoch: 0,
source: 'passive-scroll' as const,
destinationId: null,
signDestinationId: null,
});
const motionAllowed = shallowRef(true);

View File

@@ -8,6 +8,8 @@ const sample = (
): SignControllerSample => ({
now: 0,
source: 'passive-scroll',
navigationEpoch: 0,
navigationDestinationId: null,
direction: 1,
acceleration: 0,
deltaY: 16,
@@ -26,12 +28,12 @@ describe('clock-driven sign lifecycle', () => {
expect(SIGN_ROUTE_ORDER.map((id) => signRouteFor(id))).toEqual([
{ id: 'start', label: 'Start', marker: '01', eligible: false, corridor: null, triggerMaximumVh: 0 },
{ id: 'profile', label: 'Profile', marker: '02', eligible: true, corridor: 'left', triggerMaximumVh: 0.55 },
{ id: 'experience', label: 'Experience', marker: '03', eligible: true, corridor: 'right', triggerMaximumVh: 0.55 },
{ id: 'infrastructure', label: 'Infrastructure', marker: '04', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55 },
{ id: 'selected-builds', label: 'Selected builds', marker: '05', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55 },
{ id: 'additional-work', label: 'Additional work', marker: '06', eligible: true, corridor: 'left', triggerMaximumVh: 0.55 },
{ id: 'experience', label: 'Experience', marker: '03', eligible: true, corridor: 'left', triggerMaximumVh: 0.55 },
{ id: 'infrastructure', label: 'Infrastructure', marker: '04', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55, dwellOffsetX: -1.6 },
{ id: 'selected-builds', label: 'Selected builds', marker: '05', eligible: true, corridor: 'right', triggerMaximumVh: 0.55 },
{ id: 'additional-work', label: 'Additional work', marker: '06', eligible: true, corridor: 'outer-left', triggerMaximumVh: 0.55 },
{ id: 'toolkit', label: 'Toolkit', marker: '07', eligible: true, corridor: 'outer-left', triggerMaximumVh: 0.55 },
{ id: 'contact', label: 'Contact', marker: '08', eligible: true, corridor: 'right', triggerMaximumVh: 0.55 },
{ id: 'contact', label: 'Contact', marker: '08', eligible: true, corridor: 'outer-right', triggerMaximumVh: 0.55 },
]);
});
@@ -103,6 +105,36 @@ describe('clock-driven sign lifecycle', () => {
});
});
it('uses the portrait section gap instead of latching behind a full-width panel', () => {
const controller = createSignController();
expect(controller.sample(sample({
viewportWidth: 390,
viewportHeight: 844,
candidateDistanceVh: 0.3,
}))).toBeNull();
expect(controller.sample(sample({
now: 16,
viewportWidth: 390,
viewportHeight: 844,
candidateDistanceVh: 0.22,
}))).toMatchObject({ targetId: 'profile', phase: 'anticipate', viewportProfile: 'tall' });
});
it('uses the short-landscape section gap when no horizontal corridor is wide enough', () => {
const controller = createSignController();
expect(controller.sample(sample({
viewportWidth: 844,
viewportHeight: 390,
candidateDistanceVh: 0.42,
}))).toBeNull();
expect(controller.sample(sample({
now: 16,
viewportWidth: 844,
viewportHeight: 390,
candidateDistanceVh: 0.38,
}))).toMatchObject({ targetId: 'profile', phase: 'anticipate', viewportProfile: 'short' });
});
it('keeps the latched viewport profile immutable across a responsive resize', () => {
const controller = createSignController();
expect(controller.sample(sample())).toMatchObject({ viewportProfile: 'wide' });
@@ -166,7 +198,6 @@ describe('clock-driven sign lifecycle', () => {
});
it.each([
['explicit navigation', { source: 'explicit-navigation' as const }],
['history restoration', { source: 'history-restoration' as const }],
['initial positioning', { source: 'initial' as const }],
['disabled motion', { motionAllowed: false }],
@@ -178,6 +209,459 @@ describe('clock-driven sign lifecycle', () => {
expect(controller.needsFrame()).toBe(false);
});
it('consumes an eligible navigator destination once without requiring scroll input', () => {
const controller = createSignController();
const navigation = sample({
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'selected-builds',
direction: 0,
deltaY: 0,
crossedBoundaries: 0,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 2,
});
expect(controller.sample(navigation)).toMatchObject({
targetId: 'selected-builds',
phase: 'anticipate',
navigationArrival: true,
});
expect(controller.sample({ ...navigation, now: 1_200 })).toMatchObject({
targetId: 'selected-builds',
phase: 'dwell',
});
expect(controller.sample({ ...navigation, now: 2_600 })).toMatchObject({
targetId: 'selected-builds',
phase: 'depart',
progress: 0,
});
expect(controller.sample({ ...navigation, now: 3_300 })).toBeNull();
expect(controller.sample({ ...navigation, now: 4_000 })).toBeNull();
expect(controller.needsFrame()).toBe(false);
});
it('coalesces rapid navigator clicks to the latest concealed target without label mutation', () => {
const controller = createSignController();
expect(controller.sample(sample({
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
}))).toMatchObject({ targetId: 'profile', phase: 'anticipate' });
expect(controller.sample(sample({
now: 100,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'selected-builds',
direction: 0,
deltaY: 0,
}))).toMatchObject({ targetId: 'selected-builds', phase: 'anticipate' });
expect(controller.sample(sample({
now: 1_300,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'selected-builds',
direction: 0,
deltaY: 0,
}))).toMatchObject({ targetId: 'selected-builds', phase: 'dwell' });
});
it('does not queue a second presentation for a repeated same-target navigator click', () => {
const controller = createSignController();
const navigation = sample({
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'selected-builds',
direction: 0,
deltaY: 0,
});
controller.sample(navigation);
controller.sample({ ...navigation, now: 1_200 });
expect(controller.sample({
...navigation,
now: 1_300,
navigationEpoch: 2,
})).toMatchObject({ targetId: 'selected-builds', phase: 'dwell' });
controller.sample({ ...navigation, now: 2_600, navigationEpoch: 2 });
controller.sample({ ...navigation, now: 3_300, navigationEpoch: 2 });
expect(controller.sample({
...navigation,
now: 3_650,
navigationEpoch: 2,
})).toBeNull();
expect(controller.sample({
...navigation,
now: 4_000,
navigationEpoch: 3,
activeSectionId: 'selected-builds',
})).toBeNull();
expect(controller.needsFrame()).toBe(false);
});
it('promotes a rearmed passive sign when the prior navigator target is clicked again', () => {
const controller = createSignController();
const navigation = sample({
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
});
controller.sample(navigation);
controller.sample({ ...navigation, now: 1_200, activeSectionId: 'profile' });
controller.sample({ ...navigation, now: 2_600, activeSectionId: 'profile' });
controller.sample({ ...navigation, now: 3_300, activeSectionId: 'profile' });
controller.sample(sample({
now: 3_400,
direction: -1,
deltaY: -800,
crossedBoundaries: 1,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.8,
}));
controller.sample(sample({
now: 3_650,
direction: 1,
deltaY: 120,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.5,
}));
controller.sample(sample({
now: 4_850,
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
candidateId: 'experience',
}));
expect(controller.sample({
...navigation,
now: 4_900,
navigationEpoch: 2,
activeSectionId: 'profile',
})).toMatchObject({
targetId: 'profile',
phase: 'dwell',
navigationArrival: true,
});
});
it('preserves a full readable dwell when navigation promotes a late passive dwell', () => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
const promoted = sample({
now: 2_590,
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
});
expect(controller.sample(promoted)).toMatchObject({
targetId: 'profile',
phase: 'dwell',
navigationArrival: true,
});
expect(controller.sample({ ...promoted, now: 3_989 })).toMatchObject({
phase: 'dwell',
navigationArrival: true,
});
expect(controller.sample({ ...promoted, now: 3_990 })).toMatchObject({
phase: 'depart',
progress: 0,
navigationArrival: true,
});
});
it('promotes a rearmed sign queued during cooldown when its navigator target is clicked', () => {
const controller = createSignController();
const navigation = sample({
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
});
controller.sample(navigation);
controller.sample({ ...navigation, now: 1_200, activeSectionId: 'profile' });
controller.sample({ ...navigation, now: 2_600, activeSectionId: 'profile' });
controller.sample(sample({
now: 2_800,
direction: -1,
deltaY: -800,
crossedBoundaries: 1,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.8,
}));
controller.sample(sample({
now: 3_300,
direction: 0,
deltaY: 0,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.8,
}));
expect(controller.sample(sample({
now: 3_400,
direction: 1,
deltaY: 120,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.5,
}))).toBeNull();
expect(controller.needsFrame()).toBe(true);
expect(controller.sample({
...navigation,
now: 3_450,
navigationEpoch: 2,
activeSectionId: 'profile',
})).toBeNull();
expect(controller.sample({
...navigation,
now: 3_650,
navigationEpoch: 2,
activeSectionId: 'profile',
})).toMatchObject({
targetId: 'profile',
phase: 'anticipate',
navigationArrival: true,
});
});
it('preserves a readable label and queues only the latest navigator destination', () => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
expect(controller.sample(sample({
now: 1_300,
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'experience',
direction: 0,
deltaY: 0,
}))).toMatchObject({ targetId: 'profile', phase: 'dwell' });
expect(controller.sample(sample({
now: 1_400,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'toolkit',
direction: 0,
deltaY: 0,
}))).toMatchObject({ targetId: 'profile', phase: 'dwell' });
controller.sample(sample({
now: 2_600,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'toolkit',
direction: 0,
deltaY: 0,
}));
controller.sample(sample({
now: 3_300,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'toolkit',
direction: 0,
deltaY: 0,
}));
expect(controller.sample(sample({
now: 3_650,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'toolkit',
direction: 0,
deltaY: 0,
}))).toMatchObject({
targetId: 'toolkit',
phase: 'anticipate',
navigationArrival: true,
});
});
it('cancels an obsolete queued destination when navigation returns to the visible target', () => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
expect(controller.sample(sample({
now: 1_300,
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'toolkit',
direction: 0,
deltaY: 0,
activeSectionId: 'toolkit',
}))).toMatchObject({ targetId: 'profile', phase: 'dwell' });
expect(controller.sample(sample({
now: 1_400,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}))).toMatchObject({
targetId: 'profile',
phase: 'dwell',
navigationArrival: true,
});
controller.sample(sample({
now: 2_800,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}));
controller.sample(sample({
now: 3_500,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}));
expect(controller.sample(sample({
now: 3_850,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}))).toBeNull();
expect(controller.needsFrame()).toBe(false);
expect(controller.sample(sample({
now: 3_900,
direction: -1,
deltaY: -40,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.6,
}))).toBeNull();
expect(controller.sample(sample({
now: 4_000,
direction: 1,
deltaY: 40,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.5,
}))).toBeNull();
});
it('honors a navigator return to a previously completed route after a readable sign', () => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 }));
controller.sample(sample({
now: 3_650,
activeSectionId: 'profile',
candidateId: 'experience',
candidateDistanceVh: 0.5,
}));
controller.sample(sample({
now: 4_850,
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
candidateId: 'experience',
}));
expect(controller.sample(sample({
now: 5_000,
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}))).toMatchObject({ targetId: 'experience', phase: 'dwell' });
controller.sample(sample({
now: 6_250,
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}));
controller.sample(sample({
now: 6_950,
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}));
expect(controller.sample(sample({
now: 7_300,
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}))).toMatchObject({
targetId: 'profile',
phase: 'anticipate',
navigationArrival: true,
});
});
it('queues a fresh navigator arrival for the cooldown route after moving elsewhere', () => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 }));
expect(controller.sample(sample({
now: 3_400,
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'experience',
}))).toBeNull();
expect(controller.sample(sample({
now: 3_650,
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}))).toMatchObject({
targetId: 'profile',
phase: 'anticipate',
navigationArrival: true,
});
});
it.each([
['multiple crossed sections', { crossedBoundaries: 2 }],
['a jump over 0.75 viewport heights', { deltaY: 601 }],
@@ -435,6 +919,69 @@ describe('clock-driven sign lifecycle', () => {
}))).toMatchObject({ targetId: 'profile', phase: 'depart', progress: 0 });
});
it('keeps a passed sign opaque until its spatial departure is well underway', () => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }));
expect(controller.sample(sample({
now: 2_985,
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}))).toMatchObject({
phase: 'depart',
progress: 0.55,
opacity: 1,
});
expect(controller.sample(sample({
now: 3_230,
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
}))).toMatchObject({
phase: 'depart',
progress: 0.9,
opacity: expect.closeTo(0.04, 1),
});
});
it('discards a queued forward jump after a meaningful reverse retreat', () => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
controller.sample(sample({
now: 1_300,
deltaY: 2_400,
crossedBoundaries: 4,
activeSectionId: 'selected-builds',
candidateId: 'additional-work',
candidateDistanceVh: 0.4,
}));
controller.sample(sample({
now: 1_500,
direction: -1,
deltaY: -2_400,
crossedBoundaries: 4,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.8,
}));
controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 }));
expect(controller.sample(sample({
now: 3_650,
direction: 0,
deltaY: 0,
activeSectionId: 'start',
candidateId: 'selected-builds',
candidateDistanceVh: 0.4,
}))).toBeNull();
});
it('does not retrigger a completed route sign during small boundary oscillations', () => {
const controller = createSignController();
controller.sample(sample());
@@ -459,6 +1006,164 @@ describe('clock-driven sign lifecycle', () => {
}))).toBeNull();
});
it('re-arms a completed sign only after retreating beyond the trigger hysteresis', () => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 }));
expect(controller.sample(sample({
now: 3_500,
direction: -1,
deltaY: -600,
crossedBoundaries: 1,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.8,
}))).toBeNull();
expect(controller.sample(sample({
now: 3_650,
direction: 1,
deltaY: 120,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.5,
}))).toMatchObject({ targetId: 'profile', phase: 'anticipate' });
});
it('does not let a departing sign overwrite a meaningful reverse rearm', () => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }));
expect(controller.sample(sample({
now: 2_800,
direction: -1,
deltaY: -800,
crossedBoundaries: 1,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.8,
}))).toMatchObject({ targetId: 'profile', phase: 'depart' });
controller.sample(sample({
now: 3_300,
direction: 0,
deltaY: 0,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.8,
}));
expect(controller.sample(sample({
now: 3_400,
direction: 1,
deltaY: 120,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.5,
}))).toBeNull();
expect(controller.needsFrame()).toBe(true);
expect(controller.sample(sample({
now: 3_650,
direction: 0,
deltaY: 0,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.5,
}))).toMatchObject({ targetId: 'profile', phase: 'anticipate' });
});
it('re-arms every later route after one large reverse retreat', () => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 }));
controller.sample(sample({
now: 3_650,
activeSectionId: 'profile',
candidateId: 'experience',
candidateDistanceVh: 0.5,
}));
controller.sample(sample({
now: 4_850,
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
candidateId: 'experience',
}));
controller.sample(sample({
now: 6_250,
direction: 0,
deltaY: 0,
activeSectionId: 'experience',
candidateId: 'infrastructure',
}));
controller.sample(sample({
now: 6_950,
direction: 0,
deltaY: 0,
activeSectionId: 'experience',
candidateId: 'infrastructure',
}));
controller.sample(sample({
now: 7_100,
direction: -1,
deltaY: -2_400,
crossedBoundaries: 2,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.8,
}));
controller.sample(sample({ now: 7_300 }));
controller.sample(sample({ now: 8_500, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 9_900, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 10_600, direction: 0, deltaY: 0 }));
expect(controller.sample(sample({
now: 10_950,
activeSectionId: 'profile',
candidateId: 'experience',
candidateDistanceVh: 0.5,
}))).toMatchObject({ targetId: 'experience', phase: 'anticipate' });
});
it.each([
['an unrequested explicit jump', 'explicit-navigation' as const],
['history restoration', 'history-restoration' as const],
])('starts a fresh passive traversal after %s', (_label, source) => {
const controller = createSignController();
controller.sample(sample());
controller.sample(sample({ now: 1_200, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 2_600, direction: 0, deltaY: 0 }));
controller.sample(sample({ now: 3_300, direction: 0, deltaY: 0 }));
expect(controller.sample(sample({
now: 3_400,
source,
navigationEpoch: 1,
navigationDestinationId: null,
direction: 0,
deltaY: 0,
activeSectionId: 'start',
candidateId: null,
candidateDistanceVh: null,
}))).toBeNull();
expect(controller.sample(sample({
now: 3_500,
source: 'passive-scroll',
navigationEpoch: 2,
direction: 1,
deltaY: 120,
activeSectionId: 'start',
candidateId: 'profile',
candidateDistanceVh: 0.5,
}))).toMatchObject({ targetId: 'profile', phase: 'anticipate' });
});
it('starts a fresh passive sign traversal after explicit navigation', () => {
const controller = createSignController();
controller.sample(sample());
@@ -469,6 +1174,8 @@ describe('clock-driven sign lifecycle', () => {
expect(controller.sample(sample({
now: 3_400,
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'contact',
activeSectionId: 'contact',
candidateId: null,
candidateDistanceVh: null,
@@ -476,6 +1183,8 @@ describe('clock-driven sign lifecycle', () => {
expect(controller.sample(sample({
now: 3_500,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'start',
activeSectionId: 'start',
}))).toBeNull();
@@ -484,4 +1193,64 @@ describe('clock-driven sign lifecycle', () => {
phase: 'anticipate',
});
});
it('does not let a stale navigator sign re-complete a traversal restarted at Start', () => {
const controller = createSignController();
const profileNavigation = sample({
source: 'explicit-navigation',
navigationEpoch: 1,
navigationDestinationId: 'profile',
candidateId: null,
candidateDistanceVh: null,
});
controller.sample(profileNavigation);
controller.sample({
...profileNavigation,
now: 1_200,
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
});
controller.sample({
...profileNavigation,
now: 2_600,
direction: 0,
deltaY: 0,
activeSectionId: 'profile',
});
expect(controller.sample(sample({
now: 2_800,
source: 'explicit-navigation',
navigationEpoch: 2,
navigationDestinationId: 'start',
direction: 0,
deltaY: 0,
activeSectionId: 'start',
candidateId: null,
candidateDistanceVh: null,
}))).toMatchObject({ targetId: 'profile', phase: 'depart' });
expect(controller.sample(sample({
now: 2_900,
viewportWidth: 390,
viewportHeight: 844,
candidateDistanceVh: 0.22,
}))).toMatchObject({ targetId: 'profile', phase: 'depart' });
expect(controller.sample(sample({
now: 3_300,
direction: 0,
deltaY: 0,
activeSectionId: 'start',
}))).toBeNull();
expect(controller.sample(sample({
now: 3_650,
direction: 0,
deltaY: 0,
viewportWidth: 390,
viewportHeight: 844,
}))).toMatchObject({
targetId: 'profile',
phase: 'anticipate',
});
});
});

View File

@@ -565,11 +565,15 @@ describe("procedural world models", () => {
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();
@@ -577,8 +581,14 @@ describe("procedural world models", () => {
const face = sign.group.getObjectByName("sign-face") as {
userData: { textureUpdateCount: number };
material: {
depthWrite: boolean;
opacity: number;
map: { anisotropy: number; image: { width: number; height: 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;
@@ -606,7 +616,25 @@ describe("procedural world models", () => {
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);
@@ -656,6 +684,50 @@ describe("procedural world models", () => {
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("left");
resources.dispose();
});
it("begins sign departure from the rendered approach pose without a teleport", () => {
const resources = new SceneResourceBag();
const sign = createSign(resources);
@@ -681,21 +753,26 @@ describe("procedural world models", () => {
resources.dispose();
});
it("defers responsive pose changes for the complete lifecycle, including concealed anticipation", () => {
it("reflows a live sign to a new responsive pose without a resize teleport", () => {
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 });
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);
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(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();
@@ -709,23 +786,70 @@ describe("procedural world models", () => {
opacity: 1,
viewportProfile: "wide",
});
expect(rebuilt.inspect(camera, 844, 390)?.viewportProfile).toBe("wide");
expect(rebuilt.inspect(camera, 844, 390)?.viewportProfile).toBe("short");
rebuiltResources.dispose();
});
it("departs laterally without crossing the chase camera plane or growing toward the car", () => {
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 dwellX = sign.group.position.x;
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.9, opacity: 0.1 });
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(dwellX);
expect(sign.group.position.z).toBeLessThanOrEqual(0);
expect(sign.group.scale.x).toBeLessThan(dwellScale);
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();
});
@@ -746,7 +870,82 @@ describe("procedural world models", () => {
resources.dispose();
});
it("places route signs in the corridor opposite their destination content", () => {
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("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);
@@ -762,7 +961,7 @@ describe("procedural world models", () => {
expect(profileBounds.maxX).toBeLessThan(0.08);
sign.update(null);
sign.update({ targetId: "experience", phase: "dwell", progress: 1, opacity: 1 });
sign.update({ targetId: "contact", phase: "dwell", progress: 1, opacity: 1 });
sign.group.updateMatrixWorld(true);
const experienceBounds = projectedBounds(profileFace, camera);
expect(experienceBounds.minX).toBeGreaterThan(0);