@jgengine/editor 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +325 -0
  2. package/dist/AssetBrowser.d.ts +20 -0
  3. package/dist/AssetBrowser.js +49 -0
  4. package/dist/CatalogsPanel.d.ts +10 -0
  5. package/dist/CatalogsPanel.js +41 -0
  6. package/dist/CollectionsPanel.d.ts +10 -0
  7. package/dist/CollectionsPanel.js +35 -0
  8. package/dist/DebugDraw.d.ts +17 -0
  9. package/dist/DebugDraw.js +233 -0
  10. package/dist/EditorApp.d.ts +26 -0
  11. package/dist/EditorApp.js +333 -0
  12. package/dist/EditorCameraDriver.d.ts +5 -0
  13. package/dist/EditorCameraDriver.js +18 -0
  14. package/dist/EditorChrome.d.ts +22 -0
  15. package/dist/EditorChrome.js +559 -0
  16. package/dist/EditorContextMenu.d.ts +11 -0
  17. package/dist/EditorContextMenu.js +12 -0
  18. package/dist/InspectorPanel.d.ts +14 -0
  19. package/dist/InspectorPanel.js +110 -0
  20. package/dist/MaterialDropZone.d.ts +10 -0
  21. package/dist/MaterialDropZone.js +77 -0
  22. package/dist/OutlinerPanel.d.ts +14 -0
  23. package/dist/OutlinerPanel.js +84 -0
  24. package/dist/PerfProbe.d.ts +5 -0
  25. package/dist/PerfProbe.js +34 -0
  26. package/dist/PrefabsPanel.d.ts +13 -0
  27. package/dist/PrefabsPanel.js +42 -0
  28. package/dist/RuntimePlayBridge.d.ts +18 -0
  29. package/dist/RuntimePlayBridge.js +140 -0
  30. package/dist/ScatterPreview.d.ts +11 -0
  31. package/dist/ScatterPreview.js +30 -0
  32. package/dist/SchemaInspector.d.ts +19 -0
  33. package/dist/SchemaInspector.js +78 -0
  34. package/dist/SelectionGizmo.d.ts +25 -0
  35. package/dist/SelectionGizmo.js +455 -0
  36. package/dist/StandaloneEditor.d.ts +44 -0
  37. package/dist/StandaloneEditor.js +92 -0
  38. package/dist/TerrainPanel.d.ts +7 -0
  39. package/dist/TerrainPanel.js +59 -0
  40. package/dist/TerrainSculpt.d.ts +14 -0
  41. package/dist/TerrainSculpt.js +415 -0
  42. package/dist/TriggerInspector.d.ts +13 -0
  43. package/dist/TriggerInspector.js +60 -0
  44. package/dist/agent/AgentPanel.d.ts +13 -0
  45. package/dist/agent/AgentPanel.js +156 -0
  46. package/dist/agent/context.d.ts +27 -0
  47. package/dist/agent/context.js +39 -0
  48. package/dist/agent/endpoint.d.ts +68 -0
  49. package/dist/agent/endpoint.js +186 -0
  50. package/dist/agent/toolBridge.d.ts +35 -0
  51. package/dist/agent/toolBridge.js +93 -0
  52. package/dist/agent/turn.d.ts +71 -0
  53. package/dist/agent/turn.js +113 -0
  54. package/dist/chromeFields.d.ts +15 -0
  55. package/dist/chromeFields.js +13 -0
  56. package/dist/chromeStyles.d.ts +3 -0
  57. package/dist/chromeStyles.js +3 -0
  58. package/dist/index.d.ts +20 -0
  59. package/dist/index.js +19 -0
  60. package/dist/mcp/bridgeServer.d.ts +13 -0
  61. package/dist/mcp/bridgeServer.js +1 -0
  62. package/dist/mcp/loadGameCatalogs.d.ts +24 -0
  63. package/dist/mcp/loadGameCatalogs.js +87 -0
  64. package/dist/mcp/rpcRequest.d.ts +22 -0
  65. package/dist/mcp/rpcRequest.js +97 -0
  66. package/dist/mcp/tools.d.ts +8 -0
  67. package/dist/mcp/tools.js +764 -0
  68. package/dist/outlinerModel.d.ts +76 -0
  69. package/dist/outlinerModel.js +95 -0
  70. package/dist/perfMarks.d.ts +25 -0
  71. package/dist/perfMarks.js +36 -0
  72. package/dist/session.d.ts +400 -0
  73. package/dist/session.js +1032 -0
  74. package/dist/uiStore.d.ts +108 -0
  75. package/dist/uiStore.js +128 -0
  76. package/dist/useF2Chord.d.ts +2 -0
  77. package/dist/useF2Chord.js +32 -0
  78. package/dist/useStoreSelector.d.ts +31 -0
  79. package/dist/useStoreSelector.js +51 -0
  80. package/dist/viewportContextMenu.d.ts +24 -0
  81. package/dist/viewportContextMenu.js +32 -0
  82. package/package.json +58 -0
  83. package/skills/jgengine/SKILL.md +823 -0
  84. package/skills/jgengine/api.md +416 -0
  85. package/skills/jgengine/capabilities.md +21 -0
  86. package/skills/jgengine/reference-cartridge.md +72 -0
  87. package/skills/jgengine/reference-orphans.md +9 -0
  88. package/skills/jgengine/reference-systems.md +60 -0
  89. package/skills/jgengine-editor/SKILL.md +382 -0
  90. package/skills/jgengine-editor/api.md +416 -0
  91. package/skills/jgengine-editor/capabilities.md +29 -0
  92. package/skills/jgengine-editor/reference.md +64 -0
@@ -0,0 +1,60 @@
1
+ # Composable systems (`defineSystem`)
2
+
3
+ Single public authoring path: `defineGame({ systems, loop?, … })` from `@jgengine/shell/defineGame` (or core). Systems are meaningful capabilities — not micro-ticks.
4
+
5
+ ## API
6
+
7
+ ```ts
8
+ import { defineSystem } from "@jgengine/core/game/defineSystem";
9
+ import { compileSystemSchedule, DEFAULT_FIXED_STAGES, DEFAULT_FRAME_STAGES } from "@jgengine/core/game/systemSchedule";
10
+ import { composeGameLoop, installSystems } from "@jgengine/core/game/systemRuntime";
11
+
12
+ export const combat = defineSystem({
13
+ id: "combat",
14
+ feature: "quest", // optional — enables ctx.game.quest without features: { quest: true }
15
+ dependsOn: ["movement"], // optional — must also be in systems[]
16
+ tick: {
17
+ type: "fixed", // | "frame" | "interval" | "manual"
18
+ rate: 60, // fixed only (Hz); default 60
19
+ every: 1, // interval only (game-seconds)
20
+ stage: "combat",
21
+ after: "movement", // optional local order within the stage
22
+ before: "cleanup",
23
+ },
24
+ create(ctx) {},
25
+ start(ctx) {},
26
+ update(ctx, dt) {},
27
+ events: {
28
+ "entity.died"(ctx, event) {},
29
+ },
30
+ save: { key: "combat", snapshot: () => state, hydrate: (data) => { state = data; } },
31
+ replicate: (ctx) => ({ key: "combat-pub", snapshot: () => pub, hydrate: (d) => { pub = d; } }),
32
+ reset(ctx) {},
33
+ dispose(ctx) {},
34
+ });
35
+ ```
36
+
37
+ `defineGame` OR-merges `feature` flags, then `composeGameLoop(systems, loop)` so systems install on `onInit` and tick before residual `loop.onTick`. Classic games migrate by moving fan-out out of `onTick` into systems while keeping boot in `loop`.
38
+
39
+ ## Timing channels
40
+
41
+ | `tick.type` | When it runs |
42
+ |-------------|--------------|
43
+ | `fixed` | Accumulator at `rate` Hz (default 60); multi-subscribe OK |
44
+ | `frame` | Once per `onTick` with the frame's game `dt` |
45
+ | `interval` | Every `every` game-seconds (own accumulator per system) |
46
+ | `manual` | Installed only; call `systemsOf(ctx)?.runManual(ctx, id)` |
47
+ | *(omit)* | Event-only — handlers in `events` |
48
+
49
+ Multiple systems may share a channel. Order is stage tables + optional `before`/`after` — **never import order**. `compileSystemSchedule` validates unique ids, `dependsOn`, and cycles.
50
+
51
+ Default stages: fixed `input → movement → combat → ai → activities → cleanup`; frame includes those plus `animation → camera → effects`.
52
+
53
+ ## Cartridge path
54
+
55
+ Cartridge remains a convenience wrapper. Express the same shape as ordinary systems (`runFlow`, wave director, combat) under `defineGame({ systems })` when you outgrow the cartridge schema — one authoring model, no rewrite cliff.
56
+
57
+ ## Ownership
58
+
59
+ - **Save / replicate** — system modules register via `ctx.game.registerSave` / `registerReplicate` at install.
60
+ - **Reset / dispose** — `loop.onReset` / `loop.onDispose` (composed) run system hooks.
@@ -0,0 +1,382 @@
1
+ ---
2
+ name: jgengine-editor
3
+ description: Open the scene editor to place spawns, zones, and ranges visually.
4
+ ---
5
+
6
+ # JGengine Editor
7
+
8
+ ## When to use
9
+
10
+ Authoring or inspecting spatial game data: terrain context, player/mob/boss spawns, zone disks, aggro/leash/discover radii, corridor paths. Prefer this over guessing `x,y,z` in TS tables.
11
+
12
+ ## Open (human / agent browser)
13
+
14
+ ```
15
+ bun run dev:runner
16
+ # http://localhost:<port>/?game=the-robots&mode=editor
17
+ # http://localhost:<port>/?game=vice-isle&mode=editor
18
+ ```
19
+
20
+ The editor ships everywhere as a **lazy chunk** — production `/play` (and the public `/games/<id>` pages) download it only when summoned with F2+E or `?mode=editor`. In the dev runner, **Save (Ctrl+S)** writes the scene straight to `Games/<id>/src/editor.scene.json` through the dev server's `/__jgengine/save` endpoint; the file auto-loads on the next editor open as an overlay (same-id objects win over the game's derived `editorLayers`, new objects append) and is plain JSON a game can import as runtime data. Fold long-lived edits back into source tables when they stabilize. Outside the dev server (production pages) the Save button hides and Export JSON remains the only exit. The F2+D Tune tab has the same seam: **Save to source** rewrites changed tunable literals directly in `Games/<id>/src/*.ts`.
21
+
22
+ ## Standalone games ship the editor too
23
+
24
+ `npx jgengine create` scaffolds all of this outside the monorepo: the game's `main.tsx` summons the editor on F2+E or `?mode=editor` as a lazy `@jgengine/editor` chunk, and `vite.config.ts` mounts the published save middleware so Save (Ctrl+S) writes `src/editor.scene.json` and Tune's Save-to-source rewrites literals:
25
+
26
+ ```ts
27
+ import { standaloneSavePlugin } from "@jgengine/node/devSavePlugin";
28
+ export default defineConfig({ plugins: [react(), tailwindcss(), standaloneSavePlugin()] });
29
+ ```
30
+
31
+ Monorepo-shaped hosts use the general form — `devSavePlugin((gameId) => srcDirFor(gameId))` — and `handleSaveRequest` is the transport-free core for a non-Vite dev server.
32
+
33
+ ## Standalone — the editor without a game (any project)
34
+
35
+ The same editor runs **gameless**, on any folder, for people who aren't building a jgengine game. Two zero-config entry points, one shared seam:
36
+
37
+ ```
38
+ npx jgengine editor [dir] # open the 3D editor on a folder (default cwd); Ctrl+S writes dir/editor.scene.json
39
+ # [--assets <dir>] pick the model folder · [--port <n>] · [--out <workspace>]
40
+ ```
41
+
42
+ `jgengine editor` scaffolds a tiny Vite workspace (`.jgengine-editor/`) and serves the target folder: its `editor.scene.json` opens as the scene and every `.glb`/`.gltf` under it (or `--assets`) becomes a placeable asset. The **desktop app** exposes the identical editor at `?mode=editor` — no game loaded, blank world. Both mount `@jgengine/editor`'s `StandaloneEditor`, which drops the editor over a blank flat world instead of a game (a slim strip also lets you **📂 Open scene** / **🧊 Add assets** at runtime):
43
+
44
+ ```tsx
45
+ import { StandaloneEditor, createBlankPlayable, blankWorld, downloadSaver } from "@jgengine/editor";
46
+
47
+ // Turn-key: blank world, built-in pickers, Save → dev-server endpoint or a browser download.
48
+ <StandaloneEditor sceneId="my-scene" save={downloadSaver("scene.json")} />;
49
+
50
+ // Or build the gameless host yourself and mount EditorApp over it:
51
+ const playable = createBlankPlayable({ assets: [{ id: "tree", url: "/models/tree.glb" }], world: blankWorld("seed") });
52
+ ```
53
+
54
+ The server half is `@jgengine/node/editorHostPlugin` — a Vite plugin that serves the folder's scene + models at `/__jgengine/manifest`, streams model bytes, and writes Save back. `buildEditorManifest(sceneDir, assetsDir)` is its pure core (read the scene, scan the folder); `editorScaffold(engineVersion)` (from `@jgengine/jgengine/templates`) emits the workspace files:
55
+
56
+ ```ts
57
+ import { editorHostPlugin, buildEditorManifest } from "@jgengine/node/editorHostPlugin";
58
+ import { editorScaffold } from "@jgengine/jgengine/templates";
59
+
60
+ export default defineConfig({ plugins: [react(), editorHostPlugin({ dir: process.cwd() })] });
61
+ const manifest = buildEditorManifest(sceneDir, assetsDir); // { scene, assets: [{ id, url, label }] }
62
+ const files = editorScaffold("0.10.0"); // index.html, vite.config.ts, src/main.tsx, …
63
+ ```
64
+
65
+ ## Modes: edit · walk · play
66
+
67
+ - **edit** — frozen sim, orbit inspection camera, gizmos and chrome.
68
+ - **walk** — frozen sim, the game's own camera/movement; roam the world with markers drawn in place.
69
+ - **play** — the real game (loop, HUD, camera) with a floating exit chip.
70
+
71
+ Toolbar buttons, `F2+E` (edit ↔ play, same chord family as F2+D devtools), chip/F2+E returns to edit, or RPC: `{ method: "set_mode", mode: "play" }`. The editor session (document, undo, selection) survives mode switches. **F2+E summons the editor from any running `mode=play` game** — dev runner or the website's `/games/<id>` pages, dev and production alike, no URL change needed.
72
+
73
+ ## Perf: the editor measures itself
74
+
75
+ - Toolbar shows a live `fps · draws · tris` pill (red under 30fps).
76
+ - `F2+D` opens the engine devtools panel (debug mode) — sim phase bars name the exact lag culprit.
77
+ - Agents: `{ method: "editor_status" }` includes a `perf` sample and current `mode`; `{ method: "perf_report" }` returns the devtools snapshot (fps, sim phases, culprit hints). The perf sample separates **editor-authoring cost** from frame/sim cost — `raycastMs` (viewport picks), `rebuildMs` (preview-mesh rebuilds), `authoringMs` (their sum) — so "the editor feels laggy" resolves to a number. Sculpt/paint rebuilds only the stroke's **dirty region**, not the whole mesh. Never guess at lag — pull the report.
78
+
79
+ ## Game opt-in (optional, thin)
80
+
81
+ ```ts
82
+ // Games/<id>/src/index.tsx — also export editorCatalogs for Data-panel tuning
83
+ export { game } from "./game.config";
84
+ export { editorLayers } from "./editorLayers";
85
+ ```
86
+
87
+ `editorLayers` → doc. Optional `editorCatalogs` seeds `document.catalogs` (`list|get|set_catalog_entry`; `findEditorCatalogEntry`).
88
+
89
+ ## Author the scene, don't hardcode it — render it at runtime
90
+
91
+ Scene content (paths, foliage, terrain, gameplay spots) belongs in the **editor document**, not in
92
+ bespoke render code with hardcoded coordinates. Author it in the 3D editor, save `editor.scene.json`,
93
+ and render it generically at runtime with **`<AuthoredScene document={doc} field={ctx.world.ground} placeObjects />`**
94
+ from `@jgengine/shell/scene` — draped paths, foliage (`resolveScatter`), studios/generators, and catalog props
95
+ (`resolveAuthoredObjects` → object store / `objectModels`). Or onInit: `placeAuthoredObjects(ctx.scene.object,
96
+ resolveAuthoredObjects(doc), heightAt)` / `placeAuthoredObjectsFromDocument` / `markerCatalogId`.
97
+ `<AuthoredPaths>` / `<AuthoredObjects document field />` for one slice. Terrain: `environment({ sculpt })`.
98
+ Never hand-roll path meshes or `markers.filter(meta.catalogId)` place loops.
99
+
100
+ This rule is **gated**: `bun run check-content-gate` (wired into `check-types`) fails a game that hard-codes
101
+ world geometry instead of rendering an editor document, and flags source files dense with hand-placed
102
+ coordinate literals. Two shrinking baselines (`scripts/content-gate-baseline.json`,
103
+ `scripts/coordinate-literal-baseline.json`) list today's offenders so `main` stays green; they may only
104
+ lose entries. After migrating a game to `<AuthoredScene>`, reseed with `bun run check-content-gate --update`
105
+ and commit the trimmed baselines. Legitimately procedural / geometry-free games live in the `EXEMPT` map in
106
+ `scripts/check-content-gate.ts`. The end-to-end editor→`<AuthoredScene>` path this depends on is proven
107
+ browserless by `scripts/editor-mvp.test.ts`.
108
+
109
+ ## The F2 chord family — three modes, all agent-usable headless
110
+
111
+ - **F2+D — debug mode**: engine devtools overlay (Perf/Tune/Logs/Net/Keys/Col). A plain F2 tap does nothing; F2 is only the chord holder.
112
+ - **F2+C — canvas mode**: HUD layout (`document.ui.panels`) — drag/resize; TSX props fallback-only.
113
+ - **F2+E — editor mode**: this scene editor.
114
+
115
+ Agents never need a user-launched server: `bun run drive` boots the dev server **and** headless Chromium itself, and every `GamePlayerShell` page installs `window.__jgengineAgent` — one RPC surface covering all three modes (`--rpc` prefers it, falls back to the raw editor host). Unknown verbs delegate to the live editor host, so all editor verbs below work through it too.
116
+
117
+ ```
118
+ bun run drive <id> --rpc '{"method":"agent_status"}' # which modes are live
119
+ bun run drive <id> --rpc '{"method":"debug_snapshot"}' # lean perf/logs/tunables report
120
+ bun run drive <id> --rpc '{"method":"debug_report"}' # full devtools snapshot
121
+ bun run drive <id> --rpc '{"method":"canvas_state"}' # HUD panels + placements
122
+ bun run drive <id> --rpc '{"method":"canvas_move_panel","id":"minimap","anchor":"top-right"}' --shot hud
123
+ bun run drive <id> --rpc '{"method":"editor_summon"}' --wait 2000 --rpc '{"method":"scene_summary"}'
124
+ bun run drive <id> --mode editor --rpc '{"method":"set_transform","id":"boss","x":-90,"z":-650}' --rpc '{"method":"export_document"}'
125
+ ```
126
+
127
+ Canvas verbs: `canvas_state`, `canvas_set_editing`, `canvas_move_panel`, `canvas_resize_panel`, `canvas_list_panel_types`, `canvas_reset`. Debug: `debug_open`, `debug_snapshot`, `debug_report`. Editor extras: `save_scene` → `editor.scene.json`. Menu-gated: `--click`/`--key` first. Pure document edits: headless CLI below.
128
+
129
+ ## Agent RPC (same verbs as UI)
130
+
131
+ **Agent panel** (toolbar): shared GUI/RPC undo — `packAgentContext` / `routeToolCall` / `runAgentTurn` / `undoAgentPatch`; `JGENGINE_EDITOR_AGENT_URL` + `JGENGINE_EDITOR_AGENT_KEY` (see `reference.md`).
132
+
133
+ Browser console / injected host:
134
+
135
+ ```js
136
+ window.__jgengineEditorHost.handle({ method: "scene_summary" })
137
+ window.__jgengineEditorHost.handle({ method: "camera_goto", id: "boss_warrior" })
138
+ window.__jgengineEditorHost.handle({ method: "set_transform", id: "boss_warrior", x: -90, z: -650 })
139
+ window.__jgengineEditorHost.handle({ method: "export_document" })
140
+ ```
141
+
142
+ Headless (live editor + screenshots, or document-only CLI):
143
+
144
+ ```
145
+ bun run drive the-robots --mode editor --wait 3000 --rpc '{"method":"editor_status"}' --shot check
146
+ bun run drive the-robots --mode editor --rpc '{"method":"set_mode","mode":"play"}' --wait 2000 --shot playing
147
+ bun packages/editor/src/mcp/cli.ts --game the-robots --rpc '{"method":"list_layers"}'
148
+ bun packages/editor/src/mcp/cli.ts --game the-robots --serve # POST localhost:17373/rpc
149
+ bun packages/editor/src/mcp/cli.ts --game the-robots --stdio # MCP JSON-RPC on stdin/stdout
150
+ ```
151
+
152
+ The CLI/HTTP bridge validates every RPC payload and a game's `editorLayers` export at load time — malformed JSON, unknown `method`, or wrong-shaped document fields get path-specific diagnostics instead of crashing.
153
+
154
+ Viewport: click anything to select — editor gizmos hit directly, world geometry snaps to the nearest marker/volume/path/note, repeat-click cycles stacked candidates, shift/ctrl-click multi-selects — then TransformControls (W move / E rotate marker / R scale volume: radius, cylinder height, or box half-extents). Multi-selection drags move every selected object. Snap button cycles ground / grid / off (grid also snaps rotation to 15°); `G` toggles the reference grid. Outliner groups by kind (notes included) with ×N dedup rows; `N` cycles instances of the selected row; ctrl-click adds to selection.
155
+
156
+ Authoring: **+ Add** menu places markers, volumes (sphere/box/cylinder), notes, and draws paths (click points, Enter finish, Esc cancel; shift-click keeps placing). Select a path, click a vertex sphere to move/insert/delete points. `Ctrl+D` duplicates, `Ctrl+C/X/V` copy/cut/paste (system clipboard gets the JSON fragment too), `Ctrl+A` selects all visible, Delete removes, arrows/PgUp/PgDn nudge by grid step (Shift ×5), `F` frames the selection, `?` opens the shortcut sheet. Inspector edits labels, kind, display color, note text, radius/height/half-extents, and coalesces typed edits into single undo steps. **Import** loads an exported JSON back in (success/error toasts); Export downloads, `⧉` copies the JSON. Edits autosave to a per-game localStorage draft — reopening the editor offers Restore/Discard; the header shows an amber ● while the document differs from the game's authored layers. Layer visibility and snap prefs persist per game in localStorage.
157
+
158
+ ## Vegetation volumes — density is one slider
159
+
160
+ **+ Add → Vegetation (box/circle)** places a fill area; its inspector section has an **item** field (`grass`, or any render-catalog id like a tree/bush model) and a **density /m² slider** (plus exact number, scale range, spacing, reroll seed) with a live `≈ N placements over M m²` readout. Data rides `volume.meta` in the saved scene — no new schema.
161
+
162
+ Consume in the game (`@jgengine/core/world/vegetation`):
163
+
164
+ ```ts
165
+ import { grassPatchesFromVegetation, resolveVegetation } from "@jgengine/core/world/vegetation";
166
+
167
+ grass: grassPatchesFromVegetation(sceneDoc), // item "grass" → shell blade patches, density = blades/m²
168
+ for (const p of resolveVegetation(sceneDoc)) // everything else → deterministic placements
169
+ ctx.world.object.place(p.item, p.x, 0, p.z, { rotation: p.rotation, visual: { scale: p.scale } });
170
+ ```
171
+
172
+ Same volume, same seed → same field every run; drag the slider, save, done.
173
+
174
+ ## Terrain sculpting — brushes on the live heightfield
175
+
176
+ Toolbar **Terrain** (or press `T`) enters the sculpt tool. **Create terrain** lays an editable
177
+ heightfield over the scene; brushes then reshape it with live feedback:
178
+
179
+ - **Raise / Lower** — push ground up or dig it down · **Smooth** — average toward neighbours ·
180
+ **Flatten** — level to a sampled or numeric height · **Noise** — fractal roughening (seeded, repeatable) ·
181
+ **Ramp** — drag low→high to grade a straight slope.
182
+ - Live controls: radius, strength, falloff (smooth/linear/none), shape (circle/square), spacing,
183
+ invert modifier, flatten height, noise seed. A preview ring tracks the cursor.
184
+ - A whole drag commits as **one** undoable stroke (compact vertex delta — the terrain document is
185
+ never copied per move). `Ctrl+Z` / `Ctrl+Y` undo/redo strokes like any edit; strokes serialize with
186
+ the scene under `document.terrain`.
187
+
188
+ The sculpt engine is the reusable `@jgengine/core/world/terraform` seam — build, edit, and consume it
189
+ outside the editor too:
190
+
191
+ ```ts
192
+ import {
193
+ createTerrainSnapshot, // fresh flat heightfield over an Aabb
194
+ editableTerrainFromSnapshot, // rebuild a live EditableTerrain over the game's ground
195
+ beginTerraformStroke, // batch many stamps into one compact TerraformDelta
196
+ applyDeltaToSnapshot, // redo a stroke onto a snapshot (copy-on-write)
197
+ revertDeltaFromSnapshot, // undo a stroke onto a snapshot (copy-on-write)
198
+ } from "@jgengine/core/world/terraform";
199
+
200
+ const snapshot = createTerrainSnapshot({ bounds: { minX: -100, minZ: -100, maxX: 100, maxZ: 100 }, cellSize: 2 });
201
+ const terrain = editableTerrainFromSnapshot(snapshot, ctx.world.ground);
202
+ const stroke = beginTerraformStroke(terrain);
203
+ stroke.stamp({ mode: "raise", center: [0, 0], radius: 12, strength: 1 });
204
+ const delta = stroke.delta(); // one undoable step
205
+ const next = applyDeltaToSnapshot(snapshot, delta); // serializable, back to document.terrain
206
+ ```
207
+
208
+ ### Material painting
209
+
210
+ The terrain tool's **Paint** sub-mode paints material layers (grass/dirt/rock/sand/mud/snow/road/gravel)
211
+ onto the same heightfield — the surface id is stored per cell in `document.terrain.surfaces` and colors the
212
+ mesh. Click/drag paints the selected material; **Alt-click** samples (eyedropper); **Fill all** / **Clear
213
+ paint** blanket the terrain; **Auto rules** paint by slope ("on steep slopes") or height ("on high ground").
214
+ Each stroke/fill is one undoable `paintTerrain` command (compact `SurfaceDelta`). Consume with
215
+ `beginSurfaceStroke`, `fillSurfaceDelta`, `autoPaintDelta`, and `surfaceAt` from `@jgengine/core/world/terraform`.
216
+
217
+ ### Material layer stack + weighted blend
218
+
219
+ Beyond one dominant surface per cell, terrain carries a reorderable **material layer stack**
220
+ (`document.terrain.layers: TerrainMaterialLayer[]`), each layer a palette `surface` plus render params
221
+ (`roughness`, `tiling`, `triplanar`, `tint`, `opacity`) — carried as data so a runtime game reads them
222
+ straight off the snapshot. Per-cell **weighted blend** lets a cell mix layers (0.6 grass + 0.4 dirt);
223
+ weights live in `document.terrain.weights` (flat `cols*rows*layers.length`, **lazy** — absent until a blend
224
+ is painted, so single-layer terrains stay compact). Pre-2.0 documents auto-upgrade via
225
+ `migrateTerrainSnapshot` (derives layers from painted surfaces). Undo is compact: `setTerrainLayers`
226
+ (snapshot) and `blendTerrain` (`WeightDelta`). Consume with `setLayers`, `blendPaintDelta`,
227
+ `beginBlendStroke`, `weightsAt`, and `migrateTerrainSnapshot` from `@jgengine/core/world/terraform`.
228
+
229
+ ```ts
230
+ import { editableTerrainFromSnapshot, beginBlendStroke } from "@jgengine/core/world/terraform";
231
+ session.dispatch({ type: "setTerrainLayers", layers: [{ id: "grass", surface: "grass", roughness: 0.9 }, { id: "dirt", surface: "dirt" }] });
232
+ const live = editableTerrainFromSnapshot(session.getState().document.terrain);
233
+ const stroke = beginBlendStroke(live);
234
+ stroke.stamp({ mode: "paint", center: [0, 0], radius: 8, surface: "dirt", strength: 1 });
235
+ session.dispatch({ type: "blendTerrain", delta: stroke.delta() }); // one undoable weighted-blend step
236
+ ```
237
+
238
+ ### Runtime consumption — the sculpt seam
239
+
240
+ An authored heightfield drives a game's live ground through `environment({ sculpt })`: pass the
241
+ `document.terrain` snapshot and its offsets layer over the base terrain in `resolveEnvironmentField`, so
242
+ **both the rendered mesh and player collision** reflect the sculpt through the one field every consumer
243
+ already reads (see `Games/tower-guard`). `sculptedField(base, snapshot)` composes them directly.
244
+
245
+ ## Foliage / scatter regions
246
+
247
+ **+ Add → Foliage region (lasso)** draws a closed polygon on the terrain (click points, Enter to finish) —
248
+ a scatter path of `kind: "scatter"`. Its inspector drives deterministic, **GPU-instanced** scatter that
249
+ previews live in the viewport as you drag: **density /m²**, spacing, a weighted **species palette**
250
+ (add/remove rows of item id + weight), scale range, **max slope** and **height** masks, **edge fade**
251
+ (feather the border), align-to-slope, and a seed with a reroll button. Same seed → same field. Rules ride
252
+ `path.meta`. **Clearance zones** keep foliage off gameplay — don't hand-carve the polygon around
253
+ spawns/plots/paths. Tag any marker/volume with a **clearance** (m) in its inspector (spawns/objectives
254
+ auto-clear by kind); `resolveScatter` repels foliage from every clearance zone in the document, and the
255
+ region's **auto-avoid gameplay spots** toggle turns it off per region (manual `avoid` discs only). The
256
+ matching terrain half: `environment({ clearings: clearanceZonesFrom(doc) })` flattens the ground under
257
+ those same spots (a level pad even under a mound), so one clearance zone clears foliage *and* levels terrain
258
+ (`Games/tower-guard` scatters one arena-wide region and lets the path + plots carve themselves out).
259
+ Consume in a game with `resolveScatter(doc, terrain, options?)` from `@jgengine/core/world/scatterRegion`
260
+ (`clearanceZonesFrom`, `ClearanceOptions` scope which kinds/ids clear),
261
+ then render the instances with `<InstancedScatter instances={…} />` from `@jgengine/shell/scatter` — real
262
+ per-species proxy models (trunked trees, stacked pines, round bushes, faceted rocks, grass tufts) grouped
263
+ into per-chunk GPU-instanced draws that frustum-cull independently (`chunkScatterInstances`). The same
264
+ renderer powers the editor preview. **Convert to objects**: `convertScatterToObjects` (RPC `convert_scatter`)
265
+ detaches a region into individually-editable placed prop markers.
266
+
267
+ Headless: `create_terrain`, `sculpt_terrain {mode,x,z,radius,strength,…}`, `paint_terrain {surface,x,z,radius}`,
268
+ `fill_terrain {surface}`, `auto_paint {surface,minSlope,minHeight,…}`, `terrain_materials`, `terrain_layers`,
269
+ `set_terrain_layers {layers}`, `blend_terrain {surface,x,z,radius?}`, `terrain_summary`,
270
+ `add_foliage {points,density,item}`, `convert_scatter {pathId}`, and `scatter_summary` RPC verbs drive and
271
+ assert terrain + foliage authoring without WebGL (`bun packages/editor/src/mcp/cli.ts`).
272
+
273
+ ## Prefabs — reusable object stamps
274
+
275
+ **Prefabs** tab (left aside) makes reuse across a scene, or across games, a first-class op:
276
+ select objects, name them, **Make prefab** extracts them into a serializable fragment centered
277
+ on its own bounds centroid (so it drops in consistently anywhere). **Insert** stamps a fresh,
278
+ freshly-id'd instance at the camera focus point, tagging every inserted object's
279
+ `meta.prefabId`/`meta.prefabInstanceId`. **Detach** breaks the link (content stays, tags clear)
280
+ without touching anything else. Prefabs live in `document.prefabs` — export/import the document,
281
+ or lift just that array, to reuse a prefab library in another game.
282
+
283
+ ```ts
284
+ import { createPrefabFragment, findEditorPrefab } from "@jgengine/core/editor/index";
285
+
286
+ session.dispatch({ type: "createPrefab", id: "camp", name: "Camp", ids: ["tent", "fire"] });
287
+ session.dispatch({ type: "insertPrefab", prefabId: "camp", at: { x: 100, y: 0, z: 0 } });
288
+ session.dispatch({ type: "detachPrefabInstance", instanceId }); // breaks the link, keeps content
289
+ session.dispatch({ type: "deletePrefab", prefabId: "camp" }); // library entry only; placed instances unaffected
290
+ ```
291
+
292
+ Headless: `create_prefab {id,name,ids}`, `insert_prefab {prefabId,x?,y?,z?}` (defaults to camera
293
+ focus), `detach_prefab_instance {instanceId}`, `delete_prefab {prefabId}`, `list_prefabs`.
294
+
295
+ ## Collections — named selection sets and locked production groups
296
+
297
+ **Sets** tab bookmarks the current selection under a name for later restore/add-to/remove-from —
298
+ and doubles as a production group with **lock** (blocks `translate`/`setTransform`/`remove`/
299
+ `removeMany` on its members — moving or deleting a locked group is refused at the session level,
300
+ not just the UI), **color**, and **visible** flags. Collections live in `document.collections` and
301
+ survive export/import; removing a member object prunes it from every collection automatically.
302
+
303
+ ```ts
304
+ import { findEditorCollection, isEditorObjectLocked } from "@jgengine/core/editor/index";
305
+
306
+ session.dispatch({ type: "createCollection", id: "pack", name: "Wolf pack", memberIds: ["a", "b"] });
307
+ session.dispatch({ type: "setCollectionFlags", id: "pack", patch: { locked: true, color: "#f59e0b" } });
308
+ session.dispatch({ type: "selectCollection", id: "pack" }); // restores the selection
309
+ ```
310
+
311
+ Headless: `list_collections`, `create_collection {id,name,memberIds?}`, `rename_collection`,
312
+ `delete_collection`, `set_collection_members`, `add_to_collection`, `remove_from_collection`,
313
+ `set_collection_flags {id,color?,locked?,visible?}`, `select_collection {id}`.
314
+
315
+ ## Batch property edit and drag-drop material assignment
316
+
317
+ Multi-select then dispatch `batchSetProperties` to patch color/label/meta across every kind in
318
+ one undo step — the primitive behind "replace selected, edit once":
319
+
320
+ ```ts
321
+ session.dispatch({ type: "batchSetProperties", ids: selection, patch: { color: "#0ff", meta: { tier: 2 } } });
322
+ ```
323
+
324
+ The asset browser's **Materials** palette (top of the Assets panel) is drag-source-only chips —
325
+ drop one onto an outliner row to `assignMaterial` (stamps `meta.materialId`) that object, or drop
326
+ it in the viewport: hits a tagged object → assigns; hits bare terrain → paints that material at
327
+ the drop point (same undoable stroke as the Paint tool). Headless: `batch_set_properties
328
+ {ids,color?,label?,meta?}`, `assign_material {ids,materialId}`.
329
+
330
+ ## Scene hierarchy — parent / child
331
+
332
+ The outliner has a **By kind** and a **Hierarchy** view (nested tree, expand/collapse). Any object can be
333
+ **parented** under another via the inspector's *parent* dropdown (or the `set_parent` RPC) — moving or
334
+ translating a parent carries its whole subtree by the same delta, and cycles are refused. Parenting rides
335
+ `parentId` on the object itself, so it serializes, undoes, and translates through the existing commands.
336
+ Helpers in `@jgengine/core/editor/index`: `editorRoots`, `editorChildren`, `editorParentOf`,
337
+ `collectDescendants`, `wouldCreateCycle`. Headless: `set_parent {ids, parentId}` and `hierarchy`.
338
+
339
+ ## Responsiveness — selector subscriptions + virtualization
340
+
341
+ The editor stays smooth on large scenes via two seams (`@jgengine/editor`):
342
+ - `useStoreSelector(store, selector, isEqual?)` subscribes a component to a **slice** of the session/UI
343
+ store through `useSyncExternalStore`, so it rerenders only when that slice changes — UI-only churn
344
+ (gizmo mode, snapping, active tool) no longer rerenders the outliner/inspector. `shallowArrayEqual`
345
+ is the selection-list comparator. Adopters: `OutlinerPanel`, `PrefabsPanel`, `CollectionsPanel`,
346
+ and `EditorChrome`'s `InspectorPanel`.
347
+ - `virtualWindow(scrollTop, viewportHeight, rowHeight, rowCount, overscan?)` is the pure windowing math
348
+ behind the outliner's fixed-height virtual list — a scene with thousands of objects only mounts the
349
+ visible handful of rows. The status bar shows live object + foliage-instance counts next to the fps pill.
350
+
351
+ ## Parametric studios — the editor is an extension seam
352
+
353
+ The editor is not a fixed tool: **anyone can register a new "studio"** (a parametric asset with a
354
+ slider inspector) with one call and **zero engine edits**. Two seams, one inspector generator.
355
+
356
+ **Scene kind** — geometry authored as a path / volume / marker. `registerSceneKind` (`@jgengine/core/scene/sceneKinds`) supplies a `schema`, an optional pure `resolve`, and `+ Add` metadata; the editor **auto-generates the whole slider inspector** from the schema (`range | number | bool | select | color | text | seed | weightedList`) — no per-kind JSX. Pair it with `registerSceneKindRenderer` (`@jgengine/shell/scene/sceneKindRenderers`) and `AuthoredScene` renders it. Built-in kinds: `scatter`, `water`, `grass_field`. Generic primitives to compose (never re-roll): `placeAlongPath` (`world/pathInstances`), `sagCurve`/`catenaryCurve` (`world/catenary`), `readNamedSockets` (`scene/modelSockets`).
357
+
358
+ **Generator asset** — a slider-driven prop placed as a marker. `registerAssetGenerator` (`@jgengine/core/scene/assetGenerator`) turns `(params, seed) → parts`; a placed instance stores only `{ assetId, params, seed }` and `resolveGeneratorAsset` re-resolves it at runtime (never baked). Built-in: `building`. The inspector shows the generator's schema on any marker whose `meta.assetId` names one. Declare the generator's front via `GeneratedAsset.forward` (default +Z, `DEFAULT_FORWARD` in `@jgengine/core/scene/facing`) so a product-shot stage or placement tool can auto-orient it — see `jgengine-ui` → "Cinematic studio shots" (`StudioStage`'s `faceCamera`).
359
+
360
+ Full copyable walkthrough (pole-line + bookcase, zero engine edits): **`examples/studios/README.md`**; enforced by `scripts/studioSeam.test.ts`.
361
+
362
+ ### Drive studio sliders headlessly (agents)
363
+
364
+ Parametric params live in `meta`; patch via `set_meta` / `set_path` / `set_marker` / `set_note` (merge + schema validate). Catalogs: same SchemaInspector + coalesce via `list|get|set_catalog_entry`:
365
+
366
+ ```
367
+ bun run drive <id> --mode editor --rpc '{"method":"set_meta","id":"grove","patch":{"density":0.6,"seed":"r2"}}'
368
+ bun packages/editor/src/mcp/cli.ts --game tower-guard --rpc '{"method":"set_catalog_entry","catalogId":"towers","entryId":"tower_archer","patch":{"damage":12}}'
369
+ ```
370
+
371
+ ## Core APIs (`editor/`)
372
+
373
+ - `@jgengine/core/editor/index` — document, session, commands, undo
374
+ - `@jgengine/core/editor/types` — markers, volumes, paths
375
+ - `@jgengine/core/editor/document` — normalize/merge/export
376
+ - `@jgengine/core/editor/commands` — `createEditorSession`
377
+ - `@jgengine/editor` — `EditorApp`, host RPC, bridge server, agent panel (dev-only package)
378
+
379
+ ## Do not
380
+
381
+ - Statically import `@jgengine/editor` from `GameHost` or game entry code — summon it only as a lazy chunk (`await import("@jgengine/editor")`)
382
+ - Treat mesh modeling as in-scope (placement/world tools only)