SCENEGOD/lanes/A-stage.md
type-two e29edc808b plan: SCENEGOD master plan + 3-lane instructions + log scaffolding
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:44:07 +10:00

6.0 KiB
Raw Blame History

Lane A — Stage: viewport, assets on stage, retarget port

You own: scenegod/web/index.html, style.css, room3d.js, stage.js, dock.js. Milestone: M1 (do not start M2 items until the orchestrator flips this line).

A1. index.html skeleton (first commit)

  • Layout: full-viewport CSS grid — left dock #dock (280px), center #view, bottom #timeline (260px, empty div — Lane B mounts into it), right #inspector (260px). Dark UI, system font; steal the visual language of MESHGOD's rigroom (read /Users/johnking/Documents/MESHGOD/meshgod/web/rigroom.html).
  • Importmap: copy rigroom's exactly (same three.js version + addons paths) so behavior matches the code you're porting.
  • Script entry: <script type="module" src="/web/main-stage.js">? No — keep entry inline in index.html: import {Stage} from './stage.js' etc., create the Stage, Dock, and (when it exists) new Timeline(stage) guarded by a dynamic import that tolerates timeline.js not existing yet: try { const {Timeline} = await import('./timeline.js'); ... } catch {}. That keeps lanes decoupled — your page must never be broken by B's absence.
  • Style: style.css; leave a /* === LANE B === */ marker at the bottom.

A2. room3d.js — port from rigroom.html (the crown jewels)

Source: /Users/johnking/Documents/MESHGOD/meshgod/web/rigroom.html lines ~125290 (loaders, canon/boneMap, captureRest, bakeRetarget, stats, disposeRoot). Port rules:

  • Make it a real ES module with the exports listed in PLAN §4.2. No DOM, no globals, no toast() — throw Errors, callers handle UI.
  • Parameterize the target: rigroom's bakeRetarget closes over the global char. New signature bakeRetarget(clip, srcRoot, srcRest, tgtRoot, tgtRest) -> AnimationClip. Keep the algorithm byte-for-byte otherwise: 30fps sampling, world-space rotation delta from source rest applied to target rest, parent-first world quat propagation, hips world-position track scaled by target/source hips height ratio, quaternion sign continuity (dot < 0 → negate), and the duplicate-FBX-hierarchy dedupe (bind only first bone per canon key).
  • Keep canon() exactly as-is (mixamorig\d*[:_]? strip) — it encodes real Mixamo quirks.
  • Self-check: room3d.js gets a tiny exported _selftest() that builds two 3-bone synthetic skeletons with different rests, bakes a 1s clip across, and asserts end-pose world quats match within 1e-3. Wire it to ?selftest=1 on the page; log the result.

A3. stage.js — the Stage class (PLAN §4.2 API)

  • Renderer setup: copy rigroom's (antialias, PMREM + RoomEnvironment IBL — metals gotcha), hemisphere + key directional light as default lights (replaced by scene light entities when present), circle floor + grid, OrbitControls director camera.
  • addEntity(desc):
    • character/prop: fetch /assets/file?path=, parseAny, normalize scale like rigroom (fit ~1.7m for characters; props keep native size but expose scale), ground to y=0, attach to a THREE.Group wrapper — ALL transforms (gizmo + timeline) act on the wrapper, never the loaded root. For characters: captureRest once at load, store on the entity; create AnimationMixer on the wrapper.
    • backdrop: params.modeplane: textured PlaneGeometry, height from image aspect × params.width (default 10m); corner: plane + floor plane sharing the image bottom third (cheap "L" street corner); dome: 40m sphere, side: BackSide, texture on inside. SRGBColorSpace on all image textures.
    • camera: PerspectiveCamera in a wrapper + a small frustum helper mesh so it's visible/selectable in the director view. params.fov.
    • light: key → DirectionalLight + helper; ambient → replaces the default hemisphere intensity. Adding the first key light dims defaults.
  • Selection: raycast on click → select(id), outline or bbox highlight, TransformControls gizmo (W/E/R = move/rotate/scale) on the wrapper; gizmo drag end → onChange(entity).
  • prepareClip(id, path, clipIndex): fetch+parse the clip source, cache {srcRoot, srcRest, anims} per path (Map), bake per (entity, path, index) and cache the baked clip. Baking is the expensive step — never re-bake on seek.
  • renderActiveCamera(canvas): render the active camera to a small PiP canvas overlay (M2) and, for Lane C's final render, to the main canvas at exact size. Keep director-orbit rendering the default main view.
  • captureState()/applyState(): round-trip with scene JSON entities. Assets with source.type:"upload" (drag-and-dropped local files) live only in memory — mark them in the dock and warn on save.

A4. dock.js

  • Tabs: Characters / Animations / Props / Backdrops from GET /assets/tree (Lane C; until it exists use a hardcoded mock=1 tree + local file-drop — drag any glb/fbx/image onto the page adds it as source.type:"upload").
  • Click or drag-onto-viewport → stage.addEntity. Animations are NOT added to the stage; dragging one onto a character in the viewport (raycast under drop point) or onto its inspector calls prepareClip + M1 temp "play once" button. In M2, B takes over clip placement (dock drag → timeline track).
  • Inspector panel: selected entity's label, kind, transform numbers (editable), params (fov slider for cameras, color+intensity for lights, backdrop mode), delete button, "⏺ key" button that emits window.dispatchEvent(new CustomEvent('scenegod:capturekey',{detail:{id}})) — Lane B listens; you don't call timeline code directly.

M1 acceptance (log it with evidence)

  • ?selftest=1 retarget check passes.
  • Load lady.glb (put test files in assets/ yourself — grab from MESHGOD library banks), drop street.jpg as plane backdrop, gizmo-move the lady, drop walk.fbx on her → she walks in place. Screenshot in log dir welcome (logs/shots/, yours to create).
  • Page loads clean with timeline.js absent AND with server absent (mock=1).

ORCHESTRATOR UPDATES

(none yet — check back each session)