@jgengine/editor 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +12 -0
- package/dist/AssetBrowser.d.ts +33 -1
- package/dist/AssetBrowser.js +64 -2
- package/dist/AssetDropZone.d.ts +9 -0
- package/dist/AssetDropZone.js +65 -0
- package/dist/CatalogsPanel.d.ts +4 -2
- package/dist/CatalogsPanel.js +162 -9
- package/dist/CityPreview.d.ts +11 -0
- package/dist/CityPreview.js +45 -0
- package/dist/CollectionsPanel.js +5 -5
- package/dist/DebugDraw.d.ts +3 -1
- package/dist/DebugDraw.js +37 -19
- package/dist/EditorApp.d.ts +15 -1
- package/dist/EditorApp.js +147 -35
- package/dist/EditorChrome.d.ts +27 -6
- package/dist/EditorChrome.js +314 -130
- package/dist/InspectorPanel.d.ts +17 -7
- package/dist/InspectorPanel.js +173 -27
- package/dist/LightingPanel.d.ts +11 -0
- package/dist/LightingPanel.js +99 -0
- package/dist/MaterialsWorkspacePanel.d.ts +12 -0
- package/dist/MaterialsWorkspacePanel.js +86 -0
- package/dist/ParentPickerMenu.d.ts +13 -0
- package/dist/ParentPickerMenu.js +33 -0
- package/dist/PerfProbe.js +33 -0
- package/dist/PrefabsPanel.js +5 -5
- package/dist/RuntimePlayBridge.d.ts +5 -3
- package/dist/RuntimePlayBridge.js +6 -13
- package/dist/ScatterPreview.d.ts +3 -1
- package/dist/ScatterPreview.js +10 -7
- package/dist/SchemaInspector.js +21 -8
- package/dist/ScriptingPanel.d.ts +15 -0
- package/dist/ScriptingPanel.js +46 -0
- package/dist/SelectionGizmo.js +114 -30
- package/dist/StandaloneEditor.d.ts +16 -9
- package/dist/StandaloneEditor.js +19 -20
- package/dist/TerrainPanel.d.ts +3 -2
- package/dist/TerrainPanel.js +16 -5
- package/dist/TerrainSculpt.js +98 -59
- package/dist/agent/AgentPanel.d.ts +5 -2
- package/dist/agent/AgentPanel.js +11 -6
- package/dist/agent/toolBridge.js +5 -0
- package/dist/assetImport.d.ts +36 -0
- package/dist/assetImport.js +66 -0
- package/dist/authoredComponentMeta.d.ts +25 -0
- package/dist/authoredComponentMeta.js +65 -0
- package/dist/gizmoPivot.d.ts +4 -0
- package/dist/gizmoPivot.js +34 -0
- package/dist/handlers/catalogs.d.ts +3 -0
- package/dist/handlers/catalogs.js +137 -0
- package/dist/handlers/collections.d.ts +3 -0
- package/dist/handlers/collections.js +57 -0
- package/dist/handlers/context.d.ts +36 -0
- package/dist/handlers/context.js +1 -0
- package/dist/handlers/document.d.ts +3 -0
- package/dist/handlers/document.js +350 -0
- package/dist/handlers/grids.d.ts +3 -0
- package/dist/handlers/grids.js +142 -0
- package/dist/handlers/hierarchy.d.ts +3 -0
- package/dist/handlers/hierarchy.js +55 -0
- package/dist/handlers/index.d.ts +8 -0
- package/dist/handlers/index.js +25 -0
- package/dist/handlers/minimap.d.ts +9 -0
- package/dist/handlers/minimap.js +32 -0
- package/dist/handlers/runtime.d.ts +3 -0
- package/dist/handlers/runtime.js +191 -0
- package/dist/handlers/streets.d.ts +3 -0
- package/dist/handlers/streets.js +159 -0
- package/dist/handlers/terrain.d.ts +3 -0
- package/dist/handlers/terrain.js +204 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/listDocumentTriggers.d.ts +42 -0
- package/dist/listDocumentTriggers.js +49 -0
- package/dist/materialAssignments.d.ts +63 -0
- package/dist/materialAssignments.js +84 -0
- package/dist/mcp/fieldSpec.d.ts +32 -0
- package/dist/mcp/fieldSpec.js +174 -0
- package/dist/mcp/rpcRequest.d.ts +8 -5
- package/dist/mcp/rpcRequest.js +72 -93
- package/dist/mcp/tools.js +105 -912
- package/dist/networkSnapshot.d.ts +82 -0
- package/dist/networkSnapshot.js +73 -0
- package/dist/parentCandidates.d.ts +12 -0
- package/dist/parentCandidates.js +25 -0
- package/dist/session.d.ts +91 -4
- package/dist/session.js +36 -1017
- package/dist/shell/AnimationPanel.d.ts +17 -0
- package/dist/shell/AnimationPanel.js +134 -0
- package/dist/shell/AssetThumbnail.d.ts +12 -0
- package/dist/shell/AssetThumbnail.js +23 -0
- package/dist/shell/BottomDock.d.ts +25 -0
- package/dist/shell/BottomDock.js +40 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +56 -0
- package/dist/shell/ConsolePanel.d.ts +8 -0
- package/dist/shell/ConsolePanel.js +45 -0
- package/dist/shell/ContentBrowser.d.ts +21 -0
- package/dist/shell/ContentBrowser.js +130 -0
- package/dist/shell/HierarchyPanel.d.ts +26 -0
- package/dist/shell/HierarchyPanel.js +387 -0
- package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
- package/dist/shell/NetworkWorkspacePanel.js +49 -0
- package/dist/shell/PlayModeBar.d.ts +12 -0
- package/dist/shell/PlayModeBar.js +27 -0
- package/dist/shell/ProfilerPanel.d.ts +10 -0
- package/dist/shell/ProfilerPanel.js +41 -0
- package/dist/shell/SceneToolbar.d.ts +43 -0
- package/dist/shell/SceneToolbar.js +122 -0
- package/dist/shell/StatusBar.d.ts +18 -0
- package/dist/shell/StatusBar.js +41 -0
- package/dist/shell/TopAppBar.d.ts +35 -0
- package/dist/shell/TopAppBar.js +64 -0
- package/dist/shell/ViewportOverlays.d.ts +23 -0
- package/dist/shell/ViewportOverlays.js +136 -0
- package/dist/shell/WorkspaceRail.d.ts +20 -0
- package/dist/shell/WorkspaceRail.js +31 -0
- package/dist/shell/cameraTelemetry.d.ts +27 -0
- package/dist/shell/cameraTelemetry.js +24 -0
- package/dist/shell/commandRegistry.d.ts +58 -0
- package/dist/shell/commandRegistry.js +133 -0
- package/dist/shell/consoleSink.d.ts +11 -0
- package/dist/shell/consoleSink.js +17 -0
- package/dist/shell/consoleStore.d.ts +27 -0
- package/dist/shell/consoleStore.js +43 -0
- package/dist/shell/fields.d.ts +39 -0
- package/dist/shell/fields.js +104 -0
- package/dist/shell/formatSavedRelative.d.ts +2 -0
- package/dist/shell/formatSavedRelative.js +14 -0
- package/dist/shell/glbThumbnailCache.d.ts +45 -0
- package/dist/shell/glbThumbnailCache.js +241 -0
- package/dist/shell/hierarchyA11y.d.ts +21 -0
- package/dist/shell/hierarchyA11y.js +35 -0
- package/dist/shell/icons.d.ts +13 -0
- package/dist/shell/icons.js +85 -0
- package/dist/shell/layoutStore.d.ts +68 -0
- package/dist/shell/layoutStore.js +156 -0
- package/dist/shell/pathFlythrough.d.ts +37 -0
- package/dist/shell/pathFlythrough.js +54 -0
- package/dist/shell/perfHistory.d.ts +70 -0
- package/dist/shell/perfHistory.js +130 -0
- package/dist/shell/renameObject.d.ts +3 -0
- package/dist/shell/renameObject.js +25 -0
- package/dist/shell/theme.d.ts +36 -0
- package/dist/shell/theme.js +37 -0
- package/dist/shell/ui.d.ts +84 -0
- package/dist/shell/ui.js +90 -0
- package/dist/shell/useGlbThumbnail.d.ts +8 -0
- package/dist/shell/useGlbThumbnail.js +19 -0
- package/dist/uiStore.d.ts +27 -0
- package/dist/uiStore.js +10 -0
- package/dist/viewportContextMenu.d.ts +4 -2
- package/dist/viewportContextMenu.js +5 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-editor/SKILL.md +18 -3
- package/skills/jgengine-editor/api.md +403 -20
- package/skills/jgengine-editor/capabilities.md +46 -0
- package/skills/jgengine-editor/reference.md +87 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/OutlinerPanel.d.ts +0 -14
- package/dist/OutlinerPanel.js +0 -84
- package/skills/jgengine/reference-cartridge.md +0 -72
package/dist/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, onExitEditor, }) {
|
|
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,12 @@ 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"));
|
|
172
|
+
// F2+Q leaves the editor entirely (back to the game) — only meaningful when summoned over one.
|
|
173
|
+
useF2Chord("KeyQ", useCallback(() => onExitEditor?.(), [onExitEditor]));
|
|
177
174
|
useEffect(() => session.subscribe(() => setTick((value) => value + 1)), [session]);
|
|
178
175
|
useEffect(() => ui.subscribe(() => setTick((value) => value + 1)), [ui]);
|
|
176
|
+
useEffect(() => layout.subscribe(() => setTick((value) => value + 1)), [layout]);
|
|
179
177
|
const copySelection = useCallback(() => {
|
|
180
178
|
const current = session.getState();
|
|
181
179
|
if (current.selection.length === 0)
|
|
@@ -194,6 +192,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
194
192
|
if (ui.getState().contextMenu !== null)
|
|
195
193
|
ui.patch({ contextMenu: null });
|
|
196
194
|
}, [ui]);
|
|
195
|
+
const [parentPicker, setParentPicker] = useState(null);
|
|
196
|
+
const openBottomTab = useCallback((tab) => {
|
|
197
|
+
layout.patch({ bottomOpen: true, bottomTab: tab });
|
|
198
|
+
}, [layout]);
|
|
197
199
|
const runContextAction = useCallback((action) => {
|
|
198
200
|
const menu = ui.getState().contextMenu;
|
|
199
201
|
closeContextMenu();
|
|
@@ -246,10 +248,29 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
246
248
|
const id = `prefab_${Date.now().toString(36)}`;
|
|
247
249
|
const name = `Prefab ${current.document.prefabs.length + 1}`;
|
|
248
250
|
api.handle({ method: "create_prefab", id, name, ids: [...selection] });
|
|
249
|
-
|
|
251
|
+
layout.patch({ leftOpen: true, leftPage: "prefabs" });
|
|
250
252
|
notify(`Created prefab “${name}”`);
|
|
251
253
|
}
|
|
252
254
|
return;
|
|
255
|
+
case "parentTo": {
|
|
256
|
+
const ids = selection.length > 0
|
|
257
|
+
? selection
|
|
258
|
+
: menu?.hitId !== null && menu?.hitId !== undefined
|
|
259
|
+
? [menu.hitId]
|
|
260
|
+
: [];
|
|
261
|
+
if (ids.length === 0 || menu === null)
|
|
262
|
+
return;
|
|
263
|
+
setParentPicker({ clientX: menu.clientX, clientY: menu.clientY, ids: [...ids] });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
case "unparent": {
|
|
267
|
+
const ids = selection.length > 0 ? selection : menu?.hitId !== null && menu?.hitId !== undefined ? [menu.hitId] : [];
|
|
268
|
+
if (ids.length === 0)
|
|
269
|
+
return;
|
|
270
|
+
api.handle({ method: "set_parent", ids: [...ids], parentId: null });
|
|
271
|
+
notify(ids.length === 1 ? "Unparented object" : `Unparented ${ids.length} objects`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
253
274
|
case "addMarker":
|
|
254
275
|
if (ground !== null) {
|
|
255
276
|
session.dispatch({
|
|
@@ -301,10 +322,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
301
322
|
}
|
|
302
323
|
return;
|
|
303
324
|
case "openAssets":
|
|
304
|
-
|
|
325
|
+
openBottomTab("content");
|
|
305
326
|
return;
|
|
306
327
|
}
|
|
307
|
-
}, [api, closeContextMenu, copySelection, notify, session, ui]);
|
|
328
|
+
}, [api, closeContextMenu, copySelection, layout, notify, openBottomTab, session, ui]);
|
|
308
329
|
useEffect(() => {
|
|
309
330
|
const onKey = (event) => {
|
|
310
331
|
if (event.defaultPrevented)
|
|
@@ -314,6 +335,11 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
314
335
|
docSaveRef.current.doSave();
|
|
315
336
|
return;
|
|
316
337
|
}
|
|
338
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
|
|
339
|
+
event.preventDefault();
|
|
340
|
+
setPaletteQuery((value) => (value === null ? "" : null));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
317
343
|
const target = event.target;
|
|
318
344
|
const isTyping = target instanceof HTMLInputElement ||
|
|
319
345
|
target instanceof HTMLTextAreaElement ||
|
|
@@ -335,18 +361,26 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
335
361
|
}
|
|
336
362
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "b") {
|
|
337
363
|
event.preventDefault();
|
|
338
|
-
|
|
364
|
+
openBottomTab("content");
|
|
339
365
|
return;
|
|
340
366
|
}
|
|
341
367
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a") {
|
|
342
368
|
event.preventDefault();
|
|
343
|
-
const
|
|
369
|
+
const current = session.getState();
|
|
344
370
|
const visibility = api.getVisibility();
|
|
345
371
|
const ids = [
|
|
346
|
-
...
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
...
|
|
372
|
+
...current.document.markers
|
|
373
|
+
.filter((m) => visibility[m.kind] !== false && m.hidden !== true)
|
|
374
|
+
.map((m) => m.id),
|
|
375
|
+
...current.document.volumes
|
|
376
|
+
.filter((v) => visibility[v.kind] !== false && v.hidden !== true)
|
|
377
|
+
.map((v) => v.id),
|
|
378
|
+
...current.document.paths
|
|
379
|
+
.filter((p) => visibility[p.kind] !== false && p.hidden !== true)
|
|
380
|
+
.map((p) => p.id),
|
|
381
|
+
...(visibility["note"] !== false
|
|
382
|
+
? current.document.annotations.filter((n) => n.hidden !== true).map((n) => n.id)
|
|
383
|
+
: []),
|
|
350
384
|
];
|
|
351
385
|
if (ids.length > 0)
|
|
352
386
|
session.dispatch({ type: "select", ids });
|
|
@@ -387,6 +421,10 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
387
421
|
}
|
|
388
422
|
if (event.key === "Escape") {
|
|
389
423
|
setHelpOpen(false);
|
|
424
|
+
if (paletteQuery !== null) {
|
|
425
|
+
setPaletteQuery(null);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
390
428
|
const current = ui.getState();
|
|
391
429
|
if (current.contextMenu !== null) {
|
|
392
430
|
ui.patch({ contextMenu: null });
|
|
@@ -479,17 +517,7 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
479
517
|
};
|
|
480
518
|
window.addEventListener("keydown", onKey);
|
|
481
519
|
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), []);
|
|
520
|
+
}, [session, ui, api, notify, copySelection, openBottomTab, paletteQuery]);
|
|
493
521
|
const placeAsset = (entry) => {
|
|
494
522
|
const focus = api.getFocusTarget();
|
|
495
523
|
const focused = session.getState();
|
|
@@ -497,7 +525,7 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
497
525
|
const selected = focused.document.markers.find((marker) => marker.id === focusedId)?.position ??
|
|
498
526
|
focused.document.volumes.find((volume) => volume.id === focusedId)?.center;
|
|
499
527
|
const position = focus ?? selected ?? { x: 0, y: 0, z: 0 };
|
|
500
|
-
api.handle({
|
|
528
|
+
const response = api.handle({
|
|
501
529
|
method: "place_asset",
|
|
502
530
|
id: entry.id,
|
|
503
531
|
kind: entry.kind === "model" ? "prop" : entry.kind,
|
|
@@ -505,16 +533,61 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
505
533
|
y: position.y,
|
|
506
534
|
z: position.z,
|
|
507
535
|
});
|
|
536
|
+
if (response.ok)
|
|
537
|
+
notify(`Placed ${entry.label}`);
|
|
538
|
+
else
|
|
539
|
+
notify(`Place failed: ${response.error ?? "unknown error"}`, "error");
|
|
508
540
|
};
|
|
541
|
+
const importModels = useCallback(async (files) => {
|
|
542
|
+
if (files.length === 0 || importBusy)
|
|
543
|
+
return;
|
|
544
|
+
setImportBusy(true);
|
|
545
|
+
try {
|
|
546
|
+
const next = await loadDroppedAssets(files, importAsset);
|
|
547
|
+
if (next.length === 0) {
|
|
548
|
+
notify("No .glb / .gltf models found in the drop", "error");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const entries = next.map(editorAssetFromImport);
|
|
552
|
+
for (const entry of entries) {
|
|
553
|
+
if (entry.url !== undefined)
|
|
554
|
+
onRegisterAsset?.(entry.id, entry.url);
|
|
555
|
+
}
|
|
556
|
+
setImportedAssets((current) => mergeEditorAssets(current, entries));
|
|
557
|
+
layout.patch({ bottomOpen: true, bottomTab: "content" });
|
|
558
|
+
const durable = entries.filter((entry) => entry.url !== undefined && !entry.url.startsWith("blob:")).length;
|
|
559
|
+
const ephemeral = entries.length - durable;
|
|
560
|
+
if (ephemeral === 0) {
|
|
561
|
+
notify(entries.length === 1 ? `Imported ${entries[0].label}` : `Imported ${entries.length} models`);
|
|
562
|
+
}
|
|
563
|
+
else if (durable === 0) {
|
|
564
|
+
notify(entries.length === 1
|
|
565
|
+
? `Imported ${entries[0].label} (session-only — no host importer)`
|
|
566
|
+
: `Imported ${entries.length} models (session-only — no host importer)`);
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
notify(`Imported ${entries.length} models (${ephemeral} session-only)`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
notify(`Import failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
setImportBusy(false);
|
|
577
|
+
}
|
|
578
|
+
}, [importAsset, importBusy, layout, notify, onRegisterAsset]);
|
|
509
579
|
const importFile = (file) => {
|
|
510
580
|
void file.text().then((text) => {
|
|
511
581
|
try {
|
|
512
582
|
session.dispatch({ type: "importJson", json: text });
|
|
513
583
|
const doc = session.getState().document;
|
|
514
584
|
notify(`Imported ${editorDocumentSize(doc)} objects from ${file.name}`);
|
|
585
|
+
consoleStore.log("info", "import", `Imported ${editorDocumentSize(doc)} objects from ${file.name}`);
|
|
515
586
|
}
|
|
516
587
|
catch (error) {
|
|
517
|
-
|
|
588
|
+
const message = error instanceof Error ? error.message : "invalid JSON";
|
|
589
|
+
notify(`Import failed: ${message}`, "error");
|
|
590
|
+
consoleStore.log("error", "import", `Import failed: ${message}`);
|
|
518
591
|
}
|
|
519
592
|
});
|
|
520
593
|
};
|
|
@@ -527,45 +600,156 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineJson, s
|
|
|
527
600
|
notify("Clipboard unavailable — use Export instead", "error");
|
|
528
601
|
}
|
|
529
602
|
};
|
|
530
|
-
const
|
|
531
|
-
|
|
603
|
+
const exportJson = () => {
|
|
604
|
+
downloadText(`${gameId}-editor.json`, session.exportJson(true));
|
|
605
|
+
notify(`Exported ${gameId}-editor.json`);
|
|
606
|
+
};
|
|
607
|
+
// Reference compare, not a serialize: stringifying the whole document per edit dispatch burned
|
|
608
|
+
// milliseconds on every nudge/stroke. Undoing back to pristine may leave the dot on (history
|
|
609
|
+
// restores clones), which is the cheap side of wrong for an unsaved-edits hint.
|
|
610
|
+
const baselineDirty = baselineDocument !== undefined && state.document !== baselineDocument;
|
|
611
|
+
const dirty = docSave.available ? docSave.dirty : baselineDirty;
|
|
532
612
|
const startPlacement = (tool) => {
|
|
533
|
-
setAddOpen(false);
|
|
534
613
|
ui.startPlacement(tool);
|
|
535
614
|
};
|
|
615
|
+
const selectWorkspace = (workspace) => {
|
|
616
|
+
layout.setWorkspace(workspace);
|
|
617
|
+
if (workspace === "terrain")
|
|
618
|
+
ui.setTool("terrain");
|
|
619
|
+
else if (ui.getState().tool === "terrain")
|
|
620
|
+
ui.setTool("select");
|
|
621
|
+
};
|
|
622
|
+
const paletteObjects = useMemo(() => {
|
|
623
|
+
const doc = state.document;
|
|
624
|
+
const objects = [];
|
|
625
|
+
for (const marker of doc.markers)
|
|
626
|
+
objects.push({ id: marker.id, label: marker.label ?? marker.id, kind: marker.kind });
|
|
627
|
+
for (const volume of doc.volumes)
|
|
628
|
+
objects.push({ id: volume.id, label: volume.label ?? volume.id, kind: volume.kind });
|
|
629
|
+
for (const path of doc.paths)
|
|
630
|
+
objects.push({ id: path.id, label: path.label ?? path.id, kind: path.kind });
|
|
631
|
+
for (const note of doc.annotations)
|
|
632
|
+
objects.push({ id: note.id, label: note.text.slice(0, 48) || note.id, kind: "note" });
|
|
633
|
+
return objects;
|
|
634
|
+
}, [state.document]);
|
|
635
|
+
const paletteCommands = useMemo(() => buildPaletteCommands({
|
|
636
|
+
setGizmoMode: (mode) => ui.patch({ gizmoMode: mode }),
|
|
637
|
+
setTool: (tool) => ui.setTool(tool),
|
|
638
|
+
toggleGrid: () => ui.patch({ showGrid: !ui.getState().showGrid }),
|
|
639
|
+
toggleContours: () => ui.patch({ showContours: !ui.getState().showContours }),
|
|
640
|
+
toggleSurfaceGrid: () => ui.patch({ showSurfaceGrid: !ui.getState().showSurfaceGrid }),
|
|
641
|
+
toggleElevation: () => ui.patch({ showElevation: !ui.getState().showElevation }),
|
|
642
|
+
setSnapMode: (mode) => ui.patch({ snapMode: mode }),
|
|
643
|
+
frameAll: () => api.handle({ method: "camera_frame" }),
|
|
644
|
+
frameSelection: () => {
|
|
645
|
+
const selected = session.getState().selection[0];
|
|
646
|
+
if (selected !== undefined)
|
|
647
|
+
api.handle({ method: "camera_goto", id: selected });
|
|
648
|
+
else
|
|
649
|
+
api.handle({ method: "camera_frame" });
|
|
650
|
+
},
|
|
651
|
+
undo: () => session.dispatch({ type: "undo" }),
|
|
652
|
+
redo: () => session.dispatch({ type: "redo" }),
|
|
653
|
+
save: () => docSaveRef.current.doSave(),
|
|
654
|
+
exportJson,
|
|
655
|
+
importJson: () => importInputRef.current?.click(),
|
|
656
|
+
copyJson: copyExportJson,
|
|
657
|
+
setMode: (mode) => api.setMode(mode),
|
|
658
|
+
startPlacement,
|
|
659
|
+
openBottomTab,
|
|
660
|
+
toggleLeftDock: () => layout.patch({ leftOpen: !layout.getState().leftOpen }),
|
|
661
|
+
toggleRightDock: () => layout.patch({ rightOpen: !layout.getState().rightOpen }),
|
|
662
|
+
toggleHelp: () => setHelpOpen((value) => !value),
|
|
663
|
+
resetLayout: () => layout.reset(),
|
|
664
|
+
objects: paletteObjects,
|
|
665
|
+
gotoObject: (id) => {
|
|
666
|
+
session.dispatch({ type: "select", ids: [id] });
|
|
667
|
+
api.handle({ method: "camera_goto", id });
|
|
668
|
+
},
|
|
669
|
+
}), [api, layout, openBottomTab, paletteObjects, session, ui]);
|
|
536
670
|
const placement = uiState.placement;
|
|
537
671
|
const placementHint = placement === null
|
|
538
672
|
? null
|
|
539
673
|
: placement.tool === "path"
|
|
540
674
|
? `Drawing ${placement.kind}: click to add points (${uiState.pathDraft.length}) · Enter finish · Esc cancel`
|
|
541
675
|
: `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
|
-
|
|
676
|
+
const railActive = uiState.tool === "terrain" ? "terrain" : layoutState.workspace;
|
|
677
|
+
// Only advertise the exit chord when there is a game to return to.
|
|
678
|
+
const shortcuts = onExitEditor !== undefined ? [...SHORTCUTS, { keys: "F2+Q", action: "Exit editor to game" }] : SHORTCUTS;
|
|
679
|
+
return (_jsxs("div", { className: "pointer-events-none absolute inset-0 z-50 flex flex-col text-xs text-neutral-100", children: [_jsx(TopAppBar, { gameId: gameId, onExitToGame: onExitEditor, dirty: dirty, saveState: docSave.saveState, lastSavedAt: docSave.lastSavedAt, saveAvailable: docSave.available, saveError: docSave.saveError, canUndo: session.canUndo(), canRedo: session.canRedo(), onUndo: () => session.dispatch({ type: "undo" }), onRedo: () => session.dispatch({ type: "redo" }), onSave: docSave.doSave, onPlay: () => api.setMode("play"), onWalk: () => api.setMode("walk"), onHud: () => api.setMode("hud"), onImport: () => importInputRef.current?.click(), onExport: exportJson, onCopyJson: copyExportJson, onOpenPalette: () => setPaletteQuery(""), onToggleHelp: () => setHelpOpen((value) => !value), onResetLayout: () => layout.reset() }), _jsx("input", { ref: importInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: (event) => {
|
|
680
|
+
const file = event.target.files?.[0];
|
|
681
|
+
if (file !== undefined)
|
|
682
|
+
importFile(file);
|
|
683
|
+
event.target.value = "";
|
|
684
|
+
} }), _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: () => {
|
|
685
|
+
const selected = session.getState().selection[0];
|
|
686
|
+
if (selected !== undefined)
|
|
687
|
+
api.handle({ method: "camera_goto", id: selected });
|
|
688
|
+
else
|
|
689
|
+
api.handle({ method: "camera_frame" });
|
|
690
|
+
}, 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"
|
|
691
|
+
? "Network workspace dock"
|
|
692
|
+
: layoutState.workspace === "materials"
|
|
693
|
+
? "Materials workspace dock"
|
|
694
|
+
: layoutState.workspace === "scripting"
|
|
695
|
+
? "Scripting workspace dock"
|
|
696
|
+
: "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) => {
|
|
697
|
+
const badge = page.id === "collections"
|
|
698
|
+
? state.document.collections.length
|
|
699
|
+
: page.id === "prefabs"
|
|
700
|
+
? state.document.prefabs.length
|
|
701
|
+
: page.id === "catalogs"
|
|
702
|
+
? api.getCatalogDefinitions().length
|
|
703
|
+
: null;
|
|
704
|
+
const selected = layoutState.leftPage === page.id;
|
|
705
|
+
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));
|
|
706
|
+
})), _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) => {
|
|
707
|
+
const current = session.getState().selection;
|
|
708
|
+
if (!current.includes(id))
|
|
709
|
+
session.dispatch({ type: "select", ids: [id] });
|
|
710
|
+
ui.patch({
|
|
711
|
+
contextMenu: {
|
|
712
|
+
clientX: point.clientX,
|
|
713
|
+
clientY: point.clientY,
|
|
714
|
+
hitId: id,
|
|
715
|
+
ground: null,
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
} }))] }), _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"
|
|
719
|
+
? "border-rose-400/40 bg-rose-950/90 text-rose-100 shadow-rose-950/40"
|
|
720
|
+
: "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({
|
|
721
|
+
hitId: uiState.contextMenu.hitId,
|
|
722
|
+
selection: state.selection,
|
|
723
|
+
canPaste: clipboardFragment !== null,
|
|
724
|
+
canUnparent: (() => {
|
|
725
|
+
const ids = state.selection.length > 0
|
|
726
|
+
? state.selection
|
|
727
|
+
: uiState.contextMenu.hitId !== null
|
|
728
|
+
? [uiState.contextMenu.hitId]
|
|
729
|
+
: [];
|
|
730
|
+
return ids.some((id) => editorParentOf(state.document, id) !== undefined);
|
|
731
|
+
})(),
|
|
732
|
+
}), onPick: runContextAction, onClose: closeContextMenu })) : null, parentPicker !== null ? (_jsx(ParentPickerMenu, { x: parentPicker.clientX, y: parentPicker.clientY, candidates: listParentCandidates(state.document, parentPicker.ids), onPick: (parentId) => {
|
|
733
|
+
const ids = [...parentPicker.ids];
|
|
734
|
+
setParentPicker(null);
|
|
735
|
+
const result = api.handle({ method: "set_parent", ids, parentId });
|
|
736
|
+
if (!result.ok) {
|
|
737
|
+
notify(result.error ?? "Parent to failed", "error");
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (parentId === null) {
|
|
741
|
+
notify(ids.length === 1 ? "Unparented object" : `Unparented ${ids.length} objects`);
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
notify(ids.length === 1
|
|
745
|
+
? `Parented to ${parentId}`
|
|
746
|
+
: `Parented ${ids.length} objects to ${parentId}`);
|
|
747
|
+
}
|
|
748
|
+
}, 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: [[
|
|
749
|
+
["content", "Content Browser"],
|
|
750
|
+
["console", "Console"],
|
|
751
|
+
["profiler", "Profiler"],
|
|
752
|
+
["animation", "Animation"],
|
|
753
|
+
["assistant", "AI Assistant"],
|
|
754
|
+
].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
755
|
}
|