@jgengine/editor 0.11.0 → 0.13.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 (171) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +12 -0
  3. package/dist/AssetBrowser.d.ts +33 -1
  4. package/dist/AssetBrowser.js +64 -2
  5. package/dist/AssetDropZone.d.ts +9 -0
  6. package/dist/AssetDropZone.js +65 -0
  7. package/dist/CatalogsPanel.d.ts +4 -2
  8. package/dist/CatalogsPanel.js +162 -9
  9. package/dist/CityPreview.d.ts +11 -0
  10. package/dist/CityPreview.js +45 -0
  11. package/dist/CollectionsPanel.js +5 -5
  12. package/dist/DebugDraw.d.ts +3 -1
  13. package/dist/DebugDraw.js +37 -19
  14. package/dist/EditorApp.d.ts +15 -1
  15. package/dist/EditorApp.js +147 -35
  16. package/dist/EditorChrome.d.ts +27 -6
  17. package/dist/EditorChrome.js +314 -130
  18. package/dist/InspectorPanel.d.ts +17 -7
  19. package/dist/InspectorPanel.js +173 -27
  20. package/dist/LightingPanel.d.ts +11 -0
  21. package/dist/LightingPanel.js +99 -0
  22. package/dist/MaterialsWorkspacePanel.d.ts +12 -0
  23. package/dist/MaterialsWorkspacePanel.js +86 -0
  24. package/dist/ParentPickerMenu.d.ts +13 -0
  25. package/dist/ParentPickerMenu.js +33 -0
  26. package/dist/PerfProbe.js +33 -0
  27. package/dist/PrefabsPanel.js +5 -5
  28. package/dist/RuntimePlayBridge.d.ts +5 -3
  29. package/dist/RuntimePlayBridge.js +6 -13
  30. package/dist/ScatterPreview.d.ts +3 -1
  31. package/dist/ScatterPreview.js +10 -7
  32. package/dist/SchemaInspector.js +21 -8
  33. package/dist/ScriptingPanel.d.ts +15 -0
  34. package/dist/ScriptingPanel.js +46 -0
  35. package/dist/SelectionGizmo.js +114 -30
  36. package/dist/StandaloneEditor.d.ts +16 -9
  37. package/dist/StandaloneEditor.js +19 -20
  38. package/dist/TerrainPanel.d.ts +3 -2
  39. package/dist/TerrainPanel.js +16 -5
  40. package/dist/TerrainSculpt.js +98 -59
  41. package/dist/agent/AgentPanel.d.ts +5 -2
  42. package/dist/agent/AgentPanel.js +11 -6
  43. package/dist/agent/toolBridge.js +5 -0
  44. package/dist/assetImport.d.ts +36 -0
  45. package/dist/assetImport.js +66 -0
  46. package/dist/authoredComponentMeta.d.ts +25 -0
  47. package/dist/authoredComponentMeta.js +65 -0
  48. package/dist/gizmoPivot.d.ts +4 -0
  49. package/dist/gizmoPivot.js +34 -0
  50. package/dist/handlers/catalogs.d.ts +3 -0
  51. package/dist/handlers/catalogs.js +137 -0
  52. package/dist/handlers/collections.d.ts +3 -0
  53. package/dist/handlers/collections.js +57 -0
  54. package/dist/handlers/context.d.ts +36 -0
  55. package/dist/handlers/context.js +1 -0
  56. package/dist/handlers/document.d.ts +3 -0
  57. package/dist/handlers/document.js +350 -0
  58. package/dist/handlers/grids.d.ts +3 -0
  59. package/dist/handlers/grids.js +142 -0
  60. package/dist/handlers/hierarchy.d.ts +3 -0
  61. package/dist/handlers/hierarchy.js +55 -0
  62. package/dist/handlers/index.d.ts +8 -0
  63. package/dist/handlers/index.js +25 -0
  64. package/dist/handlers/minimap.d.ts +9 -0
  65. package/dist/handlers/minimap.js +32 -0
  66. package/dist/handlers/runtime.d.ts +3 -0
  67. package/dist/handlers/runtime.js +191 -0
  68. package/dist/handlers/streets.d.ts +3 -0
  69. package/dist/handlers/streets.js +159 -0
  70. package/dist/handlers/terrain.d.ts +3 -0
  71. package/dist/handlers/terrain.js +204 -0
  72. package/dist/index.d.ts +3 -2
  73. package/dist/index.js +3 -2
  74. package/dist/listDocumentTriggers.d.ts +42 -0
  75. package/dist/listDocumentTriggers.js +49 -0
  76. package/dist/materialAssignments.d.ts +63 -0
  77. package/dist/materialAssignments.js +84 -0
  78. package/dist/mcp/fieldSpec.d.ts +32 -0
  79. package/dist/mcp/fieldSpec.js +174 -0
  80. package/dist/mcp/rpcRequest.d.ts +8 -5
  81. package/dist/mcp/rpcRequest.js +72 -93
  82. package/dist/mcp/tools.js +105 -912
  83. package/dist/networkSnapshot.d.ts +82 -0
  84. package/dist/networkSnapshot.js +73 -0
  85. package/dist/parentCandidates.d.ts +12 -0
  86. package/dist/parentCandidates.js +25 -0
  87. package/dist/session.d.ts +91 -4
  88. package/dist/session.js +36 -1017
  89. package/dist/shell/AnimationPanel.d.ts +17 -0
  90. package/dist/shell/AnimationPanel.js +134 -0
  91. package/dist/shell/AssetThumbnail.d.ts +12 -0
  92. package/dist/shell/AssetThumbnail.js +23 -0
  93. package/dist/shell/BottomDock.d.ts +25 -0
  94. package/dist/shell/BottomDock.js +40 -0
  95. package/dist/shell/CommandPalette.d.ts +12 -0
  96. package/dist/shell/CommandPalette.js +56 -0
  97. package/dist/shell/ConsolePanel.d.ts +8 -0
  98. package/dist/shell/ConsolePanel.js +45 -0
  99. package/dist/shell/ContentBrowser.d.ts +21 -0
  100. package/dist/shell/ContentBrowser.js +130 -0
  101. package/dist/shell/HierarchyPanel.d.ts +26 -0
  102. package/dist/shell/HierarchyPanel.js +387 -0
  103. package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
  104. package/dist/shell/NetworkWorkspacePanel.js +49 -0
  105. package/dist/shell/PlayModeBar.d.ts +12 -0
  106. package/dist/shell/PlayModeBar.js +27 -0
  107. package/dist/shell/ProfilerPanel.d.ts +10 -0
  108. package/dist/shell/ProfilerPanel.js +41 -0
  109. package/dist/shell/SceneToolbar.d.ts +43 -0
  110. package/dist/shell/SceneToolbar.js +122 -0
  111. package/dist/shell/StatusBar.d.ts +18 -0
  112. package/dist/shell/StatusBar.js +41 -0
  113. package/dist/shell/TopAppBar.d.ts +35 -0
  114. package/dist/shell/TopAppBar.js +64 -0
  115. package/dist/shell/ViewportOverlays.d.ts +23 -0
  116. package/dist/shell/ViewportOverlays.js +136 -0
  117. package/dist/shell/WorkspaceRail.d.ts +20 -0
  118. package/dist/shell/WorkspaceRail.js +31 -0
  119. package/dist/shell/cameraTelemetry.d.ts +27 -0
  120. package/dist/shell/cameraTelemetry.js +24 -0
  121. package/dist/shell/commandRegistry.d.ts +58 -0
  122. package/dist/shell/commandRegistry.js +133 -0
  123. package/dist/shell/consoleSink.d.ts +11 -0
  124. package/dist/shell/consoleSink.js +17 -0
  125. package/dist/shell/consoleStore.d.ts +27 -0
  126. package/dist/shell/consoleStore.js +43 -0
  127. package/dist/shell/fields.d.ts +39 -0
  128. package/dist/shell/fields.js +104 -0
  129. package/dist/shell/formatSavedRelative.d.ts +2 -0
  130. package/dist/shell/formatSavedRelative.js +14 -0
  131. package/dist/shell/glbThumbnailCache.d.ts +45 -0
  132. package/dist/shell/glbThumbnailCache.js +241 -0
  133. package/dist/shell/hierarchyA11y.d.ts +21 -0
  134. package/dist/shell/hierarchyA11y.js +35 -0
  135. package/dist/shell/icons.d.ts +13 -0
  136. package/dist/shell/icons.js +85 -0
  137. package/dist/shell/layoutStore.d.ts +68 -0
  138. package/dist/shell/layoutStore.js +156 -0
  139. package/dist/shell/pathFlythrough.d.ts +37 -0
  140. package/dist/shell/pathFlythrough.js +54 -0
  141. package/dist/shell/perfHistory.d.ts +70 -0
  142. package/dist/shell/perfHistory.js +130 -0
  143. package/dist/shell/renameObject.d.ts +3 -0
  144. package/dist/shell/renameObject.js +25 -0
  145. package/dist/shell/theme.d.ts +36 -0
  146. package/dist/shell/theme.js +37 -0
  147. package/dist/shell/ui.d.ts +84 -0
  148. package/dist/shell/ui.js +90 -0
  149. package/dist/shell/useGlbThumbnail.d.ts +8 -0
  150. package/dist/shell/useGlbThumbnail.js +19 -0
  151. package/dist/uiStore.d.ts +27 -0
  152. package/dist/uiStore.js +10 -0
  153. package/dist/viewportContextMenu.d.ts +4 -2
  154. package/dist/viewportContextMenu.js +5 -1
  155. package/package.json +4 -4
  156. package/skills/jgengine/SKILL.md +8 -4
  157. package/skills/jgengine/api.md +182 -81
  158. package/skills/jgengine/capabilities.md +104 -2
  159. package/skills/jgengine/recipes/minimal-game.md +88 -0
  160. package/skills/jgengine-editor/SKILL.md +18 -3
  161. package/skills/jgengine-editor/api.md +403 -20
  162. package/skills/jgengine-editor/capabilities.md +46 -0
  163. package/skills/jgengine-editor/reference.md +87 -0
  164. package/skills/level-design/SKILL.md +54 -0
  165. package/skills/level-design/agents/openai.yaml +4 -0
  166. package/skills/level-design/references/application-playbook.md +192 -0
  167. package/skills/level-design/references/field-guide.md +337 -0
  168. package/skills/level-design/references/live-editor-loop.md +179 -0
  169. package/dist/OutlinerPanel.d.ts +0 -14
  170. package/dist/OutlinerPanel.js +0 -84
  171. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -4,10 +4,12 @@ export declare function PathDraftPreview({ points }: {
4
4
  points: readonly EditorVec3[];
5
5
  }): import("react").JSX.Element;
6
6
  /** Renders every visible marker, volume, path, and note from a document as in-scene 3D gizmos. */
7
- export declare function EditorLayerOverlays({ document, visibility, selection, onSelect, activePathPoint, groundHeightAt, }: {
7
+ export declare function EditorLayerOverlays({ document, visibility, selection, hoverId, onSelect, activePathPoint, groundHeightAt, }: {
8
8
  document: EditorDocument;
9
9
  visibility: EditorKindVisibility;
10
10
  selection: readonly string[];
11
+ /** Pre-selection hover id from viewport pointer pick; ignored when already selected. */
12
+ hoverId?: string | null;
11
13
  onSelect: (id: string) => void;
12
14
  activePathPoint?: {
13
15
  pathId: string;
package/dist/DebugDraw.js CHANGED
@@ -87,25 +87,38 @@ function volumeWireframe(volume) {
87
87
  pushRing(out, radius, 0, "z");
88
88
  return new Float32Array(out);
89
89
  }
90
- const MarkerMesh = memo(function MarkerMesh({ marker, selected, onSelect, sharedSphere, sharedCone, groundHeightAt, }) {
90
+ /** Shell-cyan selection fill — replaces the old white debug tint. */
91
+ const SELECTION_COLOR = "#67e8f9";
92
+ /** Pre-selection hover fill (softer cyan, not selected). */
93
+ const HOVER_COLOR = "#22d3ee";
94
+ function emphasisColor(selected, hovered, base) {
95
+ if (selected)
96
+ return SELECTION_COLOR;
97
+ if (hovered)
98
+ return HOVER_COLOR;
99
+ return base;
100
+ }
101
+ const MarkerMesh = memo(function MarkerMesh({ marker, selected, hovered, onSelect, sharedSphere, sharedCone, groundHeightAt, }) {
91
102
  const color = colorFor(marker.kind, marker.color);
92
- const scale = selected ? 1.25 : 1;
103
+ const scale = selected ? 1.28 : hovered ? 1.12 : 1;
93
104
  const baseY = groundHeightAt !== undefined ? groundHeightAt(marker.position.x, marker.position.z) : marker.position.y;
105
+ const fill = emphasisColor(selected, hovered, color);
94
106
  return (_jsxs("group", { position: [marker.position.x, baseY + 1.2, marker.position.z], "rotation-y": marker.rotationY ?? 0, scale: scale, userData: { jgEditorId: marker.id }, onClick: (event) => {
95
107
  event.stopPropagation();
96
108
  onSelect(marker.id);
97
- }, children: [_jsx("mesh", { geometry: sharedSphere, children: _jsx("meshBasicMaterial", { color: selected ? "#ffffff" : color }) }), _jsx("mesh", { position: [0, 1.35, 0], geometry: sharedCone, children: _jsx("meshBasicMaterial", { color: color }) })] }));
109
+ }, children: [_jsx("mesh", { geometry: sharedSphere, children: _jsx("meshBasicMaterial", { color: fill, transparent: true, opacity: selected ? 1 : hovered ? 0.9 : 1 }) }), selected || hovered ? (_jsx("mesh", { geometry: sharedSphere, scale: 1.18, children: _jsx("meshBasicMaterial", { color: fill, transparent: true, opacity: selected ? 0.28 : 0.18, depthWrite: false }) })) : null, _jsx("mesh", { position: [0, 1.35, 0], geometry: sharedCone, children: _jsx("meshBasicMaterial", { color: selected || hovered ? fill : color }) })] }));
98
110
  });
99
111
  /** Shape-true wireframe — ground ring plus vertical profile for spheres, cylinders, and boxes. */
100
- const VolumeShapeLines = memo(function VolumeShapeLines({ volume, selected, onSelect, }) {
112
+ const VolumeShapeLines = memo(function VolumeShapeLines({ volume, selected, hovered, onSelect, }) {
101
113
  const color = colorFor(volume.kind, volume.color);
102
114
  const object = useMemo(() => {
103
115
  const geometry = new THREE.BufferGeometry();
104
116
  geometry.setAttribute("position", new THREE.BufferAttribute(volumeWireframe(volume), 3));
105
117
  const material = new THREE.LineBasicMaterial({
106
- color: selected ? "#ffffff" : color,
118
+ color: emphasisColor(selected, hovered, color),
107
119
  transparent: true,
108
- opacity: selected ? 1 : 0.75,
120
+ opacity: selected ? 1 : hovered ? 0.9 : 0.75,
121
+ linewidth: selected || hovered ? 2 : 1,
109
122
  });
110
123
  return new THREE.LineSegments(geometry, material);
111
124
  }, [
@@ -117,6 +130,7 @@ const VolumeShapeLines = memo(function VolumeShapeLines({ volume, selected, onSe
117
130
  volume.halfExtents?.z,
118
131
  color,
119
132
  selected,
133
+ hovered,
120
134
  ]);
121
135
  useEffect(() => () => {
122
136
  object.geometry.dispose();
@@ -127,13 +141,13 @@ const VolumeShapeLines = memo(function VolumeShapeLines({ volume, selected, onSe
127
141
  onSelect(volume.id);
128
142
  }, children: _jsx("primitive", { object: object }) }));
129
143
  });
130
- const PathRibbon = memo(function PathRibbon({ path, selected, activePointIndex, onSelect, groundHeightAt, }) {
144
+ const PathRibbon = memo(function PathRibbon({ path, selected, hovered, activePointIndex, onSelect, groundHeightAt, }) {
131
145
  const color = colorFor(path.kind, path.color);
132
146
  const yAt = (point) => groundHeightAt !== undefined ? groundHeightAt(point.x, point.z) : point.y;
133
147
  const object = useMemo(() => {
134
148
  if (path.points.length < 2)
135
149
  return null;
136
- const stride = !selected && path.points.length > 80 ? 2 : 1;
150
+ const stride = !selected && !hovered && path.points.length > 80 ? 2 : 1;
137
151
  const picked = [];
138
152
  for (let i = 0; i < path.points.length; i += stride) {
139
153
  picked.push(path.points[i]);
@@ -151,12 +165,12 @@ const PathRibbon = memo(function PathRibbon({ path, selected, activePointIndex,
151
165
  const geometry = new THREE.BufferGeometry();
152
166
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
153
167
  const material = new THREE.LineBasicMaterial({
154
- color: selected ? "#ffffff" : color,
168
+ color: emphasisColor(selected, hovered, color),
155
169
  transparent: true,
156
- opacity: selected ? 1 : 0.85,
170
+ opacity: selected ? 1 : hovered ? 0.92 : 0.85,
157
171
  });
158
172
  return new THREE.Line(geometry, material);
159
- }, [path.points, color, selected, groundHeightAt]);
173
+ }, [path.points, color, selected, hovered, groundHeightAt]);
160
174
  useEffect(() => () => {
161
175
  if (object === null)
162
176
  return;
@@ -172,13 +186,14 @@ const PathRibbon = memo(function PathRibbon({ path, selected, activePointIndex,
172
186
  ? path.points.map((point, index) => (_jsxs("mesh", { position: [point.x, yAt(point) + 0.8, point.z], children: [_jsx("sphereGeometry", { args: [index === activePointIndex ? 0.7 : 0.45, 8, 8] }), _jsx("meshBasicMaterial", { color: index === activePointIndex ? "#22d3ee" : "#ffffff" })] }, index)))
173
187
  : null] }));
174
188
  });
175
- const NotePin = memo(function NotePin({ note, selected, onSelect, groundHeightAt, }) {
189
+ const NotePin = memo(function NotePin({ note, selected, hovered, onSelect, groundHeightAt, }) {
176
190
  const color = colorFor("note", note.color);
177
191
  const baseY = groundHeightAt !== undefined ? groundHeightAt(note.position.x, note.position.z) : note.position.y;
192
+ const fill = emphasisColor(selected, hovered, color);
178
193
  return (_jsxs("group", { position: [note.position.x, baseY + 1, note.position.z], userData: { jgEditorId: note.id }, onClick: (event) => {
179
194
  event.stopPropagation();
180
195
  onSelect(note.id);
181
- }, children: [_jsxs("mesh", { scale: selected ? 1.3 : 1, children: [_jsx("octahedronGeometry", { args: [0.6] }), _jsx("meshBasicMaterial", { color: selected ? "#ffffff" : color })] }), _jsx(Html, { center: true, distanceFactor: 40, style: { pointerEvents: "none" }, children: _jsx("div", { style: {
196
+ }, children: [_jsxs("mesh", { scale: selected ? 1.32 : hovered ? 1.15 : 1, children: [_jsx("octahedronGeometry", { args: [0.6] }), _jsx("meshBasicMaterial", { color: fill, transparent: true, opacity: selected ? 1 : hovered ? 0.9 : 1 })] }), selected || hovered ? (_jsxs("mesh", { scale: 1.22, children: [_jsx("octahedronGeometry", { args: [0.6] }), _jsx("meshBasicMaterial", { color: fill, transparent: true, opacity: selected ? 0.28 : 0.16, depthWrite: false })] })) : null, _jsx(Html, { center: true, distanceFactor: 40, style: { pointerEvents: "none" }, children: _jsx("div", { style: {
182
197
  transform: "translateY(-24px)",
183
198
  whiteSpace: "nowrap",
184
199
  maxWidth: "220px",
@@ -216,8 +231,9 @@ export function PathDraftPreview({ points }) {
216
231
  return (_jsxs("group", { children: [object !== null ? _jsx("primitive", { object: object }) : null, points.map((point, index) => (_jsxs("mesh", { position: [point.x, point.y + 0.8, point.z], children: [_jsx("sphereGeometry", { args: [0.5, 8, 8] }), _jsx("meshBasicMaterial", { color: "#22d3ee" })] }, index)))] }));
217
232
  }
218
233
  /** Renders every visible marker, volume, path, and note from a document as in-scene 3D gizmos. */
219
- export function EditorLayerOverlays({ document, visibility, selection, onSelect, activePathPoint, groundHeightAt, }) {
234
+ export function EditorLayerOverlays({ document, visibility, selection, hoverId = null, onSelect, activePathPoint, groundHeightAt, }) {
220
235
  const selected = useMemo(() => new Set(selection), [selection]);
236
+ const isHovered = (id) => hoverId === id && !selected.has(id);
221
237
  const sharedSphere = useMemo(() => new THREE.SphereGeometry(0.85, MARKER_SEGMENTS, MARKER_SEGMENTS), []);
222
238
  const sharedCone = useMemo(() => new THREE.ConeGeometry(0.35, 0.9, 6), []);
223
239
  useEffect(() => () => {
@@ -225,9 +241,11 @@ export function EditorLayerOverlays({ document, visibility, selection, onSelect,
225
241
  sharedCone.dispose();
226
242
  }, [sharedSphere, sharedCone]);
227
243
  const visibilityKey = useMemo(() => JSON.stringify(visibility), [visibility]);
228
- const volumes = useMemo(() => document.volumes.filter((volume) => isVisible(visibility, volume.kind)), [document.volumes, visibilityKey, visibility]);
229
- const paths = useMemo(() => document.paths.filter((path) => isVisible(visibility, path.kind)), [document.paths, visibilityKey, visibility]);
230
- const markers = useMemo(() => document.markers.filter((marker) => isVisible(visibility, marker.kind)), [document.markers, visibilityKey, visibility]);
231
- const notes = useMemo(() => (isVisible(visibility, "note") ? document.annotations : []), [document.annotations, visibilityKey, visibility]);
232
- return (_jsxs("group", { children: [volumes.map((volume) => (_jsx(VolumeShapeLines, { volume: volume, selected: selected.has(volume.id), onSelect: onSelect }, volume.id))), paths.map((path) => (_jsx(PathRibbon, { path: path, selected: selected.has(path.id), activePointIndex: activePathPoint != null && activePathPoint.pathId === path.id ? activePathPoint.index : null, onSelect: onSelect, groundHeightAt: groundHeightAt }, path.id))), markers.map((marker) => (_jsx(MarkerMesh, { marker: marker, selected: selected.has(marker.id), onSelect: onSelect, sharedSphere: sharedSphere, sharedCone: sharedCone, groundHeightAt: groundHeightAt }, marker.id))), notes.map((note) => (_jsx(NotePin, { note: note, selected: selected.has(note.id), onSelect: onSelect, groundHeightAt: groundHeightAt }, note.id)))] }));
244
+ const volumes = useMemo(() => document.volumes.filter((volume) => isVisible(visibility, volume.kind) && volume.hidden !== true), [document.volumes, visibilityKey, visibility]);
245
+ const paths = useMemo(() => document.paths.filter((path) => isVisible(visibility, path.kind) && path.hidden !== true), [document.paths, visibilityKey, visibility]);
246
+ const markers = useMemo(() => document.markers.filter((marker) => isVisible(visibility, marker.kind) && marker.hidden !== true), [document.markers, visibilityKey, visibility]);
247
+ const notes = useMemo(() => isVisible(visibility, "note")
248
+ ? document.annotations.filter((note) => note.hidden !== true)
249
+ : [], [document.annotations, visibilityKey, visibility]);
250
+ return (_jsxs("group", { children: [volumes.map((volume) => (_jsx(VolumeShapeLines, { volume: volume, selected: selected.has(volume.id), hovered: isHovered(volume.id), onSelect: onSelect }, volume.id))), paths.map((path) => (_jsx(PathRibbon, { path: path, selected: selected.has(path.id), hovered: isHovered(path.id), activePointIndex: activePathPoint != null && activePathPoint.pathId === path.id ? activePathPoint.index : null, onSelect: onSelect, groundHeightAt: groundHeightAt }, path.id))), markers.map((marker) => (_jsx(MarkerMesh, { marker: marker, selected: selected.has(marker.id), hovered: isHovered(marker.id), onSelect: onSelect, sharedSphere: sharedSphere, sharedCone: sharedCone, groundHeightAt: groundHeightAt }, marker.id))), notes.map((note) => (_jsx(NotePin, { note: note, selected: selected.has(note.id), hovered: isHovered(note.id), onSelect: onSelect, groundHeightAt: groundHeightAt }, note.id)))] }));
233
251
  }
@@ -1,11 +1,13 @@
1
1
  import { type ComponentType } from "react";
2
2
  import type { EditorCatalogDefinition, EditorLayersInput } from "@jgengine/core/editor/index";
3
3
  import type { PlayableGame } from "@jgengine/shell/registry";
4
+ import { type EditorNetworkPresenceInput } from "./networkSnapshot.js";
4
5
  import { type EditorRunMode } from "./session.js";
5
6
  /** Props for mounting the scene editor over a playable game. */
6
7
  export interface EditorAppProps {
7
8
  gameId: string;
8
9
  playable: PlayableGame;
10
+ /** Layers to open. Defaults to `playable.editorLayers`, so a game shipping its document via `defineGame({ editorLayers })` needs no extra wiring. */
9
11
  layers?: EditorLayersInput;
10
12
  /** Game-exported gameplay catalog definitions (schemas + defaults) for the Data panel / catalog RPC. */
11
13
  catalogs?: readonly EditorCatalogDefinition[];
@@ -15,6 +17,18 @@ export interface EditorAppProps {
15
17
  mode: EditorRunMode;
16
18
  onExit: () => void;
17
19
  }> | null;
20
+ /**
21
+ * Optional live multiplayer presence/session feed for the Network workspace.
22
+ * Adapter config always comes from `playable.game.multiplayer`; this only supplies real
23
+ * session identity and presence rows. Omit when the host has nothing live — the panel stays honest.
24
+ */
25
+ networkPresence?: EditorNetworkPresenceInput;
26
+ /**
27
+ * Called to close the editor and return to the game. Passed by `GameHost` when the editor was
28
+ * summoned over a running game; when set, the chrome shows an "Exit to game" control. Omit for the
29
+ * standalone editor, which has no game to return to.
30
+ */
31
+ onExitEditor?: () => void;
18
32
  }
19
33
  /** Persists an exported document JSON; resolves with where it landed or why it failed. */
20
34
  export type EditorSaveFn = (json: string) => Promise<{
@@ -23,4 +37,4 @@ export type EditorSaveFn = (json: string) => Promise<{
23
37
  error?: string;
24
38
  }>;
25
39
  /** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
26
- export declare function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }: EditorAppProps): import("react").JSX.Element;
40
+ export declare function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor }: EditorAppProps): import("react").JSX.Element;
package/dist/EditorApp.js CHANGED
@@ -2,23 +2,28 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { editorDocumentBounds, findEditorMarker } from "@jgengine/core/editor/index";
4
4
  import { getSaveEndpoint } from "@jgengine/core/devtools/saveEndpoint";
5
+ import { listActiveHudLayouts, subscribeActiveHudLayouts } from "@jgengine/core/ui/hudLayout";
6
+ import { HudLayoutPersistProvider } from "@jgengine/react/hudLayoutPersist";
5
7
  import { useGameContext } from "@jgengine/react/provider";
6
8
  import { GamePlayerShell } from "@jgengine/shell/GamePlayerShell";
7
9
  import { assetsFromCatalog } from "./AssetBrowser.js";
8
10
  import { EditorCameraDriver } from "./EditorCameraDriver.js";
9
11
  import { EditorChrome } from "./EditorChrome.js";
10
12
  import { EditorLayerOverlays, PathDraftPreview } from "./DebugDraw.js";
13
+ import { AssetDropZone } from "./AssetDropZone.js";
11
14
  import { MaterialDropZone } from "./MaterialDropZone.js";
12
15
  import { PerfProbe } from "./PerfProbe.js";
13
16
  import { ScatterPreview } from "./ScatterPreview.js";
17
+ import { CityPreview } from "./CityPreview.js";
14
18
  import { SelectionGizmo, ViewportSelect } from "./SelectionGizmo.js";
15
19
  import { TerrainReadout } from "./TerrainReadout.js";
16
20
  import { TerrainReadoutHud } from "./TerrainReadoutHud.js";
17
21
  import { TerrainSculpt } from "./TerrainSculpt.js";
18
22
  import { createTerrainReadoutStore } from "./terrainReadoutStore.js";
19
23
  import { RuntimePlayInspectorChrome, RuntimePlayPublisher } from "./RuntimePlayBridge.js";
24
+ import { buildEditorNetworkSnapshot, } from "./networkSnapshot.js";
20
25
  import { createEditorHost } from "./session.js";
21
- import { createEditorUiStore } from "./uiStore.js";
26
+ import { createEditorUiStore, } from "./uiStore.js";
22
27
  import { useF2Chord } from "./useF2Chord.js";
23
28
  import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
24
29
  function endpointSaver(gameId) {
@@ -120,7 +125,14 @@ function EditorWorldOverlay({ api, ui, world, readout, }) {
120
125
  const showElevation = useStoreSelector(ui, (s) => s.showElevation);
121
126
  const pathPoint = useStoreSelector(ui, (s) => s.pathPoint);
122
127
  const pathDraft = useStoreSelector(ui, (s) => s.pathDraft);
128
+ const hoverId = useStoreSelector(ui, (s) => s.hoverId);
123
129
  const groundHeightAt = useCallback((x, z) => ctx.world.groundHeightAt(x, z), [ctx.world]);
130
+ // Publish the live composed ground field so the `bake_minimap` RPC can rasterize authored terrain.
131
+ // Only available while the viewport is mounted; the cleanup nulls it so headless callers get null.
132
+ useEffect(() => {
133
+ api.setTerrainSampler(ctx.world.ground);
134
+ return () => api.setTerrainSampler(null);
135
+ }, [api, ctx.world]);
124
136
  const readoutRegion = useMemo(() => readoutRegionFor(world), [world]);
125
137
  const readoutActive = showContours || showSurfaceGrid || showElevation;
126
138
  // Debounced terrain version: rebuilds the overlay after sculpt edits settle, not every stroke frame.
@@ -141,7 +153,7 @@ function EditorWorldOverlay({ api, ui, world, readout, }) {
141
153
  unsubscribe();
142
154
  };
143
155
  }, [session, readoutActive]);
144
- return (_jsxs(_Fragment, { children: [_jsx(PerfProbe, { api: api }), _jsx(EditorCameraDriver, { api: api }), _jsx(ViewportSelect, { api: api, ui: ui }), _jsx(MaterialDropZone, { api: api }), _jsx(TerrainSculpt, { api: api, ui: ui, world: world }), _jsx(ScatterPreview, { api: api }), showGrid ? _jsx("gridHelper", { args: [400, 80, "#3b4252", "#20242e"], position: [0, 0.05, 0] }) : null, readoutActive ? (_jsx(TerrainReadout, { groundHeightAt: groundHeightAt, region: readoutRegion, showContours: showContours, showSurfaceGrid: showSurfaceGrid, version: terrainVersion, readout: readout })) : null, _jsx(EditorLayerOverlays, { document: document, visibility: visibility, selection: selection, onSelect: (id) => session.dispatch({ type: "select", ids: [id] }), activePathPoint: pathPoint, groundHeightAt: groundHeightAt }), pathDraft.length > 0 ? _jsx(PathDraftPreview, { points: pathDraft }) : null, _jsx(SelectionGizmo, { session: session, ui: ui, groundSnap: groundHeightAt })] }));
156
+ return (_jsxs(_Fragment, { children: [_jsx(PerfProbe, { api: api }), _jsx(EditorCameraDriver, { api: api }), _jsx(ViewportSelect, { api: api, ui: ui }), _jsx(MaterialDropZone, { api: api }), _jsx(AssetDropZone, { api: api }), _jsx(TerrainSculpt, { api: api, ui: ui, world: world }), _jsx(ScatterPreview, { api: api }), _jsx(CityPreview, { api: api }), showGrid ? _jsx("gridHelper", { args: [400, 80, "#3b4252", "#20242e"], position: [0, 0.05, 0] }) : null, readoutActive ? (_jsx(TerrainReadout, { groundHeightAt: groundHeightAt, region: readoutRegion, showContours: showContours, showSurfaceGrid: showSurfaceGrid, version: terrainVersion, readout: readout })) : null, _jsx(EditorLayerOverlays, { document: document, visibility: visibility, selection: selection, hoverId: hoverId, onSelect: (id) => session.dispatch({ type: "select", ids: [id] }), activePathPoint: pathPoint, groundHeightAt: groundHeightAt }), pathDraft.length > 0 ? _jsx(PathDraftPreview, { points: pathDraft }) : null, _jsx(SelectionGizmo, { session: session, ui: ui, groundSnap: groundHeightAt })] }));
145
157
  }
146
158
  /** Default skin for the play/walk escape chip; swap via `EditorAppProps.modeChip`, or pass `null` to hide it (F2+E still exits). */
147
159
  function EditorModeChip({ mode, onExit }) {
@@ -157,6 +169,51 @@ function EditorModeChipHost({ api, mode, chip, }) {
157
169
  const Chip = chip;
158
170
  return _jsx(Chip, { mode: mode, onExit: onExit });
159
171
  }
172
+ /**
173
+ * HUD-layout authoring surface, mounted in `hud` run mode over the game's real `HudCanvas`.
174
+ * Enables drag/resize editing on every mounted HUD layout (including ones that mount after this
175
+ * opens), so panel placement is reachable straight from the editor — no separate F2+C chord. Layout
176
+ * edits flow through the injected `HudLayoutPersistProvider` (`onPanelCommit` → undoable
177
+ * `setUiPanel`). When the author finishes (the canvas "Done" button, Esc, or F2+C toggles editing
178
+ * off), HUD mode exits back to edit. F2+E also exits. Editing is turned back off for every layout on
179
+ * unmount.
180
+ */
181
+ function HudLayoutEditingHost({ api }) {
182
+ const exit = useCallback(() => api.setMode("edit"), [api]);
183
+ useF2Chord("KeyE", exit);
184
+ useEffect(() => {
185
+ const tracked = new Map();
186
+ let disposed = false;
187
+ const arm = (layout) => {
188
+ if (tracked.has(layout))
189
+ return;
190
+ // Force editing on first, then watch: a later transition back to `editing: false`
191
+ // (author pressed Done/Esc/F2+C) is the signal to leave HUD mode.
192
+ layout.setEditing(true);
193
+ const unsubscribe = layout.subscribe((state) => {
194
+ if (!disposed && !state.editing)
195
+ exit();
196
+ });
197
+ tracked.set(layout, unsubscribe);
198
+ };
199
+ const sync = () => {
200
+ for (const layout of listActiveHudLayouts())
201
+ arm(layout);
202
+ };
203
+ sync();
204
+ const unsubscribeRegistry = subscribeActiveHudLayouts(sync);
205
+ return () => {
206
+ disposed = true;
207
+ unsubscribeRegistry();
208
+ for (const [layout, unsubscribe] of tracked) {
209
+ unsubscribe();
210
+ layout.setEditing(false);
211
+ }
212
+ };
213
+ }, [exit]);
214
+ const hasPanels = listActiveHudLayouts().length > 0;
215
+ return (_jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-4 z-[60] flex justify-center", children: _jsxs("div", { className: "pointer-events-auto flex items-center gap-3 rounded-full border border-white/10 bg-black/80 px-4 py-1.5 text-[11px] text-neutral-100 shadow-lg shadow-black/40 backdrop-blur-md", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-cyan-400 shadow-[0_0_6px_rgba(34,211,238,0.8)]" }), hasPanels ? "HUD layout — drag panels to place; edits save to the scene" : "This game mounts no HUD panels to lay out"] }), _jsx("button", { type: "button", className: "rounded-full bg-white/[0.06] px-2.5 py-1 ring-1 ring-inset ring-white/10 transition-colors hover:bg-white/15", onClick: exit, children: "Done (F2+E)" })] }) }));
216
+ }
160
217
  /** Prefer a grounded spawn for the first shot — full-world framing clips past default far=300. */
161
218
  function resolveEditorCamera(document) {
162
219
  const spawn = findEditorMarker(document, "player_spawn") ??
@@ -187,9 +244,14 @@ function resolveEditorCamera(document) {
187
244
  };
188
245
  }
189
246
  /** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
190
- export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }) {
247
+ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor }) {
191
248
  const resolvedModeChip = modeChip === undefined ? EditorModeChip : modeChip;
192
249
  const saveFn = useMemo(() => save ?? endpointSaver(gameId), [save, gameId]);
250
+ const networkSnapshot = useMemo(() => buildEditorNetworkSnapshot({
251
+ gameId,
252
+ multiplayer: playable.game.multiplayer,
253
+ presence: networkPresence,
254
+ }), [gameId, playable, networkPresence]);
193
255
  const uiStoreRef = useRef(null);
194
256
  if (uiStoreRef.current === null)
195
257
  uiStoreRef.current = createEditorUiStore();
@@ -208,15 +270,20 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
208
270
  return [];
209
271
  }
210
272
  }, [playable]);
273
+ const resolvedLayers = layers ?? playable.editorLayers;
211
274
  const host = useMemo(() => {
212
275
  const created = createEditorHost({
213
276
  gameId,
214
- layers,
277
+ layers: resolvedLayers,
215
278
  catalogs,
216
279
  assets: catalogAssets,
217
280
  });
218
- return { ...created, baselineJson: created.session.exportJson(true) };
219
- }, [gameId, layers, catalogs, catalogAssets]);
281
+ return {
282
+ ...created,
283
+ baselineJson: created.session.exportJson(true),
284
+ baselineDocument: created.session.getState().document,
285
+ };
286
+ }, [gameId, resolvedLayers, catalogs, catalogAssets]);
220
287
  useEffect(() => host.dispose, [host]);
221
288
  useEffect(() => host.api.subscribeMode(setModeState), [host]);
222
289
  const [pendingDraft, setPendingDraft] = useState(null);
@@ -260,8 +327,13 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
260
327
  host.api.setVisibility({ ...host.api.getVisibility(), ...prefs.visibility });
261
328
  }
262
329
  ui.patch({
330
+ ...(prefs.gizmoSpace === undefined ? {} : { gizmoSpace: prefs.gizmoSpace }),
331
+ ...(prefs.gizmoPivot === undefined ? {} : { gizmoPivot: prefs.gizmoPivot }),
263
332
  ...(prefs.snapMode === undefined ? {} : { snapMode: prefs.snapMode }),
264
333
  ...(prefs.gridSize === undefined ? {} : { gridSize: prefs.gridSize }),
334
+ ...(prefs.rotationSnapDeg === undefined ? {} : { rotationSnapDeg: prefs.rotationSnapDeg }),
335
+ ...(prefs.scaleSnap === undefined ? {} : { scaleSnap: prefs.scaleSnap }),
336
+ ...(prefs.cameraProjection === undefined ? {} : { cameraProjection: prefs.cameraProjection }),
265
337
  ...(prefs.showGrid === undefined ? {} : { showGrid: prefs.showGrid }),
266
338
  ...(prefs.showContours === undefined ? {} : { showContours: prefs.showContours }),
267
339
  ...(prefs.showSurfaceGrid === undefined ? {} : { showSurfaceGrid: prefs.showSurfaceGrid }),
@@ -271,8 +343,13 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
271
343
  const state = ui.getState();
272
344
  savePrefs(gameId, {
273
345
  visibility: host.api.getVisibility(),
346
+ gizmoSpace: state.gizmoSpace,
347
+ gizmoPivot: state.gizmoPivot,
274
348
  snapMode: state.snapMode,
275
349
  gridSize: state.gridSize,
350
+ rotationSnapDeg: state.rotationSnapDeg,
351
+ scaleSnap: state.scaleSnap,
352
+ cameraProjection: state.cameraProjection,
276
353
  showGrid: state.showGrid,
277
354
  showContours: state.showContours,
278
355
  showSurfaceGrid: state.showSurfaceGrid,
@@ -306,6 +383,7 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
306
383
  };
307
384
  }, [gameId]);
308
385
  const initialCamera = useMemo(() => resolveEditorCamera(host.session.getState().document), [host]);
386
+ const cameraProjection = useStoreSelector(ui, (state) => state.cameraProjection);
309
387
  const editorPlayable = useMemo(() => {
310
388
  const frozenLoop = {
311
389
  onInit: playable.loop.onInit,
@@ -317,6 +395,53 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
317
395
  onReset: playable.loop.onReset,
318
396
  onDispose: playable.loop.onDispose,
319
397
  };
398
+ const { target, span, far } = initialCamera;
399
+ // Free-orbit inspection camera shared by edit and HUD-layout modes: no pointer lock, so the HUD
400
+ // panels stay draggable while the authored world renders behind them. Orthographic projection
401
+ // remounts the R3F canvas (Canvas.orthographic is mount-time); zoom scales with scene span so
402
+ // large worlds stay framed without a hand-tuned default.
403
+ const orthoZoom = Math.max(4, Math.min(80, 420 / Math.max(span, 1)));
404
+ const inspectionCamera = {
405
+ rig: "inspection",
406
+ projection: cameraProjection,
407
+ frustum: {
408
+ fov: 55,
409
+ near: 0.5,
410
+ far,
411
+ ...(cameraProjection === "orthographic" ? { zoom: orthoZoom } : {}),
412
+ },
413
+ inspection: {
414
+ target,
415
+ initialDistance: span,
416
+ initialPosition: {
417
+ x: target.x + span * 0.35,
418
+ y: target.y + span * 0.55,
419
+ z: target.z + span * 0.75,
420
+ },
421
+ minDistance: 4,
422
+ maxDistance: Math.max(far * 0.9, span * 8),
423
+ pan: true,
424
+ rotateSpeed: 0.7,
425
+ zoomSpeed: 1.1,
426
+ minPolarAngle: 0.15,
427
+ maxPolarAngle: Math.PI * 0.48,
428
+ },
429
+ };
430
+ if (mode === "hud") {
431
+ const BaseUI = playable.GameUI;
432
+ return {
433
+ ...playable,
434
+ GameUI: function EditorHudUi() {
435
+ return (_jsxs(_Fragment, { children: [BaseUI !== undefined ? _jsx(BaseUI, {}) : null, _jsx(HudLayoutEditingHost, { api: host.api })] }));
436
+ },
437
+ WorldOverlay: function EditorHudOverlay() {
438
+ return _jsx(PerfProbe, { api: host.api });
439
+ },
440
+ shadows: false,
441
+ loop: frozenLoop,
442
+ camera: inspectionCamera,
443
+ };
444
+ }
320
445
  if (mode === "play") {
321
446
  const BaseUI = playable.GameUI;
322
447
  const BaseOverlay = playable.WorldOverlay;
@@ -326,7 +451,7 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
326
451
  GameUI: function EditorPlayUi() {
327
452
  const onExit = useCallback(() => host.api.setMode("edit"), []);
328
453
  useF2Chord("KeyE", onExit);
329
- return (_jsxs(_Fragment, { children: [BaseUI !== undefined ? _jsx(BaseUI, {}) : null, useBuiltinPlayChrome ? (_jsx(RuntimePlayInspectorChrome, { api: host.api, onExit: onExit })) : (_jsx(EditorModeChipHost, { api: host.api, mode: "play", chip: resolvedModeChip }))] }));
454
+ return (_jsxs(_Fragment, { children: [BaseUI !== undefined ? _jsx(BaseUI, {}) : null, useBuiltinPlayChrome ? (_jsx(RuntimePlayInspectorChrome, { gameId: gameId, api: host.api, onExit: onExit })) : (_jsx(EditorModeChipHost, { api: host.api, mode: "play", chip: resolvedModeChip }))] }));
330
455
  },
331
456
  WorldOverlay: function EditorPlayOverlay({ ctx }) {
332
457
  return (_jsxs(_Fragment, { children: [BaseOverlay !== undefined ? _jsx(BaseOverlay, { ctx: ctx }) : null, _jsx(PerfProbe, { api: host.api }), _jsx(RuntimePlayPublisher, { api: host.api })] }));
@@ -349,10 +474,17 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
349
474
  const WorldOverlay = function EditorOverlay() {
350
475
  return _jsx(EditorWorldOverlay, { api: host.api, ui: ui, world: playable.game.world, readout: readout });
351
476
  };
477
+ const registerImportedAsset = (id, url) => {
478
+ try {
479
+ playable.game.assets.register(id, { url });
480
+ }
481
+ catch {
482
+ // Catalog may reject late registration in some hosts; place_asset still keeps meta.url.
483
+ }
484
+ };
352
485
  const GameUI = function EditorUi() {
353
- return _jsx(EditorChrome, { gameId: gameId, session: host.api.getSession(), api: host.api, assets: catalogAssets, ui: ui, baselineJson: host.baselineJson, save: saveFn });
486
+ return (_jsx(EditorChrome, { gameId: gameId, session: host.api.getSession(), api: host.api, assets: catalogAssets, ui: ui, baselineDocument: host.baselineDocument, save: saveFn, networkSnapshot: networkSnapshot, onRegisterAsset: registerImportedAsset, onExitEditor: onExitEditor }));
354
487
  };
355
- const { target, span, far } = initialCamera;
356
488
  return {
357
489
  ...playable,
358
490
  GameUI,
@@ -361,32 +493,12 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
361
493
  loop: frozenLoop,
362
494
  // Drop FPS gun chrome; keep world only.
363
495
  // WorldOverlay above is the editor layers, not PandoraViewmodel.
364
- camera: {
365
- rig: "inspection",
366
- frustum: {
367
- fov: 55,
368
- near: 0.5,
369
- far,
370
- },
371
- inspection: {
372
- target,
373
- initialDistance: span,
374
- initialPosition: {
375
- x: target.x + span * 0.35,
376
- y: target.y + span * 0.55,
377
- z: target.z + span * 0.75,
378
- },
379
- minDistance: 4,
380
- maxDistance: Math.max(far * 0.9, span * 8),
381
- pan: true,
382
- rotateSpeed: 0.7,
383
- zoomSpeed: 1.1,
384
- minPolarAngle: 0.15,
385
- maxPolarAngle: Math.PI * 0.48,
386
- },
387
- },
496
+ camera: inspectionCamera,
388
497
  };
389
- }, [playable, host, gameId, initialCamera, catalogAssets, ui, readout, mode, saveFn, resolvedModeChip]);
498
+ }, [playable, host, gameId, initialCamera, cameraProjection, catalogAssets, ui, readout, mode, saveFn, resolvedModeChip, networkSnapshot, onExitEditor]);
390
499
  const showElevation = useStoreSelector(ui, (s) => s.showElevation);
391
- return (_jsxs("div", { className: "relative h-full w-full bg-neutral-950", "data-jg-editor": "1", "data-jg-editor-game": gameId, children: [_jsx(GamePlayerShell, { playable: editorPlayable }), mode === "edit" && showElevation ? _jsx(TerrainReadoutHud, { readout: readout }) : null, pendingDraft !== null && mode === "edit" ? (_jsxs("div", { className: "absolute left-1/2 top-14 z-[60] flex -translate-x-1/2 items-center gap-3 rounded-xl border border-amber-400/30 bg-amber-950/90 px-4 py-2 text-xs text-amber-100 shadow-2xl shadow-black/50 backdrop-blur-md", children: [_jsx("span", { children: "Unsaved edits from a previous session found." }), _jsx("button", { type: "button", className: "rounded-md bg-gradient-to-b from-amber-500 to-amber-600 px-2.5 py-1 font-semibold text-white shadow-md shadow-amber-950/50 transition-colors hover:from-amber-400 hover:to-amber-500", onClick: restoreDraft, children: "Restore" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2.5 py-1 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/15", onClick: discardDraft, children: "Discard" })] })) : null] }));
500
+ const onPanelCommit = useCallback((id, panel) => {
501
+ host.session.dispatch({ type: "setUiPanel", id, patch: panel });
502
+ }, [host]);
503
+ return (_jsxs("div", { className: "relative h-full w-full bg-neutral-950", "data-jg-editor": "1", "data-jg-editor-game": gameId, children: [_jsx(HudLayoutPersistProvider, { onPanelCommit: onPanelCommit, children: _jsx(GamePlayerShell, { playable: editorPlayable }) }), mode === "edit" && showElevation ? _jsx(TerrainReadoutHud, { readout: readout }) : null, pendingDraft !== null && mode === "edit" ? (_jsxs("div", { className: "absolute left-1/2 top-14 z-[60] flex -translate-x-1/2 items-center gap-3 rounded-xl border border-amber-400/30 bg-amber-950/90 px-4 py-2 text-xs text-amber-100 shadow-2xl shadow-black/50 backdrop-blur-md", children: [_jsx("span", { children: "Unsaved edits from a previous session found." }), _jsx("button", { type: "button", className: "rounded-md bg-gradient-to-b from-amber-500 to-amber-600 px-2.5 py-1 font-semibold text-white shadow-md shadow-amber-950/50 transition-colors hover:from-amber-400 hover:to-amber-500", onClick: restoreDraft, children: "Restore" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2.5 py-1 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/15", onClick: discardDraft, children: "Discard" })] })) : null] }));
392
504
  }
@@ -1,22 +1,43 @@
1
- import { type EditorSession } from "@jgengine/core/editor/index";
1
+ import { type EditorDocument, type EditorSession } from "@jgengine/core/editor/index";
2
2
  import { type EditorAssetEntry } from "./AssetBrowser.js";
3
3
  import type { EditorHostApi } from "./session.js";
4
+ import { type AssetImporter } from "./assetImport.js";
4
5
  import { type EditorUiStore } from "./uiStore.js";
6
+ import type { EditorNetworkSnapshot } from "./networkSnapshot.js";
5
7
  /**
6
- * The full editor UI shell — toolbar, left panels (outliner/prefabs/sets/layers), viewport overlays,
7
- * the selector-subscribed {@link InspectorPanel}, the embedded {@link AgentPanel}, and the asset
8
- * browser — wired to the session, UI store, and host RPC. Mounted by `EditorApp`; not a game-author entry point.
8
+ * The full editor UI shell — global app bar, contextual scene toolbar, workspace rail, resizable
9
+ * hierarchy/inspector docks, tabbed bottom dock (content browser, console, profiler, AI
10
+ * assistant), viewport overlays, and status bar — wired to the session, UI store, layout store,
11
+ * and host RPC. Mounted by `EditorApp`; not a game-author entry point.
9
12
  */
10
- export declare function EditorChrome({ gameId, session, api, assets, ui, baselineJson, save, }: {
13
+ export declare function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset, onRegisterAsset, onExitEditor, }: {
11
14
  gameId: string;
12
15
  session: EditorSession;
13
16
  api: EditorHostApi;
14
17
  assets: readonly EditorAssetEntry[];
15
18
  ui: EditorUiStore;
16
- baselineJson?: string;
19
+ /** The document as loaded — drives the header unsaved indicator by reference compare. */
20
+ baselineDocument?: EditorDocument;
17
21
  save?: (json: string) => Promise<{
18
22
  ok: boolean;
19
23
  path?: string;
20
24
  error?: string;
21
25
  }>;
26
+ /** Closes the editor and returns to the game; set only when summoned over a game. Shows "Exit to game". */
27
+ onExitEditor?: () => void;
28
+ /**
29
+ * Network workspace inspection payload (adapter config + optional host presence).
30
+ * Built by `EditorApp` from the game definition; live presence rows only when the host injects them.
31
+ */
32
+ networkSnapshot: EditorNetworkSnapshot;
33
+ /**
34
+ * How dropped/imported models become durable. Default: the standalone host importer
35
+ * (`POST /__jgengine/import-asset`); when no host answers, imports degrade to blob URLs.
36
+ */
37
+ importAsset?: AssetImporter;
38
+ /**
39
+ * Optional side-effect after a model import succeeds: register the id/url into the live game
40
+ * asset catalog so AuthoredObjects can resolve the mesh without remounting the playable.
41
+ */
42
+ onRegisterAsset?: (id: string, url: string) => void;
22
43
  }): import("react").JSX.Element;