@jgengine/editor 0.19.0 → 0.21.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 (71) hide show
  1. package/CHANGELOG.md +346 -0
  2. package/dist/ClipPreviewLayer.d.ts +2 -3
  3. package/dist/ClipPreviewLayer.js +10 -28
  4. package/dist/CreatorApp.d.ts +12 -0
  5. package/dist/CreatorApp.js +107 -0
  6. package/dist/DebugDraw.js +3 -1
  7. package/dist/EditorApp.d.ts +14 -2
  8. package/dist/EditorApp.js +29 -14
  9. package/dist/EditorChrome.d.ts +9 -1
  10. package/dist/EditorChrome.js +54 -35
  11. package/dist/InspectorPanel.d.ts +3 -1
  12. package/dist/InspectorPanel.js +12 -8
  13. package/dist/MaterialAssetEditor.d.ts +13 -0
  14. package/dist/MaterialAssetEditor.js +109 -0
  15. package/dist/MaterialPreview.d.ts +16 -0
  16. package/dist/MaterialPreview.js +41 -0
  17. package/dist/MaterialsWorkspacePanel.d.ts +14 -2
  18. package/dist/MaterialsWorkspacePanel.js +78 -4
  19. package/dist/RuntimePlayBridge.js +1 -6
  20. package/dist/SelectionGizmo.js +23 -14
  21. package/dist/SimulationInspector.d.ts +9 -0
  22. package/dist/SimulationInspector.js +47 -0
  23. package/dist/TerrainSculpt.js +3 -1
  24. package/dist/agent/toolBridge.js +2 -0
  25. package/dist/authoringPlacement.d.ts +21 -0
  26. package/dist/authoringPlacement.js +34 -0
  27. package/dist/handlers/context.d.ts +1 -0
  28. package/dist/handlers/document.d.ts +1 -1
  29. package/dist/handlers/document.js +29 -1
  30. package/dist/handlers/hierarchy.d.ts +1 -1
  31. package/dist/handlers/hierarchy.js +4 -0
  32. package/dist/handlers/runtime.js +20 -17
  33. package/dist/handlers/terrain.js +52 -12
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.js +1 -0
  36. package/dist/materialControls.d.ts +32 -0
  37. package/dist/materialControls.js +79 -0
  38. package/dist/mcp/fieldSpec.d.ts +1 -1
  39. package/dist/mcp/fieldSpec.js +11 -2
  40. package/dist/mcp/importOptionalGameModule.d.ts +2 -0
  41. package/dist/mcp/importOptionalGameModule.js +13 -0
  42. package/dist/mcp/loadGameCatalogs.js +5 -12
  43. package/dist/mcp/rpcRequest.d.ts +3 -3
  44. package/dist/mcp/rpcRequest.js +10 -4
  45. package/dist/mcp/tools.js +10 -1
  46. package/dist/modelAnimationAuthoring.d.ts +27 -7
  47. package/dist/modelAnimationAuthoring.js +69 -72
  48. package/dist/session.d.ts +62 -0
  49. package/dist/session.js +56 -7
  50. package/dist/shell/AnimationGraphPanel.js +18 -10
  51. package/dist/shell/PlayModeBar.d.ts +2 -1
  52. package/dist/shell/PlayModeBar.js +2 -2
  53. package/dist/shell/SceneToolbar.d.ts +2 -1
  54. package/dist/shell/SceneToolbar.js +5 -4
  55. package/dist/shell/TopAppBar.d.ts +2 -1
  56. package/dist/shell/TopAppBar.js +2 -2
  57. package/dist/shell/animGraphPreview.d.ts +2 -0
  58. package/dist/shell/animGraphPreview.js +5 -2
  59. package/dist/shell/clipPreview.d.ts +1 -0
  60. package/dist/useDocumentSave.d.ts +20 -0
  61. package/dist/useDocumentSave.js +33 -0
  62. package/package.json +6 -5
  63. package/skills/ce-handoff/SKILL.md +1 -1
  64. package/skills/jgengine/SKILL.md +22 -2
  65. package/skills/jgengine/api.md +28 -17
  66. package/skills/jgengine/recipes/minimal-game.md +7 -2
  67. package/skills/jgengine-editor/SKILL.md +9 -1
  68. package/skills/jgengine-editor/api.md +102 -16
  69. package/skills/jgengine-editor/capabilities.md +40 -0
  70. package/skills/jgengine-editor/reference.md +149 -1
  71. package/skills/jgengine-editor/references/live-sync.md +15 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## @jgengine/core/editor
6
6
 
7
- - `ApplyDocumentPatchResult` (type): type ApplyDocumentPatchResult = | { ok: true; document: EditorDocument; revision: number; patch: DocumentPatch } | { ok: false; error: string } — Result of applying a {@link DocumentPatch} to a document + revision pair.
7
+ - `ApplyDocumentPatchResult` (type): type ApplyDocumentPatchResult = | { ok: true; document: EditorDocument; revision: number; patch: DocumentPatch } | { ok: false; error: string; commandIndex?: number } — Result of applying a {@link DocumentPatch} to a document + revision pair.
8
8
  - `AsciiGridExportOptions` (interface): interface AsciiGridExportOptions — Options for rendering a grid layer back out as an ASCII/glyph map.
9
9
  - `AsciiGridImportOptions` (interface): interface AsciiGridImportOptions — Options for importing an ASCII/glyph map into a grid layer. Provide `glyphMap` (char → value id) or a `palette` whose entries carry `glyph`s (or both — `glyphMap` wins on conflict). Any glyph not mapped, and any short-row padding, resolves to `empty`.
10
10
  - `CURRENT_GRID_SCHEMA_VERSION` (const): const CURRENT_GRID_SCHEMA_VERSION: 1 — The grid-layer cell-schema version this build writes and migrates toward.
@@ -17,7 +17,7 @@
17
17
  - `DocumentBakeOptions` (interface): interface DocumentBakeOptions extends MinimapBakeOptions — Options for {@link bakeMinimapFromDocument}.
18
18
  - `DocumentLiveEvent` (interface): interface DocumentLiveEvent — Event emitted when the authoritative document changes on a {@link DocumentLiveSync}.
19
19
  - `DocumentLiveSync` (interface): interface DocumentLiveSync — Two-way live-sync bus: document patches out, runtime state deltas back.
20
- - `DocumentPatch` (type): type DocumentPatch = | { type: "snapshot"; revision?: number; baseRevision: number; document: EditorDocument; } | { type: "commands"; revision?: number; baseRevision: number; commands: readonly EditorCommand[]; } — One versioned document mutation on the live-sync stream. `snapshot` replaces the whole document; `commands` replays structural editor commands onto the current document. `baseRevision` must match the receiver's current revision unless `force` is set (document authority from the editor).
20
+ - `DocumentPatch` (type): type DocumentPatch = | { type: "snapshot"; revision?: number; baseRevision: number; document: EditorDocument; } | { type: "commands"; revision?: number; baseRevision: number; commands: readonly EditorCommand[]; } — One versioned document mutation on the live-sync stream. `snapshot` replaces the whole document; `commands` replays structural editor commands onto the current document. `baseRevision` must match the receiver's current revision unless `force` is set (document authority from the editor). `revision` preserves the authority cursor when replaying: snapshots may jump forward for reconnect recovery; commands must be the next revision.
21
21
  - `ENTITY_CATALOG_ID` (const): const ENTITY_CATALOG_ID: "entities" — Well-known catalog id for authored entity definitions. A `mob`/`boss` marker's `catalogId` references an entry (row) in this catalog, and {@link entityEntryFromCatalog} turns that row into the runtime {@link GameContextEntityEntry} a spawn consumes — so an entity's stats and speed are authored in the editor Data tab, not hardcoded in game TS.
22
22
  - `EditorCatalogData` (interface): interface EditorCatalogData — Persisted values for one gameplay data catalog (weapons, waves, economy, …). Schemas are not stored here — they come from the game's `editorCatalogs` export and drive SchemaInspector.
23
23
  - `EditorCatalogDefinition` (interface): interface EditorCatalogDefinition — Game-exported catalog definition: a `ParamSchema` plus default entries. Schemas stay in code; entry values merge into `document.catalogs` and are what the editor/RPC edits and saves.
@@ -53,6 +53,7 @@
53
53
  - `EditorSessionState` (interface): interface EditorSessionState — The document plus current selection at a point in editor history.
54
54
  - `EditorSkyPreset` (type): type EditorSkyPreset = "day" | "dusk" | "night" — Named sky look stored on the scene document; matches runtime `SkyEnvironmentConfig.preset`.
55
55
  - `EditorTerrain` (type): type EditorTerrain = TerraformSnapshot — A sculpted heightfield authored in the editor: the {@link TerraformSnapshot} of offset deltas over the game's base ground. Serializes with the scene; a game rebuilds the field with `editableTerrainFromSnapshot`.
56
+ - `EditorTransactionResult` (type): type EditorTransactionResult = | { ok: true; state: EditorSessionState; changed: boolean } | { ok: false; error: string; commandIndex?: number } — Atomic command-batch outcome; a failure identifies the zero-based command index.
56
57
  - `EditorUiDocument` (interface): interface EditorUiDocument — Scene-document HUD section: panel id → layout. Single source of truth for placement.
57
58
  - `EditorUiPanelLayout` (interface): interface EditorUiPanelLayout — Authored layout for one HUD panel inside `editor.scene.json` → `ui.panels`.
58
59
  - `EditorVec3` (type): type EditorVec3 = { x: number; y: number; z: number } — A world-space point used across editor markers, volumes, and paths.
@@ -166,6 +167,18 @@
166
167
  - `EditorDispatchOptions` (interface): interface EditorDispatchOptions — Per-dispatch options; `coalesce` merges consecutive same-key edits into one undo step.
167
168
  - `EditorSession` (interface): interface EditorSession — Stateful, undoable handle for driving scene edits from UI or an MCP agent.
168
169
  - `EditorSessionState` (interface): interface EditorSessionState — The document plus current selection at a point in editor history.
170
+ - `EditorTransactionResult` (type): type EditorTransactionResult = | { ok: true; state: EditorSessionState; changed: boolean } | { ok: false; error: string; commandIndex?: number } — Atomic command-batch outcome; a failure identifies the zero-based command index.
171
+
172
+ ## @jgengine/core/editor/creatorStorage
173
+
174
+ - `CreatorConfig` (interface): interface CreatorConfig<TPlayable> — Creator configuration shared by a host and its lazily loaded editor.
175
+ - `CreatorDocument` (interface): interface CreatorDocument — A named scene save with a storage-format version and optimistic revision.
176
+ - `CreatorDocumentStorage` (interface): interface CreatorDocumentStorage — Injected durable storage. Save rejects stale revisions; null creates a new id.
177
+ - `CreatorPolicy` (interface): interface CreatorPolicy — Bounds and catalog permissions for player-authored documents.
178
+ - `createCreatorDocumentStorage` (function): function createCreatorDocumentStorage(config: { storage: KeyValueStorage; key: string; policy: CreatorPolicy }): CreatorDocumentStorage — Durable browser-compatible adapter over injected key/value storage; one atomic catalog write.
179
+ - `exportCreatorDocument` (function): function exportCreatorDocument(document: EditorDocument, policy: CreatorPolicy): string — Exports validated, schema-versioned scene JSON.
180
+ - `importCreatorDocument` (function): function importCreatorDocument(json: string, policy: CreatorPolicy): EditorDocument — Imports scene JSON using the same validation as commands and durable saves.
181
+ - `validateCreatorDocument` (function): function validateCreatorDocument(value: unknown, policy: CreatorPolicy): EditorDocument — Validates schema, finite work budgets, and approved catalog references before cloning.
169
182
 
170
183
  ## @jgengine/core/editor/directives
171
184
 
@@ -250,10 +263,10 @@
250
263
 
251
264
  ## @jgengine/core/editor/liveSync
252
265
 
253
- - `ApplyDocumentPatchResult` (type): type ApplyDocumentPatchResult = | { ok: true; document: EditorDocument; revision: number; patch: DocumentPatch } | { ok: false; error: string } — Result of applying a {@link DocumentPatch} to a document + revision pair.
266
+ - `ApplyDocumentPatchResult` (type): type ApplyDocumentPatchResult = | { ok: true; document: EditorDocument; revision: number; patch: DocumentPatch } | { ok: false; error: string; commandIndex?: number } — Result of applying a {@link DocumentPatch} to a document + revision pair.
254
267
  - `DocumentLiveEvent` (interface): interface DocumentLiveEvent — Event emitted when the authoritative document changes on a {@link DocumentLiveSync}.
255
268
  - `DocumentLiveSync` (interface): interface DocumentLiveSync — Two-way live-sync bus: document patches out, runtime state deltas back.
256
- - `DocumentPatch` (type): type DocumentPatch = | { type: "snapshot"; revision?: number; baseRevision: number; document: EditorDocument; } | { type: "commands"; revision?: number; baseRevision: number; commands: readonly EditorCommand[]; } — One versioned document mutation on the live-sync stream. `snapshot` replaces the whole document; `commands` replays structural editor commands onto the current document. `baseRevision` must match the receiver's current revision unless `force` is set (document authority from the editor).
269
+ - `DocumentPatch` (type): type DocumentPatch = | { type: "snapshot"; revision?: number; baseRevision: number; document: EditorDocument; } | { type: "commands"; revision?: number; baseRevision: number; commands: readonly EditorCommand[]; } — One versioned document mutation on the live-sync stream. `snapshot` replaces the whole document; `commands` replays structural editor commands onto the current document. `baseRevision` must match the receiver's current revision unless `force` is set (document authority from the editor). `revision` preserves the authority cursor when replaying: snapshots may jump forward for reconnect recovery; commands must be the next revision.
257
270
  - `RuntimeEntityState` (interface): interface RuntimeEntityState — One live entity row the runtime may stream to the editor (play-mode inspector feed).
258
271
  - `RuntimeStateDelta` (interface): interface RuntimeStateDelta — Incremental runtime state for the reverse channel. Entity rows upsert by id; `removeIds` drop rows; `tunables` shallow-merge. Ephemeral unless written back as a document patch.
259
272
  - `RuntimeStateSnapshot` (interface): interface RuntimeStateSnapshot — Full ephemeral runtime view held on the reverse channel — never mutates the document.
@@ -265,12 +278,38 @@
265
278
  - `runtimeEntityWriteBackCommand` (function): function runtimeEntityWriteBackCommand(document: EditorDocument, entity: RuntimeEntityState): EditorCommand | null — Builds an undoable document command from an ephemeral runtime entity row (write-back). Returns null when there is nothing to promote. Does not mutate document or clear the override — the caller dispatches the command and then clears the override.
266
279
  - `subscribeDocumentLiveSyncInstall` (function): function subscribeDocumentLiveSyncInstall(listener: () => void): () => void — Subscribe to install/uninstall of the global live-sync bus (AuthoredScene re-attaches when the editor host mounts over a running game).
267
280
 
281
+ ## @jgengine/core/editor/materialAuthoring
282
+
283
+ - `authoredMaterialAssignments` (function): function authoredMaterialAssignments(meta: Record<string, unknown> | undefined): MaterialAssignment[] — Read named assignments from authored marker metadata without inferring whole-model overrides.
284
+ - `modelWithAuthoredMaterials` (function): function modelWithAuthoredMaterials(model: ModelConfig | undefined, document: Pick<EditorDocument, "markers" | "materialAssets">, instanceId: string): ModelConfig | undefined — Apply authored references; document assets and selectors override model defaults of the same identity.
285
+ - `parseEditorMaterialAsset` (function): function parseEditorMaterialAsset(value: unknown): MaterialAsset — Validate and copy a material asset at the editor boundary.
286
+
268
287
  ## @jgengine/core/editor/minimap
269
288
 
270
289
  - `DocumentBakeOptions` (interface): interface DocumentBakeOptions extends MinimapBakeOptions — Options for {@link bakeMinimapFromDocument}.
271
290
  - `bakeMinimapFromDocument` (function): function bakeMinimapFromDocument(doc: EditorDocument, sampleHeight: (x: number, z: number) => number, options: DocumentBakeOptions = {}): { background: string; mapBounds: MinimapBakeBounds } — Bakes a minimap image from a scene document plus a terrain height sampler: sizes the footprint to the authored objects (or an explicit `bounds`), tints biome-colored volumes, and rasterizes the terrain top-down. Returns `{ background, mapBounds }` for the `Minimap` props. The editor runs this as its bake action and stores the result on the document.
272
291
  - `documentBakeZones` (function): function documentBakeZones(doc: EditorDocument): MinimapBakeZone[] — The biome-tint zones a document contributes to a bake: every volume carrying a `color` becomes a polygon tint over its footprint. (The optional stretch from #1036 — a bare bake still reads as terrain without any.)
273
292
 
293
+ ## @jgengine/core/editor/modelNodeVisibility
294
+
295
+ - `authoredHiddenNodes` (function): function authoredHiddenNodes(meta: Record<string, unknown> | undefined): readonly string[] | undefined — Read marker visibility metadata; an empty list clears model defaults.
296
+ - `modelWithAuthoredNodeVisibility` (function): function modelWithAuthoredNodeVisibility(model: ModelConfig | undefined, marker: Pick<EditorMarker, "meta"> | undefined): ModelConfig | undefined — Apply a placement's authored visibility without erasing omitted game model defaults.
297
+ - `parseModelHiddenNodes` (function): function parseModelHiddenNodes(value: unknown): readonly string[] | undefined — Validate and copy exact node names; an empty list selects no hidden subtrees.
298
+
299
+ ## @jgengine/core/editor/movementCatalog
300
+
301
+ - `AuthoredMovementBinding` (interface): interface AuthoredMovementBinding — Stable runtime objects whose numeric getters resolve the current authored document.
302
+ - `AuthoredMovementConfig` (interface): interface AuthoredMovementConfig — Only authored values, ready to overlay on a game's existing movement and physics policy.
303
+ - `AuthoredMovementDefaults` (interface): interface AuthoredMovementDefaults — Game-owned callbacks, collision and physics settings retained beneath authored numeric controls.
304
+ - `AuthoredMovementDiagnostic` (interface): interface AuthoredMovementDiagnostic — A persisted movement value rejected before it can replace the game's current setting.
305
+ - `AuthoredMovementResult` (interface): interface AuthoredMovementResult — Valid partial tuning plus located rejected values; absent rows leave game defaults intact.
306
+ - `AuthoredMovementValues` (interface): interface AuthoredMovementValues — Numeric character controls a game elects to author through its own named catalog rows.
307
+ - `MovementCatalogDocumentLike` (interface): interface MovementCatalogDocumentLike — Structural authored catalog input; an immutable EditorDocument satisfies it.
308
+ - `bindAuthoredMovement` (function): function bindAuthoredMovement(read: () => AuthoredMovementResult, defaults: AuthoredMovementDefaults): AuthoredMovementBinding — Binds live authored numeric controls onto stable runtime objects, preserving the game's callbacks, collision, backend and other policy. The movement controller reads these getters each step.
309
+ - `createAuthoredMovementReader` (function): function createAuthoredMovementReader(getDocument: () => MovementCatalogDocumentLike, catalogId: string, entryId: string): () => AuthoredMovementResult — Memoizes movement resolution by immutable document identity; reads the latest document on every call while catalog lookup and diagnostics run only after authoring changes.
310
+ - `createMovementSchema` (function): function createMovementSchema(defaults: AuthoredMovementValues): ParamSchema — Creates editor/RPC controls only for the values a game supplies, with those game's defaults. Gravity retains the game's sign convention; collision height is positive, other controls nonnegative. Jump release is bounded to 0..1; supplied deeper jump controls share a collapsed response group.
311
+ - `readAuthoredMovement` (function): function readAuthoredMovement(document: MovementCatalogDocumentLike, catalogId: string, entryId: string): AuthoredMovementResult — Reads one game-named row as partial runtime tuning, retaining game defaults for omitted or invalid values. Rejected values include their document path and repair; no policy callbacks are replaced.
312
+
274
313
  ## @jgengine/core/editor/runtimeInspector
275
314
 
276
315
  - `RuntimeInspectorGetResult` (interface): interface RuntimeInspectorGetResult — One resolved runtime field returned by `runtime_get`.
@@ -284,6 +323,20 @@
284
323
  - `runtimeEntityMetaWriteBackCommand` (function): function runtimeEntityMetaWriteBackCommand(document: EditorDocument, entity: RuntimeEntityState): EditorCommand | null — Promotes ephemeral runtime `values` into an undoable meta patch on a document-linked object. Returns null when the id is not in the document or there are no values.
285
324
  - `summarizeRuntimeInspector` (function): function summarizeRuntimeInspector(snapshot: RuntimeStateSnapshot, overrides: Readonly<Record<string, RuntimeEntityState>>, play: RuntimePlayControl): RuntimeInspectorSummary — Builds the compact reverse-channel summary used by the play-mode inspector panel and the `runtime_summary` bridge RPC.
286
325
 
326
+ ## @jgengine/core/editor/simulation
327
+
328
+ - `AuthoredEmitter` (interface): interface AuthoredEmitter — A named emitter authored in metres, with game-owned appearance and attachment ids.
329
+ - `AuthoredFireArea` (interface): interface AuthoredFireArea — A bounded fire grid; fuel and damage policy remain game content.
330
+ - `AuthoredFlockHabitat` (interface): interface AuthoredFlockHabitat — An authored habitat selects a game species and optionally follows a document path.
331
+ - `EditorSimulation` (interface): interface EditorSimulation — Authored simulation inputs. Runtime particles, heat, accumulation and actors save separately.
332
+ - `decodeEditorSimulation` (function): function decodeEditorSimulation(value: unknown, document?: Pick<EditorDocument, "paths">): EditorSimulation — Decode authoring inputs with located diagnostics and bounded work before any document mutation.
333
+
334
+ ## @jgengine/core/editor/staticPrefab
335
+
336
+ - `StaticPrefabBake` (interface): interface StaticPrefabBake — Persisted static export settings; source parts remain editable in the prefab fragment.
337
+ - `StaticPrefabBox` (interface): interface StaticPrefabBox — A solid or clearance volume in the prefab's unnormalized local asset space.
338
+ - `parseStaticPrefabBake` (function): function parseStaticPrefabBake(value: unknown): StaticPrefabBake — Validate and copy static prefab export settings from document or RPC input.
339
+
287
340
  ## @jgengine/core/editor/streamer
288
341
 
289
342
  - `StreamUpdate` (interface): interface StreamUpdate — The residency change produced by one {@link WorldStreamer.update}.
@@ -393,13 +446,15 @@
393
446
  - `AssetBrowser` (function): function AssetBrowser({ assets, session, onPlace, }: { assets: readonly EditorAssetEntry[]; session: EditorSession; onPlace: (entry: EditorAssetEntry) => void; }): React.JSX.Element — Searchable panel for placing catalog assets or an empty marker into the scene.
394
447
  - `AssetImporter` (type): type AssetImporter = (file: File) => Promise<StandaloneAsset | null> — Persists a dropped model file through the editor host so it survives reload as a durable catalog asset, returning the host-assigned id/url (which the manifest scan re-lists on reload). Resolves `null` when no host is listening — a plain browser mount with no dev server — so the caller can fall back to a blob URL.
395
448
  - `BlankPlayableOptions` (interface): interface BlankPlayableOptions — Options for the blank, gameless world the standalone editor authors over.
449
+ - `CreatorApp` (function): function CreatorApp({ gameId, config, onExit }: { gameId: string; config: CreatorConfig; onExit: () => void }): React.JSX.Element — Production creator catalog and embedded editor, reached through the game's menu.
450
+ - `CreatorConfig` (type): type CreatorConfig = CoreCreatorConfig<PlayableGame> — Game-owned creator policy, durable storage and document-to-runtime composition.
396
451
  - `DEFAULT_PAINT_SETTINGS` (const): const DEFAULT_PAINT_SETTINGS: PaintSettings — The terrain tool's default paint controls.
397
452
  - `DEFAULT_SCULPT_SETTINGS` (const): const DEFAULT_SCULPT_SETTINGS: SculptSettings — The terrain tool's default brush controls.
398
453
  - `EDITOR_AGENT_KEY_ENV` (const): const EDITOR_AGENT_KEY_ENV: "JGENGINE_EDITOR_AGENT_KEY" — Env var name for the optional Bearer token (`JGENGINE_EDITOR_AGENT_KEY`).
399
454
  - `EDITOR_AGENT_KEY_FALLBACK_ENV` (const): const EDITOR_AGENT_KEY_FALLBACK_ENV: "ANTHROPIC_API_KEY" — Fallback API-key env when `JGENGINE_EDITOR_AGENT_KEY` is unset (`ANTHROPIC_API_KEY`).
400
455
  - `EDITOR_AGENT_URL_ENV` (const): const EDITOR_AGENT_URL_ENV: "JGENGINE_EDITOR_AGENT_URL" — Env var name for the remote agent HTTP URL (`JGENGINE_EDITOR_AGENT_URL`).
401
456
  - `EDITOR_MCP_TOOLS` (const): const EDITOR_MCP_TOOLS: readonly EditorMcpTool[] — Full set of MCP tools an agent can call to drive the live scene editor.
402
- - `EditorApp` (function): function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor }: EditorAppProps): React.JSX.Element — Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes.
457
+ - `EditorApp` (function): function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor, createPlaytest, validateDocument, playerCreator, allowedAssets, allowedKinds, maxImportBytes, creatorPolicy }: EditorAppProps): React.JSX.Element — Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes.
403
458
  - `EditorAppProps` (interface): interface EditorAppProps — Props for mounting the scene editor over a playable game.
404
459
  - `EditorAssetEntry` (interface): interface EditorAssetEntry — A searchable, placeable asset shown in the editor's asset browser panel.
405
460
  - `EditorAssetInfo` (interface): interface EditorAssetInfo — A placeable asset entry offered in the editor's asset browser.
@@ -408,9 +463,12 @@
408
463
  - `EditorBridgeServer` (interface): interface EditorBridgeServer — A running editor bridge server: its bound port, URL, and a stop handle.
409
464
  - `EditorBridgeServerOptions` (interface): interface EditorBridgeServerOptions — Options for starting the editor's HTTP bridge server: host api, port, hostname.
410
465
  - `EditorCameraDriver` (const): const EditorCameraDriver: React.MemoExoticComponent<({ api }: { api: EditorHostApi; }) => null> — Drives the editor orbit camera from the host's focus target. When the target carries only a point, the camera pans to keep it centered (the historical behavior). When it also carries placement (`distance`/`pitch`/`yaw`/`height`), the camera is repositioned to that orbit pose — how `camera_goto`/`camera_frame` compose an aerial without the KeyF-buries-in-terrain problem.
411
- - `EditorChrome` (function): function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset = importAssetToHost, onRegisterAsset, onExitEditor, }: { gameId: string; session: EditorSession; api: EditorHostApi; assets: readonly EditorAssetEntry[]; ui: EditorUiStore; /** The document… — The full editor UI shell — global app bar, contextual scene toolbar, workspace rail, resizable hierarchy/inspector docks, tabbed bottom dock (content browser, console, profiler, AI assistant), viewport overlays, and status bar — wired to the session, UI store, layout store, and host RPC. Mounted by `EditorApp`; not a game-author entry point.
466
+ - `EditorChrome` (function): function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset = importAssetToHost, onRegisterAsset, onExitEditor, allowedKinds, exitLabel, maxImportBytes, documentSave, draftAutosave = true, creatorPolicy, }: { gameId: string; session: EditorSession; … — The full editor UI shell — global app bar, contextual scene toolbar, workspace rail, resizable hierarchy/inspector docks, tabbed bottom dock (content browser, console, profiler, AI assistant), viewport overlays, and status bar — wired to the session, UI store, layout store, and host RPC. Mounted by `EditorApp`; not a game-author entry point.
412
467
  - `EditorHostApi` (interface): interface EditorHostApi — The live editor's global control surface — session, visibility, camera focus, assets, mode, RPC.
413
468
  - `EditorLayerOverlays` (function): function EditorLayerOverlays({ document, visibility, selection, hoverId = null, onSelect, activePathPoint, groundHeightAt, }: { document: EditorDocument; visibility: EditorKindVisibility; selection: readonly string[]; /** Pre-selection hover id from viewport pointer pick; ignored when already select… — Renders every visible marker, volume, path, and note from a document as in-scene 3D gizmos.
469
+ - `EditorMaterialSlotInfo` (type): type EditorMaterialSlotInfo = Readonly<Omit<ModelMaterialSlotInfo, "uvSets">> & { readonly uvSets: readonly number[] } — Canonical imported slot names and mesh prerequisites reported by the loaded model.
470
+ - `EditorMaterialSlotInventory` (type): type EditorMaterialSlotInventory = EditorMaterialSlotReport & { id: string; sourceUrl?: string } — A marker's current imported-model inventory, including an explicit availability state.
471
+ - `EditorMaterialSlotReport` (type): type EditorMaterialSlotReport = | { status: "ready"; slots: readonly EditorMaterialSlotInfo[] } | { status: "loading" } | { status: "unavailable"; reason: string } — A live model-source observation; loading and unavailable reports never contain fabricated slots.
414
472
  - `EditorMcpTool` (interface): interface EditorMcpTool — One MCP tool descriptor — same verbs as the in-browser host RPC.
415
473
  - `EditorNetworkAdapterKind` (type): type EditorNetworkAdapterKind = MultiplayerAdapterConfig["kind"] | "unknown" — Transport kind declared by the game, or `"unknown"` when the config is unreadable.
416
474
  - `EditorNetworkPresenceActor` (interface): interface EditorNetworkPresenceActor — One online actor from a host-supplied presence feed. Fields mirror {@link PresencePoseRow} so hosts can forward backend rows without inventing shape.
@@ -469,6 +527,11 @@
469
527
  - `MATERIAL_DRAG_MIME` (const): const MATERIAL_DRAG_MIME: "application/x-jgengine-material" — Custom drag mime carrying a material id — read by hierarchy rows and the viewport drop zone.
470
528
  - `assetsFromCatalog` (function): function assetsFromCatalog(ids: readonly string[], resolve?: (id: string) => { url?: string; clips?: readonly string[] } | null): EditorAssetEntry[] — Turns a game's asset catalog ids into editor asset entries for the browser panel.
471
529
 
530
+ ## @jgengine/editor/CreatorApp
531
+
532
+ - `CreatorApp` (function): function CreatorApp({ gameId, config, onExit }: { gameId: string; config: CreatorConfig; onExit: () => void }): React.JSX.Element — Production creator catalog and embedded editor, reached through the game's menu.
533
+ - `CreatorConfig` (type): type CreatorConfig = CoreCreatorConfig<PlayableGame> — Game-owned creator policy, durable storage and document-to-runtime composition.
534
+
472
535
  ## @jgengine/editor/DebugDraw
473
536
 
474
537
  - `EditorLayerOverlays` (function): function EditorLayerOverlays({ document, visibility, selection, hoverId = null, onSelect, activePathPoint, groundHeightAt, }: { document: EditorDocument; visibility: EditorKindVisibility; selection: readonly string[]; /** Pre-selection hover id from viewport pointer pick; ignored when already select… — Renders every visible marker, volume, path, and note from a document as in-scene 3D gizmos.
@@ -476,7 +539,7 @@
476
539
 
477
540
  ## @jgengine/editor/EditorApp
478
541
 
479
- - `EditorApp` (function): function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor }: EditorAppProps): React.JSX.Element — Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes.
542
+ - `EditorApp` (function): function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor, createPlaytest, validateDocument, playerCreator, allowedAssets, allowedKinds, maxImportBytes, creatorPolicy }: EditorAppProps): React.JSX.Element — Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes.
480
543
  - `EditorAppProps` (interface): interface EditorAppProps — Props for mounting the scene editor over a playable game.
481
544
  - `EditorSaveFn` (type): type EditorSaveFn = (json: string) => Promise<{ ok: boolean; path?: string; error?: string }> — Persists an exported document JSON; resolves with where it landed or why it failed.
482
545
 
@@ -486,7 +549,11 @@
486
549
 
487
550
  ## @jgengine/editor/EditorChrome
488
551
 
489
- - `EditorChrome` (function): function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset = importAssetToHost, onRegisterAsset, onExitEditor, }: { gameId: string; session: EditorSession; api: EditorHostApi; assets: readonly EditorAssetEntry[]; ui: EditorUiStore; /** The document… — The full editor UI shell — global app bar, contextual scene toolbar, workspace rail, resizable hierarchy/inspector docks, tabbed bottom dock (content browser, console, profiler, AI assistant), viewport overlays, and status bar — wired to the session, UI store, layout store, and host RPC. Mounted by `EditorApp`; not a game-author entry point.
552
+ - `EditorChrome` (function): function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset = importAssetToHost, onRegisterAsset, onExitEditor, allowedKinds, exitLabel, maxImportBytes, documentSave, draftAutosave = true, creatorPolicy, }: { gameId: string; session: EditorSession; … — The full editor UI shell — global app bar, contextual scene toolbar, workspace rail, resizable hierarchy/inspector docks, tabbed bottom dock (content browser, console, profiler, AI assistant), viewport overlays, and status bar — wired to the session, UI store, layout store, and host RPC. Mounted by `EditorApp`; not a game-author entry point.
553
+
554
+ ## @jgengine/editor/MaterialAssetEditor
555
+
556
+ - `MaterialAssetEditorProps` (interface): interface MaterialAssetEditorProps — A callback-driven physical material editor; the host owns persistence, undo and preview rendering.
490
557
 
491
558
  ## @jgengine/editor/PerfProbe
492
559
 
@@ -502,6 +569,10 @@
502
569
  - `SelectionGizmo` (const): const SelectionGizmo: React.MemoExoticComponent<({ session, ui, groundSnap, }: { session: EditorSession; ui: EditorUiStore; groundSnap?: ((x: number, z: number) => number) | undefined; }) => React.JSX.Element | null> — Drag-to-transform gizmo bound to the current selection, dispatching editor commands on release. Mounts the shared shell `TransformGizmo` and maps release poses onto editor commands (multi-select translate, volume scale, path vertex move).
503
570
  - `ViewportSelect` (const): const ViewportSelect: React.MemoExoticComponent<({ api, ui }: { api: EditorHostApi; ui: EditorUiStore; }) => null> — Canvas click-to-select and click-to-place. Document objects pick by screen proximity (registration always matches what you see) with click-cycling through stacked candidates and shift/ctrl additive selection; everything else picks by occlusion-ordered raycast against the tagged scene graph. When a placement tool is armed, clicks author new markers, volumes, notes, or path points at the ground hit instead of selecting.
504
571
 
572
+ ## @jgengine/editor/SimulationInspector
573
+
574
+ - `SimulationInspector` (function): function SimulationInspector({ session, creatorPolicy }: { session: EditorSession; creatorPolicy?: CreatorPolicy }): React.JSX.Element — Production authoring controls share the same validated command and undo history as RPC.
575
+
505
576
  ## @jgengine/editor/StandaloneEditor
506
577
 
507
578
  - `AssetImporter` (type): type AssetImporter = (file: File) => Promise<StandaloneAsset | null> — Persists a dropped model file through the editor host so it survives reload as a durable catalog asset, returning the host-assigned id/url (which the manifest scan re-lists on reload). Resolves `null` when no host is listening — a plain browser mount with no dev server — so the caller can fall back to a blob URL.
@@ -620,7 +691,7 @@
620
691
 
621
692
  ## @jgengine/editor/handlers/hierarchy
622
693
 
623
- - `hierarchyHandlers` (const): const hierarchyHandlers: Pick< HandlerTable, | "set_parent" | "hierarchy" | "list_prefabs" | "create_prefab" | "insert_prefab" | "detach_prefab_instance" | "delete_prefab" > — Parent/child hierarchy and prefab library verbs.
694
+ - `hierarchyHandlers` (const): const hierarchyHandlers: Pick< HandlerTable, | "set_parent" | "hierarchy" | "list_prefabs" | "create_prefab" | "insert_prefab" | "detach_prefab_instance" | "delete_prefab" | "set_prefab_static_bake" > — Parent/child hierarchy and prefab library verbs.
624
695
 
625
696
  ## @jgengine/editor/handlers/minimap
626
697
 
@@ -656,6 +727,14 @@
656
727
  - `MaterialDocumentSlice` (interface): interface MaterialDocumentSlice — Minimal document slice the materials inventory needs.
657
728
  - `MaterialObjectKind` (type): type MaterialObjectKind = "marker" | "volume" | "path" — Document-wide material assignment inventory for the Materials workspace. Reads real `meta.materialId` stamps from placeables — no thumbnails or faked previews.
658
729
 
730
+ ## @jgengine/editor/materialControls
731
+
732
+ - `MaterialControl` (interface): interface MaterialControl — One physical surface field in the material workspace.
733
+ - `MaterialControlGroup` (interface): interface MaterialControlGroup — A family-specific group of primary controls; maps and full physical fields remain available.
734
+ - `advancedMaterialControls` (const): const advancedMaterialControls: readonly MaterialControl[] — Complete supported physical scalar/color fields, independent of the six primary groups.
735
+ - `materialAuthoringNotes` (function): function materialAuthoringNotes(asset: MaterialAsset): readonly string[] — Reports geometry prerequisites and renderer approximations to authors.
736
+ - `materialControlGroups` (function): function materialControlGroups(family: MaterialFamily): readonly MaterialControlGroup[] — Returns six primary groups suited to the material family without hiding the physical model.
737
+
659
738
  ## @jgengine/editor/mcp/bridgeServer
660
739
 
661
740
  - `EditorBridgeServer` (interface): interface EditorBridgeServer — A running editor bridge server: its bound port, URL, and a stop handle.
@@ -672,7 +751,7 @@
672
751
 
673
752
  ## @jgengine/editor/mcp/fieldSpec
674
753
 
675
- - `RPC_FIELD_SCHEMAS` (const): const RPC_FIELD_SCHEMAS: Record<EditorBridgeRequest["method"], readonly RpcFieldSpec[]> — Per-method field type table — the single source of truth for the RPC boundary. A field listed here is type-checked when present (see `decodeEditorBridgeRequest`); existence is left to `EditorHostApi.handle` (whose per-method guards return honest `ok:false` diagnostics for missing/unknown targets). Type-checking presented fields is what keeps a fuzzed value — a string where a number belongs, a scalar where an object belongs — from reaching a live session uncoerced. Unknown extra fields are ignored so a superset payload stays forward-compatible.
754
+ - `RPC_FIELD_SCHEMAS` (const): const RPC_FIELD_SCHEMAS: Record<EditorBridgeRequest["method"], readonly RpcFieldSpec[]> — Per-method field type table — the single source of truth for the RPC boundary. A field listed here is type-checked when present (see `decodeEditorBridgeRequest`); existence is left to `EditorHostApi.handle` (whose per-method guards return honest `ok:false` diagnostics for missing/unknown targets). Type-checking presented fields is what keeps a fuzzed value — a string where a number belongs, a scalar where an object belongs — from reaching a live session uncoerced. Undeclared top-level fields are rejected by the decoder for every method.
676
755
  - `RpcFieldKind` (type): type RpcFieldKind = "string" | "number" | "boolean" | "string[]" | "object" | "object[]" | "vec3" | "value" — The value type a request field is expected to carry.
677
756
  - `RpcFieldSpec` (interface): interface RpcFieldSpec — One field's expected shape in a request; `oneOf`/`nullable`/`required`/`of` refine the check.
678
757
  - `buildInputSchema` (function): function buildInputSchema(method: EditorBridgeRequest["method"]): Record<string, unknown> — Generates the MCP tool `inputSchema` for a method from its {@link RPC_FIELD_SCHEMAS} entry.
@@ -696,7 +775,7 @@
696
775
 
697
776
  - `DecodeRpcRequestResult` (type): type DecodeRpcRequestResult = | { ok: true; request: EditorBridgeRequest } | { ok: false; errors: RpcRequestDiagnostic[] } — Result of {@link decodeEditorBridgeRequest}: a request whose `method` is a real one, or the diagnostic that rejected it.
698
777
  - `RpcRequestDiagnostic` (interface): interface RpcRequestDiagnostic — One field-level failure surfaced while decoding an untrusted RPC request.
699
- - `decodeEditorBridgeRequest` (function): function decodeEditorBridgeRequest(raw: unknown): DecodeRpcRequestResult — Validates an untrusted JSON-decoded RPC payload (from `--rpc`, the HTTP bridge, or an agent tool call) before it reaches `EditorHostApi.handle`: confirms it is a plain object carrying a known `method`, then type-checks every field the method understands against {@link RPC_FIELD_SCHEMAS}. A garbled method, or a field whose value is the wrong type (a string where a number belongs, a scalar where an object belongs), is rejected here with a path-specific diagnostic instead of flowing into a live session on a blind cast. Missing fields and unknown extra fields are left for `handle` to interpret so the boundary stays forward-compatible.
778
+ - `decodeEditorBridgeRequest` (function): function decodeEditorBridgeRequest(raw: unknown): DecodeRpcRequestResult — Validates an untrusted JSON-decoded RPC payload (from `--rpc`, the HTTP bridge, or an agent tool call) before it reaches `EditorHostApi.handle`: confirms it is a plain object carrying a known `method`, then type-checks every field the method understands against {@link RPC_FIELD_SCHEMAS}. A garbled method, or a field whose value is the wrong type (a string where a number belongs, a scalar where an object belongs), or an undeclared field is rejected here with a path-specific diagnostic instead of flowing into a live session on a blind cast. Missing fields are left for `handle` to interpret.
700
779
 
701
780
  ## @jgengine/editor/mcp/stdioServer
702
781
 
@@ -724,11 +803,11 @@
724
803
  - `clearAnimGraph` (function): function clearAnimGraph(setting: AnimationSetting | undefined): AnimationSetting | undefined — Removes a stored graph so `states`/`oneShots` (or clip roles) drive the placement again.
725
804
  - `defaultCustomConfig` (function): function defaultCustomConfig(clips: readonly string[]): AuthoredAnimationConfig — A concrete custom config derived from a rigged asset's clip roles — the `custom` mode seed.
726
805
  - `effectiveAnimGraph` (function): function effectiveAnimGraph(setting: AnimationSetting | undefined, clips: readonly string[]): { graph: AnimGraph; source: AnimGraphSource } | null — The graph a placement plays at run time: its stored `graph`, else the locomotion graph of its custom `states`/`oneShots`, else the one clip roles derive for `"auto"` and no-override placements. `null` for `"none"`, a single-clip config, or a rig whose clips have no idle.
727
- - `readAnimationSetting` (function): function readAnimationSetting(meta: Record<string, unknown> | undefined): AnimationSetting | undefined — Reads and shape-validates the animation override from a marker's meta; undefined when absent/invalid.
806
+ - `readAnimationSetting` (function): function readAnimationSetting(meta: Record<string, unknown> | undefined): AnimationSetting | undefined — Reads a whole valid override, retaining authored fields; malformed values return undefined.
728
807
  - `setAnimationMode` (function): function setAnimationMode(setting: AnimationSetting | undefined, mode: AnimationMode, clips: readonly string[] = []): AnimationSetting | undefined — Switches authoring mode. `custom` seeds from the asset's clip-role defaults (so the user starts from a working config, then overrides) when there is no existing object config; the other modes are the bare string/undefined values.
729
- - `setLocomotionClip` (function): function setLocomotionClip(setting: AnimationSetting | undefined, role: LocomotionRole, clipName: string | null): AuthoredAnimationConfig — Sets or clears (null) a locomotion state clip; forces the setting into `custom`.
730
- - `setLocomotionNumber` (function): function setLocomotionNumber(setting: AnimationSetting | undefined, key: LocomotionNumber, value: number | null): AuthoredAnimationConfig — Sets or clears (null) a numeric locomotion tuning.
731
- - `setOneShotClip` (function): function setOneShotClip(setting: AnimationSetting | undefined, event: string, clipName: string | null): AuthoredAnimationConfig — Binds or clears (null) a one-shot event to a clip.
808
+ - `setLocomotionClip` (function): function setLocomotionClip(setting: AnimationSetting | undefined, role: LocomotionRole, clipName: string | null, clips: readonly string[] = []): AuthoredAnimationConfig — Sets or clears (null) a locomotion state clip; forces the setting into `custom`.
809
+ - `setLocomotionNumber` (function): function setLocomotionNumber(setting: AnimationSetting | undefined, key: LocomotionNumber, value: number | null, clips: readonly string[] = []): AuthoredAnimationConfig — Sets or clears (null) a numeric locomotion tuning.
810
+ - `setOneShotClip` (function): function setOneShotClip(setting: AnimationSetting | undefined, event: string, clipName: string | readonly string[] | null): AuthoredAnimationConfig — Binds or clears (null) a one-shot event's clip or variant list.
732
811
  - `setTransitionDuration` (function): function setTransitionDuration(setting: AnimationSetting | undefined, graph: AnimGraph, layerId: string, index: number, duration: number): AuthoredAnimationConfig — Sets one transition's crossfade seconds in `graph` and stores the result, so editing a derived graph turns it into an authored one.
733
812
  - `storeAnimGraph` (function): function storeAnimGraph(setting: AnimationSetting | undefined, graph: AnimGraph): AuthoredAnimationConfig — Stores `graph` on the placement, keeping its other fields; the stored graph wins at play time.
734
813
 
@@ -768,6 +847,9 @@
768
847
  - `EditorFocusTarget` (interface): interface EditorFocusTarget — Where the editor orbit camera looks, plus optional placement so a single `camera_goto`/ `camera_frame` can compose an aerial. `x/y/z` is the orbit target (pan-only when that is all that is set — the historical behavior); `distance`, `pitch` (degrees above the horizon), `yaw` (degrees), and `height` reposition the camera around that target when provided.
769
848
  - `EditorHostApi` (interface): interface EditorHostApi — The live editor's global control surface — session, visibility, camera focus, assets, mode, RPC.
770
849
  - `EditorKindVisibility` (interface): interface EditorKindVisibility — Per-kind show/hide flags for the editor's layer panel.
850
+ - `EditorMaterialSlotInfo` (type): type EditorMaterialSlotInfo = Readonly<Omit<ModelMaterialSlotInfo, "uvSets">> & { readonly uvSets: readonly number[] } — Canonical imported slot names and mesh prerequisites reported by the loaded model.
851
+ - `EditorMaterialSlotInventory` (type): type EditorMaterialSlotInventory = EditorMaterialSlotReport & { id: string; sourceUrl?: string } — A marker's current imported-model inventory, including an explicit availability state.
852
+ - `EditorMaterialSlotReport` (type): type EditorMaterialSlotReport = | { status: "ready"; slots: readonly EditorMaterialSlotInfo[] } | { status: "loading" } | { status: "unavailable"; reason: string } — A live model-source observation; loading and unavailable reports never contain fabricated slots.
771
853
  - `EditorPerfSample` (interface): interface EditorPerfSample — Rolling frame-rate sample published by the in-canvas PerfProbe.
772
854
  - `EditorRunMode` (type): type EditorRunMode = "edit" | "walk" | "play" | "hud" — How the editor hosts the game: frozen placement view, roamable world, the real game, or HUD-layout authoring.
773
855
  - `EditorSession` (interface): interface EditorSession — Stateful, undoable handle for driving scene edits from UI or an MCP agent.
@@ -812,7 +894,7 @@
812
894
 
813
895
  ## @jgengine/editor/shell/TopAppBar
814
896
 
815
- - `TopAppBar` (function): function TopAppBar({ gameId, onExitToGame, dirty, saveState, lastSavedAt = null, saveAvailable, saveError, canUndo, canRedo, onUndo, onRedo, onSave, onPlay, onWalk, onHud, onImport, onExport, onCopyJson, onOpenPalette, onToggleHelp, onResetLayout, }: { gameId: string; /** When set (editor summoned o… — Global application bar: identity + save state on the left, command palette in the center, history / run controls / document actions on the right. Pause and Step stay disabled in edit mode; Play mode mounts {@link PlayModeBar}, which wires the same controls to the runtime pause/step RPCs so mode switches keep shell chrome.
897
+ - `TopAppBar` (function): function TopAppBar({ gameId, onExitToGame, exitLabel = "Exit to game", dirty, saveState, lastSavedAt = null, saveAvailable, saveError, canUndo, canRedo, onUndo, onRedo, onSave, onPlay, onWalk, onHud, onImport, onExport, onCopyJson, onOpenPalette, onToggleHelp, onResetLayout, }: { gameId: string; /**… — Global application bar: identity + save state on the left, command palette in the center, history / run controls / document actions on the right. Pause and Step stay disabled in edit mode; Play mode mounts {@link PlayModeBar}, which wires the same controls to the runtime pause/step RPCs so mode switches keep shell chrome.
816
898
  - `TopBarSaveState` (type): type TopBarSaveState = "idle" | "saving" | "saved" | "error" — Document save lifecycle mirrored from `useDocumentSave`.
817
899
 
818
900
  ## @jgengine/editor/shell/ViewportOverlays
@@ -987,6 +1069,10 @@
987
1069
  - `createEditorUiStore` (function): function createEditorUiStore(): EditorUiStore — Creates the shared UI store the editor chrome and viewport both drive.
988
1070
  - `newPlacementId` (function): function newPlacementId(prefix: string): string — Generates a fresh scene-object id for a placement tool click.
989
1071
 
1072
+ ## @jgengine/editor/useDocumentSave
1073
+
1074
+ - `EditorDocumentSave` (interface): interface EditorDocumentSave — Document save lifecycle owned by the mounted authoring session, across viewport modes.
1075
+
990
1076
  ## @jgengine/editor/useF2Chord
991
1077
 
992
1078
  - `useF2Chord` (function): function useF2Chord(code: string, onChord: () => void): void — Listens for the engine's F2+<key> chord family and fires on the given code (e.g. "KeyE").
@@ -4,6 +4,18 @@
4
4
 
5
5
  Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*.
6
6
 
7
+ ## authored-material-slots — read validated material assignments from editor marker metadata
8
+
9
+ - `authoredMaterialAssignments` (function) · `import { authoredMaterialAssignments } from "@jgengine/core/editor/materialAuthoring"`
10
+
11
+ ## creator-export — export validated player scenes with approved catalogs and finite budgets
12
+
13
+ - `exportCreatorDocument` (function) · `import { exportCreatorDocument } from "@jgengine/core/editor/creatorStorage"`
14
+
15
+ ## creator-storage — save and reopen named versioned player creations through injected durable storage
16
+
17
+ - `createCreatorDocumentStorage` (function) · `import { createCreatorDocumentStorage } from "@jgengine/core/editor/creatorStorage"`
18
+
7
19
  ## editor-catalogs — Persist gameplay tuning rows on the scene document.
8
20
 
9
21
  - `EditorCatalogData` (interface) · `import { EditorCatalogData } from "@jgengine/core/editor"`
@@ -68,6 +80,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
68
80
  - `createDocumentLiveSync` (function) · `import { createDocumentLiveSync } from "@jgengine/core/editor"`
69
81
  - `runtimeEntityWriteBackCommand` (function) · `import { runtimeEntityWriteBackCommand } from "@jgengine/core/editor"`
70
82
 
83
+ ## editor-material-controls — Explain geometry prerequisites and approximations for a material asset.
84
+
85
+ - `materialAuthoringNotes` (function) · `import { materialAuthoringNotes } from "@jgengine/editor/materialControls"`
86
+ - `materialControlGroups` (function) · `import { materialControlGroups } from "@jgengine/editor/materialControls"`
87
+
88
+ ## editor-movement — bind live character tuning while preserving distinctive game policy
89
+
90
+ - `bindAuthoredMovement` (function) · `import { bindAuthoredMovement } from "@jgengine/core/editor/movementCatalog"`
91
+ - `createAuthoredMovementReader` (function) · `import { createAuthoredMovementReader } from "@jgengine/core/editor/movementCatalog"`
92
+ - `createMovementSchema` (function) · `import { createMovementSchema } from "@jgengine/core/editor/movementCatalog"`
93
+ - `readAuthoredMovement` (function) · `import { readAuthoredMovement } from "@jgengine/core/editor/movementCatalog"`
94
+
71
95
  ## editor-runtime-inspector — gate a play-mode frame against pause/step control
72
96
 
73
97
  - `consumeRuntimePlayStep` (function) · `import { consumeRuntimePlayStep } from "@jgengine/core/editor"`
@@ -82,6 +106,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
82
106
  - `bakeMinimapFromDocument` (function) · `import { bakeMinimapFromDocument } from "@jgengine/core/editor"`
83
107
  - `documentBakeZones` (function) · `import { documentBakeZones } from "@jgengine/core/editor"`
84
108
 
109
+ ## model-node-visibility — read authored per-placement model subtree selections
110
+
111
+ - `authoredHiddenNodes` (function) · `import { authoredHiddenNodes } from "@jgengine/core/editor/modelNodeVisibility"`
112
+
113
+ ## player-creator — create, edit, save, playtest and reopen bounded player scenes through the production editor
114
+
115
+ - `CreatorApp` (function) · `import { CreatorApp } from "@jgengine/editor"`
116
+
85
117
  ## scene-ownership — audit a whole ownership manifest for boundary violations
86
118
 
87
119
  - `auditManifest` (function) · `import { auditManifest } from "@jgengine/core/scene/sceneOwnership"`
@@ -90,6 +122,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
90
122
  - `isSceneOwnershipManifest` (function) · `import { isSceneOwnershipManifest } from "@jgengine/core/scene/sceneOwnership"`
91
123
  - `ownershipKey` (function) · `import { ownershipKey } from "@jgengine/core/scene/sceneOwnership"`
92
124
 
125
+ ## simulation-inspector — author weather schedules, effects, fire, wind zones, forces and habitats with validated undoable controls
126
+
127
+ - `SimulationInspector` (function) · `import { SimulationInspector } from "@jgengine/editor/SimulationInspector"`
128
+
129
+ ## validate-simulation — diagnose authored weather, effect, fire, force and habitat documents before publication
130
+
131
+ - `decodeEditorSimulation` (function) · `import { decodeEditorSimulation } from "@jgengine/core/editor/simulation"`
132
+
93
133
  ## world-convergence — derive environment coordinate content from the scene document
94
134
 
95
135
  - `environmentContentFromDocument` (function) · `import { environmentContentFromDocument } from "@jgengine/core/editor"`
@@ -8,6 +8,38 @@ in-memory session and, when every RPC succeeds, writes the session document to
8
8
  the GUI's Ctrl+S through `devSavePlugin`). Without `--save`, `--rpc` mutations are discarded on
9
9
  exit — use it for read-only inspection only, or capture `export_document` yourself.
10
10
 
11
+ The source-only CLI writer validates the full document before replacing any saved bytes, matching the dev save endpoint. Validation failures report document paths and leave the existing file intact.
12
+
13
+ Scene and catalog modules are optional. A present module with a broken dependency, initialization error, or invalid export returns load diagnostics; the CLI and stdio host stop before opening a session or saving. Only absent optional files resolve to empty content.
14
+
15
+ `dispatch` with `setEnvironment` validates only the candidate environment before changing the session, undo history, or live document revision. Supported presets are `day`, `dusk`, and `night`; custom palette, sun, intensity, and fog fields remain explicit authored values. Invalid inputs report `$.environment` paths. Validated nested fields are copied, so mutating the command afterward cannot change the document. Passing `environment: undefined` removes the bag and remains undoable. Repair unsupported authored values through an explicit editor command; preserve the author's palette and fog rather than substituting a default.
16
+
17
+ ## Atomic authoring batches
18
+
19
+ Use `document_revision` to inspect the current revision, then send one `push_document_patch` for a complete placement operation. Commands use the `EditorCommand` shapes from `@jgengine/core/editor/commands`, rather than RPC verb shapes. Earlier commands' ids are available to later commands in the same batch:
20
+
21
+ ```json
22
+ {
23
+ "method": "push_document_patch",
24
+ "patch": {
25
+ "type": "commands",
26
+ "baseRevision": 0,
27
+ "commands": [
28
+ { "type": "addMarker", "marker": { "id": "placed_a", "kind": "prop", "catalogId": "my-model", "position": { "x": 1, "y": 0, "z": 2 } } },
29
+ { "type": "addPath", "path": { "id": "route_a", "kind": "route", "points": [{ "x": 0, "y": 0, "z": 0 }, { "x": 1, "y": 0, "z": 2 }] } },
30
+ { "type": "createCollection", "id": "region_a", "name": "Region A", "memberIds": ["placed_a", "route_a"] }
31
+ ]
32
+ }
33
+ }
34
+ ```
35
+
36
+ `my-model` is a model in the game's own catalog. Send this JSON through `--rpc-file <file> --save`, the live bridge, or the embedded agent's existing tool. Human code can use `session.transaction(commands)` with the same commands and undo contract.
37
+
38
+ - All commands stage before commit. A malformed command, missing/locked target, parent cycle, or cross-kind object-id collision rejects the whole batch, preserving the document, selection, undo/redo history, revision, and live subscribers. Errors include `commands[N]`; the RPC also returns `result.commandIndex` and the unchanged `result.revision`.
39
+ - A successful edit emits one session notification, publishes one document revision, and forms one undo/redo step. Selection-only changes do not publish or consume document history. `undo`/`redo` commands cannot run inside a transaction.
40
+ - Existing same-kind object ids upsert without automatic renaming. Repeating an already-current transform or stable-id placement succeeds without publishing another revision or consuming history. After a revision conflict, inspect the latest document before retrying against its revision. `force` skips only the revision check; validation and rollback still apply.
41
+ - Explicit copy operations such as `duplicate`, `addFragment`, and `insertPrefab` retain their existing generated-id behavior. Use direct `addMarker`/`addVolume`/`addPath`/`addNote` commands when gameplay needs caller-owned stable ids.
42
+
11
43
  # Editor agent panel (embedded)
12
44
 
13
45
  Toolbar **Agent** opens a dockable chat panel in `EditorChrome`. Tool calls use the same editor RPC verbs as the MCP/CLI bridge and the GUI — one session undo stack, interleaved with human edits.
@@ -45,6 +77,7 @@ Each `toolCalls[].name` is an editor RPC method (`set_transform`, `select`, …)
45
77
  The bridge is trustworthy so agents author instead of hardcoding — every path is honest about failure:
46
78
 
47
79
  - **Rejected mutations return `ok:false` with a reason.** A locked/cyclic `set_transform`, a `set_parent` that would form a cycle, a collection/prefab verb targeting a missing id, and a batch verb that matches nothing all fail loudly — never a phantom `{ok:true}`.
80
+ - **Command patches are atomic.** `push_document_patch` commands share `EditorSession.transaction` with core live sync: a bad command rolls back the entire batch, and an accepted batch has one undo step and document publication. See [atomic authoring batches](#atomic-authoring-batches).
48
81
  - **One decode/migrate boundary.** `decodeEditorDocument` (`@jgengine/core/editor`) validates every field with a path-specific diagnostic (`$.markers[2].position`) and migrates forward; `import_document` and a `push_document_patch` **snapshot** both clear it, so a malformed or old document fails or migrates loudly rather than corrupting a live session.
49
82
  - **Document-global id uniqueness.** Placeable ids (markers/volumes/paths/notes) are one namespace: adds re-id on collision, and a single imported document that reuses an id is rejected with its path — a duplicate-id import is impossible. Combine paths (`mergeEditorDocuments`, duplicate, overlay) re-id instead.
50
83
  - **Schema-validated input.** `decodeEditorBridgeRequest` type-checks each field a method understands against the per-method schema before it reaches `handle` — a fuzzed value (string where a number belongs, scalar where an object belongs) is rejected at the `--rpc`/HTTP/stdio/agent-tool boundary, never cast in blind. Missing/unknown fields are left to `handle`'s guards so the boundary stays forward-compatible.
@@ -97,13 +130,40 @@ export const STASH_KIND = definePlaceableMarkerKind({
97
130
 
98
131
  A placed rigged asset's animation override lives at `marker.meta.animation` and reaches the game through `markerAnimation` → `ModelConfig.animation`. It is `"auto"`, `"none"` or a config with `states`, `oneShots` and an optional stored `graph` (an `AnimGraph`, validated with `parseAnimGraph` on read).
99
132
 
100
- - **Inspector → Animation** picks the mode and binds idle/walk/run and one-shot clips.
133
+ - **Inspector → Animation** picks the mode and binds idle/walk/run and one-shot clips. Custom playback exposes rate and Hold pose; choosing Single clip explicitly replaces locomotion states and a stored graph, then exposes seek time and looping. Playback edits preserve the remaining configuration. One-shot lists retain all variants; Ctrl/Cmd-click selects several clips.
134
+ - The first locomotion edit after single-clip playback seeds the rig's detected role clips, preserving deeper playback settings. Missing idle keeps an explicitly chosen single clip active; without one, playback stays at bind pose and reports the incomplete mapping. Missing walk keeps the idle pose while moving and displays a repair hint. A stored graph remains authoritative.
101
135
  - **Animation workspace → Graph** (rail button, or the bottom dock's Animation tab) shows the graph the selected placement plays: layers, states and transitions, with the source (stored, built from states, or derived from clip roles).
102
136
  - The scrub slider replays the graph runtime from its entry states. Parameter sliders come from the graph's blend points and conditions.
103
137
  - Trigger buttons record a press at the playhead, so scrubbing back and forth replays it. The rig at the camera focus is posed from the runtime's output through the shell's `createGraphPose`.
104
138
  - Editing a crossfade, or **Store graph**, writes the graph to `meta.animation.graph` as one undoable edit. **Use derived graph** drops it.
105
139
  - Scripted: `set_meta` with `{ "animation": { "graph": { ... } } }` on the marker; see the `jgengine-world` recipe `character-animation.md` for the graph shape.
106
140
 
141
+ For a held single-clip pose, use `set_meta` with `{ "animation": { "clip": "Idle", "paused": true, "time": 0.75, "timeScale": 1, "loop": true } }`. Read it with `get_marker`, inspect it in play, adjust through the inspector, and save through the host's normal Save action. Reopen the document and read the same marker id to verify the saved values. `time` seeks single clips; graph playback has its own timeline. A placement without a moving entity remains at speed zero unless the game supplies animation parameters.
142
+
143
+ A game-owned player spawn can use the same marker's `catalogId` to associate the rig's catalog clips with the Animation inspector. In `defineGame`, set `scenePlacement: { excludeKinds: [...ENTITY_MARKER_KINDS, "player_spawn"] }` (`ENTITY_MARKER_KINDS` comes from `@jgengine/core/world/authoredObjects`). The canonical spawn marker remains available to the game's existing entity-spawn and `markerAnimation` composition, while automatic object placement skips that kind and keeps ordinary props. This exclusion list replaces the default mob/boss list. It controls placement eligibility. Live editor synchronization removes previously authored static objects that become excluded; outside synchronization, existing store objects remain. Game-owned entities are unaffected.
144
+
145
+ ## Character movement tuning
146
+
147
+ `createMovementSchema` (`@jgengine/core/editor/movementCatalog`) creates ordinary Data-tab controls from game-supplied defaults. Export a named `EditorCatalogDefinition` with that schema and named rows, then use `set_catalog_entry` or the GUI to adjust them. Only supplied controls appear: walk speed, signed world gravity, jump velocity, step height, acceleration, braking, air control, sprint multiplier, crouch multiplier and optional maximum climb grade. No movement style or capsule/backend is selected for the game.
148
+
149
+ ```ts
150
+ const readMovement = createAuthoredMovementReader(
151
+ () => getDocumentLiveSync()?.getDocument() ?? editorLayers,
152
+ "player_motion",
153
+ "player",
154
+ );
155
+ const authored = bindAuthoredMovement(readMovement, {
156
+ movement: existingMovement,
157
+ physics: existingPhysics,
158
+ });
159
+ ```
160
+
161
+ Pass `authored.movement` to the playable configuration and `authored.physics` to the game definition's physics. These stable objects expose live numeric getters; policy callbacks, collision settings and backend remain the game's choices. The reader scans only when the immutable document changes. `authored.diagnostics` reports invalid persisted values with document paths and repair guidance; invalid values retain the explicit game fallback. Supply `readMovement().config.walkSpeed` when resolving the game's selected player catalog entry, preserving its other stats and poses. Walk speed applies when that entity spawns or reseats; this API does not overwrite existing entities every frame.
162
+
163
+ Use the normal edit → play → inspect → adjust → save → reload loop. `get_catalog_entry` inspects saved controls; a fresh `import_document` or reopened scene must produce the same reader result. Runtime tuning requires this explicit binding; merely adding a Data row does not change gameplay. Capsule dimensions and terrain/controller backend configuration remain outside this catalog contract.
164
+
165
+ `maxClimbGrade` limits heightfield rise per horizontal distance, including airborne travel. Expose it only when the game requires that policy; the optional `climbGradeHeight` sampler remains a game-owned callback retained by the binding. This is independent of a capsule's slope angle and does not select collision geometry.
166
+
107
167
  ## Pure API (`@jgengine/editor`)
108
168
 
109
169
  ```ts
@@ -182,8 +242,30 @@ save); never hardcode entity stats where the document should own them.
182
242
 
183
243
  ## Asset import
184
244
 
245
+ A prop marker can store `meta.hiddenNodes` as an array of exact imported node names through an undoable `setMarker` metadata edit. Runtime props and the Materials model preview share `modelWithAuthoredNodeVisibility` (`@jgengine/core/editor/modelNodeVisibility`): omitted metadata preserves game model defaults, while `[]` clears that selection. JSON import and editor transactions reject malformed or blank names. Node hiding belongs to the cloned individual model; it does not make originally invisible source nodes visible. Nonempty selections bypass automatic prop batching, and generic scatter rejects them. Entity/player models use explicit `ModelConfig.hiddenNodes`; this marker adapter does not hydrate entity templates.
246
+
185
247
  Dropping a `.glb`/`.gltf` into the editor persists it durably, but where depends on the project shape (`editorHostPlugin`, `@jgengine/node`). A **standalone folder workspace** copies the bytes into the scanned asset folder and re-lists them under a scan-stable id (`importEditorAsset`); the id comes from the file's workspace-relative path, so a placement referencing it resolves after reload. A **promoted game** (one with a typed `src/game/assets.ts` catalog — `isPromotedProject`) instead copies the bytes into `public/<basePath>/imported/` and rewrites `assets.ts` to add a durable `extras` entry to its `buildCatalog({ extras })` call (`importPromotedAsset` → `upsertCatalogExtra`), so the **shipped** game serves and resolves the asset through its own typed catalog rather than a dev-only route. The id matches what a folder rescan would produce, the rewrite is idempotent (re-import of the same id collapses to a single entry), and an unparseable or multi-`buildCatalog` source throws so the host falls back to the folder-scan import rather than corrupt the file.
186
248
 
249
+ ## Material assets and named slots
250
+
251
+ The Materials workspace authors `document.materialAssets` through `upsert_material_asset {asset,coalesce?}`. Each versioned `MaterialAsset` has a stable ID, sparse surface fields, explicit capability declarations, texture metadata and provenance (`@jgengine/core/material/materialAsset`). `materialCapabilitiesForAsset(asset)` derives the shader requirements for its fields, maps and fabric/hair adapters; artist edits union those with existing declarations, while RPC validation still rejects undeclared requirements. Empty fields inherit the imported value; resetting a field removes its override. Renaming does not change the ID. Editable wool, cotton, silk, metal, glass, plastic, stone and hair-card starting points describe response; games own textures, models and art direction.
252
+
253
+ Use `assign_material_asset {ids,materialId,selector}` with a mesh name, material slot name or slot index. Every supplied selector field must match. Separate character skin, eyes, clothing and metal parts with separate assignments. Empty selectors fail rather than flatten a whole imported model. `list_material_assets` reads the library; `remove_material_asset {id}` removes unused assets; `clear_material_assets {ids}` removes selected objects' assignments and restores imported materials. Changes join the scene undo/redo and save path. The workspace exposes roughly six family-specific groups, full physical fields and per-map UV/sampler settings. Neutral and document-light previews use the runtime material renderer; the latter uses the authored environment and fixed sun, while game lighting can add other lights or motion.
254
+
255
+ Before assigning, select the model and open Materials, then call `list_material_slots {id}` with its authored marker ID. It reports canonical loaded mesh/slot names, local slot indices, material types, UV sets and tangents, including for a document with no material assets yet. `ready` returns observed slots; `loading` and `unavailable` return `ok:false` with an explicit status/reason instead of pretending empty data is loaded. Headless CLI cannot load the renderer. The typed host `getMaterialSlots`/`reportMaterialSlots` seam shares the workspace observation, retains up to 64 inspected model sources and rejects stale reports after a marker or catalog URL changes. Choosing another library asset edits its controls; assign it to a slot to change the selected model.
256
+
257
+ For generated building-kit models, reuse this same document library through `defineBuildingKit({ materialAssets: document.materialAssets, parts })`. Copy a selected marker's `authoredMaterialAssignments(marker.meta)` into the corresponding model variant's `materialAssignments`; do not create a second kit material catalog. The shell applies named assignments before instanced harvesting, preserving omitted native slots. This path rejects newly assigned height/displacement maps before allocation; choose an individual model renderer for them. Palette materials remain the separate broad surface path; the kit does not author scene placement or repair navigation.
258
+
259
+ Texture roles define the actual shader channels: roughness G, metalness B, AO R, sheen roughness A, clearcoat roughness G, specular intensity A, transmission R and thickness G. Anisotropy RG stores direction and B strength. Color/emissive/sheen-color/specular-color maps use sRGB; normal and numeric maps use linear data. Alpha maps use G; base-color alpha remains in its color map. Supply real runtime URLs and declare UV set, transform, normal convention, sampler, compression and physical size. Compression metadata does not transcode an image; physical size documents metres while UV transforms control rendering. Repack unsupported channels instead of silently relabelling them.
260
+
261
+ The installed renderer is Three r182. Its [physical material](https://threejs.org/docs/pages/MeshPhysicalMaterial.html) supports sheen, anisotropy, clearcoat, specular/IOR, transmission/volume and iridescence. Check the installed shader before adding controls: online docs can cover later releases. Transmission needs content behind the surface, adds rendering work and should use opacity 1. Alpha coverage cuts or blends the surface; it does not implement refraction. Directional response needs suitable UVs/normals or authored tangents, and must be reviewed after rotating the mesh or light.
262
+
263
+ [HDRP's fabric distinction](https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.0/manual/understand-fabrics.html) separates diffuse cotton/wool fuzz from directional silk. The fuzzy adapter uses native grazing sheen; the woven adapter adds UV-oriented thread normals and roughness variation. Thread scale is cycles per UV, weave direction is radians, normal strength changes slope and variation changes roughness. Neither creates fibre silhouettes, cloth deformation or collision. Explicit surface values override appearance-adapter defaults.
264
+
265
+ [Unreal shading models](https://dev.epicgames.com/documentation/en-us/unreal-engine/shading-models-in-unreal-engine) have dedicated hair and cloth responses, including hair scatter/tangent/backlit inputs. [Unreal's groom workflow](https://dev.epicgames.com/documentation/en-us/unreal-engine/hair-rendering-and-simulation-in-unreal-engine) also owns strand geometry and simulation. JGengine's hair cards/strand meshes use an explicit approximation: directional physical highlights, authored coverage and card backlighting. It provides no hair BSDF, multiple scattering, groom, collision or simulation, and its skin family does not supply subsurface scattering. Similar control names do not imply parity. Review front-lit, grazing and backlit views with the actual authored geometry.
266
+
267
+ For resource review, `materialResourceMetrics` (`@jgengine/shell/render/materialAsset`) estimates loaded texture-view bytes and identifies masked, blended and transmission materials; shared uploads can make that byte estimate an upper bound. Opt in to `measureProjectedOverdraw(renderer,scene,camera,{width,height})` (`@jgengine/shell/render/projectedOverdraw`) to sample actual projected mesh fragments with native alpha discard. It requires float rendering/blending/readback, stalls the GPU, restores scene/renderer ownership and disposes its temporary resources. Depth rejection is disabled, so overlap is an upper bound rather than production early-Z cost; shadow, refraction and postprocessing passes and GPU timing are excluded. Queue and coverage flags overlap. Compare normal renderer draw counters separately, and use hardware measurements for performance claims.
268
+
187
269
  ## Grid / tile layers
188
270
 
189
271
  Grid-addressed content (rooms, tactics maps, farms, nav/rule layers) lives on the scene document as
@@ -229,3 +311,69 @@ undoable `setMinimapBake` command. Runtime feeds the stored `background`/`bounds
229
311
  error inline.
230
312
  - RPC/CLI verb: `bake_minimap` (optional `padding`, `resolution`, `waterLevel`). The bake path uses
231
313
  no `Date`/`Math.random`, so the same scene + sampler always bakes byte-identical output.
314
+
315
+ ## Terrain and viewport iteration
316
+
317
+ `create_terrain` rejects invalid dimensions and more than 1,000,000 vertices before allocation; increase `cellSize` for larger maps. Brush coordinates, radius and strength must be finite, radius positive, strength nonnegative. A ramp requires both `toX` and `toZ`; blend strength is 0–1.
318
+
319
+ `blend_terrain` adds a missing material layer and paints it as one authored action: one notification and one undo. A missed or invalid brush changes neither the layer stack nor undo/redo. `set_terrain_layers` and `EditableTerrain.setLayers` preserve blends by stable layer id across additions, reorder and material parameter edits. Removing a painted layer normalizes surviving weights; replacing every painted layer clears the obsolete buffer.
320
+
321
+ Viewport placement honors grid snap for marker, zone, note and path points on X/Z, then resamples the authored sculpt composed over the shared base ground field. Paths select along visible segments as well as vertices. Multi-selection gizmos translate the whole group; rotate/scale remain available for individual supported objects.
322
+
323
+ Terrain-only scenes work with `camera_frame` / Frame all: the authored footprint and sculpt relief contribute to document bounds without a temporary marker. Native runner captures use `drive editor --param editor=standalone` and the same editor RPCs.
324
+
325
+ Grid placement resamples the authored sculpt composed over the live base ground field at snapped XZ for markers, volumes, notes and draft path points. It uses the same terrain/path policy as the ground overlays (including the ground field’s off-map behavior), and flat Y=0 when no ground sampler is mounted. Nonfinite surface heights reject placement without changing the document or clearing the tool.
326
+
327
+ ## Static prefab export
328
+
329
+ Keep source parts in `EditorPrefab.fragment` and author export settings with `set_prefab_static_bake`
330
+ (or the `setPrefabStaticBake` command): `prefabId` and `bake: { assetId, collisionBoxes?, clearances? }`.
331
+ `bake: null` clears settings. Save/reopen and undo/redo retain settings alongside the editable parts;
332
+ an invalid setting rejects the whole command without changing history. Each box uses local
333
+ `min`/`max` XYZ triples; clearances add a unique `id`. Clearances require explicit solid boxes and
334
+ may touch their edges but must not overlap them. These are authored constraints, not automatic
335
+ proof that an entrance is navigable for a particular character.
336
+
337
+ ```json
338
+ {"method":"set_prefab_static_bake","prefabId":"courtyard-arch","bake":{"assetId":"own:arch","collisionBoxes":[{"min":[-2,0,-1],"max":[-1,3,1]},{"min":[1,0,-1],"max":[2,3,1]}],"clearances":[{"id":"entry","min":[-1,0,-1],"max":[1,2,1]}]}}
339
+ ```
340
+
341
+ Export pinned source bytes offline with `bakeStaticPrefab` from
342
+ `@jgengine/assets/staticPrefabBake` (Node/Bun only; see `jgengine-assets`). Register the returned
343
+ `dims`, `collisionMesh` and `anchor: "origin"` with the output URL in the game's asset catalog.
344
+ The shared model resolver preserves that authored origin for rendering and collider consumption.
345
+ World placements remain catalog markers in `editor.scene.json`; baking never replaces or moves
346
+ them. Validate the game's real walker/route clearance before advertising walkable interiors.
347
+
348
+ ## Production creator
349
+
350
+ The game owns its menu and scene rules. The shared creator owns named durable documents, editor mounting, Save and isolated playtests:
351
+
352
+ ```tsx
353
+ import { createCreatorDocumentStorage, type CreatorConfig, type CreatorPolicy } from "@jgengine/core/editor/creatorStorage";
354
+ import { GameHost } from "@jgengine/shell/GameHost";
355
+ import type { PlayableGame } from "@jgengine/shell/registry";
356
+
357
+ const policy: CreatorPolicy = {
358
+ maxDocuments: 8, maxBytes: 128_000, maxObjects: 48,
359
+ maxPathPoints: 128, maxGridCells: 0, maxTerrainVertices: 0,
360
+ allowedKinds: ["player_spawn", "prop", "checkpoint", "finish"],
361
+ allowedAssets: ["own:platform"], allowedCatalogIds: ["platform"],
362
+ validate(document) { validateGameRules(document); },
363
+ };
364
+ const creator: CreatorConfig<PlayableGame> = {
365
+ policy,
366
+ storage: createCreatorDocumentStorage({ storage: localStorage, key: "my-game:creator:v1", policy }),
367
+ initialDocument: () => authoredStartingDocument,
368
+ createPlayable: document => buildGameFromDocument(document),
369
+ };
370
+
371
+ <GameHost playable={game} editor={() => import("@jgengine/editor")}
372
+ creator={creator} creatorOpen={creatorOpen} onCreatorOpenChange={setCreatorOpen} />;
373
+ ```
374
+
375
+ Keep `creator` and its callbacks stable across renders. `createPlayable` recomposes rendering, world and gameplay from the supplied document; do not close over the bundled starting scene. `initialDocument` supplies game-authored content, not an SDK genre preset. Creator saves are version 1 named records and reject optimistic revision conflicts. Saving at the maximum safe integer revision rejects with `Invalid scene revision` before writing; the last valid catalog stays readable. The injected storage promise must resolve only after durable success. The browser adapter writes one bounded catalog value and surfaces access/quota failures; hosted storage can implement the same list/load/save interface with transactional revisions.
376
+
377
+ The approved Add kinds and asset palette are filtered in the editor. The same validator applies before commands and transactions commit, on import, and on save. External asset URLs, prefab/directive expansion and undeclared simulation budgets are rejected. `maxSimulationParticles`, `maxFireCells`, `maxForceFields` and `maxPopulation` opt into bounded authored simulation (weather reserves 5,300 precipitation particles plus 256 rain-impact slots; collision effects reserve capacity for 32 concurrent child bursts alongside standing emitters); `validate` can restrict species, triggers or required markers. Save/export contain the authored scene; gameplay mutations stay in the captured playtest runtime and are discarded on Return to editor. Returning to saved scenes reopens the last durable save. Unsaved edits stay through Play/Return, and are discarded when leaving the editor catalog entry.
378
+
379
+ The repeatable snapshot → Play → Return workflow follows the iteration principle in [UEFN playtesting](https://dev.epicgames.com/documentation/en-us/fortnite/playtesting-your-island-in-unreal-editor-for-fortnite). Each Play captures a new validated snapshot; authoring stays locked during isolated play, and Save always writes the editable document.