# Lane I — Asset runtime (manifest, registry, slot hooks) **Owns:** `src/assets/`, `assets/manifest.json`, `demos/lane-i.html`, `src/demo/lane-i.ts`, PLUS surgical slot-hook edits inside `src/blob/createBlob.ts`, `src/paint/cannon.ts` (barrel build only), `src/machine/parts.ts` (mesh builds only), `src/course/greybox.ts` (scenery only). Those four are OTHER lanes' files: your edits there must be strictly additive-visual — replace direct mesh construction with a registry hook that DEFAULTS to the existing construction. Never touch physics/colliders/logic in them, and never edit contracts/world/ game/main/package.json. ## Mission Custom GLB assets drop into named slots without code changes; an empty manifest produces today's game exactly. See docs/WORKSHOP-PLAN.md for the slot table — implement every slot listed there except the UI rows. ## Deliverables 1. `src/assets/manifest.ts` — schema + loader: ```ts interface SlotEntry { url: string; offset?: [n,n,n]; rotationDeg?: [n,n,n]; scale?: number | [n,n,n]; idleClip?: string } type Manifest = Record ``` Load order (later wins): `assets/manifest.json` (fetch, may 404 → {}) → IndexedDB override (`blobbo-workshop` db: `manifest` + `blobs` stores; GLB entries may be `idb:` urls resolved to object URLs). 2. `src/assets/registry.ts` — `AssetRegistry`: - `slotObject(slot, buildFallback: () => THREE.Object3D): THREE.Object3D` — synchronous placeholder pattern: returns a Group immediately containing the fallback; if the manifest has the slot, async-load the GLB (GLTFLoader from `three/examples/jsm` — already in the three package, no new deps), then swap the group's child in place, applying offset/rotation/scale. - Cache by url (clone via `SkeletonUtils.clone` for skinned, `.clone()` else). - Load failure → keep fallback + one console.warn (never throw). - `paintableInfo(object)` — for blob.body: find first Mesh/SkinnedMesh, report { uvOk (uv attr exists & 0..1 bounded), triCount, materialCount }. 3. Blob slot specifics (in your createBlob hook): - The delivered mesh's material `map` is replaced by PaintSkin's canvas texture at integration (skin binds to whatever mesh is present); ensure the hook exposes the swapped-in paintable mesh as `blob.mesh` (the skin and buffs read it). **NOT BUILT — SkinnedMesh + `idleClip` was authored here and does not work.** `createBlob` re-parents the body MESH alone into its own group, leaving the GLB's bones outside the scene graph with no `matrixWorld` update, so the clip animates nothing while burning fixed-step CPU; and the body's geometry is re-centred/re-scaled to the collider radius, which invalidates the skeleton's bind matrices. `paintableInfo` therefore REJECTS a skinned `blob.body` (`ok` requires `!skinned`), `idleClip` is ignored with a warning and no AnimationMixer is created. Making it real means keeping the GLB root in the scene graph and fitting via a parent node instead of baking into the geometry — a design change, not a bug fix. - UV warning: if `paintableInfo.uvOk` is false, console.warn and keep the procedural sphere instead (paint must never silently break). 4. `assets/manifest.json` — ship EMPTY `{}` (packs come later via integration). 5. Demo `demos/lane-i.html` + `src/demo/lane-i.ts`: a stage that builds every slot twice — fallback vs a test manifest pointing at the four existing farm GLBs (`assets/meshes/*.glb`) — side by side, with per-slot load status text and the paintability report for blob.body. ## Acceptance - `npm run build` green. With `{}` manifest: pixel-identical construction paths (verify: no registry code path alters any existing geometry parameters — the fallback builders are the ORIGINAL code moved, not rewritten). - Demo loads all four farm GLBs into sensible slots (boot, bucket, toaster scenery, blob body) with fit transforms committed in the demo's test manifest. - Blob body swap: paint stamps visibly land on the swapped mesh in the demo (drive a scripted splat at a known point). - Failure paths: bad url → fallback + single warn; malformed manifest → `{}`. - Slot-hook edits to the four foreign files are mesh-construction-only (diff must show no collider/physics/logic lines changed). ## Contract friction Report, never edit frozen files. If a slot needs data the hook can't reach, document it for integration rather than widening interfaces.