@jgengine/editor 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/README.md +12 -0
- package/dist/AssetBrowser.d.ts +26 -1
- package/dist/AssetBrowser.js +54 -1
- package/dist/AssetDropZone.d.ts +9 -0
- package/dist/AssetDropZone.js +65 -0
- package/dist/CatalogsPanel.d.ts +4 -2
- package/dist/CatalogsPanel.js +162 -9
- package/dist/CityPreview.d.ts +11 -0
- package/dist/CityPreview.js +45 -0
- package/dist/CollectionsPanel.js +5 -5
- package/dist/DebugDraw.d.ts +3 -1
- package/dist/DebugDraw.js +37 -19
- package/dist/EditorApp.d.ts +9 -1
- package/dist/EditorApp.js +208 -37
- package/dist/EditorChrome.d.ts +25 -6
- package/dist/EditorChrome.js +310 -118
- package/dist/InspectorPanel.d.ts +17 -7
- package/dist/InspectorPanel.js +173 -27
- package/dist/LightingPanel.d.ts +11 -0
- package/dist/LightingPanel.js +99 -0
- package/dist/MaterialsWorkspacePanel.d.ts +12 -0
- package/dist/MaterialsWorkspacePanel.js +86 -0
- package/dist/ParentPickerMenu.d.ts +13 -0
- package/dist/ParentPickerMenu.js +33 -0
- package/dist/PerfProbe.js +69 -2
- package/dist/PrefabsPanel.js +5 -5
- package/dist/RuntimePlayBridge.d.ts +5 -3
- package/dist/RuntimePlayBridge.js +6 -13
- package/dist/ScatterPreview.d.ts +3 -1
- package/dist/ScatterPreview.js +10 -7
- package/dist/SchemaInspector.js +21 -8
- package/dist/ScriptingPanel.d.ts +15 -0
- package/dist/ScriptingPanel.js +46 -0
- package/dist/SelectionGizmo.js +114 -30
- package/dist/StandaloneEditor.d.ts +16 -9
- package/dist/StandaloneEditor.js +19 -20
- package/dist/TerrainPanel.d.ts +3 -2
- package/dist/TerrainPanel.js +16 -5
- package/dist/TerrainReadout.d.ts +17 -0
- package/dist/TerrainReadout.js +163 -0
- package/dist/TerrainReadoutHud.d.ts +9 -0
- package/dist/TerrainReadoutHud.js +20 -0
- package/dist/TerrainSculpt.js +98 -59
- package/dist/agent/AgentPanel.d.ts +5 -2
- package/dist/agent/AgentPanel.js +11 -6
- package/dist/agent/toolBridge.js +5 -0
- package/dist/assetImport.d.ts +36 -0
- package/dist/assetImport.js +66 -0
- package/dist/authoredComponentMeta.d.ts +25 -0
- package/dist/authoredComponentMeta.js +65 -0
- package/dist/gizmoPivot.d.ts +4 -0
- package/dist/gizmoPivot.js +34 -0
- package/dist/handlers/catalogs.d.ts +3 -0
- package/dist/handlers/catalogs.js +137 -0
- package/dist/handlers/collections.d.ts +3 -0
- package/dist/handlers/collections.js +57 -0
- package/dist/handlers/context.d.ts +36 -0
- package/dist/handlers/context.js +1 -0
- package/dist/handlers/document.d.ts +3 -0
- package/dist/handlers/document.js +350 -0
- package/dist/handlers/grids.d.ts +3 -0
- package/dist/handlers/grids.js +142 -0
- package/dist/handlers/hierarchy.d.ts +3 -0
- package/dist/handlers/hierarchy.js +55 -0
- package/dist/handlers/index.d.ts +8 -0
- package/dist/handlers/index.js +23 -0
- package/dist/handlers/minimap.d.ts +9 -0
- package/dist/handlers/minimap.js +32 -0
- package/dist/handlers/runtime.d.ts +3 -0
- package/dist/handlers/runtime.js +191 -0
- package/dist/handlers/terrain.d.ts +3 -0
- package/dist/handlers/terrain.js +204 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/listDocumentTriggers.d.ts +42 -0
- package/dist/listDocumentTriggers.js +49 -0
- package/dist/materialAssignments.d.ts +63 -0
- package/dist/materialAssignments.js +84 -0
- package/dist/mcp/fieldSpec.d.ts +32 -0
- package/dist/mcp/fieldSpec.js +173 -0
- package/dist/mcp/rpcRequest.d.ts +8 -5
- package/dist/mcp/rpcRequest.js +72 -83
- package/dist/mcp/tools.js +104 -763
- package/dist/networkSnapshot.d.ts +82 -0
- package/dist/networkSnapshot.js +73 -0
- package/dist/parentCandidates.d.ts +12 -0
- package/dist/parentCandidates.js +25 -0
- package/dist/perfPill.d.ts +21 -0
- package/dist/perfPill.js +20 -0
- package/dist/session.d.ts +167 -5
- package/dist/session.js +36 -879
- package/dist/shell/AnimationPanel.d.ts +17 -0
- package/dist/shell/AnimationPanel.js +134 -0
- package/dist/shell/AssetThumbnail.d.ts +12 -0
- package/dist/shell/AssetThumbnail.js +23 -0
- package/dist/shell/BottomDock.d.ts +25 -0
- package/dist/shell/BottomDock.js +40 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +56 -0
- package/dist/shell/ConsolePanel.d.ts +8 -0
- package/dist/shell/ConsolePanel.js +45 -0
- package/dist/shell/ContentBrowser.d.ts +21 -0
- package/dist/shell/ContentBrowser.js +130 -0
- package/dist/shell/HierarchyPanel.d.ts +26 -0
- package/dist/shell/HierarchyPanel.js +387 -0
- package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
- package/dist/shell/NetworkWorkspacePanel.js +49 -0
- package/dist/shell/PlayModeBar.d.ts +12 -0
- package/dist/shell/PlayModeBar.js +27 -0
- package/dist/shell/ProfilerPanel.d.ts +10 -0
- package/dist/shell/ProfilerPanel.js +41 -0
- package/dist/shell/SceneToolbar.d.ts +43 -0
- package/dist/shell/SceneToolbar.js +122 -0
- package/dist/shell/StatusBar.d.ts +18 -0
- package/dist/shell/StatusBar.js +41 -0
- package/dist/shell/TopAppBar.d.ts +33 -0
- package/dist/shell/TopAppBar.js +64 -0
- package/dist/shell/ViewportOverlays.d.ts +23 -0
- package/dist/shell/ViewportOverlays.js +136 -0
- package/dist/shell/WorkspaceRail.d.ts +20 -0
- package/dist/shell/WorkspaceRail.js +31 -0
- package/dist/shell/cameraTelemetry.d.ts +27 -0
- package/dist/shell/cameraTelemetry.js +24 -0
- package/dist/shell/commandRegistry.d.ts +58 -0
- package/dist/shell/commandRegistry.js +133 -0
- package/dist/shell/consoleSink.d.ts +11 -0
- package/dist/shell/consoleSink.js +17 -0
- package/dist/shell/consoleStore.d.ts +27 -0
- package/dist/shell/consoleStore.js +43 -0
- package/dist/shell/fields.d.ts +39 -0
- package/dist/shell/fields.js +104 -0
- package/dist/shell/formatSavedRelative.d.ts +2 -0
- package/dist/shell/formatSavedRelative.js +14 -0
- package/dist/shell/glbThumbnailCache.d.ts +45 -0
- package/dist/shell/glbThumbnailCache.js +241 -0
- package/dist/shell/hierarchyA11y.d.ts +21 -0
- package/dist/shell/hierarchyA11y.js +35 -0
- package/dist/shell/icons.d.ts +13 -0
- package/dist/shell/icons.js +85 -0
- package/dist/shell/layoutStore.d.ts +68 -0
- package/dist/shell/layoutStore.js +156 -0
- package/dist/shell/pathFlythrough.d.ts +37 -0
- package/dist/shell/pathFlythrough.js +54 -0
- package/dist/shell/perfHistory.d.ts +70 -0
- package/dist/shell/perfHistory.js +130 -0
- package/dist/shell/renameObject.d.ts +3 -0
- package/dist/shell/renameObject.js +25 -0
- package/dist/shell/theme.d.ts +36 -0
- package/dist/shell/theme.js +37 -0
- package/dist/shell/ui.d.ts +84 -0
- package/dist/shell/ui.js +90 -0
- package/dist/shell/useGlbThumbnail.d.ts +8 -0
- package/dist/shell/useGlbThumbnail.js +19 -0
- package/dist/terrainReadoutStore.d.ts +19 -0
- package/dist/terrainReadoutStore.js +36 -0
- package/dist/uiStore.d.ts +33 -0
- package/dist/uiStore.js +13 -0
- package/dist/viewportContextMenu.d.ts +4 -2
- package/dist/viewportContextMenu.js +5 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +35 -804
- package/skills/jgengine/api.md +190 -79
- package/skills/jgengine/capabilities.md +112 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-editor/SKILL.md +29 -362
- package/skills/jgengine-editor/api.md +519 -18
- package/skills/jgengine-editor/capabilities.md +91 -0
- package/skills/jgengine-editor/reference.md +113 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/OutlinerPanel.d.ts +0 -14
- package/dist/OutlinerPanel.js +0 -84
- package/skills/jgengine/reference-cartridge.md +0 -72
- package/skills/jgengine/reference-orphans.md +0 -9
- package/skills/jgengine/reference-systems.md +0 -60
package/dist/EditorChrome.js
CHANGED
|
@@ -1,55 +1,41 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { editorDocumentSize,
|
|
4
|
-
import { VEGETATION_VOLUME_KIND } from "@jgengine/core/world/vegetation";
|
|
3
|
+
import { editorDocumentSize, editorParentOf, extractEditorFragment, } from "@jgengine/core/editor/index";
|
|
5
4
|
import { scatterRegionEstimate, SCATTER_PATH_KIND } from "@jgengine/core/world/scatterRegion";
|
|
6
|
-
import {
|
|
7
|
-
import { AssetBrowser } from "./AssetBrowser.js";
|
|
8
|
-
import { AgentPanel } from "./agent/AgentPanel.js";
|
|
5
|
+
import { editorAssetFromImport, mergeEditorAssets, } from "./AssetBrowser.js";
|
|
9
6
|
import { CatalogsPanel } from "./CatalogsPanel.js";
|
|
10
7
|
import { CollectionsPanel } from "./CollectionsPanel.js";
|
|
11
8
|
import { EditorContextMenu } from "./EditorContextMenu.js";
|
|
12
|
-
import {
|
|
9
|
+
import { ParentPickerMenu } from "./ParentPickerMenu.js";
|
|
10
|
+
import { MaterialsWorkspacePanel } from "./MaterialsWorkspacePanel.js";
|
|
13
11
|
import { PrefabsPanel } from "./PrefabsPanel.js";
|
|
12
|
+
import { listParentCandidates } from "./parentCandidates.js";
|
|
14
13
|
import { buildEditorContextMenu, } from "./viewportContextMenu.js";
|
|
15
14
|
import { buildOutlinerGroups } from "./outlinerModel.js";
|
|
15
|
+
import { importAssetToHost, loadDroppedAssets, } from "./assetImport.js";
|
|
16
16
|
import { newPlacementId } from "./uiStore.js";
|
|
17
17
|
import { useF2Chord } from "./useF2Chord.js";
|
|
18
|
-
import { BTN, MICRO } from "./chromeStyles.js";
|
|
19
18
|
import { TerrainPanel } from "./TerrainPanel.js";
|
|
19
|
+
import { LightingPanel } from "./LightingPanel.js";
|
|
20
20
|
import { InspectorPanel } from "./InspectorPanel.js";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const [sample, setSample] = useState(null);
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
const timer = setInterval(() => setSample(api.getPerf()), PERF_POLL_MS);
|
|
42
|
-
return () => clearInterval(timer);
|
|
43
|
-
}, [api]);
|
|
44
|
-
return sample;
|
|
45
|
-
}
|
|
46
|
-
function formatTriangles(count) {
|
|
47
|
-
if (count >= 1_000_000)
|
|
48
|
-
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
49
|
-
if (count >= 1_000)
|
|
50
|
-
return `${(count / 1_000).toFixed(0)}k`;
|
|
51
|
-
return String(count);
|
|
52
|
-
}
|
|
21
|
+
import { BottomDock } from "./shell/BottomDock.js";
|
|
22
|
+
import { CommandPalette } from "./shell/CommandPalette.js";
|
|
23
|
+
import { HierarchyPanel } from "./shell/HierarchyPanel.js";
|
|
24
|
+
import { SceneToolbar } from "./shell/SceneToolbar.js";
|
|
25
|
+
import { StatusBar } from "./shell/StatusBar.js";
|
|
26
|
+
import { TopAppBar } from "./shell/TopAppBar.js";
|
|
27
|
+
import { OrientationWidget, PerformanceOverlay, ViewportUtilityPanel } from "./shell/ViewportOverlays.js";
|
|
28
|
+
import { NetworkWorkspacePanel } from "./shell/NetworkWorkspacePanel.js";
|
|
29
|
+
import { WorkspaceRail } from "./shell/WorkspaceRail.js";
|
|
30
|
+
import { buildPaletteCommands } from "./shell/commandRegistry.js";
|
|
31
|
+
import { createEditorConsoleStore } from "./shell/consoleStore.js";
|
|
32
|
+
import { installEditorConsoleSink } from "./shell/consoleSink.js";
|
|
33
|
+
import { Icon } from "./shell/icons.js";
|
|
34
|
+
import { createShellLayoutStore, } from "./shell/layoutStore.js";
|
|
35
|
+
import { createPerfHistoryStore } from "./shell/perfHistory.js";
|
|
36
|
+
import { BORDER, FOCUS_RING } from "./shell/theme.js";
|
|
37
|
+
import { IconButton, Kbd, PanelResizer } from "./shell/ui.js";
|
|
38
|
+
import { ScriptingPanel } from "./ScriptingPanel.js";
|
|
53
39
|
let clipboardFragment = null;
|
|
54
40
|
const SHORTCUTS = [
|
|
55
41
|
{ keys: "W / E / R", action: "Move · rotate · scale gizmo" },
|
|
@@ -57,13 +43,15 @@ const SHORTCUTS = [
|
|
|
57
43
|
{ keys: "F", action: "Frame selection (or whole scene)" },
|
|
58
44
|
{ keys: "G", action: "Toggle reference grid" },
|
|
59
45
|
{ keys: "N", action: "Cycle instances of selected row" },
|
|
46
|
+
{ keys: "Ctrl+K", action: "Command palette" },
|
|
60
47
|
{ keys: "Arrows", action: "Nudge selection on X/Z (Shift ×5)" },
|
|
61
48
|
{ keys: "PgUp / PgDn", action: "Nudge selection on Y (Shift ×5)" },
|
|
62
49
|
{ keys: "Ctrl+A", action: "Select all visible objects" },
|
|
63
50
|
{ keys: "Ctrl+C / X / V", action: "Copy · cut · paste selection" },
|
|
64
51
|
{ keys: "Ctrl+D", action: "Duplicate selection" },
|
|
65
52
|
{ keys: "Ctrl+Z / Y", action: "Undo · redo" },
|
|
66
|
-
{ keys: "Ctrl+B", action: "
|
|
53
|
+
{ keys: "Ctrl+B", action: "Content browser" },
|
|
54
|
+
{ keys: "Ctrl+S", action: "Save scene" },
|
|
67
55
|
{ keys: "Delete", action: "Remove selection" },
|
|
68
56
|
{ keys: "Enter / Esc", action: "Finish · cancel path drawing" },
|
|
69
57
|
{ keys: "Shift+click", action: "Multi-select · keep placing" },
|
|
@@ -83,9 +71,10 @@ function downloadText(filename, text) {
|
|
|
83
71
|
anchor.click();
|
|
84
72
|
URL.revokeObjectURL(url);
|
|
85
73
|
}
|
|
86
|
-
function useDocumentSave(session, save) {
|
|
74
|
+
function useDocumentSave(session, save, onResult) {
|
|
87
75
|
const [saveState, setSaveState] = useState("idle");
|
|
88
76
|
const [saveError, setSaveError] = useState(null);
|
|
77
|
+
const [lastSavedAt, setLastSavedAt] = useState(null);
|
|
89
78
|
const savedDocRef = useRef(session.getState().document);
|
|
90
79
|
const dirty = session.getState().document !== savedDocRef.current;
|
|
91
80
|
const doSave = () => {
|
|
@@ -98,29 +87,55 @@ function useDocumentSave(session, save) {
|
|
|
98
87
|
savedDocRef.current = document;
|
|
99
88
|
setSaveError(null);
|
|
100
89
|
setSaveState("saved");
|
|
90
|
+
setLastSavedAt(Date.now());
|
|
91
|
+
onResult?.(true, result.path === undefined ? "Scene saved" : `Scene saved to ${result.path}`);
|
|
101
92
|
}
|
|
102
93
|
else {
|
|
103
94
|
setSaveError(result.error ?? "save failed");
|
|
104
95
|
setSaveState("error");
|
|
96
|
+
onResult?.(false, `Save failed: ${result.error ?? "unknown error"}`);
|
|
105
97
|
}
|
|
106
98
|
});
|
|
107
99
|
};
|
|
108
|
-
return { available: save !== undefined, dirty, saveState, saveError, doSave };
|
|
100
|
+
return { available: save !== undefined, dirty, saveState, saveError, lastSavedAt, doSave };
|
|
109
101
|
}
|
|
102
|
+
const LEFT_PAGES = [
|
|
103
|
+
{ id: "hierarchy", label: "Hierarchy" },
|
|
104
|
+
{ id: "collections", label: "Sets" },
|
|
105
|
+
{ id: "prefabs", label: "Prefabs" },
|
|
106
|
+
{ id: "catalogs", label: "Data" },
|
|
107
|
+
];
|
|
110
108
|
/**
|
|
111
|
-
* The full editor UI shell —
|
|
112
|
-
*
|
|
113
|
-
*
|
|
109
|
+
* The full editor UI shell — global app bar, contextual scene toolbar, workspace rail, resizable
|
|
110
|
+
* hierarchy/inspector docks, tabbed bottom dock (content browser, console, profiler, AI
|
|
111
|
+
* assistant), viewport overlays, and status bar — wired to the session, UI store, layout store,
|
|
112
|
+
* and host RPC. Mounted by `EditorApp`; not a game-author entry point.
|
|
114
113
|
*/
|
|
115
|
-
export function EditorChrome({ gameId, session, api, assets, ui,
|
|
114
|
+
export function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset = importAssetToHost, onRegisterAsset, }) {
|
|
116
115
|
const [, setTick] = useState(0);
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
const
|
|
116
|
+
const layoutRef = useRef(null);
|
|
117
|
+
layoutRef.current ??= createShellLayoutStore(gameId);
|
|
118
|
+
const layout = layoutRef.current;
|
|
119
|
+
const consoleRef = useRef(null);
|
|
120
|
+
consoleRef.current ??= createEditorConsoleStore();
|
|
121
|
+
const consoleStore = consoleRef.current;
|
|
122
|
+
const perfHistoryRef = useRef(null);
|
|
123
|
+
perfHistoryRef.current ??= createPerfHistoryStore();
|
|
124
|
+
const perfHistory = perfHistoryRef.current;
|
|
125
|
+
/** Models added this session via Content Browser Import / drop — merged over the game catalog. */
|
|
126
|
+
const [importedAssets, setImportedAssets] = useState([]);
|
|
127
|
+
const [importBusy, setImportBusy] = useState(false);
|
|
128
|
+
// Bridge global RPC/agent console emits into the dock console for this chrome instance.
|
|
129
|
+
useEffect(() => installEditorConsoleSink((severity, source, message) => {
|
|
130
|
+
consoleStore.log(severity, source, message);
|
|
131
|
+
}), [consoleStore]);
|
|
132
|
+
const liveAssets = useMemo(() => mergeEditorAssets(assets, importedAssets), [assets, importedAssets]);
|
|
133
|
+
// Keep the host asset list in step with the browser so place_asset / list_assets see imports.
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
api.setAssets(liveAssets);
|
|
136
|
+
}, [api, liveAssets]);
|
|
123
137
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
138
|
+
const [paletteQuery, setPaletteQuery] = useState(null);
|
|
124
139
|
const [toast, setToast] = useState(null);
|
|
125
140
|
const toastTimer = useRef(null);
|
|
126
141
|
const notify = useCallback((text, tone = "info") => {
|
|
@@ -128,18 +143,17 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
128
143
|
clearTimeout(toastTimer.current);
|
|
129
144
|
setToast({ text, tone });
|
|
130
145
|
toastTimer.current = setTimeout(() => setToast(null), 2600);
|
|
131
|
-
|
|
146
|
+
consoleStore.log(tone === "error" ? "error" : "info", "ui", text);
|
|
147
|
+
}, [consoleStore]);
|
|
132
148
|
useEffect(() => () => {
|
|
133
149
|
if (toastTimer.current !== null)
|
|
134
150
|
clearTimeout(toastTimer.current);
|
|
135
151
|
}, []);
|
|
136
152
|
const importInputRef = useRef(null);
|
|
137
153
|
const state = session.getState();
|
|
138
|
-
const visibility = api.getVisibility();
|
|
139
|
-
const perf = usePerfSample(api);
|
|
140
154
|
const uiState = ui.getState();
|
|
141
|
-
const
|
|
142
|
-
const docSave = useDocumentSave(session, save);
|
|
155
|
+
const layoutState = layout.getState();
|
|
156
|
+
const docSave = useDocumentSave(session, save, (ok, detail) => consoleStore.log(ok ? "info" : "error", "save", detail));
|
|
143
157
|
const docSaveRef = useRef(docSave);
|
|
144
158
|
docSaveRef.current = docSave;
|
|
145
159
|
const outlinerGroups = useMemo(() => buildOutlinerGroups(state.document), [state.document]);
|
|
@@ -154,16 +168,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
154
168
|
foliage += scatterRegionEstimate(path).count;
|
|
155
169
|
return { objects, foliage };
|
|
156
170
|
}, [state.document]);
|
|
157
|
-
const showPanel = (panel) => {
|
|
158
|
-
setActivePanel(panel);
|
|
159
|
-
if (panel === "assets")
|
|
160
|
-
setBottomOpen(true);
|
|
161
|
-
else
|
|
162
|
-
setLeftOpen(true);
|
|
163
|
-
};
|
|
164
171
|
useF2Chord("KeyE", () => api.setMode("play"));
|
|
165
172
|
useEffect(() => session.subscribe(() => setTick((value) => value + 1)), [session]);
|
|
166
173
|
useEffect(() => ui.subscribe(() => setTick((value) => value + 1)), [ui]);
|
|
174
|
+
useEffect(() => layout.subscribe(() => setTick((value) => value + 1)), [layout]);
|
|
167
175
|
const copySelection = useCallback(() => {
|
|
168
176
|
const current = session.getState();
|
|
169
177
|
if (current.selection.length === 0)
|
|
@@ -182,6 +190,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
182
190
|
if (ui.getState().contextMenu !== null)
|
|
183
191
|
ui.patch({ contextMenu: null });
|
|
184
192
|
}, [ui]);
|
|
193
|
+
const [parentPicker, setParentPicker] = useState(null);
|
|
194
|
+
const openBottomTab = useCallback((tab) => {
|
|
195
|
+
layout.patch({ bottomOpen: true, bottomTab: tab });
|
|
196
|
+
}, [layout]);
|
|
185
197
|
const runContextAction = useCallback((action) => {
|
|
186
198
|
const menu = ui.getState().contextMenu;
|
|
187
199
|
closeContextMenu();
|
|
@@ -234,10 +246,29 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
234
246
|
const id = `prefab_${Date.now().toString(36)}`;
|
|
235
247
|
const name = `Prefab ${current.document.prefabs.length + 1}`;
|
|
236
248
|
api.handle({ method: "create_prefab", id, name, ids: [...selection] });
|
|
237
|
-
|
|
249
|
+
layout.patch({ leftOpen: true, leftPage: "prefabs" });
|
|
238
250
|
notify(`Created prefab “${name}”`);
|
|
239
251
|
}
|
|
240
252
|
return;
|
|
253
|
+
case "parentTo": {
|
|
254
|
+
const ids = selection.length > 0
|
|
255
|
+
? selection
|
|
256
|
+
: menu?.hitId !== null && menu?.hitId !== undefined
|
|
257
|
+
? [menu.hitId]
|
|
258
|
+
: [];
|
|
259
|
+
if (ids.length === 0 || menu === null)
|
|
260
|
+
return;
|
|
261
|
+
setParentPicker({ clientX: menu.clientX, clientY: menu.clientY, ids: [...ids] });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
case "unparent": {
|
|
265
|
+
const ids = selection.length > 0 ? selection : menu?.hitId !== null && menu?.hitId !== undefined ? [menu.hitId] : [];
|
|
266
|
+
if (ids.length === 0)
|
|
267
|
+
return;
|
|
268
|
+
api.handle({ method: "set_parent", ids: [...ids], parentId: null });
|
|
269
|
+
notify(ids.length === 1 ? "Unparented object" : `Unparented ${ids.length} objects`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
241
272
|
case "addMarker":
|
|
242
273
|
if (ground !== null) {
|
|
243
274
|
session.dispatch({
|
|
@@ -289,10 +320,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
289
320
|
}
|
|
290
321
|
return;
|
|
291
322
|
case "openAssets":
|
|
292
|
-
|
|
323
|
+
openBottomTab("content");
|
|
293
324
|
return;
|
|
294
325
|
}
|
|
295
|
-
}, [api, closeContextMenu, copySelection, notify, session, ui]);
|
|
326
|
+
}, [api, closeContextMenu, copySelection, layout, notify, openBottomTab, session, ui]);
|
|
296
327
|
useEffect(() => {
|
|
297
328
|
const onKey = (event) => {
|
|
298
329
|
if (event.defaultPrevented)
|
|
@@ -302,6 +333,11 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
302
333
|
docSaveRef.current.doSave();
|
|
303
334
|
return;
|
|
304
335
|
}
|
|
336
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
|
|
337
|
+
event.preventDefault();
|
|
338
|
+
setPaletteQuery((value) => (value === null ? "" : null));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
305
341
|
const target = event.target;
|
|
306
342
|
const isTyping = target instanceof HTMLInputElement ||
|
|
307
343
|
target instanceof HTMLTextAreaElement ||
|
|
@@ -323,18 +359,26 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
323
359
|
}
|
|
324
360
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "b") {
|
|
325
361
|
event.preventDefault();
|
|
326
|
-
|
|
362
|
+
openBottomTab("content");
|
|
327
363
|
return;
|
|
328
364
|
}
|
|
329
365
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a") {
|
|
330
366
|
event.preventDefault();
|
|
331
|
-
const
|
|
367
|
+
const current = session.getState();
|
|
332
368
|
const visibility = api.getVisibility();
|
|
333
369
|
const ids = [
|
|
334
|
-
...
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
...
|
|
370
|
+
...current.document.markers
|
|
371
|
+
.filter((m) => visibility[m.kind] !== false && m.hidden !== true)
|
|
372
|
+
.map((m) => m.id),
|
|
373
|
+
...current.document.volumes
|
|
374
|
+
.filter((v) => visibility[v.kind] !== false && v.hidden !== true)
|
|
375
|
+
.map((v) => v.id),
|
|
376
|
+
...current.document.paths
|
|
377
|
+
.filter((p) => visibility[p.kind] !== false && p.hidden !== true)
|
|
378
|
+
.map((p) => p.id),
|
|
379
|
+
...(visibility["note"] !== false
|
|
380
|
+
? current.document.annotations.filter((n) => n.hidden !== true).map((n) => n.id)
|
|
381
|
+
: []),
|
|
338
382
|
];
|
|
339
383
|
if (ids.length > 0)
|
|
340
384
|
session.dispatch({ type: "select", ids });
|
|
@@ -375,6 +419,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
375
419
|
}
|
|
376
420
|
if (event.key === "Escape") {
|
|
377
421
|
setHelpOpen(false);
|
|
422
|
+
if (paletteQuery !== null) {
|
|
423
|
+
setPaletteQuery(null);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
378
426
|
const current = ui.getState();
|
|
379
427
|
if (current.contextMenu !== null) {
|
|
380
428
|
ui.patch({ contextMenu: null });
|
|
@@ -467,17 +515,7 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
467
515
|
};
|
|
468
516
|
window.addEventListener("keydown", onKey);
|
|
469
517
|
return () => window.removeEventListener("keydown", onKey);
|
|
470
|
-
}, [session, ui, api, notify, copySelection]);
|
|
471
|
-
const kinds = useMemo(() => listEditorKinds(state.document), [state.document]);
|
|
472
|
-
const allKinds = useMemo(() => [
|
|
473
|
-
...new Set([
|
|
474
|
-
...kinds.markers,
|
|
475
|
-
...kinds.volumes,
|
|
476
|
-
...kinds.paths,
|
|
477
|
-
...(state.document.annotations.length > 0 ? ["note"] : []),
|
|
478
|
-
]),
|
|
479
|
-
].sort(), [kinds, state.document.annotations.length]);
|
|
480
|
-
const studioKinds = useMemo(() => listSceneKinds().filter((definition) => definition.addCategory !== undefined), []);
|
|
518
|
+
}, [session, ui, api, notify, copySelection, openBottomTab, paletteQuery]);
|
|
481
519
|
const placeAsset = (entry) => {
|
|
482
520
|
const focus = api.getFocusTarget();
|
|
483
521
|
const focused = session.getState();
|
|
@@ -485,7 +523,7 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
485
523
|
const selected = focused.document.markers.find((marker) => marker.id === focusedId)?.position ??
|
|
486
524
|
focused.document.volumes.find((volume) => volume.id === focusedId)?.center;
|
|
487
525
|
const position = focus ?? selected ?? { x: 0, y: 0, z: 0 };
|
|
488
|
-
api.handle({
|
|
526
|
+
const response = api.handle({
|
|
489
527
|
method: "place_asset",
|
|
490
528
|
id: entry.id,
|
|
491
529
|
kind: entry.kind === "model" ? "prop" : entry.kind,
|
|
@@ -493,16 +531,61 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
493
531
|
y: position.y,
|
|
494
532
|
z: position.z,
|
|
495
533
|
});
|
|
534
|
+
if (response.ok)
|
|
535
|
+
notify(`Placed ${entry.label}`);
|
|
536
|
+
else
|
|
537
|
+
notify(`Place failed: ${response.error ?? "unknown error"}`, "error");
|
|
496
538
|
};
|
|
539
|
+
const importModels = useCallback(async (files) => {
|
|
540
|
+
if (files.length === 0 || importBusy)
|
|
541
|
+
return;
|
|
542
|
+
setImportBusy(true);
|
|
543
|
+
try {
|
|
544
|
+
const next = await loadDroppedAssets(files, importAsset);
|
|
545
|
+
if (next.length === 0) {
|
|
546
|
+
notify("No .glb / .gltf models found in the drop", "error");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const entries = next.map(editorAssetFromImport);
|
|
550
|
+
for (const entry of entries) {
|
|
551
|
+
if (entry.url !== undefined)
|
|
552
|
+
onRegisterAsset?.(entry.id, entry.url);
|
|
553
|
+
}
|
|
554
|
+
setImportedAssets((current) => mergeEditorAssets(current, entries));
|
|
555
|
+
layout.patch({ bottomOpen: true, bottomTab: "content" });
|
|
556
|
+
const durable = entries.filter((entry) => entry.url !== undefined && !entry.url.startsWith("blob:")).length;
|
|
557
|
+
const ephemeral = entries.length - durable;
|
|
558
|
+
if (ephemeral === 0) {
|
|
559
|
+
notify(entries.length === 1 ? `Imported ${entries[0].label}` : `Imported ${entries.length} models`);
|
|
560
|
+
}
|
|
561
|
+
else if (durable === 0) {
|
|
562
|
+
notify(entries.length === 1
|
|
563
|
+
? `Imported ${entries[0].label} (session-only — no host importer)`
|
|
564
|
+
: `Imported ${entries.length} models (session-only — no host importer)`);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
notify(`Imported ${entries.length} models (${ephemeral} session-only)`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
notify(`Import failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
setImportBusy(false);
|
|
575
|
+
}
|
|
576
|
+
}, [importAsset, importBusy, layout, notify, onRegisterAsset]);
|
|
497
577
|
const importFile = (file) => {
|
|
498
578
|
void file.text().then((text) => {
|
|
499
579
|
try {
|
|
500
580
|
session.dispatch({ type: "importJson", json: text });
|
|
501
581
|
const doc = session.getState().document;
|
|
502
582
|
notify(`Imported ${editorDocumentSize(doc)} objects from ${file.name}`);
|
|
583
|
+
consoleStore.log("info", "import", `Imported ${editorDocumentSize(doc)} objects from ${file.name}`);
|
|
503
584
|
}
|
|
504
585
|
catch (error) {
|
|
505
|
-
|
|
586
|
+
const message = error instanceof Error ? error.message : "invalid JSON";
|
|
587
|
+
notify(`Import failed: ${message}`, "error");
|
|
588
|
+
consoleStore.log("error", "import", `Import failed: ${message}`);
|
|
506
589
|
}
|
|
507
590
|
});
|
|
508
591
|
};
|
|
@@ -515,45 +598,154 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
515
598
|
notify("Clipboard unavailable — use Export instead", "error");
|
|
516
599
|
}
|
|
517
600
|
};
|
|
518
|
-
const
|
|
519
|
-
|
|
601
|
+
const exportJson = () => {
|
|
602
|
+
downloadText(`${gameId}-editor.json`, session.exportJson(true));
|
|
603
|
+
notify(`Exported ${gameId}-editor.json`);
|
|
604
|
+
};
|
|
605
|
+
// Reference compare, not a serialize: stringifying the whole document per edit dispatch burned
|
|
606
|
+
// milliseconds on every nudge/stroke. Undoing back to pristine may leave the dot on (history
|
|
607
|
+
// restores clones), which is the cheap side of wrong for an unsaved-edits hint.
|
|
608
|
+
const baselineDirty = baselineDocument !== undefined && state.document !== baselineDocument;
|
|
609
|
+
const dirty = docSave.available ? docSave.dirty : baselineDirty;
|
|
520
610
|
const startPlacement = (tool) => {
|
|
521
|
-
setAddOpen(false);
|
|
522
611
|
ui.startPlacement(tool);
|
|
523
612
|
};
|
|
613
|
+
const selectWorkspace = (workspace) => {
|
|
614
|
+
layout.setWorkspace(workspace);
|
|
615
|
+
if (workspace === "terrain")
|
|
616
|
+
ui.setTool("terrain");
|
|
617
|
+
else if (ui.getState().tool === "terrain")
|
|
618
|
+
ui.setTool("select");
|
|
619
|
+
};
|
|
620
|
+
const paletteObjects = useMemo(() => {
|
|
621
|
+
const doc = state.document;
|
|
622
|
+
const objects = [];
|
|
623
|
+
for (const marker of doc.markers)
|
|
624
|
+
objects.push({ id: marker.id, label: marker.label ?? marker.id, kind: marker.kind });
|
|
625
|
+
for (const volume of doc.volumes)
|
|
626
|
+
objects.push({ id: volume.id, label: volume.label ?? volume.id, kind: volume.kind });
|
|
627
|
+
for (const path of doc.paths)
|
|
628
|
+
objects.push({ id: path.id, label: path.label ?? path.id, kind: path.kind });
|
|
629
|
+
for (const note of doc.annotations)
|
|
630
|
+
objects.push({ id: note.id, label: note.text.slice(0, 48) || note.id, kind: "note" });
|
|
631
|
+
return objects;
|
|
632
|
+
}, [state.document]);
|
|
633
|
+
const paletteCommands = useMemo(() => buildPaletteCommands({
|
|
634
|
+
setGizmoMode: (mode) => ui.patch({ gizmoMode: mode }),
|
|
635
|
+
setTool: (tool) => ui.setTool(tool),
|
|
636
|
+
toggleGrid: () => ui.patch({ showGrid: !ui.getState().showGrid }),
|
|
637
|
+
toggleContours: () => ui.patch({ showContours: !ui.getState().showContours }),
|
|
638
|
+
toggleSurfaceGrid: () => ui.patch({ showSurfaceGrid: !ui.getState().showSurfaceGrid }),
|
|
639
|
+
toggleElevation: () => ui.patch({ showElevation: !ui.getState().showElevation }),
|
|
640
|
+
setSnapMode: (mode) => ui.patch({ snapMode: mode }),
|
|
641
|
+
frameAll: () => api.handle({ method: "camera_frame" }),
|
|
642
|
+
frameSelection: () => {
|
|
643
|
+
const selected = session.getState().selection[0];
|
|
644
|
+
if (selected !== undefined)
|
|
645
|
+
api.handle({ method: "camera_goto", id: selected });
|
|
646
|
+
else
|
|
647
|
+
api.handle({ method: "camera_frame" });
|
|
648
|
+
},
|
|
649
|
+
undo: () => session.dispatch({ type: "undo" }),
|
|
650
|
+
redo: () => session.dispatch({ type: "redo" }),
|
|
651
|
+
save: () => docSaveRef.current.doSave(),
|
|
652
|
+
exportJson,
|
|
653
|
+
importJson: () => importInputRef.current?.click(),
|
|
654
|
+
copyJson: copyExportJson,
|
|
655
|
+
setMode: (mode) => api.setMode(mode),
|
|
656
|
+
startPlacement,
|
|
657
|
+
openBottomTab,
|
|
658
|
+
toggleLeftDock: () => layout.patch({ leftOpen: !layout.getState().leftOpen }),
|
|
659
|
+
toggleRightDock: () => layout.patch({ rightOpen: !layout.getState().rightOpen }),
|
|
660
|
+
toggleHelp: () => setHelpOpen((value) => !value),
|
|
661
|
+
resetLayout: () => layout.reset(),
|
|
662
|
+
objects: paletteObjects,
|
|
663
|
+
gotoObject: (id) => {
|
|
664
|
+
session.dispatch({ type: "select", ids: [id] });
|
|
665
|
+
api.handle({ method: "camera_goto", id });
|
|
666
|
+
},
|
|
667
|
+
}), [api, layout, openBottomTab, paletteObjects, session, ui]);
|
|
524
668
|
const placement = uiState.placement;
|
|
525
669
|
const placementHint = placement === null
|
|
526
670
|
? null
|
|
527
671
|
: placement.tool === "path"
|
|
528
672
|
? `Drawing ${placement.kind}: click to add points (${uiState.pathDraft.length}) · Enter finish · Esc cancel`
|
|
529
673
|
: `Placing ${placement.tool === "note" ? "note" : placement.kind}: click the world · Shift-click places more · Esc cancel`;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
674
|
+
const railActive = uiState.tool === "terrain" ? "terrain" : layoutState.workspace;
|
|
675
|
+
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, 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) => {
|
|
676
|
+
const file = event.target.files?.[0];
|
|
677
|
+
if (file !== undefined)
|
|
678
|
+
importFile(file);
|
|
679
|
+
event.target.value = "";
|
|
680
|
+
} }), _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: () => {
|
|
681
|
+
const selected = session.getState().selection[0];
|
|
682
|
+
if (selected !== undefined)
|
|
683
|
+
api.handle({ method: "camera_goto", id: selected });
|
|
684
|
+
else
|
|
685
|
+
api.handle({ method: "camera_frame" });
|
|
686
|
+
}, onStartPlacement: startPlacement, onOpenAssistant: () => openBottomTab("assistant"), assistantOpen: layoutState.bottomOpen && layoutState.bottomTab === "assistant" }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx(WorkspaceRail, { active: railActive, onSelect: selectWorkspace }), layoutState.leftOpen ? (_jsxs(_Fragment, { children: [_jsxs("aside", { className: `pointer-events-auto hidden min-h-0 flex-col overflow-hidden border-r ${BORDER} bg-[#111318] sm:flex`, style: { width: layoutState.leftWidth }, "aria-label": layoutState.workspace === "multiplayer"
|
|
687
|
+
? "Network workspace dock"
|
|
688
|
+
: layoutState.workspace === "materials"
|
|
689
|
+
? "Materials workspace dock"
|
|
690
|
+
: layoutState.workspace === "scripting"
|
|
691
|
+
? "Scripting workspace dock"
|
|
692
|
+
: "Scene hierarchy dock", children: [_jsxs("div", { className: `flex h-8 shrink-0 items-center gap-1 border-b ${BORDER} px-1.5`, children: [layoutState.workspace === "multiplayer" ? (_jsx("span", { className: "px-2 text-[11px] font-medium text-neutral-200", children: "Network" })) : layoutState.workspace === "materials" ? (_jsx("span", { className: "px-2 text-[11px] text-neutral-100", children: "Materials" })) : layoutState.workspace === "scripting" ? (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "script", size: 13, className: "ml-1 text-amber-300" }), _jsx("span", { className: "px-1 text-[11px] text-neutral-200", children: "Scripting" })] })) : (LEFT_PAGES.map((page) => {
|
|
693
|
+
const badge = page.id === "collections"
|
|
694
|
+
? state.document.collections.length
|
|
695
|
+
: page.id === "prefabs"
|
|
696
|
+
? state.document.prefabs.length
|
|
697
|
+
: page.id === "catalogs"
|
|
698
|
+
? api.getCatalogDefinitions().length
|
|
699
|
+
: null;
|
|
700
|
+
const selected = layoutState.leftPage === page.id;
|
|
701
|
+
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));
|
|
702
|
+
})), _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) => {
|
|
703
|
+
const current = session.getState().selection;
|
|
704
|
+
if (!current.includes(id))
|
|
705
|
+
session.dispatch({ type: "select", ids: [id] });
|
|
706
|
+
ui.patch({
|
|
707
|
+
contextMenu: {
|
|
708
|
+
clientX: point.clientX,
|
|
709
|
+
clientY: point.clientY,
|
|
710
|
+
hitId: id,
|
|
711
|
+
ground: null,
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
} }))] }), _jsx(PanelResizer, { orientation: "vertical", label: "Resize hierarchy panel", onResize: (delta) => layout.resize("leftWidth", delta) })] })) : null, _jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsxs("main", { className: "pointer-events-none relative min-h-0 min-w-0 flex-1", children: [_jsx(PerformanceOverlay, { api: api }), uiState.tool === "terrain" ? (_jsx(TerrainPanel, { session: session, ui: ui, api: api })) : layoutState.workspace === "lighting" ? (_jsx(LightingPanel, { session: session })) : (_jsx(ViewportUtilityPanel, { document: state.document, api: api, selectionCount: state.selection.length })), _jsx(OrientationWidget, {}), !layoutState.leftOpen ? (_jsx("div", { className: "pointer-events-auto absolute left-2.5 top-1/2 z-30 -translate-y-1/2", children: _jsx(IconButton, { icon: "chevronRight", label: "Open hierarchy panel", onClick: () => layout.patch({ leftOpen: true }) }) })) : null, !layoutState.rightOpen ? (_jsx("div", { className: "pointer-events-auto absolute right-2.5 top-1/2 z-30 -translate-y-1/2", children: _jsx(IconButton, { icon: "panel", label: "Open inspector panel", onClick: () => layout.patch({ rightOpen: true }) }) })) : null, placementHint !== null ? (_jsx("div", { className: "absolute left-1/2 top-2 z-40 -translate-x-1/2 whitespace-nowrap rounded-full border border-cyan-400/30 bg-cyan-950/85 px-4 py-1.5 text-[11px] text-cyan-100 shadow-lg shadow-cyan-950/40 backdrop-blur-md", children: placementHint })) : null, toast !== null ? (_jsx("div", { className: `absolute left-1/2 top-10 z-40 -translate-x-1/2 whitespace-nowrap rounded-full border px-4 py-1.5 text-[11px] shadow-lg backdrop-blur-md ${toast.tone === "error"
|
|
715
|
+
? "border-rose-400/40 bg-rose-950/90 text-rose-100 shadow-rose-950/40"
|
|
716
|
+
: "border-emerald-400/30 bg-emerald-950/90 text-emerald-100 shadow-emerald-950/40"}`, children: toast.text })) : null, uiState.contextMenu !== null ? (_jsx(EditorContextMenu, { x: uiState.contextMenu.clientX, y: uiState.contextMenu.clientY, actions: buildEditorContextMenu({
|
|
717
|
+
hitId: uiState.contextMenu.hitId,
|
|
718
|
+
selection: state.selection,
|
|
719
|
+
canPaste: clipboardFragment !== null,
|
|
720
|
+
canUnparent: (() => {
|
|
721
|
+
const ids = state.selection.length > 0
|
|
722
|
+
? state.selection
|
|
723
|
+
: uiState.contextMenu.hitId !== null
|
|
724
|
+
? [uiState.contextMenu.hitId]
|
|
725
|
+
: [];
|
|
726
|
+
return ids.some((id) => editorParentOf(state.document, id) !== undefined);
|
|
727
|
+
})(),
|
|
728
|
+
}), onPick: runContextAction, onClose: closeContextMenu })) : null, parentPicker !== null ? (_jsx(ParentPickerMenu, { x: parentPicker.clientX, y: parentPicker.clientY, candidates: listParentCandidates(state.document, parentPicker.ids), onPick: (parentId) => {
|
|
729
|
+
const ids = [...parentPicker.ids];
|
|
730
|
+
setParentPicker(null);
|
|
731
|
+
const result = api.handle({ method: "set_parent", ids, parentId });
|
|
732
|
+
if (!result.ok) {
|
|
733
|
+
notify(result.error ?? "Parent to failed", "error");
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (parentId === null) {
|
|
737
|
+
notify(ids.length === 1 ? "Unparented object" : `Unparented ${ids.length} objects`);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
notify(ids.length === 1
|
|
741
|
+
? `Parented to ${parentId}`
|
|
742
|
+
: `Parented ${ids.length} objects to ${parentId}`);
|
|
743
|
+
}
|
|
744
|
+
}, onClose: () => setParentPicker(null) })) : null] }), layoutState.bottomOpen ? (_jsxs(_Fragment, { children: [_jsx(PanelResizer, { orientation: "horizontal", label: "Resize bottom dock", sign: -1, onResize: (delta) => layout.resize("bottomHeight", delta) }), _jsx("div", { className: "flex shrink-0 flex-col", style: { height: layoutState.bottomHeight }, children: _jsx(BottomDock, { tab: layoutState.bottomTab, onSelectTab: (tab) => layout.patch({ bottomTab: tab }), onClose: () => layout.patch({ bottomOpen: false }), assets: liveAssets, session: session, api: api, consoleStore: consoleStore, perfHistory: perfHistory, browserView: layoutState.browserView, onSetBrowserView: (view) => layout.patch({ browserView: view }), onPlaceAsset: placeAsset, onImportModels: importModels, importBusy: importBusy }) })] })) : (_jsxs("div", { className: `pointer-events-auto flex h-7 shrink-0 items-center gap-1 border-t ${BORDER} bg-[#0e1014] px-1.5`, children: [[
|
|
745
|
+
["content", "Content Browser"],
|
|
746
|
+
["console", "Console"],
|
|
747
|
+
["profiler", "Profiler"],
|
|
748
|
+
["animation", "Animation"],
|
|
749
|
+
["assistant", "AI Assistant"],
|
|
750
|
+
].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, 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: true }), 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] }));
|
|
559
751
|
}
|