@jgengine/editor 0.18.1 → 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 (80) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/ClipPreviewLayer.d.ts +2 -3
  3. package/dist/ClipPreviewLayer.js +24 -26
  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 +61 -35
  11. package/dist/InspectorPanel.d.ts +3 -1
  12. package/dist/InspectorPanel.js +12 -8
  13. package/dist/LightingPanel.js +5 -1
  14. package/dist/MaterialAssetEditor.d.ts +13 -0
  15. package/dist/MaterialAssetEditor.js +109 -0
  16. package/dist/MaterialPreview.d.ts +16 -0
  17. package/dist/MaterialPreview.js +41 -0
  18. package/dist/MaterialsWorkspacePanel.d.ts +14 -2
  19. package/dist/MaterialsWorkspacePanel.js +78 -4
  20. package/dist/RuntimePlayBridge.js +1 -6
  21. package/dist/SelectionGizmo.js +23 -14
  22. package/dist/SimulationInspector.d.ts +9 -0
  23. package/dist/SimulationInspector.js +47 -0
  24. package/dist/TerrainSculpt.js +3 -1
  25. package/dist/agent/toolBridge.js +2 -0
  26. package/dist/authoringPlacement.d.ts +21 -0
  27. package/dist/authoringPlacement.js +34 -0
  28. package/dist/handlers/context.d.ts +1 -0
  29. package/dist/handlers/document.d.ts +1 -1
  30. package/dist/handlers/document.js +31 -1
  31. package/dist/handlers/hierarchy.d.ts +1 -1
  32. package/dist/handlers/hierarchy.js +4 -0
  33. package/dist/handlers/navmesh.d.ts +2 -0
  34. package/dist/handlers/navmesh.js +16 -4
  35. package/dist/handlers/runtime.js +20 -17
  36. package/dist/handlers/terrain.js +52 -12
  37. package/dist/index.d.ts +2 -1
  38. package/dist/index.js +1 -0
  39. package/dist/materialControls.d.ts +32 -0
  40. package/dist/materialControls.js +79 -0
  41. package/dist/mcp/fieldSpec.d.ts +1 -1
  42. package/dist/mcp/fieldSpec.js +13 -4
  43. package/dist/mcp/importOptionalGameModule.d.ts +2 -0
  44. package/dist/mcp/importOptionalGameModule.js +13 -0
  45. package/dist/mcp/loadGameCatalogs.js +5 -12
  46. package/dist/mcp/rpcRequest.d.ts +3 -3
  47. package/dist/mcp/rpcRequest.js +10 -4
  48. package/dist/mcp/tools.js +13 -4
  49. package/dist/modelAnimationAuthoring.d.ts +49 -6
  50. package/dist/modelAnimationAuthoring.js +118 -64
  51. package/dist/session.d.ts +64 -0
  52. package/dist/session.js +58 -7
  53. package/dist/shell/AnimationGraphPanel.d.ts +16 -0
  54. package/dist/shell/AnimationGraphPanel.js +180 -0
  55. package/dist/shell/AnimationPanel.d.ts +3 -1
  56. package/dist/shell/AnimationPanel.js +9 -7
  57. package/dist/shell/PlayModeBar.d.ts +2 -1
  58. package/dist/shell/PlayModeBar.js +2 -2
  59. package/dist/shell/SceneToolbar.d.ts +2 -1
  60. package/dist/shell/SceneToolbar.js +5 -4
  61. package/dist/shell/TopAppBar.d.ts +2 -1
  62. package/dist/shell/TopAppBar.js +2 -2
  63. package/dist/shell/WorkspaceRail.js +1 -1
  64. package/dist/shell/animGraphPreview.d.ts +58 -0
  65. package/dist/shell/animGraphPreview.js +95 -0
  66. package/dist/shell/clipPreview.d.ts +9 -0
  67. package/dist/useDocumentSave.d.ts +20 -0
  68. package/dist/useDocumentSave.js +33 -0
  69. package/package.json +6 -5
  70. package/skills/ce-handoff/SKILL.md +2 -2
  71. package/skills/jgengine/SKILL.md +28 -6
  72. package/skills/jgengine/api.md +139 -99
  73. package/skills/jgengine/capabilities.md +28 -0
  74. package/skills/jgengine/recipes/minimal-game.md +10 -2
  75. package/skills/jgengine-editor/SKILL.md +12 -2
  76. package/skills/jgengine-editor/api.md +120 -16
  77. package/skills/jgengine-editor/capabilities.md +40 -0
  78. package/skills/jgengine-editor/reference.md +161 -3
  79. package/skills/jgengine-editor/references/live-sync.md +15 -0
  80. package/skills/level-design/SKILL.md +1 -1
@@ -1,3 +1,5 @@
1
+ import type { AnimGraph } from "@jgengine/core/anim/animGraph";
2
+ import { type AuthoredAnimationDiagnostic, type AuthoredAnimationDocumentLike } from "@jgengine/core/world/authoredAnimation";
1
3
  /**
2
4
  * Authoring model for a placement's `ModelConfig.animation`, persisted on a marker's `meta.animation`
3
5
  * and consumed by the shell exactly like a game-authored config. Pure data + reducers, no JSX — the
@@ -38,18 +40,29 @@ export interface AuthoredAnimationStates {
38
40
  }
39
41
  /** Structurally compatible with `ModelAnimationConfig`, but every field optional for authoring. */
40
42
  export interface AuthoredAnimationConfig {
43
+ auto?: true;
44
+ clock?: "real" | "game";
41
45
  clip?: string;
42
46
  loop?: boolean;
43
47
  timeScale?: number;
48
+ paused?: boolean;
49
+ time?: number;
44
50
  states?: AuthoredAnimationStates;
45
- oneShots?: Record<string, string>;
51
+ oneShots?: Record<string, string | readonly string[]>;
52
+ /** A stored animation graph; takes over from `states`/`oneShots` at play time. */
53
+ graph?: AnimGraph;
46
54
  }
47
55
  /** The value stored at `meta.animation`. */
48
56
  export type AnimationSetting = AuthoredAnimationConfig | "auto" | "none";
49
57
  /** Which authoring mode a stored setting represents. `default` = no override key. */
50
58
  export type AnimationMode = "default" | "auto" | "none" | "custom";
51
- /** Reads and shape-validates the animation override from a marker's meta; undefined when absent/invalid. */
59
+ /** Reads a whole valid override, retaining authored fields; malformed values return undefined. */
52
60
  export declare function readAnimationSetting(meta: Record<string, unknown> | undefined): AnimationSetting | undefined;
61
+ /** Validates a placement's animation with its saved-document repair locations. @internal */
62
+ export declare function readAnimationSettingResult(document: AuthoredAnimationDocumentLike, markerId: string): {
63
+ setting: AnimationSetting | undefined;
64
+ diagnostics: readonly AuthoredAnimationDiagnostic[];
65
+ };
53
66
  /** The authoring mode a stored setting maps to. */
54
67
  export declare function animationMode(setting: AnimationSetting | undefined): AnimationMode;
55
68
  /**
@@ -61,11 +74,21 @@ export declare function setAnimationMode(setting: AnimationSetting | undefined,
61
74
  /** A concrete custom config derived from a rigged asset's clip roles — the `custom` mode seed. */
62
75
  export declare function defaultCustomConfig(clips: readonly string[]): AuthoredAnimationConfig;
63
76
  /** Sets or clears (null) a locomotion state clip; forces the setting into `custom`. */
64
- export declare function setLocomotionClip(setting: AnimationSetting | undefined, role: LocomotionRole, clipName: string | null): AuthoredAnimationConfig;
77
+ export declare function setLocomotionClip(setting: AnimationSetting | undefined, role: LocomotionRole, clipName: string | null, clips?: readonly string[]): AuthoredAnimationConfig;
65
78
  /** Sets or clears (null) a numeric locomotion tuning. */
66
- export declare function setLocomotionNumber(setting: AnimationSetting | undefined, key: LocomotionNumber, value: number | null): AuthoredAnimationConfig;
67
- /** Binds or clears (null) a one-shot event to a clip. */
68
- export declare function setOneShotClip(setting: AnimationSetting | undefined, event: string, clipName: string | null): AuthoredAnimationConfig;
79
+ export declare function setLocomotionNumber(setting: AnimationSetting | undefined, key: LocomotionNumber, value: number | null, clips?: readonly string[]): AuthoredAnimationConfig;
80
+ /** Binds or clears (null) a one-shot event's clip or variant list. */
81
+ export declare function setOneShotClip(setting: AnimationSetting | undefined, event: string, clipName: string | readonly string[] | null): AuthoredAnimationConfig;
82
+ /** Sets a single playback clip, explicitly replacing speed-driven states and a stored graph. @internal */
83
+ export declare function setPlaybackClip(setting: AnimationSetting | undefined, clip: string | null): AuthoredAnimationConfig;
84
+ /** Changes a playback scalar while preserving role mappings, variants and the stored graph. @internal */
85
+ export declare function setPlaybackNumber(setting: AnimationSetting | undefined, key: "time" | "timeScale", value: number | null): AuthoredAnimationConfig;
86
+ /** Chooses the playback clock without replacing mappings, graph, or other authored fields. @internal */
87
+ export declare function setPlaybackClock(setting: AnimationSetting | undefined, clock: "real" | "game"): AuthoredAnimationConfig;
88
+ /** Requests loaded-rig role inference, or removes that request, retaining explicit overrides whole. @internal */
89
+ export declare function setAutomaticRoles(setting: AnimationSetting | undefined, derive: boolean): AuthoredAnimationConfig;
90
+ /** Changes a playback toggle without replacing the remaining authored settings. @internal */
91
+ export declare function setPlaybackBoolean(setting: AnimationSetting | undefined, key: "paused" | "loop", value: boolean): AuthoredAnimationConfig;
69
92
  /**
70
93
  * The meta patch that persists a setting (or clears the override when undefined). Shallow-merges onto
71
94
  * `marker.meta` via `setMarker`; an undefined value drops out of the saved JSON document.
@@ -73,3 +96,23 @@ export declare function setOneShotClip(setting: AnimationSetting | undefined, ev
73
96
  export declare function animationMetaPatch(setting: AnimationSetting | undefined): {
74
97
  animation: AnimationSetting | undefined;
75
98
  };
99
+ /** Where the graph a placement plays comes from. */
100
+ export type AnimGraphSource = "authored" | "locomotion" | "auto";
101
+ /**
102
+ * The graph a placement plays at run time: its stored `graph`, else the locomotion graph of its
103
+ * custom `states`/`oneShots`, else the one clip roles derive for `"auto"` and no-override placements.
104
+ * `null` for `"none"`, a single-clip config, or a rig whose clips have no idle.
105
+ */
106
+ export declare function effectiveAnimGraph(setting: AnimationSetting | undefined, clips: readonly string[]): {
107
+ graph: AnimGraph;
108
+ source: AnimGraphSource;
109
+ } | null;
110
+ /** Stores `graph` on the placement, keeping its other fields; the stored graph wins at play time. */
111
+ export declare function storeAnimGraph(setting: AnimationSetting | undefined, graph: AnimGraph): AuthoredAnimationConfig;
112
+ /** Removes a stored graph so `states`/`oneShots` (or clip roles) drive the placement again. */
113
+ export declare function clearAnimGraph(setting: AnimationSetting | undefined): AnimationSetting | undefined;
114
+ /**
115
+ * Sets one transition's crossfade seconds in `graph` and stores the result, so editing a derived
116
+ * graph turns it into an authored one.
117
+ */
118
+ export declare function setTransitionDuration(setting: AnimationSetting | undefined, graph: AnimGraph, layerId: string, index: number, duration: number): AuthoredAnimationConfig;
@@ -1,4 +1,6 @@
1
- import { rolesFromClips } from "@jgengine/core/game/clipRoles";
1
+ import { readAuthoredAnimation } from "@jgengine/core/world/authoredAnimation";
2
+ import { animGraphFromConfig } from "@jgengine/core/anim/locomotionGraph";
3
+ import { resolveAnimationConfig, rolesFromClips } from "@jgengine/core/game/clipRoles";
2
4
  /**
3
5
  * Authoring model for a placement's `ModelConfig.animation`, persisted on a marker's `meta.animation`
4
6
  * and consumed by the shell exactly like a game-authored config. Pure data + reducers, no JSX — the
@@ -22,48 +24,14 @@ export const LOCOMOTION_NUMBERS = ["walkSpeed", "runSpeed", "fadeSec"];
22
24
  * `combat.hitReaction` / `entity.died`; the rest fire when the game calls `playEntityAnimation`.
23
25
  */
24
26
  export const ONE_SHOT_EVENTS = ["attack", "hit", "death", "jump", "interact", "cheer"];
25
- function isRecord(value) {
26
- return typeof value === "object" && value !== null && !Array.isArray(value);
27
- }
28
- /** Reads and shape-validates the animation override from a marker's meta; undefined when absent/invalid. */
27
+ /** Reads a whole valid override, retaining authored fields; malformed values return undefined. */
29
28
  export function readAnimationSetting(meta) {
30
- const value = meta?.["animation"];
31
- if (value === "auto" || value === "none")
32
- return value;
33
- if (!isRecord(value))
34
- return undefined;
35
- const config = {};
36
- if (typeof value["clip"] === "string")
37
- config.clip = value["clip"];
38
- if (typeof value["loop"] === "boolean")
39
- config.loop = value["loop"];
40
- if (typeof value["timeScale"] === "number")
41
- config.timeScale = value["timeScale"];
42
- if (isRecord(value["states"])) {
43
- const raw = value["states"];
44
- const states = {};
45
- for (const role of LOCOMOTION_ROLES)
46
- if (typeof raw[role] === "string")
47
- states[role] = raw[role];
48
- for (const key of LOCOMOTION_NUMBERS)
49
- if (typeof raw[key] === "number")
50
- states[key] = raw[key];
51
- if (Object.keys(states).length > 0)
52
- config.states = states;
53
- }
54
- if (isRecord(value["oneShots"])) {
55
- const raw = value["oneShots"];
56
- const oneShots = {};
57
- for (const [event, clip] of Object.entries(raw)) {
58
- if (typeof clip === "string")
59
- oneShots[event] = clip;
60
- else if (Array.isArray(clip) && typeof clip[0] === "string")
61
- oneShots[event] = clip[0];
62
- }
63
- if (Object.keys(oneShots).length > 0)
64
- config.oneShots = oneShots;
65
- }
66
- return config;
29
+ return readAnimationSettingResult({ markers: [{ id: "animation", meta }] }, "animation").setting;
30
+ }
31
+ /** Validates a placement's animation with its saved-document repair locations. @internal */
32
+ export function readAnimationSettingResult(document, markerId) {
33
+ const result = readAuthoredAnimation(document, markerId);
34
+ return { setting: result.animation, diagnostics: result.diagnostics };
67
35
  }
68
36
  /** The authoring mode a stored setting maps to. */
69
37
  export function animationMode(setting) {
@@ -78,14 +46,6 @@ export function animationMode(setting) {
78
46
  function asConfig(setting) {
79
47
  return setting !== undefined && setting !== "auto" && setting !== "none" ? setting : {};
80
48
  }
81
- function normalizeConfig(config) {
82
- const next = { ...config };
83
- if (next.states !== undefined && Object.keys(next.states).length === 0)
84
- delete next.states;
85
- if (next.oneShots !== undefined && Object.keys(next.oneShots).length === 0)
86
- delete next.oneShots;
87
- return next;
88
- }
89
49
  /**
90
50
  * Switches authoring mode. `custom` seeds from the asset's clip-role defaults (so the user starts from
91
51
  * a working config, then overrides) when there is no existing object config; the other modes are the
@@ -100,7 +60,7 @@ export function setAnimationMode(setting, mode, clips = []) {
100
60
  return "none";
101
61
  if (setting !== undefined && setting !== "auto" && setting !== "none")
102
62
  return setting;
103
- return normalizeConfig(defaultCustomConfig(clips));
63
+ return defaultCustomConfig(clips);
104
64
  }
105
65
  /** A concrete custom config derived from a rigged asset's clip roles — the `custom` mode seed. */
106
66
  export function defaultCustomConfig(clips) {
@@ -114,9 +74,9 @@ export function defaultCustomConfig(clips) {
114
74
  states.run = roles.run[0];
115
75
  const oneShots = {};
116
76
  for (const event of ONE_SHOT_EVENTS) {
117
- const variant = roles[event]?.[0];
118
- if (variant !== undefined)
119
- oneShots[event] = variant;
77
+ const variants = roles[event];
78
+ if (variants !== undefined && variants.length > 0)
79
+ oneShots[event] = variants.length === 1 ? variants[0] : [...variants];
120
80
  }
121
81
  const config = {};
122
82
  if (Object.keys(states).length > 0)
@@ -126,34 +86,72 @@ export function defaultCustomConfig(clips) {
126
86
  return config;
127
87
  }
128
88
  /** Sets or clears (null) a locomotion state clip; forces the setting into `custom`. */
129
- export function setLocomotionClip(setting, role, clipName) {
89
+ export function setLocomotionClip(setting, role, clipName, clips = []) {
130
90
  const config = asConfig(setting);
131
- const states = { ...config.states };
91
+ const states = { ...(config.states ?? (clipName === null ? undefined : defaultCustomConfig(clips).states)) };
132
92
  if (clipName === null)
133
93
  delete states[role];
134
94
  else
135
95
  states[role] = clipName;
136
- return normalizeConfig({ ...config, states });
96
+ return { ...config, ...(config.states !== undefined || Object.keys(states).length > 0 ? { states } : {}) };
137
97
  }
138
98
  /** Sets or clears (null) a numeric locomotion tuning. */
139
- export function setLocomotionNumber(setting, key, value) {
99
+ export function setLocomotionNumber(setting, key, value, clips = []) {
140
100
  const config = asConfig(setting);
141
- const states = { ...config.states };
101
+ const states = { ...(config.states ?? (value === null || !Number.isFinite(value) ? undefined : defaultCustomConfig(clips).states)) };
142
102
  if (value === null || !Number.isFinite(value))
143
103
  delete states[key];
144
104
  else
145
105
  states[key] = value;
146
- return normalizeConfig({ ...config, states });
106
+ return { ...config, ...(config.states !== undefined || Object.keys(states).length > 0 ? { states } : {}) };
147
107
  }
148
- /** Binds or clears (null) a one-shot event to a clip. */
108
+ /** Binds or clears (null) a one-shot event's clip or variant list. */
149
109
  export function setOneShotClip(setting, event, clipName) {
150
110
  const config = asConfig(setting);
151
111
  const oneShots = { ...config.oneShots };
152
- if (clipName === null)
112
+ if (clipName === null || (Array.isArray(clipName) && clipName.length === 0))
153
113
  delete oneShots[event];
154
114
  else
155
- oneShots[event] = clipName;
156
- return normalizeConfig({ ...config, oneShots });
115
+ oneShots[event] = typeof clipName === "string" ? clipName : clipName.length === 1 ? clipName[0] : [...clipName];
116
+ const next = { ...config, oneShots };
117
+ if (Object.keys(oneShots).length === 0)
118
+ delete next.oneShots;
119
+ return next;
120
+ }
121
+ /** Sets a single playback clip, explicitly replacing speed-driven states and a stored graph. @internal */
122
+ export function setPlaybackClip(setting, clip) {
123
+ if (clip === null) {
124
+ const { clip: _clip, ...config } = asConfig(setting);
125
+ return config;
126
+ }
127
+ const { states: _states, graph: _graph, auto: _auto, ...config } = asConfig(setting);
128
+ return { ...config, clip };
129
+ }
130
+ /** Changes a playback scalar while preserving role mappings, variants and the stored graph. @internal */
131
+ export function setPlaybackNumber(setting, key, value) {
132
+ const config = { ...asConfig(setting) };
133
+ if (value === null || !Number.isFinite(value))
134
+ delete config[key];
135
+ else
136
+ config[key] = key === "time" ? Math.max(0, value) : value;
137
+ return config;
138
+ }
139
+ /** Chooses the playback clock without replacing mappings, graph, or other authored fields. @internal */
140
+ export function setPlaybackClock(setting, clock) {
141
+ return { ...(setting === "auto" ? { auto: true } : asConfig(setting)), clock };
142
+ }
143
+ /** Requests loaded-rig role inference, or removes that request, retaining explicit overrides whole. @internal */
144
+ export function setAutomaticRoles(setting, derive) {
145
+ const config = { ...asConfig(setting) };
146
+ if (derive)
147
+ config.auto = true;
148
+ else
149
+ delete config.auto;
150
+ return config;
151
+ }
152
+ /** Changes a playback toggle without replacing the remaining authored settings. @internal */
153
+ export function setPlaybackBoolean(setting, key, value) {
154
+ return { ...asConfig(setting), [key]: value };
157
155
  }
158
156
  /**
159
157
  * The meta patch that persists a setting (or clears the override when undefined). Shallow-merges onto
@@ -162,3 +160,59 @@ export function setOneShotClip(setting, event, clipName) {
162
160
  export function animationMetaPatch(setting) {
163
161
  return { animation: setting };
164
162
  }
163
+ /**
164
+ * The graph a placement plays at run time: its stored `graph`, else the locomotion graph of its
165
+ * custom `states`/`oneShots`, else the one clip roles derive for `"auto"` and no-override placements.
166
+ * `null` for `"none"`, a single-clip config, or a rig whose clips have no idle.
167
+ */
168
+ export function effectiveAnimGraph(setting, clips) {
169
+ if (setting === "none")
170
+ return null;
171
+ if (setting !== undefined && setting !== "auto" && setting.auto === true) {
172
+ const resolved = resolveAnimationConfig(setting, clips);
173
+ const graph = resolved === undefined ? undefined : animGraphFromConfig(resolved);
174
+ return graph === undefined ? null : { graph, source: setting.graph !== undefined ? "authored" : setting.states !== undefined ? "locomotion" : "auto" };
175
+ }
176
+ if (setting !== undefined && setting !== "auto") {
177
+ if (setting.graph !== undefined)
178
+ return { graph: setting.graph, source: "authored" };
179
+ const states = setting.states;
180
+ if (states?.idle === undefined || states.idle.trim().length === 0)
181
+ return null;
182
+ const graph = animGraphFromConfig({
183
+ states: { ...states, idle: states.idle, walk: states.walk === undefined || states.walk.trim().length === 0 ? states.idle : states.walk },
184
+ ...(setting.oneShots === undefined ? {} : { oneShots: setting.oneShots }),
185
+ });
186
+ return graph === undefined ? null : { graph, source: "locomotion" };
187
+ }
188
+ const resolved = resolveAnimationConfig("auto", clips);
189
+ const graph = resolved === undefined ? undefined : animGraphFromConfig(resolved);
190
+ return graph === undefined ? null : { graph, source: "auto" };
191
+ }
192
+ /** Stores `graph` on the placement, keeping its other fields; the stored graph wins at play time. */
193
+ export function storeAnimGraph(setting, graph) {
194
+ return { ...asConfig(setting), graph };
195
+ }
196
+ /** Removes a stored graph so `states`/`oneShots` (or clip roles) drive the placement again. */
197
+ export function clearAnimGraph(setting) {
198
+ if (setting === undefined || setting === "auto" || setting === "none")
199
+ return setting;
200
+ const { graph: _graph, ...rest } = setting;
201
+ return Object.keys(rest).length === 0 ? undefined : rest;
202
+ }
203
+ /**
204
+ * Sets one transition's crossfade seconds in `graph` and stores the result, so editing a derived
205
+ * graph turns it into an authored one.
206
+ */
207
+ export function setTransitionDuration(setting, graph, layerId, index, duration) {
208
+ const next = {
209
+ ...graph,
210
+ layers: graph.layers.map((layer) => layer.id !== layerId
211
+ ? layer
212
+ : {
213
+ ...layer,
214
+ transitions: layer.transitions.map((transition, i) => i === index ? { ...transition, duration: Math.max(0, Number.isFinite(duration) ? duration : 0) } : transition),
215
+ }),
216
+ };
217
+ return storeAnimGraph(setting, next);
218
+ }
package/dist/session.d.ts CHANGED
@@ -1,7 +1,11 @@
1
+ import type { EditorSimulation } from "@jgengine/core/editor/simulation";
2
+ import type { MaterialAsset, MaterialSelector } from "@jgengine/core/material/materialAsset";
3
+ import type { StaticPrefabBake } from "@jgengine/core/editor/staticPrefab";
1
4
  import { type DocumentLiveSync, type DocumentPatch, type EditorCatalogDefinition, type EditorCommand, type EditorDocument, type EditorGridPaletteEntry, type EditorKindVisibility, type EditorLayersInput, type EditorSession, type EditorSessionState, type RuntimeEntityState, type RuntimePlayControl } from "@jgengine/core/editor/index";
2
5
  import type { TerraformMode, TerrainMaterialLayer } from "@jgengine/core/world/terraform";
3
6
  import type { ParamSchema } from "@jgengine/core/scene/sceneKinds";
4
7
  import type { TerrainField } from "@jgengine/core/world/terrain";
8
+ import type { ModelMaterialSlotInfo } from "@jgengine/shell/render/materialAsset";
5
9
  /** How the editor hosts the game: frozen placement view, roamable world, the real game, or HUD-layout authoring. */
6
10
  export type EditorRunMode = "edit" | "walk" | "play" | "hud";
7
11
  /** The accepted run modes, in canonical order — the runtime guard behind {@link EditorRunMode}. @internal */
@@ -105,6 +109,7 @@ export type EditorBridgeRequest = {
105
109
  color?: string;
106
110
  label?: string;
107
111
  rotationY?: number;
112
+ catalogId?: string;
108
113
  meta?: Record<string, unknown>;
109
114
  } | {
110
115
  method: "set_marker";
@@ -113,6 +118,7 @@ export type EditorBridgeRequest = {
113
118
  color?: string;
114
119
  label?: string;
115
120
  rotationY?: number;
121
+ catalogId?: string;
116
122
  meta?: Record<string, unknown>;
117
123
  } | {
118
124
  method: "set_note";
@@ -150,6 +156,11 @@ export type EditorBridgeRequest = {
150
156
  height?: number;
151
157
  } | {
152
158
  method: "scene_summary";
159
+ } | {
160
+ method: "get_simulation";
161
+ } | {
162
+ method: "set_simulation";
163
+ simulation: EditorSimulation | null;
153
164
  } | {
154
165
  method: "export_document";
155
166
  } | {
@@ -292,6 +303,10 @@ export type EditorBridgeRequest = {
292
303
  } | {
293
304
  method: "delete_prefab";
294
305
  prefabId: string;
306
+ } | {
307
+ method: "set_prefab_static_bake";
308
+ prefabId: string;
309
+ bake: StaticPrefabBake | null;
295
310
  } | {
296
311
  method: "list_collections";
297
312
  } | {
@@ -338,6 +353,26 @@ export type EditorBridgeRequest = {
338
353
  color?: string;
339
354
  label?: string;
340
355
  meta?: Record<string, unknown>;
356
+ } | {
357
+ method: "list_material_assets";
358
+ } | {
359
+ method: "list_material_slots";
360
+ id: string;
361
+ } | {
362
+ method: "upsert_material_asset";
363
+ asset: MaterialAsset;
364
+ coalesce?: string;
365
+ } | {
366
+ method: "remove_material_asset";
367
+ id: string;
368
+ } | {
369
+ method: "assign_material_asset";
370
+ ids: string[];
371
+ materialId: string;
372
+ selector: MaterialSelector;
373
+ } | {
374
+ method: "clear_material_assets";
375
+ ids: string[];
341
376
  } | {
342
377
  method: "assign_material";
343
378
  ids: string[];
@@ -496,6 +531,25 @@ export interface EditorAssetInfo {
496
531
  kind: string;
497
532
  url?: string;
498
533
  }
534
+ /** Canonical imported slot names and mesh prerequisites reported by the loaded model. */
535
+ export type EditorMaterialSlotInfo = Readonly<Omit<ModelMaterialSlotInfo, "uvSets">> & {
536
+ readonly uvSets: readonly number[];
537
+ };
538
+ /** A live model-source observation; loading and unavailable reports never contain fabricated slots. */
539
+ export type EditorMaterialSlotReport = {
540
+ status: "ready";
541
+ slots: readonly EditorMaterialSlotInfo[];
542
+ } | {
543
+ status: "loading";
544
+ } | {
545
+ status: "unavailable";
546
+ reason: string;
547
+ };
548
+ /** A marker's current imported-model inventory, including an explicit availability state. */
549
+ export type EditorMaterialSlotInventory = EditorMaterialSlotReport & {
550
+ id: string;
551
+ sourceUrl?: string;
552
+ };
499
553
  /** Rolling frame-rate sample published by the in-canvas PerfProbe. */
500
554
  export interface EditorPerfSample {
501
555
  /**
@@ -573,6 +627,10 @@ export interface EditorHostApi {
573
627
  subscribeFocus(listener: (target: EditorFocusTarget | null) => void): () => void;
574
628
  getAssets(): readonly EditorAssetInfo[];
575
629
  setAssets(assets: readonly EditorAssetInfo[]): void;
630
+ /** Read observed slot data for the marker's current model URL; headless or unloaded sources are unavailable. */
631
+ getMaterialSlots(id: string): EditorMaterialSlotInventory;
632
+ /** Publish a loaded-model observation only while its marker still resolves to this source URL. */
633
+ reportMaterialSlots(id: string, sourceUrl: string, report: EditorMaterialSlotReport): boolean;
576
634
  getCatalogDefinitions(): readonly EditorCatalogDefinition[];
577
635
  getPerf(): EditorPerfSample | null;
578
636
  setPerf(sample: EditorPerfSample): void;
@@ -584,6 +642,8 @@ export interface EditorHostApi {
584
642
  getTerrainSampler(): TerrainField | null;
585
643
  setTerrainSampler(field: TerrainField | null): void;
586
644
  getMode(): EditorRunMode;
645
+ /** Immutable authored snapshot captured on entering an isolated playtest. */
646
+ getPlayDocument(): EditorDocument | null;
587
647
  setMode(mode: EditorRunMode): void;
588
648
  subscribeMode(listener: (mode: EditorRunMode) => void): () => void;
589
649
  /** Play-mode pause/step gate consumed by the runtime publisher. */
@@ -609,6 +669,10 @@ export declare function createEditorHost(options: {
609
669
  y: number;
610
670
  z: number;
611
671
  } | null) => void;
672
+ validateDocument?: (document: EditorDocument) => void;
673
+ isolatedPlaytest?: boolean;
674
+ publishGlobal?: boolean;
675
+ persistAssetUrls?: boolean;
612
676
  }): {
613
677
  session: EditorSession;
614
678
  api: EditorHostApi;
package/dist/session.js CHANGED
@@ -1,6 +1,7 @@
1
- import { createDocumentLiveSync, createEditorSession, createRuntimePlayControl, installDocumentLiveSync, listEditorKinds, normalizeEditorLayers, resolveCatalogDefinitions, seedEditorCatalogs, } from "@jgengine/core/editor/index";
1
+ import { createDocumentLiveSync, cloneEditorDocument, createEditorSession, createRuntimePlayControl, installDocumentLiveSync, listEditorKinds, normalizeEditorLayers, resolveCatalogDefinitions, seedEditorCatalogs, } from "@jgengine/core/editor/index";
2
2
  import { registerBuiltinSceneKinds } from "@jgengine/core/scene/builtinSceneKinds";
3
3
  import { EDITOR_RPC_HANDLERS } from "./handlers/index.js";
4
+ import { warmNavBake } from "./handlers/navmesh.js";
4
5
  import { emitEditorConsole } from "./shell/consoleSink.js";
5
6
  registerBuiltinSceneKinds();
6
7
  /** The accepted run modes, in canonical order — the runtime guard behind {@link EditorRunMode}. @internal */
@@ -22,11 +23,19 @@ export function getEditorHost() {
22
23
  }
23
24
  /** Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. @internal */
24
25
  export function createEditorHost(options) {
26
+ warmNavBake();
25
27
  const catalogDefinitions = options.catalogs ?? [];
26
28
  const document = seedEditorCatalogs(normalizeEditorLayers(options.layers), catalogDefinitions);
27
- const session = createEditorSession(document);
29
+ let mode = "edit";
30
+ const session = createEditorSession(document, 100, (candidate) => {
31
+ if (options.isolatedPlaytest && mode === "play")
32
+ throw new Error("Return to editor before changing the authored scene");
33
+ options.validateDocument?.(candidate);
34
+ });
28
35
  const liveSync = createDocumentLiveSync(document);
29
- const uninstallLiveSync = installDocumentLiveSync(liveSync);
36
+ let uninstallLiveSync = installDocumentLiveSync(liveSync);
37
+ let playDocument = null;
38
+ let playSync = null;
30
39
  let lastMirroredDocument = session.getState().document;
31
40
  const unsubscribeSessionMirror = session.subscribe((state) => {
32
41
  if (state.document === lastMirroredDocument)
@@ -37,9 +46,17 @@ export function createEditorHost(options) {
37
46
  let visibility = {};
38
47
  let focusTarget = null;
39
48
  let assets = [...(options.assets ?? [])];
49
+ const materialSlotReports = new Map();
50
+ const materialSource = (id) => {
51
+ const marker = session.getState().document.markers.find((item) => item.id === id);
52
+ if (!marker)
53
+ return { reason: `No authored model marker ${id}.` };
54
+ const catalogId = marker.catalogId ?? marker.meta?.catalogId;
55
+ const sourceUrl = assets.find((asset) => asset.id === catalogId)?.url;
56
+ return sourceUrl ? { sourceUrl } : { reason: `Marker ${id} has no URL-backed catalog model.` };
57
+ };
40
58
  let perf = null;
41
59
  let terrainSampler = null;
42
- let mode = "edit";
43
60
  let playControl = createRuntimePlayControl(false);
44
61
  const visibilityListeners = new Set();
45
62
  const focusListeners = new Set();
@@ -70,7 +87,7 @@ export function createEditorHost(options) {
70
87
  const api = {
71
88
  gameId: options.gameId,
72
89
  getSession: () => session,
73
- getLiveSync: () => liveSync,
90
+ getLiveSync: () => playSync ?? liveSync,
74
91
  getVisibility: () => visibility,
75
92
  setVisibility(next) {
76
93
  visibility = { ...next };
@@ -99,6 +116,31 @@ export function createEditorHost(options) {
99
116
  getAssets: () => assets,
100
117
  setAssets(next) {
101
118
  assets = [...next];
119
+ for (const [id, inventory] of materialSlotReports) {
120
+ if (materialSource(id).sourceUrl !== inventory.sourceUrl)
121
+ materialSlotReports.delete(id);
122
+ }
123
+ },
124
+ getMaterialSlots(id) {
125
+ const source = materialSource(id);
126
+ const inventory = materialSlotReports.get(id);
127
+ if (source.sourceUrl === undefined || inventory?.sourceUrl !== source.sourceUrl) {
128
+ materialSlotReports.delete(id);
129
+ return { id, ...source, status: "unavailable", reason: source.reason ?? "This model has no loaded slot inventory. Select it and open the Materials workspace." };
130
+ }
131
+ return structuredClone(inventory);
132
+ },
133
+ reportMaterialSlots(id, sourceUrl, report) {
134
+ if (materialSource(id).sourceUrl !== sourceUrl)
135
+ return false;
136
+ const previous = materialSlotReports.get(id);
137
+ if (previous?.sourceUrl === sourceUrl && previous.status !== "loading" && report.status === "loading")
138
+ return true;
139
+ materialSlotReports.delete(id);
140
+ materialSlotReports.set(id, structuredClone({ ...report, id, sourceUrl }));
141
+ if (materialSlotReports.size > 64)
142
+ materialSlotReports.delete(materialSlotReports.keys().next().value);
143
+ return true;
102
144
  },
103
145
  getCatalogDefinitions: () => resolveCatalogDefinitions(session.getState().document, catalogDefinitions),
104
146
  getPerf: () => perf,
@@ -110,9 +152,16 @@ export function createEditorHost(options) {
110
152
  terrainSampler = field;
111
153
  },
112
154
  getMode: () => mode,
155
+ getPlayDocument: () => playDocument === null ? null : cloneEditorDocument(playDocument),
113
156
  setMode(next) {
114
157
  if (next === mode)
115
158
  return;
159
+ if (options.isolatedPlaytest) {
160
+ uninstallLiveSync();
161
+ playDocument = next === "play" ? cloneEditorDocument(session.getState().document) : null;
162
+ playSync = playDocument === null ? null : createDocumentLiveSync(playDocument);
163
+ uninstallLiveSync = installDocumentLiveSync(playSync ?? liveSync);
164
+ }
116
165
  mode = next;
117
166
  if (next === "play") {
118
167
  playControl = createRuntimePlayControl(false);
@@ -155,7 +204,8 @@ export function createEditorHost(options) {
155
204
  const ctx = {
156
205
  api,
157
206
  session,
158
- liveSync,
207
+ liveSync: playSync ?? liveSync,
208
+ persistAssetUrls: options.persistAssetUrls,
159
209
  gameId: options.gameId,
160
210
  catalogDefinitions: defs,
161
211
  catalogById: new Map(defs.map((definition) => [definition.id, definition])),
@@ -179,8 +229,9 @@ export function createEditorHost(options) {
179
229
  }
180
230
  },
181
231
  };
182
- const uninstallHost = installEditorHost(api);
232
+ const uninstallHost = options.publishGlobal === false ? () => { } : installEditorHost(api);
183
233
  const dispose = () => {
234
+ materialSlotReports.clear();
184
235
  unsubscribeSessionMirror();
185
236
  uninstallLiveSync();
186
237
  uninstallHost();
@@ -0,0 +1,16 @@
1
+ import type { EditorSession } from "@jgengine/core/editor/index";
2
+ import type { EditorAssetEntry } from "../AssetBrowser.js";
3
+ import type { EditorUiStore } from "../uiStore.js";
4
+ /**
5
+ * Graph mode of the Animation dock: the selected placement's animation graph (layers, states,
6
+ * transitions), a scrubbed preview driven by the headless graph runtime and shown on the rig in the
7
+ * viewport, trigger buttons that record presses on the timeline, and crossfade editing that stores
8
+ * the graph on the placement as an undoable edit.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare function AnimationGraphPanel({ session, ui, rigged, }: {
13
+ session: EditorSession;
14
+ ui: EditorUiStore;
15
+ rigged: readonly EditorAssetEntry[];
16
+ }): import("react").JSX.Element;