@jgengine/editor 0.18.1 → 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 +504 -7
- package/dist/ClipPreviewLayer.d.ts +2 -3
- package/dist/ClipPreviewLayer.js +24 -26
- 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 +61 -35
- package/dist/InspectorPanel.d.ts +3 -1
- package/dist/InspectorPanel.js +12 -8
- package/dist/LightingPanel.js +5 -1
- 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 +31 -1
- package/dist/handlers/hierarchy.d.ts +1 -1
- package/dist/handlers/hierarchy.js +4 -0
- package/dist/handlers/navmesh.d.ts +2 -0
- package/dist/handlers/navmesh.js +16 -4
- 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 +13 -4
- 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 +13 -4
- package/dist/modelAnimationAuthoring.d.ts +49 -6
- package/dist/modelAnimationAuthoring.js +118 -64
- package/dist/session.d.ts +64 -0
- package/dist/session.js +58 -7
- package/dist/shell/AnimationGraphPanel.d.ts +16 -0
- package/dist/shell/AnimationGraphPanel.js +180 -0
- package/dist/shell/AnimationPanel.d.ts +3 -1
- package/dist/shell/AnimationPanel.js +9 -7
- 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/WorkspaceRail.js +1 -1
- package/dist/shell/animGraphPreview.d.ts +58 -0
- package/dist/shell/animGraphPreview.js +95 -0
- package/dist/shell/clipPreview.d.ts +9 -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 +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-editor/SKILL.md +12 -2
- package/skills/jgengine-editor/api.md +120 -16
- package/skills/jgengine-editor/capabilities.md +40 -0
- package/skills/jgengine-editor/reference.md +161 -3
- package/skills/jgengine-editor/references/live-sync.md +15 -0
- package/skills/level-design/SKILL.md +1 -1
|
@@ -2,9 +2,8 @@ import type { EditorHostApi } from "./session.js";
|
|
|
2
2
|
import type { EditorUiStore } from "./uiStore.js";
|
|
3
3
|
/**
|
|
4
4
|
* Viewport layer that renders the active clip-preview asset playing the selected clip, mounted by
|
|
5
|
-
* `EditorApp` inside the editor's R3F scene + GameContext. Reuses the shell's
|
|
6
|
-
*
|
|
7
|
-
* the preview IS the runtime playback. Reads the live session from the editor UI store and publishes
|
|
5
|
+
* `EditorApp` inside the editor's R3F scene + GameContext. Reuses the shell's model instance,
|
|
6
|
+
* bind placement and animation lifecycle. Reads the live session from the editor UI store and publishes
|
|
8
7
|
* the selected clip's measured duration back so the dock scrubber can normalize.
|
|
9
8
|
*
|
|
10
9
|
* @internal — not a game-author entry point.
|
package/dist/ClipPreviewLayer.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Suspense, useEffect, useMemo } from "react";
|
|
3
3
|
import { useLoader } from "@react-three/fiber";
|
|
4
|
-
import * as THREE from "three";
|
|
5
4
|
import { sharedGltfLoader } from "@jgengine/shell/render/modelLoad";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { createGraphPose } from "@jgengine/shell/render/useModelAnimation";
|
|
6
|
+
import { useModelInstance } from "@jgengine/shell/render/useModelInstance";
|
|
8
7
|
import { previewAnimationConfig } from "./shell/clipPreview.js";
|
|
9
8
|
import { useStoreSelector } from "./useStoreSelector.js";
|
|
10
9
|
/** World-unit height the previewed model is normalized to, so tiny/huge rigs are both framed. */
|
|
11
10
|
const PREVIEW_TARGET_HEIGHT = 2;
|
|
12
11
|
/**
|
|
13
12
|
* Viewport layer that renders the active clip-preview asset playing the selected clip, mounted by
|
|
14
|
-
* `EditorApp` inside the editor's R3F scene + GameContext. Reuses the shell's
|
|
15
|
-
*
|
|
16
|
-
* the preview IS the runtime playback. Reads the live session from the editor UI store and publishes
|
|
13
|
+
* `EditorApp` inside the editor's R3F scene + GameContext. Reuses the shell's model instance,
|
|
14
|
+
* bind placement and animation lifecycle. Reads the live session from the editor UI store and publishes
|
|
17
15
|
* the selected clip's measured duration back so the dock scrubber can normalize.
|
|
18
16
|
*
|
|
19
17
|
* @internal — not a game-author entry point.
|
|
@@ -27,24 +25,25 @@ export function ClipPreviewLayer({ api, ui }) {
|
|
|
27
25
|
function ClipPreviewModel({ session, api, ui, }) {
|
|
28
26
|
const { source, driver } = session;
|
|
29
27
|
const gltf = useLoader(sharedGltfLoader, source.url);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
28
|
+
const graphPose = session.graphPose;
|
|
29
|
+
const posedByGraph = graphPose !== undefined;
|
|
30
|
+
const config = useMemo(() => (posedByGraph ? undefined : previewAnimationConfig(driver)), [driver, posedByGraph]);
|
|
31
|
+
const { content, scene, scale, position } = useModelInstance({ url: source.url, targetHeight: PREVIEW_TARGET_HEIGHT, animation: config });
|
|
32
|
+
const graph = graphPose?.graph;
|
|
33
|
+
const pose = useMemo(() => (graph === undefined ? null : createGraphPose(content, graph, gltf.animations)), [content, graph, gltf]);
|
|
34
|
+
useEffect(() => () => pose?.dispose(), [pose]);
|
|
35
|
+
const graphClips = graphPose?.clips;
|
|
36
|
+
const rootMotion = graphPose?.rootMotion;
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (pose !== null && graphClips !== undefined)
|
|
39
|
+
pose.apply(graphClips, rootMotion === true);
|
|
40
|
+
}, [pose, graphClips, rootMotion]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const current = ui.getState().clipPreview;
|
|
43
|
+
if (current === null || current.source.assetId !== source.assetId || current.clipDurations !== undefined)
|
|
44
|
+
return;
|
|
45
|
+
ui.patch({ clipPreview: { ...current, clipDurations: Object.fromEntries(gltf.animations.map((clip) => [clip.name, clip.duration])) } });
|
|
46
|
+
}, [gltf, ui, source.assetId]);
|
|
48
47
|
const clipName = driver.clipName;
|
|
49
48
|
useEffect(() => {
|
|
50
49
|
const clip = clipName === null ? undefined : gltf.animations.find((entry) => entry.name === clipName);
|
|
@@ -55,6 +54,5 @@ function ClipPreviewModel({ session, api, ui, }) {
|
|
|
55
54
|
}
|
|
56
55
|
}, [gltf, clipName, ui, source.assetId]);
|
|
57
56
|
const focus = api.getFocusTarget() ?? { x: 0, y: 0, z: 0 };
|
|
58
|
-
|
|
59
|
-
return (_jsx("group", { position: [focus.x, focus.y, focus.z], children: _jsx("primitive", { object: scene, scale: s, position: [-s * placement.centerX, -s * placement.minY, -s * placement.centerZ] }) }));
|
|
57
|
+
return (_jsx("group", { position: [focus.x, focus.y, focus.z], children: _jsx("primitive", { object: scene, scale: scale, position: position }) }));
|
|
60
58
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CreatorConfig as CoreCreatorConfig } from "@jgengine/core/editor/creatorStorage";
|
|
2
|
+
import type { PlayableGame } from "@jgengine/shell/registry";
|
|
3
|
+
/** Game-owned creator policy, durable storage and document-to-runtime composition. */
|
|
4
|
+
export type CreatorConfig = CoreCreatorConfig<PlayableGame>;
|
|
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 declare function CreatorApp({ gameId, config, onExit }: {
|
|
9
|
+
gameId: string;
|
|
10
|
+
config: CreatorConfig;
|
|
11
|
+
onExit: () => void;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -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.
|