feat: sign interlude, window plate and a two-button control bay
The sign closes in over the road on a gantry truss, arcs into a wider window and hands off to a flat 2D plate that mirrors its face; content steps aside for the interlude and rises after. The bay keeps sound and settings; volume and Pause motion move into the settings popover.
This commit is contained in:
@@ -289,12 +289,13 @@ describe("DriveControls", () => {
|
||||
});
|
||||
|
||||
expect(wrapper.get('[aria-label="Mute sound"]')).toBeTruthy();
|
||||
expect(wrapper.get('[aria-label="Sound volume"]')).toBeTruthy();
|
||||
expect(wrapper.get('[aria-label="Pause motion"]')).toBeTruthy();
|
||||
const settings = wrapper.get('[aria-label="Drive settings"]');
|
||||
await settings.trigger("click");
|
||||
|
||||
// Volume and Pause motion live in the settings panel (two-button bay).
|
||||
const panel = wrapper.get('[role="dialog"][aria-label="Drive settings"]');
|
||||
expect(panel.get('[aria-label="Sound volume level"]')).toBeTruthy();
|
||||
expect(panel.get('[aria-label="Pause motion"]')).toBeTruthy();
|
||||
expect(panel.text()).not.toContain("Motion: active");
|
||||
expect(panel.find('[role="status"]').exists()).toBe(false);
|
||||
expect(panel.get("select").attributes("aria-label")).toBe(
|
||||
|
||||
@@ -98,21 +98,26 @@ describe('composed route signs', () => {
|
||||
const allocation = h.resources.snapshot();
|
||||
h.render(null);
|
||||
expect(h.sign.group.visible).toBe(false);
|
||||
// Between signs the face holds the resting chapter's label.
|
||||
const resting = face.userData.textureUpdateCount;
|
||||
h.render({ ...profile, phase: 'anticipate', progress: 0, opacity: 0 });
|
||||
expect(h.sign.group.visible).toBe(false);
|
||||
expect(face.userData.textureUpdateCount).toBe(1);
|
||||
expect(face.userData.textureUpdateCount).toBe(resting + 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);
|
||||
expect(face.material.opacity).toBeLessThanOrEqual(1);
|
||||
// Docked, the 3D face hands off to the window's flat plate in 200 ms.
|
||||
h.render(profile, 120);
|
||||
expect(face.material.opacity).toBe(1);
|
||||
expect(face.userData.textureUpdateCount).toBe(1);
|
||||
expect(face.material.opacity).toBe(0);
|
||||
expect(h.sign.group.visible).toBe(false);
|
||||
expect(face.userData.textureUpdateCount).toBe(resting + 1);
|
||||
expect(h.resources.snapshot()).toEqual(allocation);
|
||||
h.render(null);
|
||||
expect(h.sign.group.visible).toBe(false);
|
||||
const between = face.userData.textureUpdateCount;
|
||||
h.render({ ...profile, targetId: 'selected-builds', phase: 'anticipate', progress: 0, opacity: 0 });
|
||||
expect(face.userData.textureUpdateCount).toBe(2);
|
||||
expect(face.userData.textureUpdateCount).toBe(between + 1);
|
||||
expect(routeLabelFor('selected-builds')).toBe('Selected builds');
|
||||
expect(h.resources.snapshot()).toEqual(allocation);
|
||||
h.resources.dispose();
|
||||
@@ -155,46 +160,57 @@ describe('composed route signs', () => {
|
||||
expectDocked(settled, h.dock);
|
||||
});
|
||||
|
||||
it('lifts off the horizon and grows into the window while approaching', () => {
|
||||
it('closes in over the road in the upper third, then arcs up into the window', () => {
|
||||
const h = setup();
|
||||
h.render({ ...profile, phase: 'approach', progress: 0, opacity: 1 }, 30);
|
||||
const early = h.render({ ...profile, phase: 'approach', progress: 0.2, opacity: 1 })!.bounds;
|
||||
const late = h.render({ ...profile, phase: 'approach', progress: 0.7, opacity: 1 })!.bounds;
|
||||
const big = h.render({ ...profile, phase: 'approach', progress: 0.69, opacity: 1 })!.bounds;
|
||||
const arcing = h.render({ ...profile, phase: 'approach', progress: 0.85, opacity: 1 })!.bounds;
|
||||
const docked = h.render(profile)!.bounds;
|
||||
expect(h.sign.dockState()).toBe('docked');
|
||||
expect(early.width).toBeLessThan(late.width);
|
||||
expect(late.width).toBeLessThan(docked.width + 1);
|
||||
expect(early.top).toBeGreaterThan(late.top);
|
||||
expect(late.top).toBeGreaterThan(docked.top - 1);
|
||||
expect(early.top).toBeGreaterThan(900 * 0.3);
|
||||
// Grows down the road to about 600 px, centred in the upper third.
|
||||
expect(early.width).toBeLessThan(big.width);
|
||||
expect(big.width).toBeGreaterThan(560);
|
||||
expect(big.width).toBeLessThan(640);
|
||||
expect((big.top + big.bottom) / 2).toBeGreaterThan(900 * 0.2);
|
||||
expect((big.top + big.bottom) / 2).toBeLessThan(900 * 0.4);
|
||||
// Then shrinks and rises into the window, landing within 2 px.
|
||||
expect(arcing.width).toBeLessThan(big.width);
|
||||
expect(arcing.top).toBeLessThan(big.top);
|
||||
expectDocked(docked, h.dock);
|
||||
});
|
||||
|
||||
it('passes overhead and out of the top of the frame as it departs', () => {
|
||||
it('leaves the window to the flat plate as it departs', () => {
|
||||
const h = setup();
|
||||
const docked = h.render(profile, 120)!.bounds;
|
||||
const passing = h.render({ ...profile, phase: 'depart', progress: 0.5, opacity: 0.6 })!.bounds;
|
||||
h.render(profile, 120);
|
||||
// The flat plate holds the window; the 3D face stays hidden as it departs.
|
||||
h.render({ ...profile, phase: 'depart', progress: 0.5, opacity: 0.6 });
|
||||
expect(h.sign.dockState()).toBe('passing');
|
||||
expect(passing.top).toBeLessThan(docked.top);
|
||||
expect(passing.width).toBeGreaterThan(docked.width);
|
||||
const gone = h.render({ ...profile, phase: 'depart', progress: 1, opacity: 0.05 })!.bounds;
|
||||
expect(gone.bottom).toBeLessThan(docked.bottom);
|
||||
expect(h.sign.group.visible).toBe(false);
|
||||
h.render(null);
|
||||
expect(h.sign.group.visible).toBe(false);
|
||||
expect(h.sign.dockState()).toBe('idle');
|
||||
});
|
||||
|
||||
it('turns toward the pointer only while docked, and settles back', () => {
|
||||
it('turns toward the pointer only while docked and hovered, and settles back', () => {
|
||||
const h = setup();
|
||||
h.render(profile, 120);
|
||||
const group = h.sign.group as unknown as { quaternion: { clone(): { angleTo(q: unknown): number }; angleTo(q: unknown): number } };
|
||||
const rest = group.quaternion.clone();
|
||||
// Without hover the plate stays flat: no tilt.
|
||||
h.setTilt({ x: 1, y: -1 });
|
||||
h.render(profile, 60);
|
||||
expect(group.quaternion.angleTo(rest)).toBeGreaterThan(0.2);
|
||||
expect(group.quaternion.angleTo(rest)).toBeLessThan(0.01);
|
||||
// Hovered, the 3D twin appears and leans at most about 4 degrees.
|
||||
h.setTilt({ x: 1, y: -1, hover: true });
|
||||
h.render(profile, 60);
|
||||
expect(h.sign.group.visible).toBe(true);
|
||||
expect(group.quaternion.angleTo(rest)).toBeGreaterThan(0.05);
|
||||
expect(group.quaternion.angleTo(rest)).toBeLessThan(0.11);
|
||||
h.setTilt({ x: 0, y: 0 });
|
||||
h.render(profile, 120);
|
||||
expect(group.quaternion.angleTo(rest)).toBeLessThan(0.01);
|
||||
h.setTilt({ x: 1, y: 1 });
|
||||
h.setTilt({ x: 1, y: 1, hover: true });
|
||||
h.render(null);
|
||||
h.render({ ...profile, phase: 'approach', progress: 0.5, opacity: 1 }, 60);
|
||||
const approaching = group.quaternion.clone();
|
||||
|
||||
Reference in New Issue
Block a user