@jgengine/editor 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +12 -0
- package/dist/AssetBrowser.d.ts +33 -1
- package/dist/AssetBrowser.js +64 -2
- 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 +15 -1
- package/dist/EditorApp.js +147 -35
- package/dist/EditorChrome.d.ts +27 -6
- package/dist/EditorChrome.js +314 -130
- 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 +33 -0
- 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/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 +25 -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/streets.d.ts +3 -0
- package/dist/handlers/streets.js +159 -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 +174 -0
- package/dist/mcp/rpcRequest.d.ts +8 -5
- package/dist/mcp/rpcRequest.js +72 -93
- package/dist/mcp/tools.js +105 -912
- 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/session.d.ts +91 -4
- package/dist/session.js +36 -1017
- 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 +35 -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/uiStore.d.ts +27 -0
- package/dist/uiStore.js +10 -0
- package/dist/viewportContextMenu.d.ts +4 -2
- package/dist/viewportContextMenu.js +5 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-editor/SKILL.md +18 -3
- package/skills/jgengine-editor/api.md +403 -20
- package/skills/jgengine-editor/capabilities.md +46 -0
- package/skills/jgengine-editor/reference.md +87 -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/dist/DebugDraw.d.ts
CHANGED
|
@@ -4,10 +4,12 @@ export declare function PathDraftPreview({ points }: {
|
|
|
4
4
|
points: readonly EditorVec3[];
|
|
5
5
|
}): import("react").JSX.Element;
|
|
6
6
|
/** Renders every visible marker, volume, path, and note from a document as in-scene 3D gizmos. */
|
|
7
|
-
export declare function EditorLayerOverlays({ document, visibility, selection, onSelect, activePathPoint, groundHeightAt, }: {
|
|
7
|
+
export declare function EditorLayerOverlays({ document, visibility, selection, hoverId, onSelect, activePathPoint, groundHeightAt, }: {
|
|
8
8
|
document: EditorDocument;
|
|
9
9
|
visibility: EditorKindVisibility;
|
|
10
10
|
selection: readonly string[];
|
|
11
|
+
/** Pre-selection hover id from viewport pointer pick; ignored when already selected. */
|
|
12
|
+
hoverId?: string | null;
|
|
11
13
|
onSelect: (id: string) => void;
|
|
12
14
|
activePathPoint?: {
|
|
13
15
|
pathId: string;
|
package/dist/DebugDraw.js
CHANGED
|
@@ -87,25 +87,38 @@ function volumeWireframe(volume) {
|
|
|
87
87
|
pushRing(out, radius, 0, "z");
|
|
88
88
|
return new Float32Array(out);
|
|
89
89
|
}
|
|
90
|
-
|
|
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,18 @@ export interface EditorAppProps {
|
|
|
15
17
|
mode: EditorRunMode;
|
|
16
18
|
onExit: () => void;
|
|
17
19
|
}> | null;
|
|
20
|
+
/**
|
|
21
|
+
* Optional live multiplayer presence/session feed for the Network workspace.
|
|
22
|
+
* Adapter config always comes from `playable.game.multiplayer`; this only supplies real
|
|
23
|
+
* session identity and presence rows. Omit when the host has nothing live — the panel stays honest.
|
|
24
|
+
*/
|
|
25
|
+
networkPresence?: EditorNetworkPresenceInput;
|
|
26
|
+
/**
|
|
27
|
+
* Called to close the editor and return to the game. Passed by `GameHost` when the editor was
|
|
28
|
+
* summoned over a running game; when set, the chrome shows an "Exit to game" control. Omit for the
|
|
29
|
+
* standalone editor, which has no game to return to.
|
|
30
|
+
*/
|
|
31
|
+
onExitEditor?: () => void;
|
|
18
32
|
}
|
|
19
33
|
/** Persists an exported document JSON; resolves with where it landed or why it failed. */
|
|
20
34
|
export type EditorSaveFn = (json: string) => Promise<{
|
|
@@ -23,4 +37,4 @@ export type EditorSaveFn = (json: string) => Promise<{
|
|
|
23
37
|
error?: string;
|
|
24
38
|
}>;
|
|
25
39
|
/** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
|
|
26
|
-
export declare function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }: EditorAppProps): import("react").JSX.Element;
|
|
40
|
+
export declare function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor }: EditorAppProps): import("react").JSX.Element;
|
package/dist/EditorApp.js
CHANGED
|
@@ -2,23 +2,28 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { editorDocumentBounds, findEditorMarker } from "@jgengine/core/editor/index";
|
|
4
4
|
import { getSaveEndpoint } from "@jgengine/core/devtools/saveEndpoint";
|
|
5
|
+
import { listActiveHudLayouts, subscribeActiveHudLayouts } from "@jgengine/core/ui/hudLayout";
|
|
6
|
+
import { HudLayoutPersistProvider } from "@jgengine/react/hudLayoutPersist";
|
|
5
7
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
8
|
import { GamePlayerShell } from "@jgengine/shell/GamePlayerShell";
|
|
7
9
|
import { assetsFromCatalog } from "./AssetBrowser.js";
|
|
8
10
|
import { EditorCameraDriver } from "./EditorCameraDriver.js";
|
|
9
11
|
import { EditorChrome } from "./EditorChrome.js";
|
|
10
12
|
import { EditorLayerOverlays, PathDraftPreview } from "./DebugDraw.js";
|
|
13
|
+
import { AssetDropZone } from "./AssetDropZone.js";
|
|
11
14
|
import { MaterialDropZone } from "./MaterialDropZone.js";
|
|
12
15
|
import { PerfProbe } from "./PerfProbe.js";
|
|
13
16
|
import { ScatterPreview } from "./ScatterPreview.js";
|
|
17
|
+
import { CityPreview } from "./CityPreview.js";
|
|
14
18
|
import { SelectionGizmo, ViewportSelect } from "./SelectionGizmo.js";
|
|
15
19
|
import { TerrainReadout } from "./TerrainReadout.js";
|
|
16
20
|
import { TerrainReadoutHud } from "./TerrainReadoutHud.js";
|
|
17
21
|
import { TerrainSculpt } from "./TerrainSculpt.js";
|
|
18
22
|
import { createTerrainReadoutStore } from "./terrainReadoutStore.js";
|
|
19
23
|
import { RuntimePlayInspectorChrome, RuntimePlayPublisher } from "./RuntimePlayBridge.js";
|
|
24
|
+
import { buildEditorNetworkSnapshot, } from "./networkSnapshot.js";
|
|
20
25
|
import { createEditorHost } from "./session.js";
|
|
21
|
-
import { createEditorUiStore } from "./uiStore.js";
|
|
26
|
+
import { createEditorUiStore, } from "./uiStore.js";
|
|
22
27
|
import { useF2Chord } from "./useF2Chord.js";
|
|
23
28
|
import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
|
|
24
29
|
function endpointSaver(gameId) {
|
|
@@ -120,7 +125,14 @@ function EditorWorldOverlay({ api, ui, world, readout, }) {
|
|
|
120
125
|
const showElevation = useStoreSelector(ui, (s) => s.showElevation);
|
|
121
126
|
const pathPoint = useStoreSelector(ui, (s) => s.pathPoint);
|
|
122
127
|
const pathDraft = useStoreSelector(ui, (s) => s.pathDraft);
|
|
128
|
+
const hoverId = useStoreSelector(ui, (s) => s.hoverId);
|
|
123
129
|
const groundHeightAt = useCallback((x, z) => ctx.world.groundHeightAt(x, z), [ctx.world]);
|
|
130
|
+
// Publish the live composed ground field so the `bake_minimap` RPC can rasterize authored terrain.
|
|
131
|
+
// Only available while the viewport is mounted; the cleanup nulls it so headless callers get null.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
api.setTerrainSampler(ctx.world.ground);
|
|
134
|
+
return () => api.setTerrainSampler(null);
|
|
135
|
+
}, [api, ctx.world]);
|
|
124
136
|
const readoutRegion = useMemo(() => readoutRegionFor(world), [world]);
|
|
125
137
|
const readoutActive = showContours || showSurfaceGrid || showElevation;
|
|
126
138
|
// Debounced terrain version: rebuilds the overlay after sculpt edits settle, not every stroke frame.
|
|
@@ -141,7 +153,7 @@ function EditorWorldOverlay({ api, ui, world, readout, }) {
|
|
|
141
153
|
unsubscribe();
|
|
142
154
|
};
|
|
143
155
|
}, [session, readoutActive]);
|
|
144
|
-
return (_jsxs(_Fragment, { children: [_jsx(PerfProbe, { api: api }), _jsx(EditorCameraDriver, { api: api }), _jsx(ViewportSelect, { api: api, ui: ui }), _jsx(MaterialDropZone, { api: api }), _jsx(TerrainSculpt, { api: api, ui: ui, world: world }), _jsx(ScatterPreview, { api: api }), showGrid ? _jsx("gridHelper", { args: [400, 80, "#3b4252", "#20242e"], position: [0, 0.05, 0] }) : null, readoutActive ? (_jsx(TerrainReadout, { groundHeightAt: groundHeightAt, region: readoutRegion, showContours: showContours, showSurfaceGrid: showSurfaceGrid, version: terrainVersion, readout: readout })) : null, _jsx(EditorLayerOverlays, { document: document, visibility: visibility, selection: selection, onSelect: (id) => session.dispatch({ type: "select", ids: [id] }), activePathPoint: pathPoint, groundHeightAt: groundHeightAt }), pathDraft.length > 0 ? _jsx(PathDraftPreview, { points: pathDraft }) : null, _jsx(SelectionGizmo, { session: session, ui: ui, groundSnap: groundHeightAt })] }));
|
|
156
|
+
return (_jsxs(_Fragment, { children: [_jsx(PerfProbe, { api: api }), _jsx(EditorCameraDriver, { api: api }), _jsx(ViewportSelect, { api: api, ui: ui }), _jsx(MaterialDropZone, { api: api }), _jsx(AssetDropZone, { api: api }), _jsx(TerrainSculpt, { api: api, ui: ui, world: world }), _jsx(ScatterPreview, { api: api }), _jsx(CityPreview, { api: api }), showGrid ? _jsx("gridHelper", { args: [400, 80, "#3b4252", "#20242e"], position: [0, 0.05, 0] }) : null, readoutActive ? (_jsx(TerrainReadout, { groundHeightAt: groundHeightAt, region: readoutRegion, showContours: showContours, showSurfaceGrid: showSurfaceGrid, version: terrainVersion, readout: readout })) : null, _jsx(EditorLayerOverlays, { document: document, visibility: visibility, selection: selection, hoverId: hoverId, onSelect: (id) => session.dispatch({ type: "select", ids: [id] }), activePathPoint: pathPoint, groundHeightAt: groundHeightAt }), pathDraft.length > 0 ? _jsx(PathDraftPreview, { points: pathDraft }) : null, _jsx(SelectionGizmo, { session: session, ui: ui, groundSnap: groundHeightAt })] }));
|
|
145
157
|
}
|
|
146
158
|
/** Default skin for the play/walk escape chip; swap via `EditorAppProps.modeChip`, or pass `null` to hide it (F2+E still exits). */
|
|
147
159
|
function EditorModeChip({ mode, onExit }) {
|
|
@@ -157,6 +169,51 @@ function EditorModeChipHost({ api, mode, chip, }) {
|
|
|
157
169
|
const Chip = chip;
|
|
158
170
|
return _jsx(Chip, { mode: mode, onExit: onExit });
|
|
159
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* HUD-layout authoring surface, mounted in `hud` run mode over the game's real `HudCanvas`.
|
|
174
|
+
* Enables drag/resize editing on every mounted HUD layout (including ones that mount after this
|
|
175
|
+
* opens), so panel placement is reachable straight from the editor — no separate F2+C chord. Layout
|
|
176
|
+
* edits flow through the injected `HudLayoutPersistProvider` (`onPanelCommit` → undoable
|
|
177
|
+
* `setUiPanel`). When the author finishes (the canvas "Done" button, Esc, or F2+C toggles editing
|
|
178
|
+
* off), HUD mode exits back to edit. F2+E also exits. Editing is turned back off for every layout on
|
|
179
|
+
* unmount.
|
|
180
|
+
*/
|
|
181
|
+
function HudLayoutEditingHost({ api }) {
|
|
182
|
+
const exit = useCallback(() => api.setMode("edit"), [api]);
|
|
183
|
+
useF2Chord("KeyE", exit);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
const tracked = new Map();
|
|
186
|
+
let disposed = false;
|
|
187
|
+
const arm = (layout) => {
|
|
188
|
+
if (tracked.has(layout))
|
|
189
|
+
return;
|
|
190
|
+
// Force editing on first, then watch: a later transition back to `editing: false`
|
|
191
|
+
// (author pressed Done/Esc/F2+C) is the signal to leave HUD mode.
|
|
192
|
+
layout.setEditing(true);
|
|
193
|
+
const unsubscribe = layout.subscribe((state) => {
|
|
194
|
+
if (!disposed && !state.editing)
|
|
195
|
+
exit();
|
|
196
|
+
});
|
|
197
|
+
tracked.set(layout, unsubscribe);
|
|
198
|
+
};
|
|
199
|
+
const sync = () => {
|
|
200
|
+
for (const layout of listActiveHudLayouts())
|
|
201
|
+
arm(layout);
|
|
202
|
+
};
|
|
203
|
+
sync();
|
|
204
|
+
const unsubscribeRegistry = subscribeActiveHudLayouts(sync);
|
|
205
|
+
return () => {
|
|
206
|
+
disposed = true;
|
|
207
|
+
unsubscribeRegistry();
|
|
208
|
+
for (const [layout, unsubscribe] of tracked) {
|
|
209
|
+
unsubscribe();
|
|
210
|
+
layout.setEditing(false);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}, [exit]);
|
|
214
|
+
const hasPanels = listActiveHudLayouts().length > 0;
|
|
215
|
+
return (_jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-4 z-[60] flex justify-center", children: _jsxs("div", { className: "pointer-events-auto flex items-center gap-3 rounded-full border border-white/10 bg-black/80 px-4 py-1.5 text-[11px] text-neutral-100 shadow-lg shadow-black/40 backdrop-blur-md", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-cyan-400 shadow-[0_0_6px_rgba(34,211,238,0.8)]" }), hasPanels ? "HUD layout — drag panels to place; edits save to the scene" : "This game mounts no HUD panels to lay out"] }), _jsx("button", { type: "button", className: "rounded-full bg-white/[0.06] px-2.5 py-1 ring-1 ring-inset ring-white/10 transition-colors hover:bg-white/15", onClick: exit, children: "Done (F2+E)" })] }) }));
|
|
216
|
+
}
|
|
160
217
|
/** Prefer a grounded spawn for the first shot — full-world framing clips past default far=300. */
|
|
161
218
|
function resolveEditorCamera(document) {
|
|
162
219
|
const spawn = findEditorMarker(document, "player_spawn") ??
|
|
@@ -187,9 +244,14 @@ function resolveEditorCamera(document) {
|
|
|
187
244
|
};
|
|
188
245
|
}
|
|
189
246
|
/** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
|
|
190
|
-
export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }) {
|
|
247
|
+
export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor }) {
|
|
191
248
|
const resolvedModeChip = modeChip === undefined ? EditorModeChip : modeChip;
|
|
192
249
|
const saveFn = useMemo(() => save ?? endpointSaver(gameId), [save, gameId]);
|
|
250
|
+
const networkSnapshot = useMemo(() => buildEditorNetworkSnapshot({
|
|
251
|
+
gameId,
|
|
252
|
+
multiplayer: playable.game.multiplayer,
|
|
253
|
+
presence: networkPresence,
|
|
254
|
+
}), [gameId, playable, networkPresence]);
|
|
193
255
|
const uiStoreRef = useRef(null);
|
|
194
256
|
if (uiStoreRef.current === null)
|
|
195
257
|
uiStoreRef.current = createEditorUiStore();
|
|
@@ -208,15 +270,20 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
|
|
|
208
270
|
return [];
|
|
209
271
|
}
|
|
210
272
|
}, [playable]);
|
|
273
|
+
const resolvedLayers = layers ?? playable.editorLayers;
|
|
211
274
|
const host = useMemo(() => {
|
|
212
275
|
const created = createEditorHost({
|
|
213
276
|
gameId,
|
|
214
|
-
layers,
|
|
277
|
+
layers: resolvedLayers,
|
|
215
278
|
catalogs,
|
|
216
279
|
assets: catalogAssets,
|
|
217
280
|
});
|
|
218
|
-
return {
|
|
219
|
-
|
|
281
|
+
return {
|
|
282
|
+
...created,
|
|
283
|
+
baselineJson: created.session.exportJson(true),
|
|
284
|
+
baselineDocument: created.session.getState().document,
|
|
285
|
+
};
|
|
286
|
+
}, [gameId, resolvedLayers, catalogs, catalogAssets]);
|
|
220
287
|
useEffect(() => host.dispose, [host]);
|
|
221
288
|
useEffect(() => host.api.subscribeMode(setModeState), [host]);
|
|
222
289
|
const [pendingDraft, setPendingDraft] = useState(null);
|
|
@@ -260,8 +327,13 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
|
|
|
260
327
|
host.api.setVisibility({ ...host.api.getVisibility(), ...prefs.visibility });
|
|
261
328
|
}
|
|
262
329
|
ui.patch({
|
|
330
|
+
...(prefs.gizmoSpace === undefined ? {} : { gizmoSpace: prefs.gizmoSpace }),
|
|
331
|
+
...(prefs.gizmoPivot === undefined ? {} : { gizmoPivot: prefs.gizmoPivot }),
|
|
263
332
|
...(prefs.snapMode === undefined ? {} : { snapMode: prefs.snapMode }),
|
|
264
333
|
...(prefs.gridSize === undefined ? {} : { gridSize: prefs.gridSize }),
|
|
334
|
+
...(prefs.rotationSnapDeg === undefined ? {} : { rotationSnapDeg: prefs.rotationSnapDeg }),
|
|
335
|
+
...(prefs.scaleSnap === undefined ? {} : { scaleSnap: prefs.scaleSnap }),
|
|
336
|
+
...(prefs.cameraProjection === undefined ? {} : { cameraProjection: prefs.cameraProjection }),
|
|
265
337
|
...(prefs.showGrid === undefined ? {} : { showGrid: prefs.showGrid }),
|
|
266
338
|
...(prefs.showContours === undefined ? {} : { showContours: prefs.showContours }),
|
|
267
339
|
...(prefs.showSurfaceGrid === undefined ? {} : { showSurfaceGrid: prefs.showSurfaceGrid }),
|
|
@@ -271,8 +343,13 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
|
|
|
271
343
|
const state = ui.getState();
|
|
272
344
|
savePrefs(gameId, {
|
|
273
345
|
visibility: host.api.getVisibility(),
|
|
346
|
+
gizmoSpace: state.gizmoSpace,
|
|
347
|
+
gizmoPivot: state.gizmoPivot,
|
|
274
348
|
snapMode: state.snapMode,
|
|
275
349
|
gridSize: state.gridSize,
|
|
350
|
+
rotationSnapDeg: state.rotationSnapDeg,
|
|
351
|
+
scaleSnap: state.scaleSnap,
|
|
352
|
+
cameraProjection: state.cameraProjection,
|
|
276
353
|
showGrid: state.showGrid,
|
|
277
354
|
showContours: state.showContours,
|
|
278
355
|
showSurfaceGrid: state.showSurfaceGrid,
|
|
@@ -306,6 +383,7 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
|
|
|
306
383
|
};
|
|
307
384
|
}, [gameId]);
|
|
308
385
|
const initialCamera = useMemo(() => resolveEditorCamera(host.session.getState().document), [host]);
|
|
386
|
+
const cameraProjection = useStoreSelector(ui, (state) => state.cameraProjection);
|
|
309
387
|
const editorPlayable = useMemo(() => {
|
|
310
388
|
const frozenLoop = {
|
|
311
389
|
onInit: playable.loop.onInit,
|
|
@@ -317,6 +395,53 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
|
|
|
317
395
|
onReset: playable.loop.onReset,
|
|
318
396
|
onDispose: playable.loop.onDispose,
|
|
319
397
|
};
|
|
398
|
+
const { target, span, far } = initialCamera;
|
|
399
|
+
// Free-orbit inspection camera shared by edit and HUD-layout modes: no pointer lock, so the HUD
|
|
400
|
+
// panels stay draggable while the authored world renders behind them. Orthographic projection
|
|
401
|
+
// remounts the R3F canvas (Canvas.orthographic is mount-time); zoom scales with scene span so
|
|
402
|
+
// large worlds stay framed without a hand-tuned default.
|
|
403
|
+
const orthoZoom = Math.max(4, Math.min(80, 420 / Math.max(span, 1)));
|
|
404
|
+
const inspectionCamera = {
|
|
405
|
+
rig: "inspection",
|
|
406
|
+
projection: cameraProjection,
|
|
407
|
+
frustum: {
|
|
408
|
+
fov: 55,
|
|
409
|
+
near: 0.5,
|
|
410
|
+
far,
|
|
411
|
+
...(cameraProjection === "orthographic" ? { zoom: orthoZoom } : {}),
|
|
412
|
+
},
|
|
413
|
+
inspection: {
|
|
414
|
+
target,
|
|
415
|
+
initialDistance: span,
|
|
416
|
+
initialPosition: {
|
|
417
|
+
x: target.x + span * 0.35,
|
|
418
|
+
y: target.y + span * 0.55,
|
|
419
|
+
z: target.z + span * 0.75,
|
|
420
|
+
},
|
|
421
|
+
minDistance: 4,
|
|
422
|
+
maxDistance: Math.max(far * 0.9, span * 8),
|
|
423
|
+
pan: true,
|
|
424
|
+
rotateSpeed: 0.7,
|
|
425
|
+
zoomSpeed: 1.1,
|
|
426
|
+
minPolarAngle: 0.15,
|
|
427
|
+
maxPolarAngle: Math.PI * 0.48,
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
if (mode === "hud") {
|
|
431
|
+
const BaseUI = playable.GameUI;
|
|
432
|
+
return {
|
|
433
|
+
...playable,
|
|
434
|
+
GameUI: function EditorHudUi() {
|
|
435
|
+
return (_jsxs(_Fragment, { children: [BaseUI !== undefined ? _jsx(BaseUI, {}) : null, _jsx(HudLayoutEditingHost, { api: host.api })] }));
|
|
436
|
+
},
|
|
437
|
+
WorldOverlay: function EditorHudOverlay() {
|
|
438
|
+
return _jsx(PerfProbe, { api: host.api });
|
|
439
|
+
},
|
|
440
|
+
shadows: false,
|
|
441
|
+
loop: frozenLoop,
|
|
442
|
+
camera: inspectionCamera,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
320
445
|
if (mode === "play") {
|
|
321
446
|
const BaseUI = playable.GameUI;
|
|
322
447
|
const BaseOverlay = playable.WorldOverlay;
|
|
@@ -326,7 +451,7 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
|
|
|
326
451
|
GameUI: function EditorPlayUi() {
|
|
327
452
|
const onExit = useCallback(() => host.api.setMode("edit"), []);
|
|
328
453
|
useF2Chord("KeyE", onExit);
|
|
329
|
-
return (_jsxs(_Fragment, { children: [BaseUI !== undefined ? _jsx(BaseUI, {}) : null, useBuiltinPlayChrome ? (_jsx(RuntimePlayInspectorChrome, { api: host.api, onExit: onExit })) : (_jsx(EditorModeChipHost, { api: host.api, mode: "play", chip: resolvedModeChip }))] }));
|
|
454
|
+
return (_jsxs(_Fragment, { children: [BaseUI !== undefined ? _jsx(BaseUI, {}) : null, useBuiltinPlayChrome ? (_jsx(RuntimePlayInspectorChrome, { gameId: gameId, api: host.api, onExit: onExit })) : (_jsx(EditorModeChipHost, { api: host.api, mode: "play", chip: resolvedModeChip }))] }));
|
|
330
455
|
},
|
|
331
456
|
WorldOverlay: function EditorPlayOverlay({ ctx }) {
|
|
332
457
|
return (_jsxs(_Fragment, { children: [BaseOverlay !== undefined ? _jsx(BaseOverlay, { ctx: ctx }) : null, _jsx(PerfProbe, { api: host.api }), _jsx(RuntimePlayPublisher, { api: host.api })] }));
|
|
@@ -349,10 +474,17 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
|
|
|
349
474
|
const WorldOverlay = function EditorOverlay() {
|
|
350
475
|
return _jsx(EditorWorldOverlay, { api: host.api, ui: ui, world: playable.game.world, readout: readout });
|
|
351
476
|
};
|
|
477
|
+
const registerImportedAsset = (id, url) => {
|
|
478
|
+
try {
|
|
479
|
+
playable.game.assets.register(id, { url });
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// Catalog may reject late registration in some hosts; place_asset still keeps meta.url.
|
|
483
|
+
}
|
|
484
|
+
};
|
|
352
485
|
const GameUI = function EditorUi() {
|
|
353
|
-
return _jsx(EditorChrome, { gameId: gameId, session: host.api.getSession(), api: host.api, assets: catalogAssets, ui: ui,
|
|
486
|
+
return (_jsx(EditorChrome, { gameId: gameId, session: host.api.getSession(), api: host.api, assets: catalogAssets, ui: ui, baselineDocument: host.baselineDocument, save: saveFn, networkSnapshot: networkSnapshot, onRegisterAsset: registerImportedAsset, onExitEditor: onExitEditor }));
|
|
354
487
|
};
|
|
355
|
-
const { target, span, far } = initialCamera;
|
|
356
488
|
return {
|
|
357
489
|
...playable,
|
|
358
490
|
GameUI,
|
|
@@ -361,32 +493,12 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip }
|
|
|
361
493
|
loop: frozenLoop,
|
|
362
494
|
// Drop FPS gun chrome; keep world only.
|
|
363
495
|
// WorldOverlay above is the editor layers, not PandoraViewmodel.
|
|
364
|
-
camera:
|
|
365
|
-
rig: "inspection",
|
|
366
|
-
frustum: {
|
|
367
|
-
fov: 55,
|
|
368
|
-
near: 0.5,
|
|
369
|
-
far,
|
|
370
|
-
},
|
|
371
|
-
inspection: {
|
|
372
|
-
target,
|
|
373
|
-
initialDistance: span,
|
|
374
|
-
initialPosition: {
|
|
375
|
-
x: target.x + span * 0.35,
|
|
376
|
-
y: target.y + span * 0.55,
|
|
377
|
-
z: target.z + span * 0.75,
|
|
378
|
-
},
|
|
379
|
-
minDistance: 4,
|
|
380
|
-
maxDistance: Math.max(far * 0.9, span * 8),
|
|
381
|
-
pan: true,
|
|
382
|
-
rotateSpeed: 0.7,
|
|
383
|
-
zoomSpeed: 1.1,
|
|
384
|
-
minPolarAngle: 0.15,
|
|
385
|
-
maxPolarAngle: Math.PI * 0.48,
|
|
386
|
-
},
|
|
387
|
-
},
|
|
496
|
+
camera: inspectionCamera,
|
|
388
497
|
};
|
|
389
|
-
}, [playable, host, gameId, initialCamera, catalogAssets, ui, readout, mode, saveFn, resolvedModeChip]);
|
|
498
|
+
}, [playable, host, gameId, initialCamera, cameraProjection, catalogAssets, ui, readout, mode, saveFn, resolvedModeChip, networkSnapshot, onExitEditor]);
|
|
390
499
|
const showElevation = useStoreSelector(ui, (s) => s.showElevation);
|
|
391
|
-
|
|
500
|
+
const onPanelCommit = useCallback((id, panel) => {
|
|
501
|
+
host.session.dispatch({ type: "setUiPanel", id, patch: panel });
|
|
502
|
+
}, [host]);
|
|
503
|
+
return (_jsxs("div", { className: "relative h-full w-full bg-neutral-950", "data-jg-editor": "1", "data-jg-editor-game": gameId, children: [_jsx(HudLayoutPersistProvider, { onPanelCommit: onPanelCommit, children: _jsx(GamePlayerShell, { playable: editorPlayable }) }), mode === "edit" && showElevation ? _jsx(TerrainReadoutHud, { readout: readout }) : null, pendingDraft !== null && mode === "edit" ? (_jsxs("div", { className: "absolute left-1/2 top-14 z-[60] flex -translate-x-1/2 items-center gap-3 rounded-xl border border-amber-400/30 bg-amber-950/90 px-4 py-2 text-xs text-amber-100 shadow-2xl shadow-black/50 backdrop-blur-md", children: [_jsx("span", { children: "Unsaved edits from a previous session found." }), _jsx("button", { type: "button", className: "rounded-md bg-gradient-to-b from-amber-500 to-amber-600 px-2.5 py-1 font-semibold text-white shadow-md shadow-amber-950/50 transition-colors hover:from-amber-400 hover:to-amber-500", onClick: restoreDraft, children: "Restore" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2.5 py-1 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/15", onClick: discardDraft, children: "Discard" })] })) : null] }));
|
|
392
504
|
}
|
package/dist/EditorChrome.d.ts
CHANGED
|
@@ -1,22 +1,43 @@
|
|
|
1
|
-
import { type EditorSession } from "@jgengine/core/editor/index";
|
|
1
|
+
import { type EditorDocument, type EditorSession } from "@jgengine/core/editor/index";
|
|
2
2
|
import { type EditorAssetEntry } from "./AssetBrowser.js";
|
|
3
3
|
import type { EditorHostApi } from "./session.js";
|
|
4
|
+
import { type AssetImporter } from "./assetImport.js";
|
|
4
5
|
import { type EditorUiStore } from "./uiStore.js";
|
|
6
|
+
import type { EditorNetworkSnapshot } from "./networkSnapshot.js";
|
|
5
7
|
/**
|
|
6
|
-
* The full editor UI shell —
|
|
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, onExitEditor, }: {
|
|
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
|
+
/** Closes the editor and returns to the game; set only when summoned over a game. Shows "Exit to game". */
|
|
27
|
+
onExitEditor?: () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Network workspace inspection payload (adapter config + optional host presence).
|
|
30
|
+
* Built by `EditorApp` from the game definition; live presence rows only when the host injects them.
|
|
31
|
+
*/
|
|
32
|
+
networkSnapshot: EditorNetworkSnapshot;
|
|
33
|
+
/**
|
|
34
|
+
* How dropped/imported models become durable. Default: the standalone host importer
|
|
35
|
+
* (`POST /__jgengine/import-asset`); when no host answers, imports degrade to blob URLs.
|
|
36
|
+
*/
|
|
37
|
+
importAsset?: AssetImporter;
|
|
38
|
+
/**
|
|
39
|
+
* Optional side-effect after a model import succeeds: register the id/url into the live game
|
|
40
|
+
* asset catalog so AuthoredObjects can resolve the mesh without remounting the playable.
|
|
41
|
+
*/
|
|
42
|
+
onRegisterAsset?: (id: string, url: string) => void;
|
|
22
43
|
}): import("react").JSX.Element;
|