Files
ozone-tech_owl_prime/src/domain/physicsConfigHash.ts
Даня Архипов 68327635ce feat: Stage 2C CAD assembly cleanup, full-height canvas, physics parity
Remove default Measurement overlay and procedural motor duplicate; force
opaque CAD materials; modular extension pitch from CAD; shared runtime/
headless physics config hash; layout e2e and Stage 2C artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 12:35:50 +00:00

90 lines
2.8 KiB
TypeScript

/**
* Stage 2C §11.1 — machine-readable runtime ↔ headless physics config hash.
* Both paths must consume the same static colliders, pusher geometry, dt, gravity.
* Hash is pure-JS (no Node crypto) so the module stays isomorphic.
*/
import { getStaticColliders } from './physicsWorldLayout';
import { PUSHER } from './pusherMotion';
import { PHYSICS_TIMESTEP_SEC, PHYSICS_GRAVITY } from './physicsTimestep';
export { PHYSICS_TIMESTEP_SEC, PHYSICS_GRAVITY } from './physicsTimestep';
export interface PhysicsConfigSnapshot {
timestep: number;
gravity: [number, number, number];
pusherHalfExtents: [number, number, number];
pusherCenterY: number;
pusherEngage: { x: number; z: number };
staticColliderCount: number;
staticColliderIds: string[];
staticColliderFingerprint: string;
}
function fingerprintColliders(): string {
const defs = getStaticColliders();
return defs
.map((d) =>
[
d.id,
d.halfExtents.map((n) => n.toFixed(5)).join(','),
d.position.map((n) => n.toFixed(5)).join(','),
d.rotation.map((n) => n.toFixed(5)).join(','),
d.friction.toFixed(4),
].join('|'),
)
.join(';');
}
/** FNV-1a 64-bit (as hex) — deterministic, isomorphic. */
function fnv1aHex(input: string): string {
let h = 0xcbf29ce484222325n;
const prime = 0x100000001b3n;
for (let i = 0; i < input.length; i++) {
h ^= BigInt(input.charCodeAt(i));
h = (h * prime) & 0xffffffffffffffffn;
}
return h.toString(16).padStart(16, '0');
}
export function buildPhysicsConfigSnapshot(timestep: number): PhysicsConfigSnapshot {
const defs = getStaticColliders();
return {
timestep,
gravity: PHYSICS_GRAVITY,
pusherHalfExtents: [...PUSHER.halfExtents] as [number, number, number],
pusherCenterY: PUSHER.centerY,
pusherEngage: { x: PUSHER.engageX, z: PUSHER.engageZ },
staticColliderCount: defs.length,
staticColliderIds: defs.map((d) => d.id),
staticColliderFingerprint: fingerprintColliders(),
};
}
export function hashPhysicsConfig(snapshot: PhysicsConfigSnapshot): string {
return fnv1aHex(JSON.stringify(snapshot));
}
export function getRuntimePhysicsConfigHash(): string {
return hashPhysicsConfig(buildPhysicsConfigSnapshot(PHYSICS_TIMESTEP_SEC));
}
export function getHeadlessPhysicsConfigHash(headlessTimestep = PHYSICS_TIMESTEP_SEC): string {
return hashPhysicsConfig(buildPhysicsConfigSnapshot(headlessTimestep));
}
export function assertRuntimeHeadlessParity(headlessTimestep = PHYSICS_TIMESTEP_SEC): {
runtime: string;
headless: string;
equal: boolean;
timestepEqual: boolean;
} {
const runtime = getRuntimePhysicsConfigHash();
const headless = getHeadlessPhysicsConfigHash(headlessTimestep);
return {
runtime,
headless,
equal: runtime === headless,
timestepEqual: headlessTimestep === PHYSICS_TIMESTEP_SEC,
};
}