Problem: only GLB/glTF can be brought in, objects have one kind and a flat meta bag, and procedural styles are a closed union. Verified on main: packages/node/src/editorHostPlugin.ts:71 throws for non-glTF; packages/editor/src/assetImport.ts:13 MODEL_FILE = /\.(glb|gltf)$/i; packages/assets/src/catalogs/build.ts CatalogExtra is { url }; packages/core/src/scene/sceneKinds.ts ParamField has no asset-ref, object-ref, vec3, curve, or gradient; space is unset in every generated index entry; packages/core/src/world/buildings.ts BuildingStyle is a closed union.
Tasks, one PR each, in order:
-
Import spec (assets package, pure). New packages/assets/src/importSpec.ts: AssetImportSpec { id: string; kind: "model" | "texture" | "material" | "sprite" | "spriteSheet" | "audio" | "font" | "hdri"; files: { path: string; role?: string }[]; meta?: Record<string, unknown> }, validateImportSpec(spec) -> { ok: true; entry } | { ok: false; reason }, classifyAssetFile(bytes, filename) -> kind | null (magic bytes for glb, png, jpg, ktx2, ogg, wav, mp3, ttf/otf/woff2, hdr, exr; JSON with frames as spriteSheet). Widen CatalogExtra to carry dims, space, clips, collisionMesh. Move the signature checks in packages/core/src/scene/assetDiagnostics.ts to classify instead of rejecting. Tests with byte fixtures. bun run gen. CHANGELOG Added.
-
Editor ingest. editorHostPlugin.ts, assetImport.ts, packages/editor/src/shell/ContentBrowser.tsx accept every kind from task 1 (drop or file picker), write under the game's public/assets/<kind>/, and register a catalog entry; packages/editor/src/mcp/tools.ts gets an importAsset tool with the same spec. Shots: the content browser showing a dropped PNG, WAV, and TTF.
-
Inspector fields. Add assetRef (with kinds filter), objectRef, vec3, curve (list of [t, v] points), gradient (list of [t, hex]) to ParamField in sceneKinds.ts and widgets in packages/editor/src/chromeFields.tsx (asset picker reuses the content browser, object picker reuses ParentPickerMenu). Tests for schema validation. Shots.
-
Game-defined components. New packages/core/src/scene/components.ts: registerComponent({ id; targets: ("marker" | "volume" | "path")[]; schema: ParamSchema; defaults }), componentsOf(object) -> Record<id, data> reading meta.components[id], setComponent(doc, objectId, id, data) as an editor command in packages/core/src/editor/commands.ts. packages/editor/src/InspectorPanel.tsx renders every registered component for the selected object with an add/remove menu. Test: a registered component round-trips through the document. Shots.
-
Populate space. In packages/assets/src/cli/pull.ts and indexGen.ts, derive space (canonical facing from the longest horizontal extent, unit scale from dims, anchor from minY) with a per-pack override table in packages/assets/src/packs/*.ts; regenerate the index JSON (bun --cwd=packages/assets run reindex or the documented command). Test on two entries.
-
Style profiles. New packages/core/src/world/styleProfile.ts: StyleProfile { id; palettes: Record<string, string>; materials?: Record<string, string>; massing?: MassingRule[] }, registerStyleProfile, resolveStyleProfile(id); register the ten existing palettes from buildings.ts as profiles and make buildings.ts and packages/shell/src/scatter/scatterProxies.ts resolve through meta.styleId with the current defaults, so output is unchanged until a game registers its own. Shots: the city with a custom profile. Closes #1689.
Rules: wait for bun run agent:bootstrap --check; branch claude/<slug> off origin/main; claim comment first; one task per PR with Refs #1689; bun run gen after export changes (JSDoc on every export); CHANGELOG bullet; check-types and test on assets, core, node, editor as touched plus check-stateful-ratchet, check-doc-symbols, check-orphan-ratchet, check-asset-availability; merge origin/main before pushing; shots via bun run shoot; never add Kenney assets; squash auto-merge; fix CI on the same branch.
Problem: only GLB/glTF can be brought in, objects have one kind and a flat meta bag, and procedural styles are a closed union. Verified on main:
packages/node/src/editorHostPlugin.ts:71throws for non-glTF;packages/editor/src/assetImport.ts:13MODEL_FILE = /\.(glb|gltf)$/i;packages/assets/src/catalogs/build.tsCatalogExtrais{ url };packages/core/src/scene/sceneKinds.tsParamFieldhas no asset-ref, object-ref, vec3, curve, or gradient;spaceis unset in every generated index entry;packages/core/src/world/buildings.tsBuildingStyleis a closed union.Tasks, one PR each, in order:
Import spec (assets package, pure). New
packages/assets/src/importSpec.ts:AssetImportSpec { id: string; kind: "model" | "texture" | "material" | "sprite" | "spriteSheet" | "audio" | "font" | "hdri"; files: { path: string; role?: string }[]; meta?: Record<string, unknown> },validateImportSpec(spec) -> { ok: true; entry } | { ok: false; reason },classifyAssetFile(bytes, filename) -> kind | null(magic bytes for glb, png, jpg, ktx2, ogg, wav, mp3, ttf/otf/woff2, hdr, exr; JSON withframesas spriteSheet). WidenCatalogExtrato carrydims,space,clips,collisionMesh. Move the signature checks inpackages/core/src/scene/assetDiagnostics.tsto classify instead of rejecting. Tests with byte fixtures.bun run gen. CHANGELOG Added.Editor ingest.
editorHostPlugin.ts,assetImport.ts,packages/editor/src/shell/ContentBrowser.tsxaccept every kind from task 1 (drop or file picker), write under the game'spublic/assets/<kind>/, and register a catalog entry;packages/editor/src/mcp/tools.tsgets animportAssettool with the same spec. Shots: the content browser showing a dropped PNG, WAV, and TTF.Inspector fields. Add
assetRef(withkindsfilter),objectRef,vec3,curve(list of[t, v]points),gradient(list of[t, hex]) toParamFieldinsceneKinds.tsand widgets inpackages/editor/src/chromeFields.tsx(asset picker reuses the content browser, object picker reusesParentPickerMenu). Tests for schema validation. Shots.Game-defined components. New
packages/core/src/scene/components.ts:registerComponent({ id; targets: ("marker" | "volume" | "path")[]; schema: ParamSchema; defaults }),componentsOf(object) -> Record<id, data>readingmeta.components[id],setComponent(doc, objectId, id, data)as an editor command inpackages/core/src/editor/commands.ts.packages/editor/src/InspectorPanel.tsxrenders every registered component for the selected object with an add/remove menu. Test: a registered component round-trips through the document. Shots.Populate
space. Inpackages/assets/src/cli/pull.tsandindexGen.ts, derivespace(canonical facing from the longest horizontal extent, unit scale fromdims, anchor fromminY) with a per-pack override table inpackages/assets/src/packs/*.ts; regenerate the index JSON (bun --cwd=packages/assets run reindexor the documented command). Test on two entries.Style profiles. New
packages/core/src/world/styleProfile.ts:StyleProfile { id; palettes: Record<string, string>; materials?: Record<string, string>; massing?: MassingRule[] },registerStyleProfile,resolveStyleProfile(id); register the ten existing palettes frombuildings.tsas profiles and makebuildings.tsandpackages/shell/src/scatter/scatterProxies.tsresolve throughmeta.styleIdwith the current defaults, so output is unchanged until a game registers its own. Shots: the city with a custom profile.Closes #1689.Rules: wait for
bun run agent:bootstrap --check; branchclaude/<slug>offorigin/main; claim comment first; one task per PR withRefs #1689;bun run genafter export changes (JSDoc on every export); CHANGELOG bullet;check-typesandteston assets, core, node, editor as touched pluscheck-stateful-ratchet,check-doc-symbols,check-orphan-ratchet,check-asset-availability; mergeorigin/mainbefore pushing; shots viabun run shoot; never add Kenney assets; squash auto-merge; fix CI on the same branch.