Skip to content

Commit a2ace1d

Browse files
committed
bitmap 3D PFP: simplify pass after /simplify review
Hot path - frameForward / frameSide cached once per frame in pfpFrame (cacheFrameVectors after applyLookStick) instead of re-deriving from camera.getWorldDirection 10× per substep. Saves ~18 matrix decomposes + 9 normalizes per frame on the move path. - applyEyeSafety skipped when playerVelocity.lengthSq() < 1e-4. Saves one octree raycast per idle frame; the previous frame's eye check still holds while the capsule is at rest. - Dropped dead playerDirection scratch (replaced by frameForward/Side). - New moveAddScratch keeps the cached frame vectors immutable across the two add operations in applyControls. Dedup - captureLookAtQuat helper extracted from beginFlyToPfp / beginFlyToIso (saved 16 lines of saved-state / lookAt / restore boilerplate from both fly setups). InstanceType<typeof THREE.X> type alias keeps param annotations in the runtime-namespace world. - SPEED_RUN_SQ + SPEED_WALK_SQ moved from a renderer-local const to bitmap-3d-physics.ts as named exports. Renderer + jest spec import one source of truth; threshold tweaks are a single edit. - Shared Playwright helpers extracted to playwright/specs/_shared/ bitmap-3d-debug.ts (Bitmap3dDebug type, readDebug, waitForState, tick, setKey, mountFixture, loadBitmapFixture). Desktop + mobile specs collapse to a single import block + mountFixture beforeEach. Folder is outside both project testDirs so Playwright doesn't try to execute it. Tidy - Dropped redundant cdr.markForCheck() from Bitmap3dE2EComponent; OnPush + (click) bindings already mark the view dirty. - Dropped (navigator as any)?.maxTouchPoints cast — it's standard Navigator since DOM lib 2018+. Comment trim (per workspace HARD RULE: keep JSDoc / "why" / sourcing / encoding / example / edge-case comment blocks; only trim bombast + before/after history TEXT INSIDE): - fitDist comment: dropped "previously used Math.atan instead of Math.tan, fix while we're here" before/after history. - finalCamera magnitude comment: dropped "previously the iso corner was at sqrt(3/2)*distance..." before/after history. - PLAYER_RADIUS comment: trimmed "bumped from 0.12 -> 0.22 to fix the wall-clip bug" before/after history to current-tense rationale. - nipplejs move-listener comment: trimmed "the old (evt, data) two-arg shape used during the initial port crashed silently..." to one-line current-tense "Two-arg (evt, data) throws on every move." All other JSDoc + sourcing + edge-case blocks (sun direction lookup, needle/rune pixel-threshold idiom, ecctrl line refs, nipplejs #61/#64 defences, three.js#21921 eye-safety, etc.) untouched per HARD RULE. Tests - jest: 279/279 (incl. all 40 bitmap-3d-physics tests with the imported SPEED_*_SQ constants). - playwright: 17/17 across desktop + mobile projects.
1 parent 9f6d6e4 commit a2ace1d

7 files changed

Lines changed: 188 additions & 168 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { expect, Page } from '@playwright/test';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
5+
/**
6+
* Shared helpers + types for the bitmap-3d Playwright specs (desktop +
7+
* mobile). Lives outside both project testDirs so Playwright doesn't try
8+
* to execute it as a spec file.
9+
*/
10+
11+
export interface Bitmap3dDebug {
12+
state: 'intro' | 'orbit' | 'fly-to-pfp' | 'pfp' | 'fly-to-iso' | 'exit-done';
13+
playerState: 'idle' | 'walking' | 'running' | 'jumping' | 'falling';
14+
pos: [number, number, number];
15+
fov: number;
16+
onFloor: boolean;
17+
vel: [number, number, number];
18+
joy: { fwd: number; right: number };
19+
look: { x: number; y: number };
20+
touchOn: boolean;
21+
pfpOn: boolean;
22+
tick(frames?: number, dt?: number): void;
23+
setKey(code: string, down: boolean): void;
24+
jump(): void;
25+
}
26+
27+
type Win = Window & { __bitmap3d?: Bitmap3dDebug };
28+
29+
/**
30+
* Block 800,000 — canonical E2E bitmap. Healthy variety of cube sizes
31+
* (1-6) so step-up (size-1 auto-climb) and jump (size-2+) both get
32+
* exercised, immutable on-chain, lives in playwright/fixtures/.
33+
*/
34+
export const loadBitmapFixture = (height = 800_000): { sizes: number[] } =>
35+
JSON.parse(
36+
fs.readFileSync(
37+
path.resolve(__dirname, `../../fixtures/bitmap-${height}.json`),
38+
'utf8',
39+
),
40+
);
41+
42+
export const readDebug = (page: Page) =>
43+
page.evaluate(() => (window as unknown as Win).__bitmap3d!);
44+
45+
/**
46+
* In-browser polling for a state value. Avoids Node<->CDP roundtrip per
47+
* probe and unlike expect.poll catches transient values reliably.
48+
*/
49+
export const waitForState = (page: Page, target: Bitmap3dDebug['state'], timeout = 30_000) =>
50+
page.waitForFunction(
51+
s => (window as unknown as Win).__bitmap3d?.state === s,
52+
target,
53+
{ timeout, polling: 100 },
54+
);
55+
56+
/**
57+
* Run N PFP physics frames at a fixed dt in-browser. Replaces real-time
58+
* rAF (which headless Chromium throttles to ~1Hz) with a deterministic
59+
* loop. 60 frames @ 1/60 dt = 1 simulated second.
60+
*/
61+
export const tick = (page: Page, frames: number) =>
62+
page.evaluate(n => (window as unknown as Win).__bitmap3d?.tick(n), frames);
63+
64+
export const setKey = (page: Page, code: string, down: boolean) =>
65+
page.evaluate(
66+
args => (window as unknown as Win).__bitmap3d?.setKey(args.code, args.down),
67+
{ code, down },
68+
);
69+
70+
/**
71+
* Common beforeEach: inject the fixture via addInitScript so it's
72+
* available before the SPA boots, navigate to /e2e/bitmap-3d, assert
73+
* the renderer mounted.
74+
*/
75+
export const mountFixture = async (page: Page, sizes: number[]): Promise<void> => {
76+
await page.addInitScript(`window.__bitmap3dFixture = ${JSON.stringify({ sizes })};`);
77+
await page.goto('/e2e/bitmap-3d');
78+
await expect(page.getByTestId('bitmap-3d-renderer')).toBeAttached();
79+
};

frontend/playwright/specs/desktop/bitmap-3d.spec.ts

Lines changed: 12 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,18 @@
1-
import { test, expect, Page } from '@playwright/test';
2-
import * as fs from 'fs';
3-
import * as path from 'path';
4-
5-
// Block 800,000 — picked as the canonical E2E bitmap. Healthy variety of
6-
// cube sizes (1-6) so step-up (size-1 auto-climb) and jump (size-2+) both
7-
// get exercised, immutable on-chain, lives forever in playwright/fixtures/.
8-
const fixture = JSON.parse(
9-
fs.readFileSync(path.resolve(__dirname, '../../fixtures/bitmap-800000.json'), 'utf8'),
10-
);
11-
12-
interface Bitmap3dDebug {
13-
state: 'intro' | 'orbit' | 'fly-to-pfp' | 'pfp' | 'fly-to-iso' | 'exit-done';
14-
playerState: 'idle' | 'walking' | 'running' | 'jumping' | 'falling';
15-
pos: [number, number, number];
16-
fov: number;
17-
onFloor: boolean;
18-
vel: [number, number, number];
19-
tick(frames?: number, dt?: number): void;
20-
setKey(code: string, down: boolean): void;
21-
jump(): void;
22-
}
23-
24-
const readDebug = (page: Page) =>
25-
page.evaluate(() => (window as unknown as { __bitmap3d?: Bitmap3dDebug }).__bitmap3d!);
26-
27-
// In-browser polling for a state value: avoids the Node<->CDP roundtrip
28-
// per probe, and unlike expect.poll catches transient values reliably.
29-
const waitForState = (page: Page, target: Bitmap3dDebug['state'], timeout = 30_000) =>
30-
page.waitForFunction(
31-
s => (window as unknown as { __bitmap3d?: { state: string } }).__bitmap3d?.state === s,
32-
target,
33-
{ timeout, polling: 100 },
34-
);
35-
36-
// Run N PFP physics frames at a fixed dt in-browser. Replaces real-time
37-
// rAF (which headless Chromium throttles to ~1Hz) with a deterministic
38-
// loop the test can wait on. 60 frames @ 1/60 dt = 1 simulated second.
39-
const tick = (page: Page, frames: number) =>
40-
page.evaluate(n => (window as unknown as { __bitmap3d?: Bitmap3dDebug }).__bitmap3d?.tick(n), frames);
41-
42-
const setKey = (page: Page, code: string, down: boolean) =>
43-
page.evaluate(
44-
args => (window as unknown as { __bitmap3d?: Bitmap3dDebug }).__bitmap3d?.setKey(args.code, args.down),
45-
{ code, down },
46-
);
1+
import { test, expect } from '@playwright/test';
2+
import {
3+
loadBitmapFixture,
4+
mountFixture,
5+
readDebug,
6+
setKey,
7+
tick,
8+
waitForState,
9+
} from '../_shared/bitmap-3d-debug';
10+
11+
const fixture = loadBitmapFixture();
4712

4813
test.describe('bitmap-3d renderer', () => {
4914
test.beforeEach(async ({ page }) => {
50-
await page.addInitScript(`window.__bitmap3dFixture = ${JSON.stringify({ sizes: fixture.sizes })};`);
51-
await page.goto('/e2e/bitmap-3d');
52-
await expect(page.getByTestId('bitmap-3d-renderer')).toBeAttached();
15+
await mountFixture(page, fixture.sizes);
5316
});
5417

5518
test('mounts the renderer and reaches the orbit state', async ({ page }) => {

frontend/playwright/specs/mobile/bitmap-3d.spec.ts

Lines changed: 11 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,17 @@
1-
import { test, expect, Page } from '@playwright/test';
2-
import * as fs from 'fs';
3-
import * as path from 'path';
4-
5-
// Same canonical fixture as the desktop spec.
6-
const fixture = JSON.parse(
7-
fs.readFileSync(path.resolve(__dirname, '../../fixtures/bitmap-800000.json'), 'utf8'),
8-
);
9-
10-
interface Bitmap3dDebug {
11-
state: string;
12-
playerState: string;
13-
pos: [number, number, number];
14-
fov: number;
15-
onFloor: boolean;
16-
joy: { fwd: number; right: number };
17-
look: { x: number; y: number };
18-
touchOn: boolean;
19-
pfpOn: boolean;
20-
tick(frames?: number, dt?: number): void;
21-
setKey(code: string, down: boolean): void;
22-
jump(): void;
23-
}
24-
25-
const readDebug = (page: Page) =>
26-
page.evaluate(() => (window as unknown as { __bitmap3d?: Bitmap3dDebug }).__bitmap3d!);
27-
28-
const waitForState = (page: Page, target: string, timeout = 30_000) =>
29-
page.waitForFunction(
30-
s => (window as unknown as { __bitmap3d?: { state: string } }).__bitmap3d?.state === s,
31-
target,
32-
{ timeout, polling: 100 },
33-
);
34-
35-
const tick = (page: Page, frames: number) =>
36-
page.evaluate(n => (window as unknown as { __bitmap3d?: Bitmap3dDebug }).__bitmap3d?.tick(n), frames);
1+
import { test, expect } from '@playwright/test';
2+
import {
3+
loadBitmapFixture,
4+
mountFixture,
5+
readDebug,
6+
tick,
7+
waitForState,
8+
} from '../_shared/bitmap-3d-debug';
9+
10+
const fixture = loadBitmapFixture();
3711

3812
test.describe('bitmap-3d renderer (mobile)', () => {
3913
test.beforeEach(async ({ page }) => {
40-
await page.addInitScript(`window.__bitmap3dFixture = ${JSON.stringify({ sizes: fixture.sizes })};`);
41-
await page.goto('/e2e/bitmap-3d');
42-
await expect(page.getByTestId('bitmap-3d-renderer')).toBeAttached();
14+
await mountFixture(page, fixture.sizes);
4315
});
4416

4517
test('renderer mounts on a touch device + reports a touch viewport', async ({ page }) => {

frontend/src/app/components/_ordpool/digital-artifact-viewer/bitmap-viewer/bitmap-3d-e2e.component.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
22

33
/**
44
* Playwright E2E mount point for the bitmap 3D renderer.
@@ -47,14 +47,14 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject } from '@
4747
standalone: false,
4848
})
4949
export class Bitmap3dE2EComponent {
50-
private cdr = inject(ChangeDetectorRef);
51-
50+
// OnPush + (click) bindings — Angular runs CD on the bound event, so
51+
// these setters don't need an explicit markForCheck.
5252
sizes: number[] | null = ((window as unknown as { __bitmap3dFixture?: { sizes: number[] } })
5353
.__bitmap3dFixture?.sizes) ?? null;
5454
pfp = false;
5555
exit = false;
5656

57-
enterPfp(): void { this.pfp = true; this.exit = false; this.cdr.markForCheck(); }
58-
exitPfp(): void { this.exit = true; this.cdr.markForCheck(); }
59-
onExitDone(): void { this.pfp = false; this.exit = false; this.cdr.markForCheck(); }
57+
enterPfp(): void { this.pfp = true; this.exit = false; }
58+
exitPfp(): void { this.exit = true; }
59+
onExitDone(): void { this.pfp = false; this.exit = false; }
6060
}

frontend/src/app/components/_ordpool/digital-artifact-viewer/bitmap-viewer/bitmap-3d-physics.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ import {
66
easeAlpha,
77
fovTarget,
88
gravityForStep,
9+
SPEED_RUN_SQ as RUN_SQ,
10+
SPEED_WALK_SQ as WALK_SQ,
911
} from './bitmap-3d-physics';
1012

1113
describe('derivePlayerState', () => {
12-
// Match the renderer's tuned thresholds so callers see the same boundary.
13-
const RUN_SQ = 1.5 * 1.5;
14-
const WALK_SQ = 0.5 * 0.5;
1514

1615
it('rising velocity in air -> jumping', () => {
1716
expect(derivePlayerState(0, 5, 0, false, false, RUN_SQ, WALK_SQ)).toBe('jumping');

frontend/src/app/components/_ordpool/digital-artifact-viewer/bitmap-viewer/bitmap-3d-physics.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88

99
export type PlayerState = 'idle' | 'walking' | 'running' | 'jumping' | 'falling';
1010

11+
/**
12+
* Horizontal-speed thresholds (squared, so the hot path skips sqrt).
13+
* Shared with the renderer's derivePlayerState call AND the jest spec,
14+
* so threshold changes are a single edit.
15+
*/
16+
export const SPEED_RUN_SQ = 1.5 * 1.5;
17+
export const SPEED_WALK_SQ = 0.5 * 0.5;
18+
1119
/**
1220
* Player state derivation.
1321
*

0 commit comments

Comments
 (0)