@jgengine/editor 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +12 -0
  3. package/dist/AssetBrowser.d.ts +26 -1
  4. package/dist/AssetBrowser.js +54 -1
  5. package/dist/AssetDropZone.d.ts +9 -0
  6. package/dist/AssetDropZone.js +65 -0
  7. package/dist/CatalogsPanel.d.ts +4 -2
  8. package/dist/CatalogsPanel.js +162 -9
  9. package/dist/CityPreview.d.ts +11 -0
  10. package/dist/CityPreview.js +45 -0
  11. package/dist/CollectionsPanel.js +5 -5
  12. package/dist/DebugDraw.d.ts +3 -1
  13. package/dist/DebugDraw.js +37 -19
  14. package/dist/EditorApp.d.ts +9 -1
  15. package/dist/EditorApp.js +208 -37
  16. package/dist/EditorChrome.d.ts +25 -6
  17. package/dist/EditorChrome.js +310 -118
  18. package/dist/InspectorPanel.d.ts +17 -7
  19. package/dist/InspectorPanel.js +173 -27
  20. package/dist/LightingPanel.d.ts +11 -0
  21. package/dist/LightingPanel.js +99 -0
  22. package/dist/MaterialsWorkspacePanel.d.ts +12 -0
  23. package/dist/MaterialsWorkspacePanel.js +86 -0
  24. package/dist/ParentPickerMenu.d.ts +13 -0
  25. package/dist/ParentPickerMenu.js +33 -0
  26. package/dist/PerfProbe.js +69 -2
  27. package/dist/PrefabsPanel.js +5 -5
  28. package/dist/RuntimePlayBridge.d.ts +5 -3
  29. package/dist/RuntimePlayBridge.js +6 -13
  30. package/dist/ScatterPreview.d.ts +3 -1
  31. package/dist/ScatterPreview.js +10 -7
  32. package/dist/SchemaInspector.js +21 -8
  33. package/dist/ScriptingPanel.d.ts +15 -0
  34. package/dist/ScriptingPanel.js +46 -0
  35. package/dist/SelectionGizmo.js +114 -30
  36. package/dist/StandaloneEditor.d.ts +16 -9
  37. package/dist/StandaloneEditor.js +19 -20
  38. package/dist/TerrainPanel.d.ts +3 -2
  39. package/dist/TerrainPanel.js +16 -5
  40. package/dist/TerrainReadout.d.ts +17 -0
  41. package/dist/TerrainReadout.js +163 -0
  42. package/dist/TerrainReadoutHud.d.ts +9 -0
  43. package/dist/TerrainReadoutHud.js +20 -0
  44. package/dist/TerrainSculpt.js +98 -59
  45. package/dist/agent/AgentPanel.d.ts +5 -2
  46. package/dist/agent/AgentPanel.js +11 -6
  47. package/dist/agent/toolBridge.js +5 -0
  48. package/dist/assetImport.d.ts +36 -0
  49. package/dist/assetImport.js +66 -0
  50. package/dist/authoredComponentMeta.d.ts +25 -0
  51. package/dist/authoredComponentMeta.js +65 -0
  52. package/dist/gizmoPivot.d.ts +4 -0
  53. package/dist/gizmoPivot.js +34 -0
  54. package/dist/handlers/catalogs.d.ts +3 -0
  55. package/dist/handlers/catalogs.js +137 -0
  56. package/dist/handlers/collections.d.ts +3 -0
  57. package/dist/handlers/collections.js +57 -0
  58. package/dist/handlers/context.d.ts +36 -0
  59. package/dist/handlers/context.js +1 -0
  60. package/dist/handlers/document.d.ts +3 -0
  61. package/dist/handlers/document.js +350 -0
  62. package/dist/handlers/grids.d.ts +3 -0
  63. package/dist/handlers/grids.js +142 -0
  64. package/dist/handlers/hierarchy.d.ts +3 -0
  65. package/dist/handlers/hierarchy.js +55 -0
  66. package/dist/handlers/index.d.ts +8 -0
  67. package/dist/handlers/index.js +23 -0
  68. package/dist/handlers/minimap.d.ts +9 -0
  69. package/dist/handlers/minimap.js +32 -0
  70. package/dist/handlers/runtime.d.ts +3 -0
  71. package/dist/handlers/runtime.js +191 -0
  72. package/dist/handlers/terrain.d.ts +3 -0
  73. package/dist/handlers/terrain.js +204 -0
  74. package/dist/index.d.ts +3 -2
  75. package/dist/index.js +3 -2
  76. package/dist/listDocumentTriggers.d.ts +42 -0
  77. package/dist/listDocumentTriggers.js +49 -0
  78. package/dist/materialAssignments.d.ts +63 -0
  79. package/dist/materialAssignments.js +84 -0
  80. package/dist/mcp/fieldSpec.d.ts +32 -0
  81. package/dist/mcp/fieldSpec.js +173 -0
  82. package/dist/mcp/rpcRequest.d.ts +8 -5
  83. package/dist/mcp/rpcRequest.js +72 -83
  84. package/dist/mcp/tools.js +104 -763
  85. package/dist/networkSnapshot.d.ts +82 -0
  86. package/dist/networkSnapshot.js +73 -0
  87. package/dist/parentCandidates.d.ts +12 -0
  88. package/dist/parentCandidates.js +25 -0
  89. package/dist/perfPill.d.ts +21 -0
  90. package/dist/perfPill.js +20 -0
  91. package/dist/session.d.ts +167 -5
  92. package/dist/session.js +36 -879
  93. package/dist/shell/AnimationPanel.d.ts +17 -0
  94. package/dist/shell/AnimationPanel.js +134 -0
  95. package/dist/shell/AssetThumbnail.d.ts +12 -0
  96. package/dist/shell/AssetThumbnail.js +23 -0
  97. package/dist/shell/BottomDock.d.ts +25 -0
  98. package/dist/shell/BottomDock.js +40 -0
  99. package/dist/shell/CommandPalette.d.ts +12 -0
  100. package/dist/shell/CommandPalette.js +56 -0
  101. package/dist/shell/ConsolePanel.d.ts +8 -0
  102. package/dist/shell/ConsolePanel.js +45 -0
  103. package/dist/shell/ContentBrowser.d.ts +21 -0
  104. package/dist/shell/ContentBrowser.js +130 -0
  105. package/dist/shell/HierarchyPanel.d.ts +26 -0
  106. package/dist/shell/HierarchyPanel.js +387 -0
  107. package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
  108. package/dist/shell/NetworkWorkspacePanel.js +49 -0
  109. package/dist/shell/PlayModeBar.d.ts +12 -0
  110. package/dist/shell/PlayModeBar.js +27 -0
  111. package/dist/shell/ProfilerPanel.d.ts +10 -0
  112. package/dist/shell/ProfilerPanel.js +41 -0
  113. package/dist/shell/SceneToolbar.d.ts +43 -0
  114. package/dist/shell/SceneToolbar.js +122 -0
  115. package/dist/shell/StatusBar.d.ts +18 -0
  116. package/dist/shell/StatusBar.js +41 -0
  117. package/dist/shell/TopAppBar.d.ts +33 -0
  118. package/dist/shell/TopAppBar.js +64 -0
  119. package/dist/shell/ViewportOverlays.d.ts +23 -0
  120. package/dist/shell/ViewportOverlays.js +136 -0
  121. package/dist/shell/WorkspaceRail.d.ts +20 -0
  122. package/dist/shell/WorkspaceRail.js +31 -0
  123. package/dist/shell/cameraTelemetry.d.ts +27 -0
  124. package/dist/shell/cameraTelemetry.js +24 -0
  125. package/dist/shell/commandRegistry.d.ts +58 -0
  126. package/dist/shell/commandRegistry.js +133 -0
  127. package/dist/shell/consoleSink.d.ts +11 -0
  128. package/dist/shell/consoleSink.js +17 -0
  129. package/dist/shell/consoleStore.d.ts +27 -0
  130. package/dist/shell/consoleStore.js +43 -0
  131. package/dist/shell/fields.d.ts +39 -0
  132. package/dist/shell/fields.js +104 -0
  133. package/dist/shell/formatSavedRelative.d.ts +2 -0
  134. package/dist/shell/formatSavedRelative.js +14 -0
  135. package/dist/shell/glbThumbnailCache.d.ts +45 -0
  136. package/dist/shell/glbThumbnailCache.js +241 -0
  137. package/dist/shell/hierarchyA11y.d.ts +21 -0
  138. package/dist/shell/hierarchyA11y.js +35 -0
  139. package/dist/shell/icons.d.ts +13 -0
  140. package/dist/shell/icons.js +85 -0
  141. package/dist/shell/layoutStore.d.ts +68 -0
  142. package/dist/shell/layoutStore.js +156 -0
  143. package/dist/shell/pathFlythrough.d.ts +37 -0
  144. package/dist/shell/pathFlythrough.js +54 -0
  145. package/dist/shell/perfHistory.d.ts +70 -0
  146. package/dist/shell/perfHistory.js +130 -0
  147. package/dist/shell/renameObject.d.ts +3 -0
  148. package/dist/shell/renameObject.js +25 -0
  149. package/dist/shell/theme.d.ts +36 -0
  150. package/dist/shell/theme.js +37 -0
  151. package/dist/shell/ui.d.ts +84 -0
  152. package/dist/shell/ui.js +90 -0
  153. package/dist/shell/useGlbThumbnail.d.ts +8 -0
  154. package/dist/shell/useGlbThumbnail.js +19 -0
  155. package/dist/terrainReadoutStore.d.ts +19 -0
  156. package/dist/terrainReadoutStore.js +36 -0
  157. package/dist/uiStore.d.ts +33 -0
  158. package/dist/uiStore.js +13 -0
  159. package/dist/viewportContextMenu.d.ts +4 -2
  160. package/dist/viewportContextMenu.js +5 -1
  161. package/package.json +4 -4
  162. package/skills/jgengine/SKILL.md +35 -804
  163. package/skills/jgengine/api.md +190 -79
  164. package/skills/jgengine/capabilities.md +112 -2
  165. package/skills/jgengine/recipes/minimal-game.md +88 -0
  166. package/skills/jgengine-editor/SKILL.md +29 -362
  167. package/skills/jgengine-editor/api.md +519 -18
  168. package/skills/jgengine-editor/capabilities.md +91 -0
  169. package/skills/jgengine-editor/reference.md +113 -0
  170. package/skills/level-design/SKILL.md +54 -0
  171. package/skills/level-design/agents/openai.yaml +4 -0
  172. package/skills/level-design/references/application-playbook.md +192 -0
  173. package/skills/level-design/references/field-guide.md +337 -0
  174. package/skills/level-design/references/live-editor-loop.md +179 -0
  175. package/dist/OutlinerPanel.d.ts +0 -14
  176. package/dist/OutlinerPanel.js +0 -84
  177. package/skills/jgengine/reference-cartridge.md +0 -72
  178. package/skills/jgengine/reference-orphans.md +0 -9
  179. package/skills/jgengine/reference-systems.md +0 -60
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Offscreen GLB → PNG thumbnail cache for the Content Browser.
3
+ * Renders real model bytes once per URL into a data URL; never invents screenshots.
4
+ * Failures stay honest (glyph fallback). Bounded concurrency + cache eviction keep
5
+ * the editor from thrashing WebGL or retaining unbounded GPU memory.
6
+ * @internal
7
+ */
8
+ import * as THREE from "three";
9
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
10
+ const THUMB_SIZE = 128;
11
+ /** Soft cap: oldest ready entries are dropped when exceeded (GPU/canvas cost). */
12
+ const MAX_CACHE = 64;
13
+ /** One render at a time — shared WebGL context is not free-threaded. */
14
+ const MAX_CONCURRENT = 1;
15
+ const cache = new Map();
16
+ const queue = [];
17
+ let active = 0;
18
+ let renderer = null;
19
+ let scene = null;
20
+ let camera = null;
21
+ let loader = null;
22
+ /** Test seam: replace the GLTF load path without spinning WebGL. */
23
+ let loadSceneForUrl = defaultLoadScene;
24
+ /** @internal */
25
+ export function setGlbThumbnailLoader(fn) {
26
+ loadSceneForUrl = fn;
27
+ }
28
+ /** @internal Restore production loader after tests. */
29
+ export function resetGlbThumbnailLoader() {
30
+ loadSceneForUrl = defaultLoadScene;
31
+ }
32
+ /** @internal Clear cache + dispose GPU resources (tests / hot dispose). */
33
+ export function clearGlbThumbnailCache() {
34
+ cache.clear();
35
+ queue.length = 0;
36
+ active = 0;
37
+ disposeRenderer();
38
+ }
39
+ function disposeRenderer() {
40
+ if (renderer !== null) {
41
+ renderer.dispose();
42
+ renderer.forceContextLoss();
43
+ renderer = null;
44
+ }
45
+ scene = null;
46
+ camera = null;
47
+ loader = null;
48
+ }
49
+ function ensureRenderer() {
50
+ if (renderer !== null && scene !== null && camera !== null) {
51
+ return { renderer, scene, camera };
52
+ }
53
+ const canvas = document.createElement("canvas");
54
+ canvas.width = THUMB_SIZE;
55
+ canvas.height = THUMB_SIZE;
56
+ const next = new THREE.WebGLRenderer({
57
+ canvas,
58
+ antialias: true,
59
+ alpha: true,
60
+ preserveDrawingBuffer: true,
61
+ powerPreference: "low-power",
62
+ });
63
+ next.setSize(THUMB_SIZE, THUMB_SIZE, false);
64
+ next.setPixelRatio(1);
65
+ next.setClearColor(0x0a0c10, 1);
66
+ next.outputColorSpace = THREE.SRGBColorSpace;
67
+ const nextScene = new THREE.Scene();
68
+ nextScene.add(new THREE.AmbientLight(0xffffff, 0.55));
69
+ const key = new THREE.DirectionalLight(0xffffff, 1.1);
70
+ key.position.set(2.5, 4, 3);
71
+ nextScene.add(key);
72
+ const fill = new THREE.DirectionalLight(0xa8c4ff, 0.35);
73
+ fill.position.set(-2, 1, -1);
74
+ nextScene.add(fill);
75
+ const nextCamera = new THREE.PerspectiveCamera(35, 1, 0.01, 1000);
76
+ renderer = next;
77
+ scene = nextScene;
78
+ camera = nextCamera;
79
+ return { renderer: next, scene: nextScene, camera: nextCamera };
80
+ }
81
+ function defaultLoadScene(url) {
82
+ loader ??= new GLTFLoader();
83
+ return new Promise((resolve, reject) => {
84
+ loader.load(url, (gltf) => resolve(gltf.scene), undefined, (err) => reject(err instanceof Error ? err : new Error(String(err))));
85
+ });
86
+ }
87
+ /**
88
+ * Place the camera so the object's bounding sphere fills most of the frame from a
89
+ * consistent three-quarter angle. Pure math — unit-tested without WebGL.
90
+ * @internal
91
+ */
92
+ export function frameCameraForBounds(box, camera, size = THUMB_SIZE) {
93
+ const center = box.getCenter(new THREE.Vector3());
94
+ const sphere = box.getBoundingSphere(new THREE.Sphere());
95
+ const radius = Math.max(sphere.radius, 1e-4);
96
+ const fov = (camera.fov * Math.PI) / 180;
97
+ const distance = (radius * 2.35) / Math.tan(fov / 2);
98
+ // Consistent three-quarter view (slightly elevated).
99
+ const dir = new THREE.Vector3(0.75, 0.55, 1).normalize();
100
+ camera.position.copy(center).addScaledVector(dir, distance);
101
+ camera.near = Math.max(distance / 100, 0.01);
102
+ camera.far = distance * 20;
103
+ camera.aspect = 1;
104
+ camera.updateProjectionMatrix();
105
+ camera.lookAt(center);
106
+ void size;
107
+ }
108
+ function disposeObject(root) {
109
+ root.traverse((obj) => {
110
+ const mesh = obj;
111
+ if (mesh.isMesh === true) {
112
+ mesh.geometry?.dispose();
113
+ const material = mesh.material;
114
+ if (Array.isArray(material)) {
115
+ for (const entry of material)
116
+ entry.dispose();
117
+ }
118
+ else if (material !== undefined) {
119
+ material.dispose();
120
+ }
121
+ }
122
+ });
123
+ }
124
+ function notify(entry) {
125
+ for (const waiter of entry.waiters)
126
+ waiter();
127
+ }
128
+ function evictIfNeeded() {
129
+ if (cache.size <= MAX_CACHE)
130
+ return;
131
+ for (const [key, entry] of cache) {
132
+ if (entry.status !== "ready")
133
+ continue;
134
+ cache.delete(key);
135
+ if (cache.size <= MAX_CACHE)
136
+ return;
137
+ }
138
+ }
139
+ async function renderUrl(url) {
140
+ if (typeof document === "undefined") {
141
+ throw new Error("GLB thumbnails require a DOM/WebGL environment");
142
+ }
143
+ const { renderer: gl, scene: scn, camera: cam } = ensureRenderer();
144
+ const root = await loadSceneForUrl(url);
145
+ const clone = root.clone(true);
146
+ scn.add(clone);
147
+ const box = new THREE.Box3().setFromObject(clone);
148
+ if (box.isEmpty()) {
149
+ scn.remove(clone);
150
+ disposeObject(clone);
151
+ throw new Error("empty bounds");
152
+ }
153
+ frameCameraForBounds(box, cam);
154
+ gl.render(scn, cam);
155
+ const dataUrl = gl.domElement.toDataURL("image/png");
156
+ scn.remove(clone);
157
+ disposeObject(clone);
158
+ // Drop the loader root too if it is not shared (defaultLoadScene creates a new scene each time).
159
+ disposeObject(root);
160
+ return dataUrl;
161
+ }
162
+ function pump() {
163
+ while (active < MAX_CONCURRENT && queue.length > 0) {
164
+ const url = queue.shift();
165
+ const entry = cache.get(url);
166
+ if (entry === undefined || entry.status !== "loading")
167
+ continue;
168
+ active += 1;
169
+ void (async () => {
170
+ try {
171
+ const dataUrl = await renderUrl(url);
172
+ const next = { status: "ready", dataUrl, waiters: entry.waiters };
173
+ cache.set(url, next);
174
+ evictIfNeeded();
175
+ notify(next);
176
+ }
177
+ catch {
178
+ const next = { status: "error", waiters: entry.waiters };
179
+ cache.set(url, next);
180
+ notify(next);
181
+ }
182
+ finally {
183
+ active -= 1;
184
+ pump();
185
+ }
186
+ })();
187
+ }
188
+ }
189
+ /**
190
+ * Snapshot of cache state for a model URL. Does not start a render — call
191
+ * {@link requestGlbThumbnail} first (the React hook does both).
192
+ * @internal
193
+ */
194
+ export function getGlbThumbnailState(url) {
195
+ if (url === undefined || url.length === 0)
196
+ return { status: "idle", dataUrl: null };
197
+ const entry = cache.get(url);
198
+ if (entry === undefined)
199
+ return { status: "idle", dataUrl: null };
200
+ if (entry.status === "ready")
201
+ return { status: "ready", dataUrl: entry.dataUrl };
202
+ if (entry.status === "error")
203
+ return { status: "error", dataUrl: null };
204
+ return { status: "loading", dataUrl: null };
205
+ }
206
+ /**
207
+ * Ensures a thumbnail render is in flight (or already cached) for `url`.
208
+ * Safe to call repeatedly; concurrent callers share one load.
209
+ * @internal
210
+ */
211
+ export function requestGlbThumbnail(url) {
212
+ if (url.length === 0)
213
+ return;
214
+ const existing = cache.get(url);
215
+ if (existing !== undefined)
216
+ return;
217
+ const waiters = new Set();
218
+ cache.set(url, {
219
+ status: "loading",
220
+ waiters,
221
+ promise: Promise.resolve(),
222
+ });
223
+ queue.push(url);
224
+ pump();
225
+ }
226
+ /**
227
+ * Subscribe to thumbnail state changes for a URL. Returns an unsubscribe.
228
+ * @internal
229
+ */
230
+ export function subscribeGlbThumbnail(url, listener) {
231
+ if (url === undefined || url.length === 0)
232
+ return () => { };
233
+ requestGlbThumbnail(url);
234
+ const entry = cache.get(url);
235
+ if (entry === undefined)
236
+ return () => { };
237
+ entry.waiters.add(listener);
238
+ return () => {
239
+ entry.waiters.delete(listener);
240
+ };
241
+ }
@@ -0,0 +1,21 @@
1
+ import type { OutlinerFlatRow } from "../outlinerModel.js";
2
+ /**
3
+ * Stable DOM id for a hierarchy treeitem so the virtualized list can expose
4
+ * `aria-activedescendant` while keyboard focus stays on the tree container.
5
+ *
6
+ * @internal
7
+ */
8
+ export declare function hierarchyRowDomId(objectId: string): string;
9
+ /**
10
+ * Object ids that arrow-key navigation can land on (skips kind group headers).
11
+ *
12
+ * @internal
13
+ */
14
+ export declare function selectableIds(rows: readonly OutlinerFlatRow[]): string[];
15
+ /**
16
+ * Resolve `aria-activedescendant` for the hierarchy tree. Pure helper so tests can lock the
17
+ * contract without mounting the virtualized panel.
18
+ *
19
+ * @internal
20
+ */
21
+ export declare function hierarchyActiveDescendantId(activeId: string | null, navigableIds: readonly string[]): string | undefined;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Stable DOM id for a hierarchy treeitem so the virtualized list can expose
3
+ * `aria-activedescendant` while keyboard focus stays on the tree container.
4
+ *
5
+ * @internal
6
+ */
7
+ export function hierarchyRowDomId(objectId) {
8
+ return `hierarchy-row-${objectId}`;
9
+ }
10
+ /**
11
+ * Object ids that arrow-key navigation can land on (skips kind group headers).
12
+ *
13
+ * @internal
14
+ */
15
+ export function selectableIds(rows) {
16
+ const ids = [];
17
+ for (const row of rows) {
18
+ if (row.type === "kindItem")
19
+ ids.push(row.ids[0]);
20
+ else if (row.type === "treeItem")
21
+ ids.push(row.id);
22
+ }
23
+ return ids;
24
+ }
25
+ /**
26
+ * Resolve `aria-activedescendant` for the hierarchy tree. Pure helper so tests can lock the
27
+ * contract without mounting the virtualized panel.
28
+ *
29
+ * @internal
30
+ */
31
+ export function hierarchyActiveDescendantId(activeId, navigableIds) {
32
+ if (activeId !== null && navigableIds.includes(activeId))
33
+ return hierarchyRowDomId(activeId);
34
+ return undefined;
35
+ }
@@ -0,0 +1,13 @@
1
+ import type { SVGProps } from "react";
2
+ /**
3
+ * Internal editor icon set: compact 16px stroke glyphs drawn in-repo so the shell has one
4
+ * consistent icon language without adding a dependency. Every glyph is original geometry.
5
+ */
6
+ export type IconName = "cursor" | "move" | "rotate" | "scale" | "terrain" | "brush" | "grid" | "magnet" | "frame" | "camera" | "eye" | "eyeOff" | "lock" | "unlock" | "search" | "filter" | "plus" | "folder" | "folderOpen" | "cube" | "pin" | "sphere" | "spline" | "note" | "play" | "pause" | "step" | "stop" | "undo" | "redo" | "save" | "import" | "export" | "settings" | "help" | "close" | "chevronRight" | "chevronDown" | "layers" | "sparkle" | "terminal" | "gauge" | "film" | "image" | "audio" | "script" | "network" | "bulb" | "walk" | "panel" | "copy" | "list" | "gridView" | "trash" | "target" | "clock" | "warning" | "info" | "error" | "command";
7
+ /** One 16px stroke icon from the shell's internal set; sized via the `size` prop. */
8
+ export declare function Icon({ name, size, ...rest }: {
9
+ name: IconName;
10
+ size?: number;
11
+ } & SVGProps<SVGSVGElement>): import("react").JSX.Element;
12
+ /** Icon assigned to a scene-object kind for hierarchy rows and inspector headers. */
13
+ export declare function kindIcon(kind: string): IconName;
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ const PATHS = {
3
+ cursor: "M4 2l8 7-3.5.5L10 13l-2 1-1.5-3.5L4 12z",
4
+ move: "M8 1v14M1 8h14M8 1L6 3m2-2l2 2M8 15l-2-2m2 2l2-2M1 8l2-2m-2 2l2 2M15 8l-2-2m2 2l-2 2",
5
+ rotate: "M13.5 8a5.5 5.5 0 1 1-2-4.2M13.5 1.5v3h-3",
6
+ scale: "M2 9v5h5M14 7V2H9M2 14l4.5-4.5M14 2L9.5 6.5",
7
+ terrain: "M1 13L5.5 5l3 5 2-3L15 13zM10 4.5l.5-1",
8
+ brush: "M10.5 2.5l3 3L7 12l-4 1 1-4zM9 4l3 3",
9
+ grid: "M2 2h12v12H2zM2 6.7h12M2 11.3h12M6.7 2v12M11.3 2v12",
10
+ magnet: "M5 2v5a3 3 0 0 0 6 0V2M5 2h2.5v3H5zM8.5 2H11v3H8.5zM5 10.5L4 13m8-2.5l1 2.5",
11
+ frame: "M2 5V2h3M11 2h3v3M14 11v3h-3M5 14H2v-3M8 6.5A1.5 1.5 0 1 1 8 9.5 1.5 1.5 0 0 1 8 6.5",
12
+ camera: "M2 5.5h3l1.5-2h3L11 5.5h3v7H2zM8 7a2.2 2.2 0 1 1 0 4.4A2.2 2.2 0 0 1 8 7",
13
+ eye: "M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8 12 12.5 8 12.5 1.5 8 1.5 8zM8 6.2A1.8 1.8 0 1 1 8 9.8 1.8 1.8 0 0 1 8 6.2",
14
+ eyeOff: "M3 3l10 10M6.3 4.2C6.8 4 7.4 3.9 8 3.9c4 0 6.5 4.1 6.5 4.1a11 11 0 0 1-2.1 2.4M4 5.6A11.4 11.4 0 0 0 1.5 8S4 12.1 8 12.1c.7 0 1.4-.1 2-.4",
15
+ lock: "M4 7.5V5.5a4 4 0 0 1 8 0v2M3.5 7.5h9V14h-9zM8 10v1.8",
16
+ unlock: "M4 7.5V5.5a4 4 0 0 1 7.8-1.2M3.5 7.5h9V14h-9zM8 10v1.8",
17
+ search: "M7 2.5A4.5 4.5 0 1 1 7 11.5 4.5 4.5 0 0 1 7 2.5M10.5 10.5L14 14",
18
+ filter: "M2 3h12L9.5 8.5V13l-3 1.5v-6z",
19
+ plus: "M8 3v10M3 8h10",
20
+ folder: "M1.5 4a1 1 0 0 1 1-1H6l1.5 1.5h6a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H2.5a1 1 0 0 1-1-1z",
21
+ folderOpen: "M1.5 4a1 1 0 0 1 1-1H6l1.5 1.5h5.5a1 1 0 0 1 1 1V7M1.5 13l1.7-6h11.6l-1.7 6z",
22
+ cube: "M8 1.5l6 3v7l-6 3-6-3v-7zM2 4.5l6 3 6-3M8 7.5V14.5",
23
+ pin: "M8 1.5A4.2 4.2 0 0 1 12.2 5.7C12.2 9 8 14.5 8 14.5S3.8 9 3.8 5.7A4.2 4.2 0 0 1 8 1.5M8 4.3a1.4 1.4 0 1 1 0 2.8 1.4 1.4 0 0 1 0-2.8",
24
+ sphere: "M8 1.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13M1.5 8h13M8 1.5c2 1.8 2 11.2 0 13M8 1.5c-2 1.8-2 11.2 0 13",
25
+ spline: "M2 13c0-5 3-2 6-6s2-5 6-5M2 13a1 1 0 1 0 .01 0M14 2a1 1 0 1 0 .01 0",
26
+ note: "M3 2h8l2 2v10H3zM11 2v2h2M5.5 7h5M5.5 10h5",
27
+ play: "M4.5 2.5l9 5.5-9 5.5z",
28
+ pause: "M4 2.5h2.8v11H4zM9.2 2.5H12v11H9.2z",
29
+ step: "M3 2.5l7 5.5-7 5.5zM12 2.5h1.5v11H12z",
30
+ stop: "M3.5 3.5h9v9h-9z",
31
+ undo: "M6 3L2.5 6.5 6 10M2.5 6.5H10a3.5 3.5 0 0 1 0 7H7",
32
+ redo: "M10 3l3.5 3.5L10 10M13.5 6.5H6a3.5 3.5 0 0 0 0 7h3",
33
+ save: "M2.5 2.5h9L13.5 4.5v9h-11zM5 2.5V6h5V2.5M5 13.5V9.5h6v4",
34
+ import: "M8 2v8M8 10l-3-3M8 10l3-3M2.5 12v1.5h11V12",
35
+ export: "M8 10V2M8 2L5 5M8 2l3 3M2.5 12v1.5h11V12",
36
+ settings: "M8 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4M8 1.5l.6 1.8 1.9-.5 1 1.7 1.8.6-.5 1.9 1.2 1.5-1.2 1.5.5 1.9-1.8.6-1 1.7-1.9-.5-.6 1.8-.6-1.8-1.9.5-1-1.7-1.8-.6.5-1.9L1.5 8l1.2-1.5-.5-1.9 1.8-.6 1-1.7 1.9.5z",
37
+ help: "M5.5 6a2.5 2.5 0 1 1 3.5 2.3c-.7.3-1 .8-1 1.7M8 12.5v.1",
38
+ close: "M3.5 3.5l9 9M12.5 3.5l-9 9",
39
+ chevronRight: "M6 3.5L10.5 8 6 12.5",
40
+ chevronDown: "M3.5 6L8 10.5 12.5 6",
41
+ layers: "M8 1.5l6.5 3.5L8 8.5 1.5 5zM2.5 8L8 11l5.5-3M2.5 11L8 14l5.5-3",
42
+ sparkle: "M8 1.5L9.3 6 14 7.5 9.3 9 8 13.5 6.7 9 2 7.5 6.7 6zM12.8 11.5l.5 1.7 1.7.5-1.7.5-.5 1.7-.5-1.7-1.7-.5 1.7-.5z",
43
+ terminal: "M2 3h12v10H2zM4.5 6l2.5 2-2.5 2M8.5 10.5h3",
44
+ gauge: "M2 12.5a6 6 0 1 1 12 0M8 12.5L11 7M8 12a.5.5 0 1 0 .01 0",
45
+ film: "M2 2.5h12v11H2zM2 5.5h12M2 10.5h12M5.5 2.5v11M10.5 2.5v11",
46
+ image: "M2 2.5h12v11H2zM2 10.5l3.5-3.5 3 3 2-2 3.5 3.5M10.5 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2",
47
+ audio: "M2.5 6h2.5L9 3v10L5 10H2.5zM11.5 5.5a4 4 0 0 1 0 5M13 3.5a6.5 6.5 0 0 1 0 9",
48
+ script: "M5.5 4L2 8l3.5 4M10.5 4L14 8l-3.5 4",
49
+ network: "M8 2a1.8 1.8 0 1 1 0 3.6A1.8 1.8 0 0 1 8 2M3 10.5a1.8 1.8 0 1 1 0 3.6 1.8 1.8 0 0 1 0-3.6M13 10.5a1.8 1.8 0 1 1 0 3.6 1.8 1.8 0 0 1 0-3.6M7 5.3L4 10.6M9 5.3l3 5.3M4.8 12.3h6.4",
50
+ bulb: "M5 6.5a3 3 0 1 1 6 0c0 1.6-1 2.1-1.3 3.2H6.3C6 8.6 5 8.1 5 6.5zM6.5 11.5h3M7 13.5h2",
51
+ walk: "M8 2a1.3 1.3 0 1 1 0 2.6A1.3 1.3 0 0 1 8 2M8 5v4M8 9l-2 5M8 9l2.5 2 .5 3M8 6l-2.5 1.5L5 10M8 6l2.5 1 1 2.5",
52
+ panel: "M2 2.5h12v11H2zM2 5.5h12M10.5 5.5v8",
53
+ copy: "M5.5 5.5h8v8h-8zM3 10.5h-.5v-8h8V3",
54
+ list: "M5.5 4h8M5.5 8h8M5.5 12h8M2.5 4h.01M2.5 8h.01M2.5 12h.01",
55
+ gridView: "M2.5 2.5h4.5v4.5H2.5zM9 2.5h4.5v4.5H9zM2.5 9h4.5v4.5H2.5zM9 9h4.5v4.5H9z",
56
+ trash: "M2.5 4h11M6 4V2.5h4V4M4 4l.7 9.5h6.6L12 4M6.7 6.7v4.6M9.3 6.7v4.6",
57
+ target: "M8 2.5a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11M8 5.7a2.3 2.3 0 1 1 0 4.6 2.3 2.3 0 0 1 0-4.6M8 1v2M8 13v2M1 8h2M13 8h2",
58
+ clock: "M8 2a6 6 0 1 1 0 12A6 6 0 0 1 8 2M8 4.5V8l2.5 1.5",
59
+ warning: "M8 2l6.5 11.5h-13zM8 6.5v3.5M8 12.2v.1",
60
+ info: "M8 2a6 6 0 1 1 0 12A6 6 0 0 1 8 2M8 7.5v4M8 5v.1",
61
+ error: "M8 2a6 6 0 1 1 0 12A6 6 0 0 1 8 2M6 6l4 4M10 6l-4 4",
62
+ command: "M6 6h4v4H6zM6 6H4.5A1.5 1.5 0 1 1 6 4.5zM10 6h1.5A1.5 1.5 0 1 0 10 4.5zM6 10H4.5A1.5 1.5 0 1 0 6 11.5zM10 10h1.5a1.5 1.5 0 1 1-1.5 1.5z",
63
+ };
64
+ /** One 16px stroke icon from the shell's internal set; sized via the `size` prop. */
65
+ export function Icon({ name, size = 14, ...rest }) {
66
+ return (_jsx("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", ...rest, children: _jsx("path", { d: PATHS[name] }) }));
67
+ }
68
+ /** Icon assigned to a scene-object kind for hierarchy rows and inspector headers. */
69
+ export function kindIcon(kind) {
70
+ if (kind === "note")
71
+ return "note";
72
+ if (kind.includes("spawn") || kind.includes("start") || kind.includes("marker"))
73
+ return "pin";
74
+ if (kind.includes("road") || kind.includes("route") || kind.includes("path") || kind.includes("line"))
75
+ return "spline";
76
+ if (kind.includes("zone") || kind.includes("aggro") || kind.includes("leash") || kind.includes("capture") || kind.includes("discover"))
77
+ return "sphere";
78
+ if (kind.includes("vegetation") || kind.includes("scatter") || kind.includes("grass") || kind.includes("tree") || kind.includes("field"))
79
+ return "terrain";
80
+ if (kind.includes("water") || kind.includes("pond") || kind.includes("soil"))
81
+ return "layers";
82
+ if (kind.includes("city") || kind.includes("prop") || kind.includes("building"))
83
+ return "cube";
84
+ return "cube";
85
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Persistent editor-shell layout state: which workspace is active, which panels are open, their
3
+ * sizes, and dock tab selections. Kept apart from `uiStore` (transient interaction state) so
4
+ * layout preferences survive reloads without entangling gizmo/placement churn. Persistence is
5
+ * throttled so drag-resizing never writes localStorage per pointer move.
6
+ */
7
+ /** Left-rail workspace modes. Scene/terrain/assets/materials/ai/multiplayer open real home panels; the rest are staged. */
8
+ export type EditorWorkspace = "scene" | "terrain" | "assets" | "materials" | "scripting" | "animation" | "audio" | "lighting" | "ai" | "multiplayer";
9
+ /** Bottom dock tool tabs. */
10
+ export type BottomDockTab = "content" | "console" | "profiler" | "animation" | "assistant";
11
+ /** Inspector header tabs. */
12
+ export type InspectorTab = "inspector" | "components" | "materials";
13
+ /** Left dock content pages (hierarchy plus the existing data panels). */
14
+ export type LeftDockPage = "hierarchy" | "collections" | "prefabs" | "catalogs";
15
+ /** Content-browser presentation density. */
16
+ export type BrowserViewMode = "grid" | "list";
17
+ /** The persisted + live shell layout state. */
18
+ export interface ShellLayoutState {
19
+ workspace: EditorWorkspace;
20
+ leftOpen: boolean;
21
+ rightOpen: boolean;
22
+ bottomOpen: boolean;
23
+ leftWidth: number;
24
+ rightWidth: number;
25
+ bottomHeight: number;
26
+ leftPage: LeftDockPage;
27
+ bottomTab: BottomDockTab;
28
+ inspectorTab: InspectorTab;
29
+ browserView: BrowserViewMode;
30
+ /** Inspector section collapse state, keyed by section id; absent = open. */
31
+ collapsed: Record<string, boolean>;
32
+ }
33
+ /** Bounds applied to persisted/live panel sizes so restored layouts can never crush the viewport. */
34
+ export declare const LAYOUT_LIMITS: {
35
+ readonly leftWidth: {
36
+ readonly min: 220;
37
+ readonly max: 420;
38
+ };
39
+ readonly rightWidth: {
40
+ readonly min: 260;
41
+ readonly max: 460;
42
+ };
43
+ readonly bottomHeight: {
44
+ readonly min: 160;
45
+ readonly max: 480;
46
+ };
47
+ };
48
+ /** Default shell layout, matching the reference composition. */
49
+ export declare const DEFAULT_LAYOUT: ShellLayoutState;
50
+ /** Normalizes any partially-persisted layout into a valid state (sizes clamped, unions guarded). */
51
+ export declare function normalizeLayout(raw: unknown): ShellLayoutState;
52
+ /** Subscribable store over {@link ShellLayoutState} with clamped resize helpers. */
53
+ export interface ShellLayoutStore {
54
+ getState(): ShellLayoutState;
55
+ patch(partial: Partial<ShellLayoutState>): void;
56
+ subscribe(listener: () => void): () => void;
57
+ /** Applies a resize delta to one panel dimension, clamped to {@link LAYOUT_LIMITS}. */
58
+ resize(dimension: "leftWidth" | "rightWidth" | "bottomHeight", delta: number): void;
59
+ /** Switches workspace; opening a workspace re-opens its home panel. */
60
+ setWorkspace(workspace: EditorWorkspace): void;
61
+ toggleSection(id: string): void;
62
+ /** Restores {@link DEFAULT_LAYOUT}. */
63
+ reset(): void;
64
+ }
65
+ /** How long resize/patch churn batches before one localStorage write. */
66
+ export declare const LAYOUT_PERSIST_DELAY_MS = 400;
67
+ /** Creates the shell layout store, seeded from localStorage when available. */
68
+ export declare function createShellLayoutStore(gameId: string): ShellLayoutStore;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Persistent editor-shell layout state: which workspace is active, which panels are open, their
3
+ * sizes, and dock tab selections. Kept apart from `uiStore` (transient interaction state) so
4
+ * layout preferences survive reloads without entangling gizmo/placement churn. Persistence is
5
+ * throttled so drag-resizing never writes localStorage per pointer move.
6
+ */
7
+ /** Bounds applied to persisted/live panel sizes so restored layouts can never crush the viewport. */
8
+ export const LAYOUT_LIMITS = {
9
+ leftWidth: { min: 220, max: 420 },
10
+ rightWidth: { min: 260, max: 460 },
11
+ bottomHeight: { min: 160, max: 480 },
12
+ };
13
+ /** Default shell layout, matching the reference composition. */
14
+ export const DEFAULT_LAYOUT = {
15
+ workspace: "scene",
16
+ leftOpen: true,
17
+ rightOpen: true,
18
+ bottomOpen: true,
19
+ leftWidth: 288,
20
+ rightWidth: 320,
21
+ bottomHeight: 236,
22
+ leftPage: "hierarchy",
23
+ bottomTab: "content",
24
+ inspectorTab: "inspector",
25
+ browserView: "grid",
26
+ collapsed: {},
27
+ };
28
+ function clamp(value, min, max) {
29
+ return Math.min(max, Math.max(min, value));
30
+ }
31
+ /** Normalizes any partially-persisted layout into a valid state (sizes clamped, unions guarded). */
32
+ export function normalizeLayout(raw) {
33
+ if (typeof raw !== "object" || raw === null)
34
+ return { ...DEFAULT_LAYOUT };
35
+ const record = raw;
36
+ const pick = (value, allowed, fallback) => typeof value === "string" && allowed.includes(value) ? value : fallback;
37
+ const num = (value, fallback, limit) => typeof value === "number" && Number.isFinite(value) ? clamp(value, limit.min, limit.max) : fallback;
38
+ const bool = (value, fallback) => (typeof value === "boolean" ? value : fallback);
39
+ return {
40
+ workspace: pick(record.workspace, ["scene", "terrain", "assets", "materials", "scripting", "animation", "audio", "lighting", "ai", "multiplayer"], DEFAULT_LAYOUT.workspace),
41
+ leftOpen: bool(record.leftOpen, DEFAULT_LAYOUT.leftOpen),
42
+ rightOpen: bool(record.rightOpen, DEFAULT_LAYOUT.rightOpen),
43
+ bottomOpen: bool(record.bottomOpen, DEFAULT_LAYOUT.bottomOpen),
44
+ leftWidth: num(record.leftWidth, DEFAULT_LAYOUT.leftWidth, LAYOUT_LIMITS.leftWidth),
45
+ rightWidth: num(record.rightWidth, DEFAULT_LAYOUT.rightWidth, LAYOUT_LIMITS.rightWidth),
46
+ bottomHeight: num(record.bottomHeight, DEFAULT_LAYOUT.bottomHeight, LAYOUT_LIMITS.bottomHeight),
47
+ leftPage: pick(record.leftPage, ["hierarchy", "collections", "prefabs", "catalogs"], DEFAULT_LAYOUT.leftPage),
48
+ bottomTab: pick(record.bottomTab, ["content", "console", "profiler", "animation", "assistant"], DEFAULT_LAYOUT.bottomTab),
49
+ inspectorTab: pick(record.inspectorTab, ["inspector", "components", "materials"], DEFAULT_LAYOUT.inspectorTab),
50
+ browserView: pick(record.browserView, ["grid", "list"], DEFAULT_LAYOUT.browserView),
51
+ collapsed: typeof record.collapsed === "object" && record.collapsed !== null
52
+ ? Object.fromEntries(Object.entries(record.collapsed).filter((entry) => typeof entry[1] === "boolean"))
53
+ : {},
54
+ };
55
+ }
56
+ function storageKey(gameId) {
57
+ return `jgeditor:shell:${gameId}`;
58
+ }
59
+ /** How long resize/patch churn batches before one localStorage write. */
60
+ export const LAYOUT_PERSIST_DELAY_MS = 400;
61
+ /** Creates the shell layout store, seeded from localStorage when available. */
62
+ export function createShellLayoutStore(gameId) {
63
+ let state = { ...DEFAULT_LAYOUT };
64
+ try {
65
+ const raw = localStorage.getItem(storageKey(gameId));
66
+ if (raw !== null)
67
+ state = normalizeLayout(JSON.parse(raw));
68
+ }
69
+ catch {
70
+ // Sandboxed/denied storage falls back to defaults.
71
+ }
72
+ const listeners = new Set();
73
+ let persistTimer = null;
74
+ const schedulePersist = () => {
75
+ if (persistTimer !== null)
76
+ return;
77
+ persistTimer = setTimeout(() => {
78
+ persistTimer = null;
79
+ try {
80
+ localStorage.setItem(storageKey(gameId), JSON.stringify(state));
81
+ }
82
+ catch {
83
+ // Persistence is best-effort.
84
+ }
85
+ }, LAYOUT_PERSIST_DELAY_MS);
86
+ };
87
+ const emit = () => {
88
+ for (const listener of listeners)
89
+ listener();
90
+ schedulePersist();
91
+ };
92
+ const store = {
93
+ getState: () => state,
94
+ patch(partial) {
95
+ state = { ...state, ...partial };
96
+ emit();
97
+ },
98
+ subscribe(listener) {
99
+ listeners.add(listener);
100
+ return () => {
101
+ listeners.delete(listener);
102
+ };
103
+ },
104
+ resize(dimension, delta) {
105
+ const limit = LAYOUT_LIMITS[dimension];
106
+ const next = clamp(state[dimension] + delta, limit.min, limit.max);
107
+ if (next === state[dimension])
108
+ return;
109
+ state = { ...state, [dimension]: next };
110
+ emit();
111
+ },
112
+ setWorkspace(workspace) {
113
+ const partial = { workspace };
114
+ if (workspace === "scene" || workspace === "terrain") {
115
+ partial.leftOpen = true;
116
+ partial.leftPage = "hierarchy";
117
+ }
118
+ if (workspace === "scripting") {
119
+ // Scripting owns the left dock (trigger list + editor); keep hierarchy page stale underneath.
120
+ partial.leftOpen = true;
121
+ partial.rightOpen = true;
122
+ partial.inspectorTab = "components";
123
+ }
124
+ if (workspace === "assets") {
125
+ partial.bottomOpen = true;
126
+ partial.bottomTab = "content";
127
+ }
128
+ // Materials home: left inventory of real meta.materialId stamps + right materials inspector.
129
+ if (workspace === "materials") {
130
+ partial.leftOpen = true;
131
+ partial.rightOpen = true;
132
+ partial.inspectorTab = "materials";
133
+ }
134
+ if (workspace === "ai") {
135
+ partial.bottomOpen = true;
136
+ partial.bottomTab = "assistant";
137
+ }
138
+ // Network owns the left dock; lighting is a viewport panel but keeps hierarchy open for context.
139
+ if (workspace === "multiplayer" || workspace === "lighting") {
140
+ partial.leftOpen = true;
141
+ }
142
+ state = { ...state, ...partial };
143
+ emit();
144
+ },
145
+ toggleSection(id) {
146
+ const collapsed = { ...state.collapsed, [id]: !(state.collapsed[id] === true) };
147
+ state = { ...state, collapsed };
148
+ emit();
149
+ },
150
+ reset() {
151
+ state = { ...DEFAULT_LAYOUT };
152
+ emit();
153
+ },
154
+ };
155
+ return store;
156
+ }
@@ -0,0 +1,37 @@
1
+ import type { EditorPath } from "@jgengine/core/editor/index";
2
+ import { type PathFollowConfig } from "@jgengine/core/nav/pathFollow";
3
+ /** A scrubbable scene path — real document polylines with enough points to sample. */
4
+ export interface ScrubbablePath {
5
+ id: string;
6
+ kind: string;
7
+ label: string;
8
+ pointCount: number;
9
+ /** Total arc length in world units (0 when degenerate). */
10
+ length: number;
11
+ }
12
+ /**
13
+ * Builds the path-follow config used by the flythrough scrubber. Pure; no document mutation.
14
+ * Returns null when the path cannot be sampled (fewer than two points).
15
+ *
16
+ * @internal
17
+ */
18
+ export declare function pathFollowConfigFromEditorPath(path: EditorPath): PathFollowConfig | null;
19
+ /**
20
+ * Lists document paths that can be scrubbed, with length metadata for the dock UI.
21
+ *
22
+ * @internal
23
+ */
24
+ export declare function listScrubbablePaths(paths: readonly EditorPath[]): ScrubbablePath[];
25
+ /**
26
+ * Samples a world position along an authored path at normalized progress `t` in `[0, 1]`.
27
+ * Clamps `t`. Returns null when the path has fewer than two points.
28
+ *
29
+ * @internal
30
+ */
31
+ export declare function samplePathAt(path: EditorPath, t: number): {
32
+ x: number;
33
+ y: number;
34
+ z: number;
35
+ distance: number;
36
+ length: number;
37
+ } | null;