ushman-threejs-tools
v0.4.0
Published
Three.js-aware helpers for the ushman ecosystem. Scene-tree canonicalization, brand checks, runtime preboot constants, inspector driver. No CLI.
Downloads
146
Maintainers
Readme
ushman-threejs-tools
Three.js-aware helpers for the ushman ecosystem. Scene-tree canonicalization, scene-tree diff, runtime preboot constants, the
Vector3/Quaternion/Object3Dbrand-checks used by the trace serializer, and the Puppeteer page-script driver pieces that inspect a running Three.js scene.
This is a library package — no CLI. Other ushman packages (@ushman/characterize, @ushman/spector) and the main ushman orchestrator's parity/ consume it.
Status
Extracted from ushman/src/adapters/web-app/sub-adapters/scene-heavy/ and the former ushman/src/tools/** tree, with the shared Three.js surface now published here.
Runtime: mixed — pure Node for canonicalizers and constants; browser-side for the inspector page-script (loaded via Puppeteer).
What this package is NOT
- Not a Three.js fork or wrapper. We use Three's public API; we don't reimplement Mesh / Material / Renderer.
- Not a parity verdict.
ushman parityowns that. - Not a characterization runner.
@ushman/characterizeowns trace capture; we just expose the brand-checks and canonicalizer it needs.
Install
npm i ushman-threejs-tools
bun add ushman-threejs-toolsthree is a peerDependency; consumers vendor their own.
Public API
// Canonicalization (Node-side, used by characterize + parity diff)
export const canonicalizeSceneTree: (value: unknown, opts?: { exemptFields?: readonly string[]; precision?: number }) => unknown;
export const diffSceneTrees: (a: unknown, b: unknown, opts?: { exemptFields?: readonly string[]; precision?: number }) => readonly { path: string; verdict: 'add' | 'remove' | 'change'; details?: unknown }[];
export type SceneTreeDiff = { path: string; verdict: 'add' | 'remove' | 'change'; details?: unknown };
// Three.js brand-check primitives (Node-side, used by trace serializers)
export const isVector3: (v: unknown) => boolean;
export const isQuaternion: (v: unknown) => boolean;
export const isMatrix4: (v: unknown) => boolean;
export const isObject3D: (v: unknown) => boolean;
export const isBufferGeometry: (v: unknown) => boolean;
export const isMaterial: (v: unknown) => boolean;
export const isTexture: (v: unknown) => boolean;
export const stableObject3DUuid: (obj: unknown) => string; // deterministic scene identity hash, not an RFC UUID
// Inspector driver (browser-side, page-script)
export const sceneInspectorPageScriptSource: string; // injectable via Puppeteer.evaluate
export const captureSceneGraph: (
page: unknown,
opts?: {
timeoutMs?: number;
pollIntervalMs?: number;
onTimeoutDiagnostics?: (diagnostics: RuntimeReadyTimeoutDiagnostics) => void | Promise<void>;
}
) => Promise<SceneGraphSnapshot>;
export const captureSceneSnapshot: (page: { evaluate: <T>(source: string) => Promise<T> }) => Promise<{
capturedAt: string;
extraction: unknown;
sceneState: unknown;
domProbe: unknown;
warnings: string[];
}>;
export const waitForRuntimeReady: (
page: unknown,
opts?: {
timeoutMs?: number;
pollIntervalMs?: number;
onTimeoutDiagnostics?: (diagnostics: RuntimeReadyTimeoutDiagnostics) => void | Promise<void>;
}
) => Promise<boolean>;
// Runtime preboot (browser-side; constants used by capture/sweep)
export const RUNTIME_PREBOOT_HOOK_SOURCE: string;
export const RUNTIME_PREBOOT_HOOK_VERSION: string;
export type RuntimePublicationMetadata = {
importMapPresent: boolean;
moduleResolution: 'bundler' | 'unknown';
publishedAtFrame: number;
score: number;
source: string;
sourcePath: string | null;
syntheticApp: boolean;
};
// SceneGraphSnapshot is the combined payload returned after the driver waits for preboot readiness.
export type SceneGraphSnapshot = {
capturedAt: string;
domProbe: unknown;
extraction: unknown;
readyViaPreboot: boolean;
sceneState: unknown;
warnings: string[];
};
export type RuntimeReadyTimeoutDiagnostics = {
capturedAt: string;
diagnosticError: string | null;
documentReadyState: string | null;
href: string | null;
prebootDebug: unknown;
prebootState: {
captureStatePresent: boolean;
debugHelperPresent: boolean;
globalRootsPresent: boolean;
};
rafFrameCount: number;
runtimeRoots: {
capture: { app: boolean; metadata: boolean; waterSystem: boolean };
global: { app: boolean; metadata: boolean; waterSystem: boolean };
};
warnings: unknown[];
};
// Diagnostics are best-effort: `prebootDebug` is only populated when the
// preboot hook injected and executed successfully enough to publish its debug
// helper before readiness timed out.
// ushman v4 scene-heavy evidence helpers (Node-side; v4 = the ushman workspace/evidence contract)
export const createThreejsRunId: (value?: Date | string) => string;
export type ThreejsBootMode = 'vite-preview' | 'vite-dev' | 'unknown';
export const resolveThreejsEvidencePaths: (workspaceRoot: string, runId: string) => {
rootDir: string;
runDir: string;
capturePath: string;
diagnosticsPath: string;
prebootTelemetryPath: string;
manifestPath: string;
};
export const writeThreejsEvidenceBundle: (input: {
workspaceRoot: string;
runId: string;
capture: SceneGraphSnapshot;
bootMode?: 'vite-preview' | 'vite-dev' | 'unknown';
gpuDiagnostics?: unknown;
prebootTelemetry?: unknown;
}) => Promise<{
manifest: {
schema: 'threejs-evidence/v1';
runId: string;
bootMode: 'vite-preview' | 'vite-dev' | 'unknown';
};
paths: ReturnType<typeof resolveThreejsEvidencePaths>;
}>;
export const buildThreejsCaptureLedgerPayloads: (input: {
runId: string;
capture: SceneGraphSnapshot;
bootMode?: 'vite-preview' | 'vite-dev' | 'unknown';
resultPath?: string;
}) => {
toolInvocation: { summary: string; details: Record<string, unknown> };
validatorResult: { verdict: 'green' | 'yellow' | 'red'; summary: string; metrics: Record<string, unknown> };
};SceneGraphSnapshot example:
const snapshot: SceneGraphSnapshot = {
capturedAt: '2026-05-13T18:22:00Z',
domProbe: { selectedPresetValue: 'lobby' },
extraction: {
runtimeAccess: {
cameraPath: 'window.__app.camera',
rendererPath: 'window.__app.renderer',
scenePath: 'window.__app.scene',
},
sceneGraph: {
totalTriangles: 128,
},
},
readyViaPreboot: true,
sceneState: {
totalTriangles: 128,
},
warnings: [],
};readyViaPreboot is false when readiness timed out before capture. The returned payload may still contain partial extraction data, but callers should treat it as degraded and inspect warnings plus any timeout diagnostics before producing parity or characterization verdicts.
Example timeout diagnostics usage:
const snapshot = await captureSceneGraph(page, {
timeoutMs: 5_000,
pollIntervalMs: 50,
onTimeoutDiagnostics: (diagnostics) => {
if (!diagnostics.prebootState.captureStatePresent) {
console.warn('Preboot never surfaced before timeout:', diagnostics);
}
},
});Subpath entries expose the lower-level helpers used by peers:
ushman-threejs-tools/inspectorcaptureSceneGraph,captureSceneSnapshot,installSceneHeavyPreboot,readPrebootHookSourcecaptureChromiumGpuDiagnostics,captureFromPage,collectInterestingGpuLines,extractGraphicsFeatureStatus,summarizeSystemInfocreateThreejsRunId,resolveThreejsEvidencePaths,writeThreejsEvidenceBundle,buildThreejsCaptureLedgerPayloadsEXTRACTION_SCRIPT,READ_SCENE_STATE_SCRIPT,DOM_RUNTIME_PROBE_SCRIPTcomposeSceneObjectExportExpression,composeSceneObjectVisibilityExpression
ushman-threejs-tools/prebootCAPTURE_THREE_TREE_KEY,RUNTIME_ROOTS_KEY,RuntimePublicationMetadata,RuntimeRootsRUNTIME_PREBOOT_HOOK_USER_SCRIPT
Why a separate package
- Three.js peer dependency. Pinning
threein the orchestrator forces every other ushman package to pin the same version. Pulling Three.js consumers into one package contains the choice. - Shared by characterize + spector + parity. Without this package, all three would re-implement
canonicalizeSceneTree. That's how round-4 lessons get re-learned. - Browser/Node split. The inspector page-script is real browser JavaScript; the canonicalizer is Node. Keeping both behind one entry-point with explicit subpath imports (
ushman-threejs-tools/inspector,ushman-threejs-tools/canonical) makes the boundary explicit. - Graphics-only. This package is for Three.js / WebGL / WebGPU donors. Browser-extension adapters, generic non-graphical web apps, and embedded webviews do not depend on it. Keeping it isolated means non-graphical work doesn't pay a Three.js peer-dep cost.
Where this fits in the family
| | |
|---|---|
| Peer-depended-on by | @ushman/characterize, @ushman/spector, ushman (parity) |
| Does NOT depend on | any other @ushman/* package |
| Has no CLI | this is a library only |
Development
bun run generate:sources
bun run build
bun run typecheck
bun testgenerate:sources bundles src/preboot/entry.ts into the generated string constant exported as RUNTIME_PREBOOT_HOOK_SOURCE.
When you change src/preboot/*.ts, rerun bun run generate:sources before bun test or bun run build so src/preboot/generated-source.ts stays in sync.
Browser-backed E2E coverage for the preboot and inspector driver helpers runs against the local fixtures under test/fixtures/preboot/. Set PUPPETEER_EXECUTABLE_PATH when Chrome or Chromium is installed outside the standard locations from test/fixtures/preboot/harness.ts; otherwise those E2E suites skip automatically.
Stability
- Root exports and documented subpath exports are the stable integration surface and should follow semver.
RUNTIME_PREBOOT_HOOK_SOURCEand the emittedthreejs-evidence/v1manifest are intended for downstream automation and should only change compatibly outside a major version.- The internal structure of the large injected browser scripts is implementation detail. Consumers should use the exported helpers instead of depending on string internals.
V3 to V4
- v4 scene-heavy evidence lives under
<workspace>/.lab/threejs/<run-id>/instead of older screenshot- or stage-oriented layouts. - Bundler-resolved Vite pages are the supported runtime target. The preboot hook no longer changes behavior based on importmap mode.
- Callers that own boot orchestration should pass the concrete
ThreejsBootModeintowriteThreejsEvidenceBundle()andbuildThreejsCaptureLedgerPayloads()instead of assuming the helper can infer it.
Runtime Notes
ushman v4here means the version 4 workspace and evidence contract used by the broader ushman pipeline.- v4 callers should boot scene-heavy candidates in production shape (
vite build && vite preview) and persist capture evidence under<workspace>/.lab/threejs/<run-id>/. This package now exposes reusable path, write, and ledger-summary helpers for that contract. - Importmap-backed externalized ESM pages now pre-import
three,three/webgpu, andthree/tslthrough the page's own import map before falling back to scene-host heuristics. When that early path wins, runtime-root metadata reportssource: 'bundle-intercept'withimportMapPresent: true. - The offscreen-worker relay bootstraps through a Blob URL before importing the donor worker script. Apps with a strict
worker-srcCSP may block that path; the hook degrades back to the original worker when bootstrapping fails, but telemetry coverage will be reduced. stableObject3DUuidis a deterministic scene identity hash for matching comparable nodes across captures. It is stable for equivalent structure, not a unique per-instance UUID.- Bundler-resolved Vite pages are the default v4 target. When the hook proves a live
scene + renderer + cameratriple but the donor app never published one itself, it may synthesizewindow.__apponly when that global is empty or already owned by a prior synthetic publication. The published runtime root is always mirrored towindow.__USHMAN_RUNTIME_ROOTS__.app; its metadata follows the exportedRuntimePublicationMetadatashape. - Injected preboot globals now include
window.__debugThreePrebootTelemetry()for lightweight readiness diagnostics andwindow.__cleanupThreePrebootTelemetry(reason?)for tearing down registered long-lived listeners and wrappers after capture. The debug payload includes the active preboot build version underprebootVersion. - When readiness times out,
RuntimeReadyTimeoutDiagnosticsreports whether the preboot capture state, runtime-roots global, and debug helper were ever observed. That distinguishes "preboot never surfaced" from "preboot surfaced but no runtime roots were published" without changing the main capture contract. - Restrictive CSP environments are still diagnostics-first in this release. The driver can now report that the preboot globals never appeared before timeout, but a dedicated same-origin module-bridge fallback for CSP-constrained pages is not implemented yet.
Telemetry Limits
- Rolling history caps such as
MAX_HISTORYandMAX_THREE_SAMPLESkeep the newest entries and drop the oldest ones once full. - Capture caps such as
MAX_UNIFORMS,MAX_PROGRAMS,MAX_SHADERS,MAX_TEXTURES, andMAX_FRAMEBUFFERSstop adding new telemetry records after the limit is reached. - Discovery caps such as
MAX_NODE_GRAPH_TRAVERSE,MAX_RUNTIME_DISCOVERY_QUEUE_NODES, andMAX_RUNTIME_DISCOVERY_VISITSstop descending once the traversal budget is exhausted. - Settled-scene sampling uses a bounded queue walk for non-exhaustive reads. When the walk short-circuits,
sceneStats.sampledistrueand the reported object/light/mesh counts should be treated as representative lower bounds rather than exact scene totals.
Cleanup Behavior
| Hook or Record | Restored by __cleanupThreePrebootTelemetry() | Notes |
|---|---|---|
| window.requestAnimationFrame | Yes | Restores the original function reference. |
| Load listener installed by the preboot hook | Yes | Removes the tracked listener if it is still registered. |
| Canvas context event listeners attached by the preboot hook | Yes | Removes the tracked webglcontext* listeners that the hook installed. |
| Importmap observer/listeners installed by the preboot hook | Yes | Disconnects the observer and removes its lifecycle listeners. |
| Patched scene.onBeforeRender callbacks | No | Existing donor objects keep the wrapped callbacks already installed. |
| Patched material/object render hooks | No | Existing donor objects keep the wrapped callbacks already installed. |
| Published runtime roots | No | Cleanup is about teardown of long-lived hook machinery, not unpublishing capture evidence. |
