@jgengine/editor 0.10.0 → 0.12.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 (179) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +12 -0
  3. package/dist/AssetBrowser.d.ts +26 -1
  4. package/dist/AssetBrowser.js +54 -1
  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 +9 -1
  15. package/dist/EditorApp.js +208 -37
  16. package/dist/EditorChrome.d.ts +25 -6
  17. package/dist/EditorChrome.js +310 -118
  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 +69 -2
  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/TerrainReadout.d.ts +17 -0
  41. package/dist/TerrainReadout.js +163 -0
  42. package/dist/TerrainReadoutHud.d.ts +9 -0
  43. package/dist/TerrainReadoutHud.js +20 -0
  44. package/dist/TerrainSculpt.js +98 -59
  45. package/dist/agent/AgentPanel.d.ts +5 -2
  46. package/dist/agent/AgentPanel.js +11 -6
  47. package/dist/agent/toolBridge.js +5 -0
  48. package/dist/assetImport.d.ts +36 -0
  49. package/dist/assetImport.js +66 -0
  50. package/dist/authoredComponentMeta.d.ts +25 -0
  51. package/dist/authoredComponentMeta.js +65 -0
  52. package/dist/gizmoPivot.d.ts +4 -0
  53. package/dist/gizmoPivot.js +34 -0
  54. package/dist/handlers/catalogs.d.ts +3 -0
  55. package/dist/handlers/catalogs.js +137 -0
  56. package/dist/handlers/collections.d.ts +3 -0
  57. package/dist/handlers/collections.js +57 -0
  58. package/dist/handlers/context.d.ts +36 -0
  59. package/dist/handlers/context.js +1 -0
  60. package/dist/handlers/document.d.ts +3 -0
  61. package/dist/handlers/document.js +350 -0
  62. package/dist/handlers/grids.d.ts +3 -0
  63. package/dist/handlers/grids.js +142 -0
  64. package/dist/handlers/hierarchy.d.ts +3 -0
  65. package/dist/handlers/hierarchy.js +55 -0
  66. package/dist/handlers/index.d.ts +8 -0
  67. package/dist/handlers/index.js +23 -0
  68. package/dist/handlers/minimap.d.ts +9 -0
  69. package/dist/handlers/minimap.js +32 -0
  70. package/dist/handlers/runtime.d.ts +3 -0
  71. package/dist/handlers/runtime.js +191 -0
  72. package/dist/handlers/terrain.d.ts +3 -0
  73. package/dist/handlers/terrain.js +204 -0
  74. package/dist/index.d.ts +3 -2
  75. package/dist/index.js +3 -2
  76. package/dist/listDocumentTriggers.d.ts +42 -0
  77. package/dist/listDocumentTriggers.js +49 -0
  78. package/dist/materialAssignments.d.ts +63 -0
  79. package/dist/materialAssignments.js +84 -0
  80. package/dist/mcp/fieldSpec.d.ts +32 -0
  81. package/dist/mcp/fieldSpec.js +173 -0
  82. package/dist/mcp/rpcRequest.d.ts +8 -5
  83. package/dist/mcp/rpcRequest.js +72 -83
  84. package/dist/mcp/tools.js +104 -763
  85. package/dist/networkSnapshot.d.ts +82 -0
  86. package/dist/networkSnapshot.js +73 -0
  87. package/dist/parentCandidates.d.ts +12 -0
  88. package/dist/parentCandidates.js +25 -0
  89. package/dist/perfPill.d.ts +21 -0
  90. package/dist/perfPill.js +20 -0
  91. package/dist/session.d.ts +167 -5
  92. package/dist/session.js +36 -879
  93. package/dist/shell/AnimationPanel.d.ts +17 -0
  94. package/dist/shell/AnimationPanel.js +134 -0
  95. package/dist/shell/AssetThumbnail.d.ts +12 -0
  96. package/dist/shell/AssetThumbnail.js +23 -0
  97. package/dist/shell/BottomDock.d.ts +25 -0
  98. package/dist/shell/BottomDock.js +40 -0
  99. package/dist/shell/CommandPalette.d.ts +12 -0
  100. package/dist/shell/CommandPalette.js +56 -0
  101. package/dist/shell/ConsolePanel.d.ts +8 -0
  102. package/dist/shell/ConsolePanel.js +45 -0
  103. package/dist/shell/ContentBrowser.d.ts +21 -0
  104. package/dist/shell/ContentBrowser.js +130 -0
  105. package/dist/shell/HierarchyPanel.d.ts +26 -0
  106. package/dist/shell/HierarchyPanel.js +387 -0
  107. package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
  108. package/dist/shell/NetworkWorkspacePanel.js +49 -0
  109. package/dist/shell/PlayModeBar.d.ts +12 -0
  110. package/dist/shell/PlayModeBar.js +27 -0
  111. package/dist/shell/ProfilerPanel.d.ts +10 -0
  112. package/dist/shell/ProfilerPanel.js +41 -0
  113. package/dist/shell/SceneToolbar.d.ts +43 -0
  114. package/dist/shell/SceneToolbar.js +122 -0
  115. package/dist/shell/StatusBar.d.ts +18 -0
  116. package/dist/shell/StatusBar.js +41 -0
  117. package/dist/shell/TopAppBar.d.ts +33 -0
  118. package/dist/shell/TopAppBar.js +64 -0
  119. package/dist/shell/ViewportOverlays.d.ts +23 -0
  120. package/dist/shell/ViewportOverlays.js +136 -0
  121. package/dist/shell/WorkspaceRail.d.ts +20 -0
  122. package/dist/shell/WorkspaceRail.js +31 -0
  123. package/dist/shell/cameraTelemetry.d.ts +27 -0
  124. package/dist/shell/cameraTelemetry.js +24 -0
  125. package/dist/shell/commandRegistry.d.ts +58 -0
  126. package/dist/shell/commandRegistry.js +133 -0
  127. package/dist/shell/consoleSink.d.ts +11 -0
  128. package/dist/shell/consoleSink.js +17 -0
  129. package/dist/shell/consoleStore.d.ts +27 -0
  130. package/dist/shell/consoleStore.js +43 -0
  131. package/dist/shell/fields.d.ts +39 -0
  132. package/dist/shell/fields.js +104 -0
  133. package/dist/shell/formatSavedRelative.d.ts +2 -0
  134. package/dist/shell/formatSavedRelative.js +14 -0
  135. package/dist/shell/glbThumbnailCache.d.ts +45 -0
  136. package/dist/shell/glbThumbnailCache.js +241 -0
  137. package/dist/shell/hierarchyA11y.d.ts +21 -0
  138. package/dist/shell/hierarchyA11y.js +35 -0
  139. package/dist/shell/icons.d.ts +13 -0
  140. package/dist/shell/icons.js +85 -0
  141. package/dist/shell/layoutStore.d.ts +68 -0
  142. package/dist/shell/layoutStore.js +156 -0
  143. package/dist/shell/pathFlythrough.d.ts +37 -0
  144. package/dist/shell/pathFlythrough.js +54 -0
  145. package/dist/shell/perfHistory.d.ts +70 -0
  146. package/dist/shell/perfHistory.js +130 -0
  147. package/dist/shell/renameObject.d.ts +3 -0
  148. package/dist/shell/renameObject.js +25 -0
  149. package/dist/shell/theme.d.ts +36 -0
  150. package/dist/shell/theme.js +37 -0
  151. package/dist/shell/ui.d.ts +84 -0
  152. package/dist/shell/ui.js +90 -0
  153. package/dist/shell/useGlbThumbnail.d.ts +8 -0
  154. package/dist/shell/useGlbThumbnail.js +19 -0
  155. package/dist/terrainReadoutStore.d.ts +19 -0
  156. package/dist/terrainReadoutStore.js +36 -0
  157. package/dist/uiStore.d.ts +33 -0
  158. package/dist/uiStore.js +13 -0
  159. package/dist/viewportContextMenu.d.ts +4 -2
  160. package/dist/viewportContextMenu.js +5 -1
  161. package/package.json +4 -4
  162. package/skills/jgengine/SKILL.md +35 -804
  163. package/skills/jgengine/api.md +190 -79
  164. package/skills/jgengine/capabilities.md +112 -2
  165. package/skills/jgengine/recipes/minimal-game.md +88 -0
  166. package/skills/jgengine-editor/SKILL.md +29 -362
  167. package/skills/jgengine-editor/api.md +519 -18
  168. package/skills/jgengine-editor/capabilities.md +91 -0
  169. package/skills/jgengine-editor/reference.md +113 -0
  170. package/skills/level-design/SKILL.md +54 -0
  171. package/skills/level-design/agents/openai.yaml +4 -0
  172. package/skills/level-design/references/application-playbook.md +192 -0
  173. package/skills/level-design/references/field-guide.md +337 -0
  174. package/skills/level-design/references/live-editor-loop.md +179 -0
  175. package/dist/OutlinerPanel.d.ts +0 -14
  176. package/dist/OutlinerPanel.js +0 -84
  177. package/skills/jgengine/reference-cartridge.md +0 -72
  178. package/skills/jgengine/reference-orphans.md +0 -9
  179. package/skills/jgengine/reference-systems.md +0 -60
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,12 @@ 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;
18
26
  }
19
27
  /** Persists an exported document JSON; resolves with where it landed or why it failed. */
20
28
  export type EditorSaveFn = (json: string) => Promise<{
@@ -23,4 +31,4 @@ export type EditorSaveFn = (json: string) => Promise<{
23
31
  error?: string;
24
32
  }>;
25
33
  /** 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;
34
+ export declare function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence }: EditorAppProps): import("react").JSX.Element;
package/dist/EditorApp.js CHANGED
@@ -2,20 +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";
19
+ import { TerrainReadout } from "./TerrainReadout.js";
20
+ import { TerrainReadoutHud } from "./TerrainReadoutHud.js";
15
21
  import { TerrainSculpt } from "./TerrainSculpt.js";
22
+ import { createTerrainReadoutStore } from "./terrainReadoutStore.js";
16
23
  import { RuntimePlayInspectorChrome, RuntimePlayPublisher } from "./RuntimePlayBridge.js";
24
+ import { buildEditorNetworkSnapshot, } from "./networkSnapshot.js";
17
25
  import { createEditorHost } from "./session.js";
18
- import { createEditorUiStore } from "./uiStore.js";
26
+ import { createEditorUiStore, } from "./uiStore.js";
19
27
  import { useF2Chord } from "./useF2Chord.js";
20
28
  import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
21
29
  function endpointSaver(gameId) {
@@ -82,7 +90,29 @@ function savePrefs(gameId, prefs) {
82
90
  return;
83
91
  }
84
92
  }
85
- function EditorWorldOverlay({ api, ui, world, }) {
93
+ /** Default readout region half-extent when a world declares no terrain bounds. */
94
+ const DEFAULT_READOUT_HALF = 64;
95
+ /**
96
+ * Derives the XZ region the terrain-readability overlay samples: the sculpt snapshot's rect if the
97
+ * world carries one, else the terrain descriptor's centered bounds, else a default square. Guides are
98
+ * editor visualization, not authored content — the region is inferred, never stored in the scene.
99
+ */
100
+ function readoutRegionFor(world) {
101
+ if (world !== undefined && world.kind === "environment") {
102
+ const sculpt = world.sculpt;
103
+ if (sculpt !== undefined) {
104
+ return { minX: sculpt.bounds.minX, minZ: sculpt.bounds.minZ, maxX: sculpt.bounds.maxX, maxZ: sculpt.bounds.maxZ };
105
+ }
106
+ const bounds = world.terrain?.bounds;
107
+ if (bounds !== undefined) {
108
+ return { minX: -bounds.w / 2, minZ: -bounds.d / 2, maxX: bounds.w / 2, maxZ: bounds.d / 2 };
109
+ }
110
+ }
111
+ return { minX: -DEFAULT_READOUT_HALF, minZ: -DEFAULT_READOUT_HALF, maxX: DEFAULT_READOUT_HALF, maxZ: DEFAULT_READOUT_HALF };
112
+ }
113
+ /** Idle window before the terrain overlay rebuilds off the edited field — bounds rebuild frequency during sculpting. */
114
+ const READOUT_REBUILD_MS = 250;
115
+ function EditorWorldOverlay({ api, ui, world, readout, }) {
86
116
  const session = api.getSession();
87
117
  const ctx = useGameContext();
88
118
  const visibilityStore = useMemo(() => ({ getState: api.getVisibility, subscribe: api.subscribeVisibility }), [api]);
@@ -90,10 +120,40 @@ function EditorWorldOverlay({ api, ui, world, }) {
90
120
  const selection = useStoreSelector(session, (s) => s.selection, shallowArrayEqual);
91
121
  const visibility = useStoreSelector(visibilityStore, (v) => v);
92
122
  const showGrid = useStoreSelector(ui, (s) => s.showGrid);
123
+ const showContours = useStoreSelector(ui, (s) => s.showContours);
124
+ const showSurfaceGrid = useStoreSelector(ui, (s) => s.showSurfaceGrid);
125
+ const showElevation = useStoreSelector(ui, (s) => s.showElevation);
93
126
  const pathPoint = useStoreSelector(ui, (s) => s.pathPoint);
94
127
  const pathDraft = useStoreSelector(ui, (s) => s.pathDraft);
128
+ const hoverId = useStoreSelector(ui, (s) => s.hoverId);
95
129
  const groundHeightAt = useCallback((x, z) => ctx.world.groundHeightAt(x, z), [ctx.world]);
96
- 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, _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 })] }));
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]);
136
+ const readoutRegion = useMemo(() => readoutRegionFor(world), [world]);
137
+ const readoutActive = showContours || showSurfaceGrid || showElevation;
138
+ // Debounced terrain version: rebuilds the overlay after sculpt edits settle, not every stroke frame.
139
+ const [terrainVersion, setTerrainVersion] = useState(0);
140
+ useEffect(() => {
141
+ if (!readoutActive)
142
+ return;
143
+ setTerrainVersion((value) => value + 1);
144
+ let timer = null;
145
+ const unsubscribe = session.subscribe(() => {
146
+ if (timer !== null)
147
+ clearTimeout(timer);
148
+ timer = setTimeout(() => setTerrainVersion((value) => value + 1), READOUT_REBUILD_MS);
149
+ });
150
+ return () => {
151
+ if (timer !== null)
152
+ clearTimeout(timer);
153
+ unsubscribe();
154
+ };
155
+ }, [session, readoutActive]);
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 })] }));
97
157
  }
98
158
  /** Default skin for the play/walk escape chip; swap via `EditorAppProps.modeChip`, or pass `null` to hide it (F2+E still exits). */
99
159
  function EditorModeChip({ mode, onExit }) {
@@ -109,6 +169,51 @@ function EditorModeChipHost({ api, mode, chip, }) {
109
169
  const Chip = chip;
110
170
  return _jsx(Chip, { mode: mode, onExit: onExit });
111
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
+ }
112
217
  /** Prefer a grounded spawn for the first shot — full-world framing clips past default far=300. */
113
218
  function resolveEditorCamera(document) {
114
219
  const spawn = findEditorMarker(document, "player_spawn") ??
@@ -139,13 +244,22 @@ function resolveEditorCamera(document) {
139
244
  };
140
245
  }
141
246
  /** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
142
- export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }) {
247
+ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence }) {
143
248
  const resolvedModeChip = modeChip === undefined ? EditorModeChip : modeChip;
144
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]);
145
255
  const uiStoreRef = useRef(null);
146
256
  if (uiStoreRef.current === null)
147
257
  uiStoreRef.current = createEditorUiStore();
148
258
  const ui = uiStoreRef.current;
259
+ const readoutStoreRef = useRef(null);
260
+ if (readoutStoreRef.current === null)
261
+ readoutStoreRef.current = createTerrainReadoutStore();
262
+ const readout = readoutStoreRef.current;
149
263
  const [mode, setModeState] = useState("edit");
150
264
  const catalogAssets = useMemo(() => {
151
265
  try {
@@ -156,15 +270,20 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
156
270
  return [];
157
271
  }
158
272
  }, [playable]);
273
+ const resolvedLayers = layers ?? playable.editorLayers;
159
274
  const host = useMemo(() => {
160
275
  const created = createEditorHost({
161
276
  gameId,
162
- layers,
277
+ layers: resolvedLayers,
163
278
  catalogs,
164
279
  assets: catalogAssets,
165
280
  });
166
- return { ...created, baselineJson: created.session.exportJson(true) };
167
- }, [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]);
168
287
  useEffect(() => host.dispose, [host]);
169
288
  useEffect(() => host.api.subscribeMode(setModeState), [host]);
170
289
  const [pendingDraft, setPendingDraft] = useState(null);
@@ -208,17 +327,33 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
208
327
  host.api.setVisibility({ ...host.api.getVisibility(), ...prefs.visibility });
209
328
  }
210
329
  ui.patch({
330
+ ...(prefs.gizmoSpace === undefined ? {} : { gizmoSpace: prefs.gizmoSpace }),
331
+ ...(prefs.gizmoPivot === undefined ? {} : { gizmoPivot: prefs.gizmoPivot }),
211
332
  ...(prefs.snapMode === undefined ? {} : { snapMode: prefs.snapMode }),
212
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 }),
213
337
  ...(prefs.showGrid === undefined ? {} : { showGrid: prefs.showGrid }),
338
+ ...(prefs.showContours === undefined ? {} : { showContours: prefs.showContours }),
339
+ ...(prefs.showSurfaceGrid === undefined ? {} : { showSurfaceGrid: prefs.showSurfaceGrid }),
340
+ ...(prefs.showElevation === undefined ? {} : { showElevation: prefs.showElevation }),
214
341
  });
215
342
  const persist = () => {
216
343
  const state = ui.getState();
217
344
  savePrefs(gameId, {
218
345
  visibility: host.api.getVisibility(),
346
+ gizmoSpace: state.gizmoSpace,
347
+ gizmoPivot: state.gizmoPivot,
219
348
  snapMode: state.snapMode,
220
349
  gridSize: state.gridSize,
350
+ rotationSnapDeg: state.rotationSnapDeg,
351
+ scaleSnap: state.scaleSnap,
352
+ cameraProjection: state.cameraProjection,
221
353
  showGrid: state.showGrid,
354
+ showContours: state.showContours,
355
+ showSurfaceGrid: state.showSurfaceGrid,
356
+ showElevation: state.showElevation,
222
357
  });
223
358
  };
224
359
  const unsubscribeVisibility = host.api.subscribeVisibility(persist);
@@ -248,6 +383,7 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
248
383
  };
249
384
  }, [gameId]);
250
385
  const initialCamera = useMemo(() => resolveEditorCamera(host.session.getState().document), [host]);
386
+ const cameraProjection = useStoreSelector(ui, (state) => state.cameraProjection);
251
387
  const editorPlayable = useMemo(() => {
252
388
  const frozenLoop = {
253
389
  onInit: playable.loop.onInit,
@@ -259,6 +395,53 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
259
395
  onReset: playable.loop.onReset,
260
396
  onDispose: playable.loop.onDispose,
261
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
+ }
262
445
  if (mode === "play") {
263
446
  const BaseUI = playable.GameUI;
264
447
  const BaseOverlay = playable.WorldOverlay;
@@ -268,7 +451,7 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
268
451
  GameUI: function EditorPlayUi() {
269
452
  const onExit = useCallback(() => host.api.setMode("edit"), []);
270
453
  useF2Chord("KeyE", onExit);
271
- 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 }))] }));
272
455
  },
273
456
  WorldOverlay: function EditorPlayOverlay({ ctx }) {
274
457
  return (_jsxs(_Fragment, { children: [BaseOverlay !== undefined ? _jsx(BaseOverlay, { ctx: ctx }) : null, _jsx(PerfProbe, { api: host.api }), _jsx(RuntimePlayPublisher, { api: host.api })] }));
@@ -289,12 +472,19 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
289
472
  };
290
473
  }
291
474
  const WorldOverlay = function EditorOverlay() {
292
- return _jsx(EditorWorldOverlay, { api: host.api, ui: ui, world: playable.game.world });
475
+ return _jsx(EditorWorldOverlay, { api: host.api, ui: ui, world: playable.game.world, readout: readout });
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
+ }
293
484
  };
294
485
  const GameUI = function EditorUi() {
295
- 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 }));
296
487
  };
297
- const { target, span, far } = initialCamera;
298
488
  return {
299
489
  ...playable,
300
490
  GameUI,
@@ -303,31 +493,12 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
303
493
  loop: frozenLoop,
304
494
  // Drop FPS gun chrome; keep world only.
305
495
  // WorldOverlay above is the editor layers, not PandoraViewmodel.
306
- camera: {
307
- rig: "inspection",
308
- frustum: {
309
- fov: 55,
310
- near: 0.5,
311
- far,
312
- },
313
- inspection: {
314
- target,
315
- initialDistance: span,
316
- initialPosition: {
317
- x: target.x + span * 0.35,
318
- y: target.y + span * 0.55,
319
- z: target.z + span * 0.75,
320
- },
321
- minDistance: 4,
322
- maxDistance: Math.max(far * 0.9, span * 8),
323
- pan: true,
324
- rotateSpeed: 0.7,
325
- zoomSpeed: 1.1,
326
- minPolarAngle: 0.15,
327
- maxPolarAngle: Math.PI * 0.48,
328
- },
329
- },
496
+ camera: inspectionCamera,
330
497
  };
331
- }, [playable, host, gameId, initialCamera, catalogAssets, ui, mode, saveFn, resolvedModeChip]);
332
- 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 }), 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] }));
498
+ }, [playable, host, gameId, initialCamera, cameraProjection, catalogAssets, ui, readout, mode, saveFn, resolvedModeChip, networkSnapshot]);
499
+ const showElevation = useStoreSelector(ui, (s) => s.showElevation);
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] }));
333
504
  }
@@ -1,22 +1,41 @@
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, }: {
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
+ /**
27
+ * Network workspace inspection payload (adapter config + optional host presence).
28
+ * Built by `EditorApp` from the game definition; live presence rows only when the host injects them.
29
+ */
30
+ networkSnapshot: EditorNetworkSnapshot;
31
+ /**
32
+ * How dropped/imported models become durable. Default: the standalone host importer
33
+ * (`POST /__jgengine/import-asset`); when no host answers, imports degrade to blob URLs.
34
+ */
35
+ importAsset?: AssetImporter;
36
+ /**
37
+ * Optional side-effect after a model import succeeds: register the id/url into the live game
38
+ * asset catalog so AuthoredObjects can resolve the mesh without remounting the playable.
39
+ */
40
+ onRegisterAsset?: (id: string, url: string) => void;
22
41
  }): import("react").JSX.Element;