@jgengine/editor 0.11.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 +26 -0
- 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 +147 -35
- package/dist/EditorChrome.d.ts +25 -6
- package/dist/EditorChrome.js +310 -130
- package/dist/InspectorPanel.d.ts +17 -7
- package/dist/InspectorPanel.js +173 -27
- package/dist/LightingPanel.d.ts +11 -0
- package/dist/LightingPanel.js +99 -0
- package/dist/MaterialsWorkspacePanel.d.ts +12 -0
- package/dist/MaterialsWorkspacePanel.js +86 -0
- package/dist/ParentPickerMenu.d.ts +13 -0
- package/dist/ParentPickerMenu.js +33 -0
- package/dist/PerfProbe.js +33 -0
- package/dist/PrefabsPanel.js +5 -5
- package/dist/RuntimePlayBridge.d.ts +5 -3
- package/dist/RuntimePlayBridge.js +6 -13
- package/dist/ScatterPreview.d.ts +3 -1
- package/dist/ScatterPreview.js +10 -7
- package/dist/SchemaInspector.js +21 -8
- package/dist/ScriptingPanel.d.ts +15 -0
- package/dist/ScriptingPanel.js +46 -0
- package/dist/SelectionGizmo.js +114 -30
- package/dist/StandaloneEditor.d.ts +16 -9
- package/dist/StandaloneEditor.js +19 -20
- package/dist/TerrainPanel.d.ts +3 -2
- package/dist/TerrainPanel.js +16 -5
- package/dist/TerrainSculpt.js +98 -59
- package/dist/agent/AgentPanel.d.ts +5 -2
- package/dist/agent/AgentPanel.js +11 -6
- package/dist/agent/toolBridge.js +5 -0
- package/dist/assetImport.d.ts +36 -0
- package/dist/assetImport.js +66 -0
- package/dist/authoredComponentMeta.d.ts +25 -0
- package/dist/authoredComponentMeta.js +65 -0
- package/dist/gizmoPivot.d.ts +4 -0
- package/dist/gizmoPivot.js +34 -0
- package/dist/handlers/catalogs.d.ts +3 -0
- package/dist/handlers/catalogs.js +137 -0
- package/dist/handlers/collections.d.ts +3 -0
- package/dist/handlers/collections.js +57 -0
- package/dist/handlers/context.d.ts +36 -0
- package/dist/handlers/context.js +1 -0
- package/dist/handlers/document.d.ts +3 -0
- package/dist/handlers/document.js +350 -0
- package/dist/handlers/grids.d.ts +3 -0
- package/dist/handlers/grids.js +142 -0
- package/dist/handlers/hierarchy.d.ts +3 -0
- package/dist/handlers/hierarchy.js +55 -0
- package/dist/handlers/index.d.ts +8 -0
- package/dist/handlers/index.js +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 -93
- package/dist/mcp/tools.js +104 -912
- package/dist/networkSnapshot.d.ts +82 -0
- package/dist/networkSnapshot.js +73 -0
- package/dist/parentCandidates.d.ts +12 -0
- package/dist/parentCandidates.js +25 -0
- package/dist/session.d.ts +77 -4
- package/dist/session.js +36 -1017
- package/dist/shell/AnimationPanel.d.ts +17 -0
- package/dist/shell/AnimationPanel.js +134 -0
- package/dist/shell/AssetThumbnail.d.ts +12 -0
- package/dist/shell/AssetThumbnail.js +23 -0
- package/dist/shell/BottomDock.d.ts +25 -0
- package/dist/shell/BottomDock.js +40 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +56 -0
- package/dist/shell/ConsolePanel.d.ts +8 -0
- package/dist/shell/ConsolePanel.js +45 -0
- package/dist/shell/ContentBrowser.d.ts +21 -0
- package/dist/shell/ContentBrowser.js +130 -0
- package/dist/shell/HierarchyPanel.d.ts +26 -0
- package/dist/shell/HierarchyPanel.js +387 -0
- package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
- package/dist/shell/NetworkWorkspacePanel.js +49 -0
- package/dist/shell/PlayModeBar.d.ts +12 -0
- package/dist/shell/PlayModeBar.js +27 -0
- package/dist/shell/ProfilerPanel.d.ts +10 -0
- package/dist/shell/ProfilerPanel.js +41 -0
- package/dist/shell/SceneToolbar.d.ts +43 -0
- package/dist/shell/SceneToolbar.js +122 -0
- package/dist/shell/StatusBar.d.ts +18 -0
- package/dist/shell/StatusBar.js +41 -0
- package/dist/shell/TopAppBar.d.ts +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/uiStore.d.ts +27 -0
- package/dist/uiStore.js +10 -0
- package/dist/viewportContextMenu.d.ts +4 -2
- package/dist/viewportContextMenu.js +5 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-editor/SKILL.md +18 -3
- package/skills/jgengine-editor/api.md +397 -18
- package/skills/jgengine-editor/capabilities.md +46 -0
- package/skills/jgengine-editor/reference.md +87 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/OutlinerPanel.d.ts +0 -14
- package/dist/OutlinerPanel.js +0 -84
- package/skills/jgengine/reference-cartridge.md +0 -72
package/dist/EditorChrome.js
CHANGED
|
@@ -1,67 +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";
|
|
16
|
-
import {
|
|
15
|
+
import { importAssetToHost, loadDroppedAssets, } from "./assetImport.js";
|
|
17
16
|
import { newPlacementId } from "./uiStore.js";
|
|
18
17
|
import { useF2Chord } from "./useF2Chord.js";
|
|
19
|
-
import { BTN, MICRO } from "./chromeStyles.js";
|
|
20
18
|
import { TerrainPanel } from "./TerrainPanel.js";
|
|
19
|
+
import { LightingPanel } from "./LightingPanel.js";
|
|
21
20
|
import { InspectorPanel } from "./InspectorPanel.js";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const [sample, setSample] = useState(null);
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
const timer = setInterval(() => setSample(api.getPerf()), PERF_POLL_MS);
|
|
43
|
-
return () => clearInterval(timer);
|
|
44
|
-
}, [api]);
|
|
45
|
-
return sample;
|
|
46
|
-
}
|
|
47
|
-
function formatTriangles(count) {
|
|
48
|
-
if (count >= 1_000_000)
|
|
49
|
-
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
50
|
-
if (count >= 1_000)
|
|
51
|
-
return `${(count / 1_000).toFixed(0)}k`;
|
|
52
|
-
return String(count);
|
|
53
|
-
}
|
|
54
|
-
const PERF_TONE_CLASS = {
|
|
55
|
-
idle: "text-neutral-500",
|
|
56
|
-
healthy: "text-emerald-400",
|
|
57
|
-
busy: "text-rose-400",
|
|
58
|
-
};
|
|
59
|
-
/** Toolbar perf pill: neutral "idle" when the loop is throttled/at rest, red only for active low fps. */
|
|
60
|
-
function PerfPill({ perf }) {
|
|
61
|
-
const tone = classifyEditorPerf(perf);
|
|
62
|
-
const rate = tone === "idle" ? "idle" : `${perf.fps.toFixed(0)} fps`;
|
|
63
|
-
return (_jsxs("span", { className: `ml-3 ${PERF_TONE_CLASS[tone]}`, children: [rate, " \u00B7 ", perf.drawCalls, " draws \u00B7 ", formatTriangles(perf.triangles), " tris"] }));
|
|
64
|
-
}
|
|
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";
|
|
65
39
|
let clipboardFragment = null;
|
|
66
40
|
const SHORTCUTS = [
|
|
67
41
|
{ keys: "W / E / R", action: "Move · rotate · scale gizmo" },
|
|
@@ -69,13 +43,15 @@ const SHORTCUTS = [
|
|
|
69
43
|
{ keys: "F", action: "Frame selection (or whole scene)" },
|
|
70
44
|
{ keys: "G", action: "Toggle reference grid" },
|
|
71
45
|
{ keys: "N", action: "Cycle instances of selected row" },
|
|
46
|
+
{ keys: "Ctrl+K", action: "Command palette" },
|
|
72
47
|
{ keys: "Arrows", action: "Nudge selection on X/Z (Shift ×5)" },
|
|
73
48
|
{ keys: "PgUp / PgDn", action: "Nudge selection on Y (Shift ×5)" },
|
|
74
49
|
{ keys: "Ctrl+A", action: "Select all visible objects" },
|
|
75
50
|
{ keys: "Ctrl+C / X / V", action: "Copy · cut · paste selection" },
|
|
76
51
|
{ keys: "Ctrl+D", action: "Duplicate selection" },
|
|
77
52
|
{ keys: "Ctrl+Z / Y", action: "Undo · redo" },
|
|
78
|
-
{ keys: "Ctrl+B", action: "
|
|
53
|
+
{ keys: "Ctrl+B", action: "Content browser" },
|
|
54
|
+
{ keys: "Ctrl+S", action: "Save scene" },
|
|
79
55
|
{ keys: "Delete", action: "Remove selection" },
|
|
80
56
|
{ keys: "Enter / Esc", action: "Finish · cancel path drawing" },
|
|
81
57
|
{ keys: "Shift+click", action: "Multi-select · keep placing" },
|
|
@@ -95,9 +71,10 @@ function downloadText(filename, text) {
|
|
|
95
71
|
anchor.click();
|
|
96
72
|
URL.revokeObjectURL(url);
|
|
97
73
|
}
|
|
98
|
-
function useDocumentSave(session, save) {
|
|
74
|
+
function useDocumentSave(session, save, onResult) {
|
|
99
75
|
const [saveState, setSaveState] = useState("idle");
|
|
100
76
|
const [saveError, setSaveError] = useState(null);
|
|
77
|
+
const [lastSavedAt, setLastSavedAt] = useState(null);
|
|
101
78
|
const savedDocRef = useRef(session.getState().document);
|
|
102
79
|
const dirty = session.getState().document !== savedDocRef.current;
|
|
103
80
|
const doSave = () => {
|
|
@@ -110,29 +87,55 @@ function useDocumentSave(session, save) {
|
|
|
110
87
|
savedDocRef.current = document;
|
|
111
88
|
setSaveError(null);
|
|
112
89
|
setSaveState("saved");
|
|
90
|
+
setLastSavedAt(Date.now());
|
|
91
|
+
onResult?.(true, result.path === undefined ? "Scene saved" : `Scene saved to ${result.path}`);
|
|
113
92
|
}
|
|
114
93
|
else {
|
|
115
94
|
setSaveError(result.error ?? "save failed");
|
|
116
95
|
setSaveState("error");
|
|
96
|
+
onResult?.(false, `Save failed: ${result.error ?? "unknown error"}`);
|
|
117
97
|
}
|
|
118
98
|
});
|
|
119
99
|
};
|
|
120
|
-
return { available: save !== undefined, dirty, saveState, saveError, doSave };
|
|
100
|
+
return { available: save !== undefined, dirty, saveState, saveError, lastSavedAt, doSave };
|
|
121
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
|
+
];
|
|
122
108
|
/**
|
|
123
|
-
* The full editor UI shell —
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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.
|
|
126
113
|
*/
|
|
127
|
-
export function EditorChrome({ gameId, session, api, assets, ui,
|
|
114
|
+
export function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset = importAssetToHost, onRegisterAsset, }) {
|
|
128
115
|
const [, setTick] = useState(0);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
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]);
|
|
135
137
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
138
|
+
const [paletteQuery, setPaletteQuery] = useState(null);
|
|
136
139
|
const [toast, setToast] = useState(null);
|
|
137
140
|
const toastTimer = useRef(null);
|
|
138
141
|
const notify = useCallback((text, tone = "info") => {
|
|
@@ -140,18 +143,17 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
140
143
|
clearTimeout(toastTimer.current);
|
|
141
144
|
setToast({ text, tone });
|
|
142
145
|
toastTimer.current = setTimeout(() => setToast(null), 2600);
|
|
143
|
-
|
|
146
|
+
consoleStore.log(tone === "error" ? "error" : "info", "ui", text);
|
|
147
|
+
}, [consoleStore]);
|
|
144
148
|
useEffect(() => () => {
|
|
145
149
|
if (toastTimer.current !== null)
|
|
146
150
|
clearTimeout(toastTimer.current);
|
|
147
151
|
}, []);
|
|
148
152
|
const importInputRef = useRef(null);
|
|
149
153
|
const state = session.getState();
|
|
150
|
-
const visibility = api.getVisibility();
|
|
151
|
-
const perf = usePerfSample(api);
|
|
152
154
|
const uiState = ui.getState();
|
|
153
|
-
const
|
|
154
|
-
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));
|
|
155
157
|
const docSaveRef = useRef(docSave);
|
|
156
158
|
docSaveRef.current = docSave;
|
|
157
159
|
const outlinerGroups = useMemo(() => buildOutlinerGroups(state.document), [state.document]);
|
|
@@ -166,16 +168,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
166
168
|
foliage += scatterRegionEstimate(path).count;
|
|
167
169
|
return { objects, foliage };
|
|
168
170
|
}, [state.document]);
|
|
169
|
-
const showPanel = (panel) => {
|
|
170
|
-
setActivePanel(panel);
|
|
171
|
-
if (panel === "assets")
|
|
172
|
-
setBottomOpen(true);
|
|
173
|
-
else
|
|
174
|
-
setLeftOpen(true);
|
|
175
|
-
};
|
|
176
171
|
useF2Chord("KeyE", () => api.setMode("play"));
|
|
177
172
|
useEffect(() => session.subscribe(() => setTick((value) => value + 1)), [session]);
|
|
178
173
|
useEffect(() => ui.subscribe(() => setTick((value) => value + 1)), [ui]);
|
|
174
|
+
useEffect(() => layout.subscribe(() => setTick((value) => value + 1)), [layout]);
|
|
179
175
|
const copySelection = useCallback(() => {
|
|
180
176
|
const current = session.getState();
|
|
181
177
|
if (current.selection.length === 0)
|
|
@@ -194,6 +190,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
194
190
|
if (ui.getState().contextMenu !== null)
|
|
195
191
|
ui.patch({ contextMenu: null });
|
|
196
192
|
}, [ui]);
|
|
193
|
+
const [parentPicker, setParentPicker] = useState(null);
|
|
194
|
+
const openBottomTab = useCallback((tab) => {
|
|
195
|
+
layout.patch({ bottomOpen: true, bottomTab: tab });
|
|
196
|
+
}, [layout]);
|
|
197
197
|
const runContextAction = useCallback((action) => {
|
|
198
198
|
const menu = ui.getState().contextMenu;
|
|
199
199
|
closeContextMenu();
|
|
@@ -246,10 +246,29 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
246
246
|
const id = `prefab_${Date.now().toString(36)}`;
|
|
247
247
|
const name = `Prefab ${current.document.prefabs.length + 1}`;
|
|
248
248
|
api.handle({ method: "create_prefab", id, name, ids: [...selection] });
|
|
249
|
-
|
|
249
|
+
layout.patch({ leftOpen: true, leftPage: "prefabs" });
|
|
250
250
|
notify(`Created prefab “${name}”`);
|
|
251
251
|
}
|
|
252
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
|
+
}
|
|
253
272
|
case "addMarker":
|
|
254
273
|
if (ground !== null) {
|
|
255
274
|
session.dispatch({
|
|
@@ -301,10 +320,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
301
320
|
}
|
|
302
321
|
return;
|
|
303
322
|
case "openAssets":
|
|
304
|
-
|
|
323
|
+
openBottomTab("content");
|
|
305
324
|
return;
|
|
306
325
|
}
|
|
307
|
-
}, [api, closeContextMenu, copySelection, notify, session, ui]);
|
|
326
|
+
}, [api, closeContextMenu, copySelection, layout, notify, openBottomTab, session, ui]);
|
|
308
327
|
useEffect(() => {
|
|
309
328
|
const onKey = (event) => {
|
|
310
329
|
if (event.defaultPrevented)
|
|
@@ -314,6 +333,11 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
314
333
|
docSaveRef.current.doSave();
|
|
315
334
|
return;
|
|
316
335
|
}
|
|
336
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
|
|
337
|
+
event.preventDefault();
|
|
338
|
+
setPaletteQuery((value) => (value === null ? "" : null));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
317
341
|
const target = event.target;
|
|
318
342
|
const isTyping = target instanceof HTMLInputElement ||
|
|
319
343
|
target instanceof HTMLTextAreaElement ||
|
|
@@ -335,18 +359,26 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
335
359
|
}
|
|
336
360
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "b") {
|
|
337
361
|
event.preventDefault();
|
|
338
|
-
|
|
362
|
+
openBottomTab("content");
|
|
339
363
|
return;
|
|
340
364
|
}
|
|
341
365
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a") {
|
|
342
366
|
event.preventDefault();
|
|
343
|
-
const
|
|
367
|
+
const current = session.getState();
|
|
344
368
|
const visibility = api.getVisibility();
|
|
345
369
|
const ids = [
|
|
346
|
-
...
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
...
|
|
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
|
+
: []),
|
|
350
382
|
];
|
|
351
383
|
if (ids.length > 0)
|
|
352
384
|
session.dispatch({ type: "select", ids });
|
|
@@ -387,6 +419,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
387
419
|
}
|
|
388
420
|
if (event.key === "Escape") {
|
|
389
421
|
setHelpOpen(false);
|
|
422
|
+
if (paletteQuery !== null) {
|
|
423
|
+
setPaletteQuery(null);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
390
426
|
const current = ui.getState();
|
|
391
427
|
if (current.contextMenu !== null) {
|
|
392
428
|
ui.patch({ contextMenu: null });
|
|
@@ -479,17 +515,7 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
479
515
|
};
|
|
480
516
|
window.addEventListener("keydown", onKey);
|
|
481
517
|
return () => window.removeEventListener("keydown", onKey);
|
|
482
|
-
}, [session, ui, api, notify, copySelection]);
|
|
483
|
-
const kinds = useMemo(() => listEditorKinds(state.document), [state.document]);
|
|
484
|
-
const allKinds = useMemo(() => [
|
|
485
|
-
...new Set([
|
|
486
|
-
...kinds.markers,
|
|
487
|
-
...kinds.volumes,
|
|
488
|
-
...kinds.paths,
|
|
489
|
-
...(state.document.annotations.length > 0 ? ["note"] : []),
|
|
490
|
-
]),
|
|
491
|
-
].sort(), [kinds, state.document.annotations.length]);
|
|
492
|
-
const studioKinds = useMemo(() => listSceneKinds().filter((definition) => definition.addCategory !== undefined), []);
|
|
518
|
+
}, [session, ui, api, notify, copySelection, openBottomTab, paletteQuery]);
|
|
493
519
|
const placeAsset = (entry) => {
|
|
494
520
|
const focus = api.getFocusTarget();
|
|
495
521
|
const focused = session.getState();
|
|
@@ -497,7 +523,7 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
497
523
|
const selected = focused.document.markers.find((marker) => marker.id === focusedId)?.position ??
|
|
498
524
|
focused.document.volumes.find((volume) => volume.id === focusedId)?.center;
|
|
499
525
|
const position = focus ?? selected ?? { x: 0, y: 0, z: 0 };
|
|
500
|
-
api.handle({
|
|
526
|
+
const response = api.handle({
|
|
501
527
|
method: "place_asset",
|
|
502
528
|
id: entry.id,
|
|
503
529
|
kind: entry.kind === "model" ? "prop" : entry.kind,
|
|
@@ -505,16 +531,61 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
505
531
|
y: position.y,
|
|
506
532
|
z: position.z,
|
|
507
533
|
});
|
|
534
|
+
if (response.ok)
|
|
535
|
+
notify(`Placed ${entry.label}`);
|
|
536
|
+
else
|
|
537
|
+
notify(`Place failed: ${response.error ?? "unknown error"}`, "error");
|
|
508
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]);
|
|
509
577
|
const importFile = (file) => {
|
|
510
578
|
void file.text().then((text) => {
|
|
511
579
|
try {
|
|
512
580
|
session.dispatch({ type: "importJson", json: text });
|
|
513
581
|
const doc = session.getState().document;
|
|
514
582
|
notify(`Imported ${editorDocumentSize(doc)} objects from ${file.name}`);
|
|
583
|
+
consoleStore.log("info", "import", `Imported ${editorDocumentSize(doc)} objects from ${file.name}`);
|
|
515
584
|
}
|
|
516
585
|
catch (error) {
|
|
517
|
-
|
|
586
|
+
const message = error instanceof Error ? error.message : "invalid JSON";
|
|
587
|
+
notify(`Import failed: ${message}`, "error");
|
|
588
|
+
consoleStore.log("error", "import", `Import failed: ${message}`);
|
|
518
589
|
}
|
|
519
590
|
});
|
|
520
591
|
};
|
|
@@ -527,45 +598,154 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
527
598
|
notify("Clipboard unavailable — use Export instead", "error");
|
|
528
599
|
}
|
|
529
600
|
};
|
|
530
|
-
const
|
|
531
|
-
|
|
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;
|
|
532
610
|
const startPlacement = (tool) => {
|
|
533
|
-
setAddOpen(false);
|
|
534
611
|
ui.startPlacement(tool);
|
|
535
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]);
|
|
536
668
|
const placement = uiState.placement;
|
|
537
669
|
const placementHint = placement === null
|
|
538
670
|
? null
|
|
539
671
|
: placement.tool === "path"
|
|
540
672
|
? `Drawing ${placement.kind}: click to add points (${uiState.pathDraft.length}) · Enter finish · Esc cancel`
|
|
541
673
|
: `Placing ${placement.tool === "note" ? "note" : placement.kind}: click the world · Shift-click places more · Esc cancel`;
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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] }));
|
|
571
751
|
}
|