@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.
- package/CHANGELOG.md +27 -1
- package/README.md +12 -0
- package/dist/AssetBrowser.d.ts +26 -1
- package/dist/AssetBrowser.js +54 -1
- package/dist/AssetDropZone.d.ts +9 -0
- package/dist/AssetDropZone.js +65 -0
- package/dist/CatalogsPanel.d.ts +4 -2
- package/dist/CatalogsPanel.js +162 -9
- package/dist/CityPreview.d.ts +11 -0
- package/dist/CityPreview.js +45 -0
- package/dist/CollectionsPanel.js +5 -5
- package/dist/DebugDraw.d.ts +3 -1
- package/dist/DebugDraw.js +37 -19
- package/dist/EditorApp.d.ts +9 -1
- package/dist/EditorApp.js +208 -37
- package/dist/EditorChrome.d.ts +25 -6
- package/dist/EditorChrome.js +310 -118
- package/dist/InspectorPanel.d.ts +17 -7
- package/dist/InspectorPanel.js +173 -27
- package/dist/LightingPanel.d.ts +11 -0
- package/dist/LightingPanel.js +99 -0
- package/dist/MaterialsWorkspacePanel.d.ts +12 -0
- package/dist/MaterialsWorkspacePanel.js +86 -0
- package/dist/ParentPickerMenu.d.ts +13 -0
- package/dist/ParentPickerMenu.js +33 -0
- package/dist/PerfProbe.js +69 -2
- package/dist/PrefabsPanel.js +5 -5
- package/dist/RuntimePlayBridge.d.ts +5 -3
- package/dist/RuntimePlayBridge.js +6 -13
- package/dist/ScatterPreview.d.ts +3 -1
- package/dist/ScatterPreview.js +10 -7
- package/dist/SchemaInspector.js +21 -8
- package/dist/ScriptingPanel.d.ts +15 -0
- package/dist/ScriptingPanel.js +46 -0
- package/dist/SelectionGizmo.js +114 -30
- package/dist/StandaloneEditor.d.ts +16 -9
- package/dist/StandaloneEditor.js +19 -20
- package/dist/TerrainPanel.d.ts +3 -2
- package/dist/TerrainPanel.js +16 -5
- package/dist/TerrainReadout.d.ts +17 -0
- package/dist/TerrainReadout.js +163 -0
- package/dist/TerrainReadoutHud.d.ts +9 -0
- package/dist/TerrainReadoutHud.js +20 -0
- package/dist/TerrainSculpt.js +98 -59
- package/dist/agent/AgentPanel.d.ts +5 -2
- package/dist/agent/AgentPanel.js +11 -6
- package/dist/agent/toolBridge.js +5 -0
- package/dist/assetImport.d.ts +36 -0
- package/dist/assetImport.js +66 -0
- package/dist/authoredComponentMeta.d.ts +25 -0
- package/dist/authoredComponentMeta.js +65 -0
- package/dist/gizmoPivot.d.ts +4 -0
- package/dist/gizmoPivot.js +34 -0
- package/dist/handlers/catalogs.d.ts +3 -0
- package/dist/handlers/catalogs.js +137 -0
- package/dist/handlers/collections.d.ts +3 -0
- package/dist/handlers/collections.js +57 -0
- package/dist/handlers/context.d.ts +36 -0
- package/dist/handlers/context.js +1 -0
- package/dist/handlers/document.d.ts +3 -0
- package/dist/handlers/document.js +350 -0
- package/dist/handlers/grids.d.ts +3 -0
- package/dist/handlers/grids.js +142 -0
- package/dist/handlers/hierarchy.d.ts +3 -0
- package/dist/handlers/hierarchy.js +55 -0
- package/dist/handlers/index.d.ts +8 -0
- package/dist/handlers/index.js +23 -0
- package/dist/handlers/minimap.d.ts +9 -0
- package/dist/handlers/minimap.js +32 -0
- package/dist/handlers/runtime.d.ts +3 -0
- package/dist/handlers/runtime.js +191 -0
- package/dist/handlers/terrain.d.ts +3 -0
- package/dist/handlers/terrain.js +204 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/listDocumentTriggers.d.ts +42 -0
- package/dist/listDocumentTriggers.js +49 -0
- package/dist/materialAssignments.d.ts +63 -0
- package/dist/materialAssignments.js +84 -0
- package/dist/mcp/fieldSpec.d.ts +32 -0
- package/dist/mcp/fieldSpec.js +173 -0
- package/dist/mcp/rpcRequest.d.ts +8 -5
- package/dist/mcp/rpcRequest.js +72 -83
- package/dist/mcp/tools.js +104 -763
- package/dist/networkSnapshot.d.ts +82 -0
- package/dist/networkSnapshot.js +73 -0
- package/dist/parentCandidates.d.ts +12 -0
- package/dist/parentCandidates.js +25 -0
- package/dist/perfPill.d.ts +21 -0
- package/dist/perfPill.js +20 -0
- package/dist/session.d.ts +167 -5
- package/dist/session.js +36 -879
- package/dist/shell/AnimationPanel.d.ts +17 -0
- package/dist/shell/AnimationPanel.js +134 -0
- package/dist/shell/AssetThumbnail.d.ts +12 -0
- package/dist/shell/AssetThumbnail.js +23 -0
- package/dist/shell/BottomDock.d.ts +25 -0
- package/dist/shell/BottomDock.js +40 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +56 -0
- package/dist/shell/ConsolePanel.d.ts +8 -0
- package/dist/shell/ConsolePanel.js +45 -0
- package/dist/shell/ContentBrowser.d.ts +21 -0
- package/dist/shell/ContentBrowser.js +130 -0
- package/dist/shell/HierarchyPanel.d.ts +26 -0
- package/dist/shell/HierarchyPanel.js +387 -0
- package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
- package/dist/shell/NetworkWorkspacePanel.js +49 -0
- package/dist/shell/PlayModeBar.d.ts +12 -0
- package/dist/shell/PlayModeBar.js +27 -0
- package/dist/shell/ProfilerPanel.d.ts +10 -0
- package/dist/shell/ProfilerPanel.js +41 -0
- package/dist/shell/SceneToolbar.d.ts +43 -0
- package/dist/shell/SceneToolbar.js +122 -0
- package/dist/shell/StatusBar.d.ts +18 -0
- package/dist/shell/StatusBar.js +41 -0
- package/dist/shell/TopAppBar.d.ts +33 -0
- package/dist/shell/TopAppBar.js +64 -0
- package/dist/shell/ViewportOverlays.d.ts +23 -0
- package/dist/shell/ViewportOverlays.js +136 -0
- package/dist/shell/WorkspaceRail.d.ts +20 -0
- package/dist/shell/WorkspaceRail.js +31 -0
- package/dist/shell/cameraTelemetry.d.ts +27 -0
- package/dist/shell/cameraTelemetry.js +24 -0
- package/dist/shell/commandRegistry.d.ts +58 -0
- package/dist/shell/commandRegistry.js +133 -0
- package/dist/shell/consoleSink.d.ts +11 -0
- package/dist/shell/consoleSink.js +17 -0
- package/dist/shell/consoleStore.d.ts +27 -0
- package/dist/shell/consoleStore.js +43 -0
- package/dist/shell/fields.d.ts +39 -0
- package/dist/shell/fields.js +104 -0
- package/dist/shell/formatSavedRelative.d.ts +2 -0
- package/dist/shell/formatSavedRelative.js +14 -0
- package/dist/shell/glbThumbnailCache.d.ts +45 -0
- package/dist/shell/glbThumbnailCache.js +241 -0
- package/dist/shell/hierarchyA11y.d.ts +21 -0
- package/dist/shell/hierarchyA11y.js +35 -0
- package/dist/shell/icons.d.ts +13 -0
- package/dist/shell/icons.js +85 -0
- package/dist/shell/layoutStore.d.ts +68 -0
- package/dist/shell/layoutStore.js +156 -0
- package/dist/shell/pathFlythrough.d.ts +37 -0
- package/dist/shell/pathFlythrough.js +54 -0
- package/dist/shell/perfHistory.d.ts +70 -0
- package/dist/shell/perfHistory.js +130 -0
- package/dist/shell/renameObject.d.ts +3 -0
- package/dist/shell/renameObject.js +25 -0
- package/dist/shell/theme.d.ts +36 -0
- package/dist/shell/theme.js +37 -0
- package/dist/shell/ui.d.ts +84 -0
- package/dist/shell/ui.js +90 -0
- package/dist/shell/useGlbThumbnail.d.ts +8 -0
- package/dist/shell/useGlbThumbnail.js +19 -0
- package/dist/terrainReadoutStore.d.ts +19 -0
- package/dist/terrainReadoutStore.js +36 -0
- package/dist/uiStore.d.ts +33 -0
- package/dist/uiStore.js +13 -0
- package/dist/viewportContextMenu.d.ts +4 -2
- package/dist/viewportContextMenu.js +5 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +35 -804
- package/skills/jgengine/api.md +190 -79
- package/skills/jgengine/capabilities.md +112 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-editor/SKILL.md +29 -362
- package/skills/jgengine-editor/api.md +519 -18
- package/skills/jgengine-editor/capabilities.md +91 -0
- package/skills/jgengine-editor/reference.md +113 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/OutlinerPanel.d.ts +0 -14
- package/dist/OutlinerPanel.js +0 -84
- package/skills/jgengine/reference-cartridge.md +0 -72
- package/skills/jgengine/reference-orphans.md +0 -9
- 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
|
-
|
|
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.
|
|
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 ? "
|
|
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
|
|
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
|
|
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.
|
|
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(() =>
|
|
232
|
-
|
|
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
|
}
|
package/dist/EditorApp.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
167
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
}
|
package/dist/EditorChrome.d.ts
CHANGED
|
@@ -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 —
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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,
|
|
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
|
-
|
|
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;
|