@jgengine/editor 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +346 -0
- package/dist/ClipPreviewLayer.d.ts +2 -3
- package/dist/ClipPreviewLayer.js +10 -28
- package/dist/CreatorApp.d.ts +12 -0
- package/dist/CreatorApp.js +107 -0
- package/dist/DebugDraw.js +3 -1
- package/dist/EditorApp.d.ts +14 -2
- package/dist/EditorApp.js +29 -14
- package/dist/EditorChrome.d.ts +9 -1
- package/dist/EditorChrome.js +54 -35
- package/dist/InspectorPanel.d.ts +3 -1
- package/dist/InspectorPanel.js +12 -8
- package/dist/MaterialAssetEditor.d.ts +13 -0
- package/dist/MaterialAssetEditor.js +109 -0
- package/dist/MaterialPreview.d.ts +16 -0
- package/dist/MaterialPreview.js +41 -0
- package/dist/MaterialsWorkspacePanel.d.ts +14 -2
- package/dist/MaterialsWorkspacePanel.js +78 -4
- package/dist/RuntimePlayBridge.js +1 -6
- package/dist/SelectionGizmo.js +23 -14
- package/dist/SimulationInspector.d.ts +9 -0
- package/dist/SimulationInspector.js +47 -0
- package/dist/TerrainSculpt.js +3 -1
- package/dist/agent/toolBridge.js +2 -0
- package/dist/authoringPlacement.d.ts +21 -0
- package/dist/authoringPlacement.js +34 -0
- package/dist/handlers/context.d.ts +1 -0
- package/dist/handlers/document.d.ts +1 -1
- package/dist/handlers/document.js +29 -1
- package/dist/handlers/hierarchy.d.ts +1 -1
- package/dist/handlers/hierarchy.js +4 -0
- package/dist/handlers/runtime.js +20 -17
- package/dist/handlers/terrain.js +52 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/materialControls.d.ts +32 -0
- package/dist/materialControls.js +79 -0
- package/dist/mcp/fieldSpec.d.ts +1 -1
- package/dist/mcp/fieldSpec.js +11 -2
- package/dist/mcp/importOptionalGameModule.d.ts +2 -0
- package/dist/mcp/importOptionalGameModule.js +13 -0
- package/dist/mcp/loadGameCatalogs.js +5 -12
- package/dist/mcp/rpcRequest.d.ts +3 -3
- package/dist/mcp/rpcRequest.js +10 -4
- package/dist/mcp/tools.js +10 -1
- package/dist/modelAnimationAuthoring.d.ts +27 -7
- package/dist/modelAnimationAuthoring.js +69 -72
- package/dist/session.d.ts +62 -0
- package/dist/session.js +56 -7
- package/dist/shell/AnimationGraphPanel.js +18 -10
- package/dist/shell/PlayModeBar.d.ts +2 -1
- package/dist/shell/PlayModeBar.js +2 -2
- package/dist/shell/SceneToolbar.d.ts +2 -1
- package/dist/shell/SceneToolbar.js +5 -4
- package/dist/shell/TopAppBar.d.ts +2 -1
- package/dist/shell/TopAppBar.js +2 -2
- package/dist/shell/animGraphPreview.d.ts +2 -0
- package/dist/shell/animGraphPreview.js +5 -2
- package/dist/shell/clipPreview.d.ts +1 -0
- package/dist/useDocumentSave.d.ts +20 -0
- package/dist/useDocumentSave.js +33 -0
- package/package.json +6 -5
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/jgengine/SKILL.md +22 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-editor/SKILL.md +9 -1
- package/skills/jgengine-editor/api.md +102 -16
- package/skills/jgengine-editor/capabilities.md +40 -0
- package/skills/jgengine-editor/reference.md +149 -1
- package/skills/jgengine-editor/references/live-sync.md +15 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { importCreatorDocument, validateCreatorDocument } from "@jgengine/core/editor/creatorStorage";
|
|
4
|
+
import { EditorApp } from "./EditorApp.js";
|
|
5
|
+
/** Production creator catalog and embedded editor, reached through the game's menu.
|
|
6
|
+
* @capability player-creator create, edit, save, playtest and reopen bounded player scenes through the production editor
|
|
7
|
+
*/
|
|
8
|
+
export function CreatorApp({ gameId, config, onExit }) {
|
|
9
|
+
const [documents, setDocuments] = useState([]);
|
|
10
|
+
const [selected, setSelected] = useState(null);
|
|
11
|
+
const [name, setName] = useState("");
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const [busy, setBusy] = useState(false);
|
|
14
|
+
const revision = useRef(null);
|
|
15
|
+
const pending = useRef(false);
|
|
16
|
+
const generation = useRef(0);
|
|
17
|
+
const refresh = useCallback(async () => {
|
|
18
|
+
try {
|
|
19
|
+
const values = await config.storage.list();
|
|
20
|
+
if (values.length > config.policy.maxDocuments)
|
|
21
|
+
throw new Error("Creator catalog exceeds document budget");
|
|
22
|
+
setDocuments(values);
|
|
23
|
+
}
|
|
24
|
+
catch (failure) {
|
|
25
|
+
setError(failure instanceof Error ? failure.message : String(failure));
|
|
26
|
+
}
|
|
27
|
+
}, [config]);
|
|
28
|
+
useEffect(() => { void refresh(); return () => { generation.current += 1; }; }, [refresh]);
|
|
29
|
+
const validate = useCallback((document) => { validateCreatorDocument(document, config.policy); }, [config]);
|
|
30
|
+
const playable = useMemo(() => selected === null ? null : config.createPlayable(selected.document), [selected, config]);
|
|
31
|
+
const save = useCallback(async (json) => {
|
|
32
|
+
if (selected === null || pending.current)
|
|
33
|
+
return { ok: false, error: "A save is already in progress" };
|
|
34
|
+
pending.current = true;
|
|
35
|
+
const currentGeneration = generation.current;
|
|
36
|
+
try {
|
|
37
|
+
const document = importCreatorDocument(json, config.policy);
|
|
38
|
+
const next = await config.storage.save({ ...selected, document }, revision.current);
|
|
39
|
+
if (generation.current === currentGeneration) {
|
|
40
|
+
revision.current = next.revision;
|
|
41
|
+
setError(null);
|
|
42
|
+
}
|
|
43
|
+
return { ok: true, path: next.name };
|
|
44
|
+
}
|
|
45
|
+
catch (failure) {
|
|
46
|
+
return { ok: false, error: failure instanceof Error ? failure.message : String(failure) };
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
pending.current = false;
|
|
50
|
+
}
|
|
51
|
+
}, [selected, config]);
|
|
52
|
+
const run = async (action) => {
|
|
53
|
+
if (busy)
|
|
54
|
+
return;
|
|
55
|
+
setBusy(true);
|
|
56
|
+
setError(null);
|
|
57
|
+
try {
|
|
58
|
+
await action();
|
|
59
|
+
}
|
|
60
|
+
catch (failure) {
|
|
61
|
+
setError(failure instanceof Error ? failure.message : String(failure));
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
setBusy(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const create = () => run(async () => {
|
|
68
|
+
if (documents.length >= config.policy.maxDocuments)
|
|
69
|
+
throw new Error("Creator catalog is full");
|
|
70
|
+
const document = validateCreatorDocument(config.initialDocument(), config.policy);
|
|
71
|
+
const value = await config.storage.save({ version: 1, id: config.createId?.() ?? crypto.randomUUID(), name: name.trim(), revision: 0, document }, null);
|
|
72
|
+
revision.current = value.revision;
|
|
73
|
+
setSelected(value);
|
|
74
|
+
});
|
|
75
|
+
const importFile = (file) => run(async () => {
|
|
76
|
+
if (documents.length >= config.policy.maxDocuments)
|
|
77
|
+
throw new Error("Creator catalog is full");
|
|
78
|
+
if (file.size > config.policy.maxBytes)
|
|
79
|
+
throw new Error("Scene exceeds byte budget");
|
|
80
|
+
const document = importCreatorDocument(await file.text(), config.policy);
|
|
81
|
+
const value = await config.storage.save({ version: 1, id: config.createId?.() ?? crypto.randomUUID(), name: name.trim(), revision: 0, document }, null);
|
|
82
|
+
revision.current = value.revision;
|
|
83
|
+
setSelected(value);
|
|
84
|
+
});
|
|
85
|
+
const reopen = (id) => run(async () => {
|
|
86
|
+
const value = await config.storage.load(id);
|
|
87
|
+
if (value === null)
|
|
88
|
+
throw new Error("Scene no longer exists");
|
|
89
|
+
validate(value.document);
|
|
90
|
+
revision.current = value.revision;
|
|
91
|
+
setSelected(value);
|
|
92
|
+
});
|
|
93
|
+
const returnToCatalog = () => {
|
|
94
|
+
generation.current += 1;
|
|
95
|
+
setSelected(null);
|
|
96
|
+
void refresh();
|
|
97
|
+
};
|
|
98
|
+
if (selected !== null && playable !== null) {
|
|
99
|
+
return _jsx(EditorApp, { gameId: `${gameId}:${selected.id}`, playable: playable, layers: selected.document, catalogs: playable.editorCatalogs, save: save, createPlaytest: config.createPlayable, validateDocument: validate, playerCreator: true, allowedAssets: config.policy.allowedAssets, allowedKinds: config.policy.allowedKinds, maxImportBytes: config.policy.maxBytes, creatorPolicy: config.policy, onExitEditor: returnToCatalog });
|
|
100
|
+
}
|
|
101
|
+
return (_jsxs("section", { "aria-label": "Scene creator", style: { padding: 24, color: "#eef2ff", background: "#111827", minHeight: "100%", fontFamily: "sans-serif" }, children: [_jsx("h1", { children: "Create and edit" }), _jsx("button", { type: "button", onClick: onExit, children: "Return to game" }), _jsxs("form", { onSubmit: (event) => { event.preventDefault(); void create(); }, style: { marginBlock: 24 }, children: [_jsxs("label", { children: ["Scene name ", _jsx("input", { value: name, maxLength: 80, onChange: (event) => setName(event.target.value) })] }), " ", _jsx("button", { type: "submit", disabled: busy || name.trim().length === 0 || documents.length >= config.policy.maxDocuments, children: "Create scene" })] }), _jsxs("label", { children: ["Import scene JSON ", _jsx("input", { type: "file", accept: "application/json,.json", "aria-label": "Import scene JSON", disabled: busy || name.trim().length === 0 || documents.length >= config.policy.maxDocuments, onChange: (event) => {
|
|
102
|
+
const file = event.target.files?.[0];
|
|
103
|
+
if (file !== undefined)
|
|
104
|
+
void importFile(file);
|
|
105
|
+
event.target.value = "";
|
|
106
|
+
} })] }), _jsxs("p", { children: [documents.length, " / ", config.policy.maxDocuments, " saved scenes"] }), error === null ? null : _jsx("p", { role: "alert", children: error }), _jsx("ul", { children: documents.map((value) => _jsxs("li", { children: [value.name, " ", _jsxs("button", { type: "button", disabled: busy, onClick: () => void reopen(value.id), children: ["Edit ", value.name] })] }, value.id)) })] }));
|
|
107
|
+
}
|
package/dist/DebugDraw.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Html } from "@react-three/drei";
|
|
3
3
|
import { memo, useEffect, useMemo } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { markerCatalogId } from "@jgengine/core/world/authoredObjects";
|
|
5
6
|
const DEFAULT_COLORS = {
|
|
6
7
|
player_spawn: "#22d3ee",
|
|
7
8
|
boss: "#f43f5e",
|
|
@@ -103,10 +104,11 @@ const MarkerMesh = memo(function MarkerMesh({ marker, selected, hovered, onSelec
|
|
|
103
104
|
const scale = selected ? 1.28 : hovered ? 1.12 : 1;
|
|
104
105
|
const baseY = groundHeightAt !== undefined ? groundHeightAt(marker.position.x, marker.position.z) : marker.position.y;
|
|
105
106
|
const fill = emphasisColor(selected, hovered, color);
|
|
107
|
+
const modelBacked = markerCatalogId(marker) !== null;
|
|
106
108
|
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) => {
|
|
107
109
|
event.stopPropagation();
|
|
108
110
|
onSelect(marker.id);
|
|
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 }) })] }));
|
|
111
|
+
}, children: [_jsx("mesh", { geometry: sharedSphere, children: _jsx("meshBasicMaterial", { color: fill, transparent: true, opacity: selected ? 1 : hovered ? 0.9 : 1, wireframe: modelBacked, depthWrite: !modelBacked }) }), selected || hovered ? (_jsx("mesh", { geometry: sharedSphere, scale: 1.18, children: _jsx("meshBasicMaterial", { color: fill, transparent: true, opacity: selected ? 0.28 : 0.18, wireframe: modelBacked, depthWrite: false }) })) : null, _jsx("mesh", { position: [0, 1.35, 0], geometry: sharedCone, children: _jsx("meshBasicMaterial", { color: selected || hovered ? fill : color }) })] }));
|
|
110
112
|
});
|
|
111
113
|
/** Shape-true wireframe — ground ring plus vertical profile for spheres, cylinders, and boxes. */
|
|
112
114
|
const VolumeShapeLines = memo(function VolumeShapeLines({ volume, selected, hovered, onSelect, }) {
|
package/dist/EditorApp.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type ComponentType } from "react";
|
|
2
|
-
import type { EditorCatalogDefinition, EditorLayersInput } from "@jgengine/core/editor/index";
|
|
2
|
+
import type { EditorCatalogDefinition, EditorDocument, EditorLayersInput } from "@jgengine/core/editor/index";
|
|
3
3
|
import type { PlayableGame } from "@jgengine/shell/registry";
|
|
4
4
|
import { type EditorNetworkPresenceInput } from "./networkSnapshot.js";
|
|
5
5
|
import { type EditorRunMode } from "./session.js";
|
|
6
|
+
import type { CreatorPolicy } from "@jgengine/core/editor/creatorStorage";
|
|
6
7
|
/** Props for mounting the scene editor over a playable game. */
|
|
7
8
|
export interface EditorAppProps {
|
|
8
9
|
gameId: string;
|
|
@@ -29,6 +30,17 @@ export interface EditorAppProps {
|
|
|
29
30
|
* standalone editor, which has no game to return to.
|
|
30
31
|
*/
|
|
31
32
|
onExitEditor?: () => void;
|
|
33
|
+
/** Build a fresh playable from a captured document for each isolated playtest. */
|
|
34
|
+
createPlaytest?: (document: EditorDocument) => PlayableGame;
|
|
35
|
+
/** Reject document edits before they enter history, live sync, or saves. */
|
|
36
|
+
validateDocument?: (document: EditorDocument) => void;
|
|
37
|
+
/** Disable developer host publication and recovery drafts for a player creator. */
|
|
38
|
+
playerCreator?: boolean;
|
|
39
|
+
/** Catalog entries the player creator may place. */
|
|
40
|
+
allowedAssets?: readonly string[];
|
|
41
|
+
allowedKinds?: readonly string[];
|
|
42
|
+
maxImportBytes?: number;
|
|
43
|
+
creatorPolicy?: CreatorPolicy;
|
|
32
44
|
}
|
|
33
45
|
/** Persists an exported document JSON; resolves with where it landed or why it failed. */
|
|
34
46
|
export type EditorSaveFn = (json: string) => Promise<{
|
|
@@ -37,4 +49,4 @@ export type EditorSaveFn = (json: string) => Promise<{
|
|
|
37
49
|
error?: string;
|
|
38
50
|
}>;
|
|
39
51
|
/** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
|
|
40
|
-
export declare function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor }: EditorAppProps): import("react").JSX.Element;
|
|
52
|
+
export declare function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor, createPlaytest, validateDocument, playerCreator, allowedAssets, allowedKinds, maxImportBytes, creatorPolicy }: EditorAppProps): import("react").JSX.Element;
|
package/dist/EditorApp.js
CHANGED
|
@@ -27,6 +27,8 @@ import { createEditorHost } from "./session.js";
|
|
|
27
27
|
import { createEditorUiStore, } from "./uiStore.js";
|
|
28
28
|
import { useF2Chord } from "./useF2Chord.js";
|
|
29
29
|
import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
|
|
30
|
+
import { EditorDocumentSaveContext, useDocumentSave } from "./useDocumentSave.js";
|
|
31
|
+
import { PlayModeBar } from "./shell/PlayModeBar.js";
|
|
30
32
|
function endpointSaver(gameId) {
|
|
31
33
|
const endpoint = getSaveEndpoint();
|
|
32
34
|
if (endpoint === null)
|
|
@@ -245,7 +247,7 @@ function resolveEditorCamera(document) {
|
|
|
245
247
|
};
|
|
246
248
|
}
|
|
247
249
|
/** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
|
|
248
|
-
export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor }) {
|
|
250
|
+
export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor, createPlaytest, validateDocument, playerCreator, allowedAssets, allowedKinds, maxImportBytes, creatorPolicy }) {
|
|
249
251
|
const resolvedModeChip = modeChip === undefined ? EditorModeChip : modeChip;
|
|
250
252
|
const saveFn = useMemo(() => save ?? endpointSaver(gameId), [save, gameId]);
|
|
251
253
|
const networkSnapshot = useMemo(() => buildEditorNetworkSnapshot({
|
|
@@ -263,13 +265,13 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip,
|
|
|
263
265
|
const readout = readoutStoreRef.current;
|
|
264
266
|
const catalogAssets = useMemo(() => {
|
|
265
267
|
try {
|
|
266
|
-
const ids = playable.game.assets.ids();
|
|
268
|
+
const ids = playable.game.assets.ids().filter((id) => allowedAssets === undefined || allowedAssets.includes(id));
|
|
267
269
|
return assetsFromCatalog(ids, (id) => playable.game.assets.resolve(id));
|
|
268
270
|
}
|
|
269
271
|
catch {
|
|
270
272
|
return [];
|
|
271
273
|
}
|
|
272
|
-
}, [playable]);
|
|
274
|
+
}, [playable, allowedAssets]);
|
|
273
275
|
const resolvedLayers = layers ?? playable.editorLayers;
|
|
274
276
|
const host = useMemo(() => {
|
|
275
277
|
const created = createEditorHost({
|
|
@@ -277,18 +279,25 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip,
|
|
|
277
279
|
layers: resolvedLayers,
|
|
278
280
|
catalogs,
|
|
279
281
|
assets: catalogAssets,
|
|
282
|
+
validateDocument,
|
|
283
|
+
isolatedPlaytest: createPlaytest !== undefined,
|
|
284
|
+
publishGlobal: playerCreator !== true,
|
|
285
|
+
persistAssetUrls: playerCreator !== true,
|
|
280
286
|
});
|
|
281
287
|
return {
|
|
282
288
|
...created,
|
|
283
289
|
baselineJson: created.session.exportJson(true),
|
|
284
290
|
baselineDocument: created.session.getState().document,
|
|
285
291
|
};
|
|
286
|
-
}, [gameId, resolvedLayers, catalogs, catalogAssets]);
|
|
292
|
+
}, [gameId, resolvedLayers, catalogs, catalogAssets, createPlaytest, validateDocument, playerCreator]);
|
|
287
293
|
useEffect(() => host.dispose, [host]);
|
|
294
|
+
const documentSave = useDocumentSave(host.session, saveFn);
|
|
288
295
|
const modeStore = useMemo(() => ({ getState: host.api.getMode, subscribe: host.api.subscribeMode }), [host]);
|
|
289
296
|
const mode = useStoreSelector(modeStore, (m) => m);
|
|
290
297
|
const [pendingDraft, setPendingDraft] = useState(null);
|
|
291
298
|
useEffect(() => {
|
|
299
|
+
if (playerCreator)
|
|
300
|
+
return;
|
|
292
301
|
const draft = readDraft(gameId);
|
|
293
302
|
if (draft !== null && draft !== host.baselineJson)
|
|
294
303
|
setPendingDraft(draft);
|
|
@@ -306,7 +315,7 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip,
|
|
|
306
315
|
clearTimeout(timer);
|
|
307
316
|
unsubscribe();
|
|
308
317
|
};
|
|
309
|
-
}, [gameId, host]);
|
|
318
|
+
}, [gameId, host, playerCreator]);
|
|
310
319
|
const restoreDraft = () => {
|
|
311
320
|
if (pendingDraft === null)
|
|
312
321
|
return;
|
|
@@ -385,6 +394,10 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip,
|
|
|
385
394
|
}, [gameId]);
|
|
386
395
|
const initialCamera = useMemo(() => resolveEditorCamera(host.session.getState().document), [host]);
|
|
387
396
|
const cameraProjection = useStoreSelector(ui, (state) => state.cameraProjection);
|
|
397
|
+
const playtestPlayable = useMemo(() => {
|
|
398
|
+
const snapshot = host.api.getPlayDocument();
|
|
399
|
+
return mode === "play" && createPlaytest !== undefined && snapshot !== null ? createPlaytest(snapshot) : playable;
|
|
400
|
+
}, [mode, host, createPlaytest, playable]);
|
|
388
401
|
const editorPlayable = useMemo(() => {
|
|
389
402
|
const frozenLoop = {
|
|
390
403
|
onInit: playable.loop.onInit,
|
|
@@ -444,15 +457,16 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip,
|
|
|
444
457
|
};
|
|
445
458
|
}
|
|
446
459
|
if (mode === "play") {
|
|
447
|
-
const
|
|
448
|
-
const
|
|
460
|
+
const playtest = playtestPlayable;
|
|
461
|
+
const BaseUI = playtest.GameUI;
|
|
462
|
+
const BaseOverlay = playtest.WorldOverlay;
|
|
449
463
|
const useBuiltinPlayChrome = resolvedModeChip === EditorModeChip;
|
|
450
464
|
return {
|
|
451
|
-
...
|
|
465
|
+
...playtest,
|
|
452
466
|
GameUI: function EditorPlayUi() {
|
|
453
467
|
const onExit = useCallback(() => host.api.setMode("edit"), []);
|
|
454
468
|
useF2Chord("KeyE", onExit);
|
|
455
|
-
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 }))] }));
|
|
469
|
+
return (_jsxs(_Fragment, { children: [BaseUI !== undefined ? _jsx(BaseUI, {}) : null, playerCreator ? (_jsx(PlayModeBar, { gameId: gameId, api: host.api, onExit: onExit, exitLabel: "Return to editor" })) : useBuiltinPlayChrome ? (_jsx(RuntimePlayInspectorChrome, { gameId: gameId, api: host.api, onExit: onExit })) : (_jsx(EditorModeChipHost, { api: host.api, mode: "play", chip: resolvedModeChip }))] }));
|
|
456
470
|
},
|
|
457
471
|
WorldOverlay: function EditorPlayOverlay({ ctx }) {
|
|
458
472
|
return (_jsxs(_Fragment, { children: [BaseOverlay !== undefined ? _jsx(BaseOverlay, { ctx: ctx }) : null, _jsx(PerfProbe, { api: host.api }), _jsx(RuntimePlayPublisher, { api: host.api })] }));
|
|
@@ -472,8 +486,9 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip,
|
|
|
472
486
|
loop: frozenLoop,
|
|
473
487
|
};
|
|
474
488
|
}
|
|
475
|
-
const
|
|
476
|
-
|
|
489
|
+
const BaseOverlay = playable.WorldOverlay;
|
|
490
|
+
const WorldOverlay = function EditorOverlay({ ctx }) {
|
|
491
|
+
return _jsxs(_Fragment, { children: [BaseOverlay !== undefined ? _jsx(BaseOverlay, { ctx: ctx }) : null, _jsx(EditorWorldOverlay, { api: host.api, ui: ui, world: playable.game.world, readout: readout })] });
|
|
477
492
|
};
|
|
478
493
|
const registerImportedAsset = (id, url) => {
|
|
479
494
|
try {
|
|
@@ -484,7 +499,7 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip,
|
|
|
484
499
|
}
|
|
485
500
|
};
|
|
486
501
|
const GameUI = function EditorUi() {
|
|
487
|
-
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 }));
|
|
502
|
+
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, allowedKinds: allowedKinds, exitLabel: playerCreator ? "Return to saved scenes" : undefined, maxImportBytes: maxImportBytes, draftAutosave: playerCreator !== true, creatorPolicy: creatorPolicy, ...(playerCreator ? { importAsset: async () => { throw new Error("Choose an asset from the approved catalog"); } } : {}) }));
|
|
488
503
|
};
|
|
489
504
|
return {
|
|
490
505
|
...playable,
|
|
@@ -496,10 +511,10 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip,
|
|
|
496
511
|
// WorldOverlay above is the editor layers, not PandoraViewmodel.
|
|
497
512
|
camera: inspectionCamera,
|
|
498
513
|
};
|
|
499
|
-
}, [playable, host, gameId, initialCamera, cameraProjection, catalogAssets, ui, readout, mode, saveFn, resolvedModeChip, networkSnapshot, onExitEditor]);
|
|
514
|
+
}, [playable, host, gameId, initialCamera, cameraProjection, catalogAssets, ui, readout, mode, saveFn, resolvedModeChip, networkSnapshot, onExitEditor, playtestPlayable, allowedKinds, playerCreator, maxImportBytes, creatorPolicy]);
|
|
500
515
|
const showElevation = useStoreSelector(ui, (s) => s.showElevation);
|
|
501
516
|
const onPanelCommit = useCallback((id, panel) => {
|
|
502
517
|
host.session.dispatch({ type: "setUiPanel", id, patch: panel });
|
|
503
518
|
}, [host]);
|
|
504
|
-
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] }));
|
|
519
|
+
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(EditorDocumentSaveContext.Provider, { value: documentSave, children: _jsx(GamePlayerShell, { playable: editorPlayable }, mode) }) }), 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] }));
|
|
505
520
|
}
|
package/dist/EditorChrome.d.ts
CHANGED
|
@@ -4,13 +4,15 @@ import type { EditorHostApi } from "./session.js";
|
|
|
4
4
|
import { type AssetImporter } from "./assetImport.js";
|
|
5
5
|
import { type EditorUiStore } from "./uiStore.js";
|
|
6
6
|
import type { EditorNetworkSnapshot } from "./networkSnapshot.js";
|
|
7
|
+
import { type EditorDocumentSave } from "./useDocumentSave.js";
|
|
8
|
+
import type { CreatorPolicy } from "@jgengine/core/editor/creatorStorage";
|
|
7
9
|
/**
|
|
8
10
|
* The full editor UI shell — global app bar, contextual scene toolbar, workspace rail, resizable
|
|
9
11
|
* hierarchy/inspector docks, tabbed bottom dock (content browser, console, profiler, AI
|
|
10
12
|
* assistant), viewport overlays, and status bar — wired to the session, UI store, layout store,
|
|
11
13
|
* and host RPC. Mounted by `EditorApp`; not a game-author entry point.
|
|
12
14
|
*/
|
|
13
|
-
export declare function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset, onRegisterAsset, onExitEditor, }: {
|
|
15
|
+
export declare function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset, onRegisterAsset, onExitEditor, allowedKinds, exitLabel, maxImportBytes, documentSave, draftAutosave, creatorPolicy, }: {
|
|
14
16
|
gameId: string;
|
|
15
17
|
session: EditorSession;
|
|
16
18
|
api: EditorHostApi;
|
|
@@ -25,6 +27,12 @@ export declare function EditorChrome({ gameId, session, api, assets, ui, baselin
|
|
|
25
27
|
}>;
|
|
26
28
|
/** Closes the editor and returns to the game; set only when summoned over a game. Shows "Exit to game". */
|
|
27
29
|
onExitEditor?: () => void;
|
|
30
|
+
allowedKinds?: readonly string[];
|
|
31
|
+
exitLabel?: string;
|
|
32
|
+
maxImportBytes?: number;
|
|
33
|
+
documentSave?: EditorDocumentSave;
|
|
34
|
+
draftAutosave?: boolean;
|
|
35
|
+
creatorPolicy?: CreatorPolicy;
|
|
28
36
|
/**
|
|
29
37
|
* Network workspace inspection payload (adapter config + optional host presence).
|
|
30
38
|
* Built by `EditorApp` from the game definition; live presence rows only when the host injects them.
|
package/dist/EditorChrome.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useContext, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { editorDocumentSize, editorParentOf, extractEditorFragment, } from "@jgengine/core/editor/index";
|
|
4
4
|
import { scatterRegionEstimate, SCATTER_PATH_KIND } from "@jgengine/core/world/scatterRegion";
|
|
5
5
|
import { editorAssetFromImport, mergeEditorAssets, } from "./AssetBrowser.js";
|
|
@@ -7,6 +7,9 @@ import { CatalogsPanel } from "./CatalogsPanel.js";
|
|
|
7
7
|
import { CollectionsPanel } from "./CollectionsPanel.js";
|
|
8
8
|
import { EditorContextMenu } from "./EditorContextMenu.js";
|
|
9
9
|
import { ParentPickerMenu } from "./ParentPickerMenu.js";
|
|
10
|
+
import { modelWithAuthoredMaterials } from "@jgengine/core/editor/materialAuthoring";
|
|
11
|
+
import { modelWithAuthoredNodeVisibility } from "@jgengine/core/editor/modelNodeVisibility";
|
|
12
|
+
import { MaterialPreview } from "./MaterialPreview.js";
|
|
10
13
|
import { MaterialsWorkspacePanel } from "./MaterialsWorkspacePanel.js";
|
|
11
14
|
import { PrefabsPanel } from "./PrefabsPanel.js";
|
|
12
15
|
import { listParentCandidates } from "./parentCandidates.js";
|
|
@@ -37,6 +40,7 @@ import { createPerfHistoryStore } from "./shell/perfHistory.js";
|
|
|
37
40
|
import { BORDER, FOCUS_RING } from "./shell/theme.js";
|
|
38
41
|
import { IconButton, Kbd, PanelResizer } from "./shell/ui.js";
|
|
39
42
|
import { ScriptingPanel } from "./ScriptingPanel.js";
|
|
43
|
+
import { EditorDocumentSaveContext, useDocumentSave } from "./useDocumentSave.js";
|
|
40
44
|
let clipboardFragment = null;
|
|
41
45
|
const SHORTCUTS = [
|
|
42
46
|
{ keys: "W / E / R", action: "Move · rotate · scale gizmo" },
|
|
@@ -72,34 +76,6 @@ function downloadText(filename, text) {
|
|
|
72
76
|
anchor.click();
|
|
73
77
|
URL.revokeObjectURL(url);
|
|
74
78
|
}
|
|
75
|
-
function useDocumentSave(session, save, onResult) {
|
|
76
|
-
const [saveState, setSaveState] = useState("idle");
|
|
77
|
-
const [saveError, setSaveError] = useState(null);
|
|
78
|
-
const [lastSavedAt, setLastSavedAt] = useState(null);
|
|
79
|
-
const savedDocRef = useRef(session.getState().document);
|
|
80
|
-
const dirty = session.getState().document !== savedDocRef.current;
|
|
81
|
-
const doSave = () => {
|
|
82
|
-
if (save === undefined || saveState === "saving")
|
|
83
|
-
return;
|
|
84
|
-
const document = session.getState().document;
|
|
85
|
-
setSaveState("saving");
|
|
86
|
-
void save(session.exportJson(true)).then((result) => {
|
|
87
|
-
if (result.ok) {
|
|
88
|
-
savedDocRef.current = document;
|
|
89
|
-
setSaveError(null);
|
|
90
|
-
setSaveState("saved");
|
|
91
|
-
setLastSavedAt(Date.now());
|
|
92
|
-
onResult?.(true, result.path === undefined ? "Scene saved" : `Scene saved to ${result.path}`);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
setSaveError(result.error ?? "save failed");
|
|
96
|
-
setSaveState("error");
|
|
97
|
-
onResult?.(false, `Save failed: ${result.error ?? "unknown error"}`);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
};
|
|
101
|
-
return { available: save !== undefined, dirty, saveState, saveError, lastSavedAt, doSave };
|
|
102
|
-
}
|
|
103
79
|
const LEFT_PAGES = [
|
|
104
80
|
{ id: "hierarchy", label: "Hierarchy" },
|
|
105
81
|
{ id: "collections", label: "Sets" },
|
|
@@ -114,7 +90,7 @@ const ANIMATION_DOCK_HEIGHT = 400;
|
|
|
114
90
|
* assistant), viewport overlays, and status bar — wired to the session, UI store, layout store,
|
|
115
91
|
* and host RPC. Mounted by `EditorApp`; not a game-author entry point.
|
|
116
92
|
*/
|
|
117
|
-
export function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset = importAssetToHost, onRegisterAsset, onExitEditor, }) {
|
|
93
|
+
export function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset = importAssetToHost, onRegisterAsset, onExitEditor, allowedKinds, exitLabel, maxImportBytes, documentSave, draftAutosave = true, creatorPolicy, }) {
|
|
118
94
|
const layoutRef = useRef(null);
|
|
119
95
|
layoutRef.current ??= createShellLayoutStore(gameId);
|
|
120
96
|
const layout = layoutRef.current;
|
|
@@ -128,6 +104,8 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineDocumen
|
|
|
128
104
|
/** Models added this session via Content Browser Import / drop — merged over the game catalog. */
|
|
129
105
|
const [importedAssets, setImportedAssets] = useState([]);
|
|
130
106
|
const [importBusy, setImportBusy] = useState(false);
|
|
107
|
+
const [materialInventory, setMaterialInventory] = useState(null);
|
|
108
|
+
const [materialPreviewFailure, setMaterialPreviewFailure] = useState(null);
|
|
131
109
|
// Bridge global RPC/agent console emits into the dock console for this chrome instance.
|
|
132
110
|
useEffect(() => installEditorConsoleSink((severity, source, message) => {
|
|
133
111
|
consoleStore.log(severity, source, message);
|
|
@@ -156,7 +134,44 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineDocumen
|
|
|
156
134
|
const state = session.getState();
|
|
157
135
|
const uiState = ui.getState();
|
|
158
136
|
const layoutState = layout.getState();
|
|
159
|
-
const
|
|
137
|
+
const materialMarker = state.document.markers.find((item) => state.selection.includes(item.id));
|
|
138
|
+
const materialSourceUrl = liveAssets.find((item) => item.id === (materialMarker?.catalogId ?? materialMarker?.meta?.catalogId))?.url;
|
|
139
|
+
const materialPreviewConfig = useMemo(() => {
|
|
140
|
+
try {
|
|
141
|
+
return { model: materialSourceUrl && materialMarker ? modelWithAuthoredNodeVisibility(modelWithAuthoredMaterials({ url: materialSourceUrl, targetHeight: 1.8, animation: "none" }, state.document, materialMarker.id), materialMarker) : undefined, error: null };
|
|
142
|
+
}
|
|
143
|
+
catch (failure) {
|
|
144
|
+
return { model: undefined, error: failure instanceof Error ? failure.message : String(failure) };
|
|
145
|
+
}
|
|
146
|
+
}, [materialSourceUrl, materialMarker, state.document]);
|
|
147
|
+
const materialSourceKey = JSON.stringify([materialMarker?.id, materialSourceUrl, materialPreviewConfig]);
|
|
148
|
+
const materialMarkerId = materialMarker?.id;
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (layoutState.workspace !== "materials" || !materialMarkerId || !materialSourceUrl)
|
|
151
|
+
return;
|
|
152
|
+
if (api.getMaterialSlots(materialMarkerId).status === "ready")
|
|
153
|
+
return;
|
|
154
|
+
api.reportMaterialSlots(materialMarkerId, materialSourceUrl, materialPreviewConfig.error
|
|
155
|
+
? { status: "unavailable", reason: materialPreviewConfig.error }
|
|
156
|
+
: { status: "loading" });
|
|
157
|
+
return () => {
|
|
158
|
+
const current = api.getMaterialSlots(materialMarkerId);
|
|
159
|
+
if (current.sourceUrl === materialSourceUrl && current.status === "loading")
|
|
160
|
+
api.reportMaterialSlots(materialMarkerId, materialSourceUrl, { status: "unavailable", reason: "The model preview closed before slot inspection completed." });
|
|
161
|
+
};
|
|
162
|
+
}, [api, materialMarkerId, materialSourceUrl, layoutState.workspace, materialPreviewConfig.error]);
|
|
163
|
+
const reportMaterialSlots = useCallback((slots) => {
|
|
164
|
+
if (materialMarkerId && materialSourceUrl && api.reportMaterialSlots(materialMarkerId, materialSourceUrl, { status: "ready", slots }))
|
|
165
|
+
setMaterialInventory({ source: materialSourceKey, slots });
|
|
166
|
+
}, [api, materialMarkerId, materialSourceUrl, materialSourceKey]);
|
|
167
|
+
const reportMaterialError = useCallback((message) => {
|
|
168
|
+
setMaterialPreviewFailure({ source: materialSourceKey, message });
|
|
169
|
+
if (message && materialMarkerId && materialSourceUrl && api.getMaterialSlots(materialMarkerId).status !== "ready")
|
|
170
|
+
api.reportMaterialSlots(materialMarkerId, materialSourceUrl, { status: "unavailable", reason: message });
|
|
171
|
+
}, [api, materialMarkerId, materialSourceUrl, materialSourceKey]);
|
|
172
|
+
const sharedSave = useContext(EditorDocumentSaveContext) ?? documentSave;
|
|
173
|
+
const localSave = useDocumentSave(session, sharedSave === undefined ? save : undefined, (ok, detail) => consoleStore.log(ok ? "info" : "error", "save", detail));
|
|
174
|
+
const docSave = sharedSave ?? localSave;
|
|
160
175
|
const docSaveRef = useRef(docSave);
|
|
161
176
|
docSaveRef.current = docSave;
|
|
162
177
|
const outlinerGroups = useMemo(() => buildOutlinerGroups(state.document), [state.document]);
|
|
@@ -577,6 +592,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineDocumen
|
|
|
577
592
|
}
|
|
578
593
|
}, [importAsset, importBusy, layout, notify, onRegisterAsset]);
|
|
579
594
|
const importFile = (file) => {
|
|
595
|
+
if (maxImportBytes !== undefined && file.size > maxImportBytes) {
|
|
596
|
+
notify("Import failed: scene exceeds byte budget", "error");
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
580
599
|
void file.text().then((text) => {
|
|
581
600
|
try {
|
|
582
601
|
session.dispatch({ type: "importJson", json: text });
|
|
@@ -681,12 +700,12 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineDocumen
|
|
|
681
700
|
const railActive = uiState.tool === "terrain" ? "terrain" : layoutState.workspace;
|
|
682
701
|
// Only advertise the exit chord when there is a game to return to.
|
|
683
702
|
const shortcuts = onExitEditor !== undefined ? [...SHORTCUTS, { keys: "F2+Q", action: "Exit editor to game" }] : SHORTCUTS;
|
|
684
|
-
return (_jsxs("div", { className: "pointer-events-none absolute inset-0 z-50 flex flex-col text-xs text-neutral-100", children: [_jsx(TopAppBar, { gameId: gameId, onExitToGame: onExitEditor, dirty: dirty, saveState: docSave.saveState, lastSavedAt: docSave.lastSavedAt, saveAvailable: docSave.available, saveError: docSave.saveError, canUndo: session.canUndo(), canRedo: session.canRedo(), onUndo: () => session.dispatch({ type: "undo" }), onRedo: () => session.dispatch({ type: "redo" }), onSave: docSave.doSave, onPlay: () => api.setMode("play"), onWalk: () => api.setMode("walk"), onHud: () => api.setMode("hud"), onImport: () => importInputRef.current?.click(), onExport: exportJson, onCopyJson: copyExportJson, onOpenPalette: () => setPaletteQuery(""), onToggleHelp: () => setHelpOpen((value) => !value), onResetLayout: () => layout.reset() }), _jsx("input", { ref: importInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: (event) => {
|
|
703
|
+
return (_jsxs("div", { className: "pointer-events-none absolute inset-0 z-50 flex flex-col text-xs text-neutral-100", children: [_jsx(TopAppBar, { exitLabel: exitLabel, gameId: gameId, onExitToGame: onExitEditor, dirty: dirty, saveState: docSave.saveState, lastSavedAt: docSave.lastSavedAt, saveAvailable: docSave.available, saveError: docSave.saveError, canUndo: session.canUndo(), canRedo: session.canRedo(), onUndo: () => session.dispatch({ type: "undo" }), onRedo: () => session.dispatch({ type: "redo" }), onSave: docSave.doSave, onPlay: () => api.setMode("play"), onWalk: () => api.setMode("walk"), onHud: () => api.setMode("hud"), onImport: () => importInputRef.current?.click(), onExport: exportJson, onCopyJson: copyExportJson, onOpenPalette: () => setPaletteQuery(""), onToggleHelp: () => setHelpOpen((value) => !value), onResetLayout: () => layout.reset() }), _jsx("input", { ref: importInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: (event) => {
|
|
685
704
|
const file = event.target.files?.[0];
|
|
686
705
|
if (file !== undefined)
|
|
687
706
|
importFile(file);
|
|
688
707
|
event.target.value = "";
|
|
689
|
-
} }), _jsx(SceneToolbar, { tool: uiState.tool, gizmoMode: uiState.gizmoMode, gizmoSpace: uiState.gizmoSpace, gizmoPivot: uiState.gizmoPivot, snapMode: uiState.snapMode, gridSize: uiState.gridSize, rotationSnapDeg: uiState.rotationSnapDeg, scaleSnap: uiState.scaleSnap, cameraProjection: uiState.cameraProjection, showGrid: uiState.showGrid, showContours: uiState.showContours, showSurfaceGrid: uiState.showSurfaceGrid, showElevation: uiState.showElevation, placementActive: placement !== null, onSetTool: (tool) => ui.setTool(tool), onSetGizmoMode: (mode) => ui.patch({ gizmoMode: mode }), onSetGizmoSpace: (space) => ui.patch({ gizmoSpace: space }), onSetGizmoPivot: (pivot) => ui.patch({ gizmoPivot: pivot }), onSetSnapMode: (mode) => ui.patch({ snapMode: mode }), onSetGridSize: (size) => ui.patch({ gridSize: size }), onSetRotationSnapDeg: (deg) => ui.patch({ rotationSnapDeg: deg }), onSetScaleSnap: (snap) => ui.patch({ scaleSnap: snap }), onSetCameraProjection: (projection) => ui.patch({ cameraProjection: projection }), onToggleGrid: () => ui.patch({ showGrid: !uiState.showGrid }), onToggleContours: () => ui.patch({ showContours: !uiState.showContours }), onToggleSurfaceGrid: () => ui.patch({ showSurfaceGrid: !uiState.showSurfaceGrid }), onToggleElevation: () => ui.patch({ showElevation: !uiState.showElevation }), onFrame: () => {
|
|
708
|
+
} }), _jsx(SceneToolbar, { allowedKinds: allowedKinds, tool: uiState.tool, gizmoMode: uiState.gizmoMode, gizmoSpace: uiState.gizmoSpace, gizmoPivot: uiState.gizmoPivot, snapMode: uiState.snapMode, gridSize: uiState.gridSize, rotationSnapDeg: uiState.rotationSnapDeg, scaleSnap: uiState.scaleSnap, cameraProjection: uiState.cameraProjection, showGrid: uiState.showGrid, showContours: uiState.showContours, showSurfaceGrid: uiState.showSurfaceGrid, showElevation: uiState.showElevation, placementActive: placement !== null, onSetTool: (tool) => ui.setTool(tool), onSetGizmoMode: (mode) => ui.patch({ gizmoMode: mode }), onSetGizmoSpace: (space) => ui.patch({ gizmoSpace: space }), onSetGizmoPivot: (pivot) => ui.patch({ gizmoPivot: pivot }), onSetSnapMode: (mode) => ui.patch({ snapMode: mode }), onSetGridSize: (size) => ui.patch({ gridSize: size }), onSetRotationSnapDeg: (deg) => ui.patch({ rotationSnapDeg: deg }), onSetScaleSnap: (snap) => ui.patch({ scaleSnap: snap }), onSetCameraProjection: (projection) => ui.patch({ cameraProjection: projection }), onToggleGrid: () => ui.patch({ showGrid: !uiState.showGrid }), onToggleContours: () => ui.patch({ showContours: !uiState.showContours }), onToggleSurfaceGrid: () => ui.patch({ showSurfaceGrid: !uiState.showSurfaceGrid }), onToggleElevation: () => ui.patch({ showElevation: !uiState.showElevation }), onFrame: () => {
|
|
690
709
|
const selected = session.getState().selection[0];
|
|
691
710
|
if (selected !== undefined)
|
|
692
711
|
api.handle({ method: "camera_goto", id: selected });
|
|
@@ -708,7 +727,7 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineDocumen
|
|
|
708
727
|
: null;
|
|
709
728
|
const selected = layoutState.leftPage === page.id;
|
|
710
729
|
return (_jsxs("button", { type: "button", role: "tab", "aria-selected": selected, onClick: () => layout.patch({ leftPage: page.id }), className: `flex h-6.5 items-center gap-1 rounded-[5px] px-2 text-[11px] transition-colors ${FOCUS_RING} ${selected ? "bg-white/[0.08] text-neutral-100" : "text-neutral-500 hover:bg-white/[0.04] hover:text-neutral-300"}`, children: [page.label, badge !== null && badge > 0 ? _jsx("span", { className: "text-[9px] tabular-nums text-neutral-500", children: badge }) : null] }, page.id));
|
|
711
|
-
})), _jsx(IconButton, { icon: "close", label: "Collapse left panel", size: 11, tone: "ghost", className: "ml-auto", onClick: () => layout.patch({ leftOpen: false }) })] }), layoutState.workspace === "multiplayer" ? (_jsx(NetworkWorkspacePanel, { snapshot: networkSnapshot })) : layoutState.workspace === "materials" ? (_jsx(MaterialsWorkspacePanel, { session: session, api: api })) : layoutState.workspace === "scripting" ? (_jsx(ScriptingPanel, { session: session, api: api })) : layoutState.leftPage === "collections" ? (_jsx(CollectionsPanel, { session: session })) : layoutState.leftPage === "prefabs" ? (_jsx(PrefabsPanel, { session: session, api: api })) : layoutState.leftPage === "catalogs" ? (_jsx(CatalogsPanel, { session: session, definitions: api.getCatalogDefinitions() })) : (_jsx(HierarchyPanel, { session: session, api: api, onAdd: () => setPaletteQuery("add "), onRowContextMenu: (point, id) => {
|
|
730
|
+
})), _jsx(IconButton, { icon: "close", label: "Collapse left panel", size: 11, tone: "ghost", className: "ml-auto", onClick: () => layout.patch({ leftOpen: false }) })] }), layoutState.workspace === "multiplayer" ? (_jsx(NetworkWorkspacePanel, { snapshot: networkSnapshot })) : layoutState.workspace === "materials" ? (_jsx(MaterialsWorkspacePanel, { session: session, api: api, materialSlots: materialInventory?.source === materialSourceKey ? materialInventory.slots : [], previewError: materialPreviewConfig.error ?? (materialPreviewFailure?.source === materialSourceKey ? materialPreviewFailure.message : null), onSave: docSave.available ? docSave.doSave : undefined, preview: (asset, mode) => materialPreviewConfig.error || (!materialPreviewConfig.model && !asset) ? null : _jsx(MaterialPreview, { asset: asset, mode: mode, model: materialPreviewConfig.model, onSlots: reportMaterialSlots, onError: reportMaterialError, environment: state.document.environment }) })) : layoutState.workspace === "scripting" ? (_jsx(ScriptingPanel, { session: session, api: api })) : layoutState.leftPage === "collections" ? (_jsx(CollectionsPanel, { session: session })) : layoutState.leftPage === "prefabs" ? (_jsx(PrefabsPanel, { session: session, api: api })) : layoutState.leftPage === "catalogs" ? (_jsx(CatalogsPanel, { session: session, definitions: api.getCatalogDefinitions() })) : (_jsx(HierarchyPanel, { session: session, api: api, onAdd: () => setPaletteQuery("add "), onRowContextMenu: (point, id) => {
|
|
712
731
|
const current = session.getState().selection;
|
|
713
732
|
if (!current.includes(id))
|
|
714
733
|
session.dispatch({ type: "select", ids: [id] });
|
|
@@ -756,5 +775,5 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineDocumen
|
|
|
756
775
|
["profiler", "Profiler"],
|
|
757
776
|
["animation", "Animation"],
|
|
758
777
|
["assistant", "AI Assistant"],
|
|
759
|
-
].map(([id, label]) => (_jsx("button", { type: "button", onClick: () => openBottomTab(id), className: `rounded-[4px] px-2 py-0.5 text-[10px] text-neutral-500 transition-colors hover:bg-white/[0.05] hover:text-neutral-300 ${FOCUS_RING}`, children: label }, id))), _jsx("span", { className: "ml-auto text-[9px] text-neutral-600", children: "Ctrl+B opens the content browser" })] }))] }), layoutState.rightOpen ? (_jsxs(_Fragment, { children: [_jsx(PanelResizer, { orientation: "vertical", label: "Resize inspector panel", sign: -1, onResize: (delta) => layout.resize("rightWidth", delta) }), _jsx("aside", { className: `pointer-events-auto hidden min-h-0 flex-col overflow-hidden border-l ${BORDER} bg-[#111318] md:flex`, style: { width: layoutState.rightWidth }, "aria-label": "Inspector dock", children: _jsx(InspectorPanel, { session: session, ui: ui, api: api, assets: liveAssets, tab: layoutState.inspectorTab, onSelectTab: (tab) => layout.patch({ inspectorTab: tab }), collapsed: layoutState.collapsed, onToggleSection: (id) => layout.toggleSection(id), onClose: () => layout.patch({ rightOpen: false }) }) })] })) : null] }), _jsx(StatusBar, { api: api, history: perfHistory, objects: sceneStats.objects, foliage: sceneStats.foliage, selectionCount: state.selection.length, autosave:
|
|
778
|
+
].map(([id, label]) => (_jsx("button", { type: "button", onClick: () => openBottomTab(id), className: `rounded-[4px] px-2 py-0.5 text-[10px] text-neutral-500 transition-colors hover:bg-white/[0.05] hover:text-neutral-300 ${FOCUS_RING}`, children: label }, id))), _jsx("span", { className: "ml-auto text-[9px] text-neutral-600", children: "Ctrl+B opens the content browser" })] }))] }), layoutState.rightOpen ? (_jsxs(_Fragment, { children: [_jsx(PanelResizer, { orientation: "vertical", label: "Resize inspector panel", sign: -1, onResize: (delta) => layout.resize("rightWidth", delta) }), _jsx("aside", { className: `pointer-events-auto hidden min-h-0 flex-col overflow-hidden border-l ${BORDER} bg-[#111318] md:flex`, style: { width: layoutState.rightWidth }, "aria-label": "Inspector dock", children: _jsx(InspectorPanel, { creatorPolicy: creatorPolicy, session: session, ui: ui, api: api, assets: liveAssets, tab: layoutState.inspectorTab, onSelectTab: (tab) => layout.patch({ inspectorTab: tab }), collapsed: layoutState.collapsed, onToggleSection: (id) => layout.toggleSection(id), onClose: () => layout.patch({ rightOpen: false }) }) })] })) : null] }), _jsx(StatusBar, { api: api, history: perfHistory, objects: sceneStats.objects, foliage: sceneStats.foliage, selectionCount: state.selection.length, autosave: draftAutosave }), paletteQuery !== null ? (_jsx(CommandPalette, { commands: paletteCommands, initialQuery: paletteQuery, onClose: () => setPaletteQuery(null) })) : null, helpOpen ? (_jsx("div", { className: "pointer-events-auto absolute inset-0 z-[70] flex items-center justify-center bg-black/50 backdrop-blur-[2px]", onClick: () => setHelpOpen(false), children: _jsxs("div", { className: "w-[28rem] max-w-[90vw] rounded-[10px] border border-white/10 bg-[#14171d] p-5 shadow-2xl shadow-black/60", onClick: (event) => event.stopPropagation(), role: "dialog", "aria-label": "Keyboard shortcuts", children: [_jsxs("div", { className: "mb-3 flex items-center", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-semibold tracking-wide text-neutral-100", children: [_jsx(Icon, { name: "command", size: 15, className: "text-cyan-300" }), "Keyboard shortcuts"] }), _jsx(IconButton, { icon: "close", label: "Close shortcuts", tone: "ghost", className: "ml-auto", onClick: () => setHelpOpen(false) })] }), _jsx("div", { className: "grid max-h-[60vh] grid-cols-[auto_1fr] items-center gap-x-4 gap-y-1.5 overflow-auto", children: shortcuts.map((entry) => (_jsxs("div", { className: "contents", children: [_jsx("span", { className: "justify-self-start", children: _jsx(Kbd, { children: entry.keys }) }), _jsx("span", { className: "text-neutral-300", children: entry.action })] }, entry.keys))) })] }) })) : null] }));
|
|
760
779
|
}
|
package/dist/InspectorPanel.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CreatorPolicy } from "@jgengine/core/editor/creatorStorage";
|
|
1
2
|
import { type EditorSession } from "@jgengine/core/editor/index";
|
|
2
3
|
import { type EditorAssetEntry } from "./AssetBrowser.js";
|
|
3
4
|
import type { EditorHostApi } from "./session.js";
|
|
@@ -10,9 +11,10 @@ import type { InspectorTab } from "./shell/layoutStore.js";
|
|
|
10
11
|
* `pathPoint` slice, so UI-only churn never rerenders it on `EditorChrome`'s render tick.
|
|
11
12
|
* @internal — mounted by `EditorChrome` inside the right dock.
|
|
12
13
|
*/
|
|
13
|
-
export declare function InspectorPanel({ session, ui, onClose, api, assets, tab, onSelectTab, collapsed, onToggleSection, }: {
|
|
14
|
+
export declare function InspectorPanel({ session, ui, onClose, api, assets, tab, onSelectTab, collapsed, onToggleSection, creatorPolicy, }: {
|
|
14
15
|
session: EditorSession;
|
|
15
16
|
ui: EditorUiStore;
|
|
17
|
+
creatorPolicy?: CreatorPolicy;
|
|
16
18
|
onClose?: () => void;
|
|
17
19
|
/** Host API for RPC-backed actions (material assignment, camera). */
|
|
18
20
|
api?: EditorHostApi;
|