@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,33 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { FOCUS_RING, INPUT_CLS, NUMERIC } from "./shell/theme.js";
4
+ /**
5
+ * Searchable parent-target menu for hierarchy / viewport "Parent toโ€ฆ".
6
+ * Parent owns candidate list (cycle-safe) and the set_parent write.
7
+ * @internal
8
+ */
9
+ export function ParentPickerMenu({ x, y, candidates, onPick, onClose, }) {
10
+ const [query, setQuery] = useState("");
11
+ const inputRef = useRef(null);
12
+ useEffect(() => {
13
+ inputRef.current?.focus();
14
+ }, []);
15
+ const filtered = useMemo(() => {
16
+ const q = query.trim().toLowerCase();
17
+ if (q.length === 0)
18
+ return candidates;
19
+ return candidates.filter((entry) => entry.label.toLowerCase().includes(q) || entry.id.toLowerCase().includes(q));
20
+ }, [candidates, query]);
21
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "pointer-events-auto fixed inset-0 z-[60]", onPointerDown: onClose, onContextMenu: (event) => event.preventDefault() }), _jsxs("div", { role: "menu", "aria-label": "Parent to object", className: "pointer-events-auto fixed z-[61] flex max-h-72 w-64 flex-col overflow-hidden rounded-md border border-white/15 bg-neutral-900/95 shadow-2xl backdrop-blur-sm", style: { left: x, top: y }, onContextMenu: (event) => event.preventDefault(), children: [_jsx("div", { className: "border-b border-white/10 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/45", children: "Parent to\u2026" }), _jsx("div", { className: "border-b border-white/10 p-1.5", children: _jsx("input", { ref: inputRef, value: query, onChange: (event) => setQuery(event.target.value), onKeyDown: (event) => {
22
+ if (event.key === "Escape") {
23
+ event.preventDefault();
24
+ onClose();
25
+ }
26
+ }, placeholder: "Search objects\u2026", "aria-label": "Filter parent candidates", className: `h-7 w-full px-2 text-[12px] ${INPUT_CLS} ${FOCUS_RING}` }) }), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto py-1", children: [_jsx("button", { type: "button", role: "menuitem", onPointerDown: (event) => {
27
+ event.stopPropagation();
28
+ onPick(null);
29
+ }, className: "flex w-full items-center px-3 py-1.5 text-left text-[13px] text-white/85 hover:bg-cyan-400/20 hover:text-white", children: "\u2014 none (root) \u2014" }), filtered.length === 0 ? (_jsx("div", { className: "px-3 py-2 text-[11px] text-white/40", children: candidates.length === 0 ? "No valid parents (would create a cycle)." : "No matches." })) : (filtered.map((entry) => (_jsxs("button", { type: "button", role: "menuitem", onPointerDown: (event) => {
30
+ event.stopPropagation();
31
+ onPick(entry.id);
32
+ }, className: "flex w-full items-center gap-2 px-3 py-1.5 text-left text-[13px] text-white/85 hover:bg-cyan-400/20 hover:text-white", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: entry.label }), entry.label !== entry.id ? (_jsx("span", { className: `shrink-0 text-[10px] text-white/35 ${NUMERIC}`, children: entry.id })) : null] }, entry.id))))] })] })] }));
33
+ }
package/dist/PerfProbe.js CHANGED
@@ -1,34 +1,101 @@
1
1
  import { useFrame, useThree } from "@react-three/fiber";
2
2
  import { memo, useRef } from "react";
3
+ import { devtools } from "@jgengine/core/devtools/devtools";
3
4
  import { editorPerfMarks } from "./perfMarks.js";
5
+ import { getCameraTelemetry } from "./shell/cameraTelemetry.js";
6
+ import { frameBudgetFromStats } from "./shell/perfHistory.js";
4
7
  const SAMPLE_WINDOW_MS = 500;
8
+ /** Sum-of-abs delta on camera position/orientation above which a frame counts as camera activity. */
9
+ const CAMERA_MOVE_EPSILON = 1e-4;
5
10
  /** In-canvas frame counter: publishes fps/draw-call samples to the editor host every 500ms. */
6
11
  export const PerfProbe = memo(function PerfProbe({ api }) {
7
12
  const gl = useThree((state) => state.gl);
13
+ const camera = useThree((state) => state.camera);
8
14
  const framesRef = useRef(0);
9
15
  const windowStartRef = useRef(0);
16
+ const activeRef = useRef(false);
17
+ const lastCamRef = useRef({ px: 0, py: 0, pz: 0, qx: 0, qy: 0, qz: 0, qw: 1 });
18
+ const lastDrawRef = useRef(-1);
19
+ const lastTriRef = useRef(-1);
10
20
  useFrame(() => {
11
21
  const now = performance.now();
12
22
  if (windowStartRef.current === 0)
13
23
  windowStartRef.current = now;
14
24
  framesRef.current += 1;
25
+ // Detect camera activity frame-to-frame so a single moved frame inside the window still counts.
26
+ const cam = lastCamRef.current;
27
+ const dPos = Math.abs(camera.position.x - cam.px) +
28
+ Math.abs(camera.position.y - cam.py) +
29
+ Math.abs(camera.position.z - cam.pz);
30
+ const dRot = Math.abs(camera.quaternion.x - cam.qx) +
31
+ Math.abs(camera.quaternion.y - cam.qy) +
32
+ Math.abs(camera.quaternion.z - cam.qz) +
33
+ Math.abs(camera.quaternion.w - cam.qw);
34
+ if (dPos > CAMERA_MOVE_EPSILON || dRot > CAMERA_MOVE_EPSILON)
35
+ activeRef.current = true;
36
+ // Publish the camera basis for the DOM orientation widget (mutable channel, no React state).
37
+ const telemetry = getCameraTelemetry();
38
+ const m = camera.matrixWorld.elements;
39
+ telemetry.rightX = m[0];
40
+ telemetry.rightY = m[1];
41
+ telemetry.rightZ = m[2];
42
+ telemetry.upX = m[4];
43
+ telemetry.upY = m[5];
44
+ telemetry.upZ = m[6];
45
+ // Camera looks down -Z; store the viewing direction so depth<0 means "toward the camera".
46
+ telemetry.forwardX = -m[8];
47
+ telemetry.forwardY = -m[9];
48
+ telemetry.forwardZ = -m[10];
49
+ telemetry.azimuth = Math.atan2(-m[8], -m[10]);
50
+ telemetry.elevation = Math.asin(Math.max(-1, Math.min(1, -m[9])));
51
+ telemetry.version += 1;
52
+ cam.px = camera.position.x;
53
+ cam.py = camera.position.y;
54
+ cam.pz = camera.position.z;
55
+ cam.qx = camera.quaternion.x;
56
+ cam.qy = camera.quaternion.y;
57
+ cam.qz = camera.quaternion.z;
58
+ cam.qw = camera.quaternion.w;
15
59
  const elapsed = now - windowStartRef.current;
16
60
  if (elapsed < SAMPLE_WINDOW_MS)
17
61
  return;
18
62
  const fps = (framesRef.current * 1000) / elapsed;
19
63
  const authoring = editorPerfMarks.flush();
64
+ const drawCalls = gl.info.render.calls;
65
+ const triangles = gl.info.render.triangles;
66
+ // Edit activity: viewport raycasts / preview-mesh rebuilds, or the rendered scene changed.
67
+ const sceneChanged = lastDrawRef.current >= 0 && (drawCalls !== lastDrawRef.current || triangles !== lastTriRef.current);
68
+ const active = activeRef.current || authoring.authoringMs > 0 || sceneChanged;
69
+ lastDrawRef.current = drawCalls;
70
+ lastTriRef.current = triangles;
71
+ // Chromium-only; other browsers omit memory rather than fabricating a value.
72
+ const heap = performance.memory;
73
+ const memoryMb = heap === undefined ? undefined : Math.round((heap.usedJSHeapSize / (1024 * 1024)) * 10) / 10;
74
+ // Real sim / outside split from FrameDriver โ†’ devtools.frame (same as debug_snapshot). Omit when
75
+ // no runtime frames have been recorded yet โ€” never invent a render series from viewport fps alone.
76
+ const budget = frameBudgetFromStats(devtools.frame.stats());
20
77
  api.setPerf({
21
78
  fps: Math.round(fps * 10) / 10,
22
79
  frameMs: Math.round((elapsed / framesRef.current) * 100) / 100,
23
- drawCalls: gl.info.render.calls,
24
- triangles: gl.info.render.triangles,
80
+ drawCalls,
81
+ triangles,
25
82
  sampledAt: now,
83
+ active,
26
84
  raycastMs: authoring.raycastMs,
27
85
  rebuildMs: authoring.rebuildMs,
28
86
  authoringMs: authoring.authoringMs,
87
+ ...(memoryMb === undefined ? {} : { memoryMb }),
88
+ ...(budget === null
89
+ ? {}
90
+ : {
91
+ simMs: budget.simMs,
92
+ outsideMs: budget.outsideMs,
93
+ ...(budget.phases === undefined ? {} : { phases: budget.phases }),
94
+ }),
29
95
  });
30
96
  framesRef.current = 0;
31
97
  windowStartRef.current = now;
98
+ activeRef.current = false;
32
99
  });
33
100
  return null;
34
101
  });
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
+ import { CONTROL, INPUT_CLS, MICRO_LABEL } from "./shell/theme.js";
3
4
  import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
4
- const INPUT = "rounded-md border border-white/10 bg-black/40 px-2 py-1 outline-none transition-colors placeholder:text-neutral-600 focus:border-cyan-400/60 focus:bg-black/60";
5
- const BTN = "rounded-md bg-white/[0.04] px-2 py-1 text-neutral-300 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/10 hover:text-neutral-100";
5
+ const BTN = `${CONTROL} px-2 py-1 text-[11px] disabled:opacity-40`;
6
6
  function newPrefabId() {
7
7
  return `prefab_${Date.now().toString(36)}_${Math.floor(Math.random() * 1296).toString(36)}`;
8
8
  }
@@ -29,14 +29,14 @@ export function PrefabsPanel({ session, api }) {
29
29
  const focus = api.getFocusTarget() ?? { x: 0, y: 0, z: 0 };
30
30
  session.dispatch({ type: "insertPrefab", prefabId: prefab.id, at: focus });
31
31
  };
32
- return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 p-2", children: [_jsxs("div", { className: "flex gap-1", children: [_jsx("input", { className: `min-w-0 flex-1 ${INPUT}`, placeholder: "New prefab name\u2026", value: name, onChange: (event) => setName(event.target.value), onKeyDown: (event) => {
32
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 p-2", children: [_jsx("div", { className: MICRO_LABEL, children: "Prefabs" }), _jsxs("div", { className: "flex gap-1", children: [_jsx("input", { className: `h-7 min-w-0 flex-1 px-2 ${INPUT_CLS}`, placeholder: "New prefab name\u2026", value: name, onChange: (event) => setName(event.target.value), onKeyDown: (event) => {
33
33
  if (event.key === "Enter")
34
34
  makeFromSelection();
35
- } }), _jsx("button", { type: "button", className: BTN, disabled: selection.length === 0 || name.trim().length === 0, onClick: makeFromSelection, children: "Make prefab" })] }), selection.length === 0 ? (_jsx("div", { className: "text-[10px] text-neutral-500", children: "Select objects, name them, then make a prefab from the selection." })) : null, _jsx("div", { className: "min-h-0 flex-1 space-y-1 overflow-auto pr-1", children: prefabs.length === 0 ? (_jsx("div", { className: "text-neutral-600", children: "No prefabs yet." })) : (prefabs.map((prefab) => {
35
+ } }), _jsx("button", { type: "button", className: BTN, disabled: selection.length === 0 || name.trim().length === 0, onClick: makeFromSelection, children: "Make prefab" })] }), selection.length === 0 ? (_jsx("div", { className: "text-[10px] text-neutral-500", children: "Select objects, name them, then make a prefab from the selection." })) : null, _jsx("div", { className: "min-h-0 flex-1 space-y-1.5 overflow-auto pr-1", children: prefabs.length === 0 ? (_jsx("div", { className: "text-[11px] text-neutral-600", children: "No prefabs yet." })) : (prefabs.map((prefab) => {
36
36
  const count = prefab.fragment.markers.length +
37
37
  prefab.fragment.volumes.length +
38
38
  prefab.fragment.paths.length +
39
39
  prefab.fragment.annotations.length;
40
- return (_jsxs("div", { className: "flex items-center gap-2 rounded-md bg-white/[0.03] p-1.5 ring-1 ring-inset ring-white/[0.06]", children: [_jsxs("div", { className: "min-w-0 flex-1 truncate", children: [_jsx("div", { className: "truncate text-neutral-100", children: prefab.name }), _jsxs("div", { className: "text-[10px] text-neutral-500", children: [count, " objects"] })] }), _jsx("button", { type: "button", className: "shrink-0 rounded-md bg-cyan-500/20 px-2 py-0.5 text-cyan-200 ring-1 ring-inset ring-cyan-400/30 transition-colors hover:bg-cyan-500/30", onClick: () => insertAt(prefab), children: "Insert" }), _jsx("button", { type: "button", className: "shrink-0 rounded-md bg-rose-500/15 px-2 py-0.5 text-rose-200 ring-1 ring-inset ring-rose-400/25 transition-colors hover:bg-rose-500/25", onClick: () => session.dispatch({ type: "deletePrefab", prefabId: prefab.id }), children: "Delete" })] }, prefab.id));
40
+ return (_jsxs("div", { className: "flex items-center gap-2 rounded-[6px] border border-white/[0.07] bg-white/[0.02] p-2", children: [_jsxs("div", { className: "min-w-0 flex-1 truncate", children: [_jsx("div", { className: "truncate text-[12px] text-neutral-100", children: prefab.name }), _jsxs("div", { className: "text-[10px] text-neutral-500", children: [count, " objects"] })] }), _jsx("button", { type: "button", className: "shrink-0 rounded-[5px] border border-cyan-400/30 bg-cyan-500/15 px-2 py-0.5 text-[10px] text-cyan-200 transition-colors hover:bg-cyan-500/25", onClick: () => insertAt(prefab), children: "Insert" }), _jsx("button", { type: "button", className: "shrink-0 rounded-[5px] border border-rose-400/25 bg-rose-500/15 px-2 py-0.5 text-[10px] text-rose-200 transition-colors hover:bg-rose-500/25", onClick: () => session.dispatch({ type: "deletePrefab", prefabId: prefab.id }), children: "Delete" })] }, prefab.id));
41
41
  })) })] }));
42
42
  }
@@ -8,11 +8,13 @@ export declare function RuntimePlayPublisher({ api }: {
8
8
  api: EditorHostApi;
9
9
  }): null;
10
10
  /**
11
- * Play-mode chrome: pause/step controls plus a compact reverse-channel entity inspector.
12
- * Transform nudges go through `runtime_set` (document write-back by default).
11
+ * Play-mode chrome: slim shell app bar (Pause/Step/Exit via runtime play controls) plus a
12
+ * compact reverse-channel entity inspector. Transform nudges go through `runtime_set`
13
+ * (document write-back by default).
13
14
  * @internal
14
15
  */
15
- export declare function RuntimePlayInspectorChrome({ api, onExit, }: {
16
+ export declare function RuntimePlayInspectorChrome({ gameId, api, onExit, }: {
17
+ gameId: string;
16
18
  api: EditorHostApi;
17
19
  onExit: () => void;
18
20
  }): import("react").JSX.Element;
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
3
3
  import { useFrame } from "@react-three/fiber";
4
4
  import { useGameContext } from "@jgengine/react/provider";
5
5
  import { MICRO } from "./chromeStyles.js";
6
+ import { PlayModeBar } from "./shell/PlayModeBar.js";
6
7
  const PUBLISH_MS = 100;
7
8
  function metaValues(meta) {
8
9
  if (meta === null || typeof meta !== "object" || Array.isArray(meta))
@@ -76,11 +77,12 @@ export function RuntimePlayPublisher({ api }) {
76
77
  return null;
77
78
  }
78
79
  /**
79
- * Play-mode chrome: pause/step controls plus a compact reverse-channel entity inspector.
80
- * Transform nudges go through `runtime_set` (document write-back by default).
80
+ * Play-mode chrome: slim shell app bar (Pause/Step/Exit via runtime play controls) plus a
81
+ * compact reverse-channel entity inspector. Transform nudges go through `runtime_set`
82
+ * (document write-back by default).
81
83
  * @internal
82
84
  */
83
- export function RuntimePlayInspectorChrome({ api, onExit, }) {
85
+ export function RuntimePlayInspectorChrome({ gameId, api, onExit, }) {
84
86
  const [play, setPlay] = useState(() => api.getPlayControl());
85
87
  const [summary, setSummary] = useState(null);
86
88
  const [selectedId, setSelectedId] = useState(null);
@@ -109,15 +111,6 @@ export function RuntimePlayInspectorChrome({ api, onExit, }) {
109
111
  }
110
112
  setDetail(JSON.stringify(got.result, null, 2));
111
113
  }, [api, selectedId, summary?.entityCount, play.pendingSteps, play.paused]);
112
- const pause = () => {
113
- api.handle({ method: "runtime_pause" });
114
- };
115
- const resume = () => {
116
- api.handle({ method: "runtime_resume" });
117
- };
118
- const step = () => {
119
- api.handle({ method: "runtime_step", frames: 1 });
120
- };
121
114
  const nudgeSelected = (axis, delta) => {
122
115
  if (selectedId === null)
123
116
  return;
@@ -136,5 +129,5 @@ export function RuntimePlayInspectorChrome({ api, onExit, }) {
136
129
  },
137
130
  });
138
131
  };
139
- return (_jsxs("div", { className: "pointer-events-none absolute inset-0 z-50 flex flex-col", children: [_jsx("div", { className: "pointer-events-none flex justify-center pt-2", children: _jsxs("div", { className: "pointer-events-auto flex items-center gap-2 rounded-full border border-white/10 bg-black/75 px-3 py-1.5 text-[11px] text-neutral-100 shadow-lg shadow-black/40 backdrop-blur-md", children: [_jsx("span", { className: "text-neutral-300", children: play.paused ? "โšโš Paused" : "โ–ถ Playing" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.08] px-2 py-0.5 hover:bg-white/15", onClick: () => (play.paused ? resume() : pause()), children: play.paused ? "Resume" : "Pause" }), _jsxs("button", { type: "button", className: "rounded-md bg-white/[0.08] px-2 py-0.5 hover:bg-white/15 disabled:opacity-40", onClick: step, disabled: !play.paused, children: ["Step", play.pendingSteps > 0 ? ` (${play.pendingSteps})` : ""] }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.08] px-2 py-0.5 hover:bg-white/15", onClick: onExit, children: "Editor (F2+E)" })] }) }), _jsxs("aside", { className: "pointer-events-auto ml-auto mt-2 mr-2 flex w-72 max-h-[70vh] flex-col overflow-hidden rounded-xl border border-white/[0.08] bg-[#0d0f13]/95 text-[11px] text-neutral-200 shadow-2xl shadow-black/50 backdrop-blur-md", children: [_jsxs("div", { className: "flex items-center border-b border-white/[0.06] px-3 py-2", children: [_jsx("div", { className: MICRO, children: "Runtime" }), _jsxs("div", { className: "ml-auto text-neutral-500", children: [summary?.entityCount ?? 0, " entities"] })] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-auto p-2", children: [(summary?.entities ?? []).map((entity) => (_jsxs("button", { type: "button", className: `mb-1 block w-full truncate rounded-md px-2 py-1 text-left ${selectedId === entity.id ? "bg-cyan-500/20 text-cyan-100" : "hover:bg-white/[0.06]"}`, onClick: () => setSelectedId(entity.id), children: [entity.id, entity.valueKeys.length > 0 ? (_jsx("span", { className: "ml-1 text-neutral-500", children: entity.valueKeys.join(",") })) : null] }, entity.id))), (summary?.entities.length ?? 0) === 0 ? (_jsx("div", { className: "px-2 py-3 text-neutral-500", children: "No live entities yet \u2014 play or push_runtime_delta." })) : null] }), selectedId !== null ? (_jsxs("div", { className: "border-t border-white/[0.06] p-2", children: [_jsxs("div", { className: "mb-1 flex gap-1", children: [_jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("x", -1), children: "\u2212X" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("x", 1), children: "+X" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("z", -1), children: "\u2212Z" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("z", 1), children: "+Z" })] }), _jsx("pre", { className: "max-h-40 overflow-auto rounded-md border border-white/[0.06] bg-black/40 p-2 text-[10px] text-neutral-400", children: detail }), _jsx("div", { className: "mt-1 text-[10px] text-neutral-500", children: "Tweaks write back to the scene document (undoable)." })] })) : null] })] }));
132
+ return (_jsxs("div", { className: "pointer-events-none absolute inset-0 z-50 flex flex-col", children: [_jsx(PlayModeBar, { gameId: gameId, api: api, onExit: onExit }), _jsxs("aside", { className: "pointer-events-auto ml-auto mt-2 mr-2 flex w-72 max-h-[70vh] flex-col overflow-hidden rounded-xl border border-white/[0.08] bg-[#0d0f13]/95 text-[11px] text-neutral-200 shadow-2xl shadow-black/50 backdrop-blur-md", children: [_jsxs("div", { className: "flex items-center border-b border-white/[0.06] px-3 py-2", children: [_jsx("div", { className: MICRO, children: "Runtime" }), _jsxs("div", { className: "ml-auto text-neutral-500", children: [summary?.entityCount ?? 0, " entities"] })] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-auto p-2", children: [(summary?.entities ?? []).map((entity) => (_jsxs("button", { type: "button", className: `mb-1 block w-full truncate rounded-md px-2 py-1 text-left ${selectedId === entity.id ? "bg-cyan-500/20 text-cyan-100" : "hover:bg-white/[0.06]"}`, onClick: () => setSelectedId(entity.id), children: [entity.id, entity.valueKeys.length > 0 ? (_jsx("span", { className: "ml-1 text-neutral-500", children: entity.valueKeys.join(",") })) : null] }, entity.id))), (summary?.entities.length ?? 0) === 0 ? (_jsx("div", { className: "px-2 py-3 text-neutral-500", children: "No live entities yet \u2014 play or push_runtime_delta." })) : null] }), selectedId !== null ? (_jsxs("div", { className: "border-t border-white/[0.06] p-2", children: [_jsxs("div", { className: "mb-1 flex gap-1", children: [_jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("x", -1), children: "\u2212X" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("x", 1), children: "+X" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("z", -1), children: "\u2212Z" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("z", 1), children: "+Z" })] }), _jsx("pre", { className: "max-h-40 overflow-auto rounded-md border border-white/[0.06] bg-black/40 p-2 text-[10px] text-neutral-400", children: detail }), _jsx("div", { className: "mt-1 text-[10px] text-neutral-500", children: "Tweaks write back to the scene document (undoable)." })] })) : null] })] }));
140
133
  }
@@ -3,7 +3,9 @@ import type { EditorHostApi } from "./session.js";
3
3
  * Deterministic instanced preview of every foliage/scatter region in the document, grounded on the
4
4
  * sculpt terrain (or the game's ground) and rebuilt whenever a region or its density changes. Renders
5
5
  * real per-species proxy models through the shared `InstancedScatter` primitive โ€” the same renderer a
6
- * game uses to consume `resolveScatter` at runtime.
6
+ * game uses to consume `resolveScatter` at runtime. Subscribes only to the document slices scatter
7
+ * actually reads (markers/volumes/paths for clearance, terrain for grounding), so selection changes
8
+ * and unrelated edits (notes, grids, catalogs) never re-resolve up to {@link MAX_INSTANCES} placements.
7
9
  * @internal โ€” mounted by `EditorApp`; not a game-author entry point.
8
10
  */
9
11
  export declare const ScatterPreview: import("react").MemoExoticComponent<({ api }: {
@@ -1,30 +1,33 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { memo, useEffect, useMemo, useState } from "react";
2
+ import { memo, useMemo } from "react";
3
3
  import { editableTerrainFromSnapshot } from "@jgengine/core/world/terraform";
4
4
  import { resolveScatter } from "@jgengine/core/world/scatterRegion";
5
5
  import { useGameContext } from "@jgengine/react/provider";
6
6
  import { InstancedScatter } from "@jgengine/shell/scatter";
7
+ import { useStoreSelector } from "./useStoreSelector.js";
7
8
  const MAX_INSTANCES = 20000;
8
9
  /**
9
10
  * Deterministic instanced preview of every foliage/scatter region in the document, grounded on the
10
11
  * sculpt terrain (or the game's ground) and rebuilt whenever a region or its density changes. Renders
11
12
  * real per-species proxy models through the shared `InstancedScatter` primitive โ€” the same renderer a
12
- * game uses to consume `resolveScatter` at runtime.
13
+ * game uses to consume `resolveScatter` at runtime. Subscribes only to the document slices scatter
14
+ * actually reads (markers/volumes/paths for clearance, terrain for grounding), so selection changes
15
+ * and unrelated edits (notes, grids, catalogs) never re-resolve up to {@link MAX_INSTANCES} placements.
13
16
  * @internal โ€” mounted by `EditorApp`; not a game-author entry point.
14
17
  */
15
18
  export const ScatterPreview = memo(function ScatterPreview({ api }) {
16
19
  const ctx = useGameContext();
17
20
  const session = api.getSession();
18
- const [, setTick] = useState(0);
19
- useEffect(() => session.subscribe(() => setTick((value) => value + 1)), [session]);
20
- const state = session.getState();
21
- const snapshot = state.document.terrain ?? null;
21
+ const markers = useStoreSelector(session, (state) => state.document.markers);
22
+ const volumes = useStoreSelector(session, (state) => state.document.volumes);
23
+ const paths = useStoreSelector(session, (state) => state.document.paths);
24
+ const snapshot = useStoreSelector(session, (state) => state.document.terrain ?? null);
22
25
  const baseGround = ctx.world.ground;
23
26
  const terrain = useMemo(() => {
24
27
  if (snapshot !== null)
25
28
  return editableTerrainFromSnapshot(snapshot, baseGround);
26
29
  return { sampleHeight: baseGround.sampleHeight, sampleNormal: baseGround.sampleNormal };
27
30
  }, [snapshot, baseGround]);
28
- const instances = useMemo(() => resolveScatter(state.document, terrain), [state.document, terrain]);
31
+ const instances = useMemo(() => resolveScatter({ markers, volumes, paths }, terrain), [markers, volumes, paths, terrain]);
29
32
  return _jsx(InstancedScatter, { instances: instances, maxInstances: MAX_INSTANCES });
30
33
  });
@@ -1,8 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { parseParams, randomizeGroupParams, resetGroupParams, } from "@jgengine/core/scene/sceneKinds";
4
- const INPUT = "rounded-md border border-white/10 bg-black/40 px-2 py-1 outline-none transition-colors placeholder:text-neutral-600 focus:border-cyan-400/60 focus:bg-black/60";
5
- const MICRO = "text-[9px] font-semibold uppercase tracking-wider text-neutral-500";
4
+ import { BORDER, CONTROL, FOCUS_RING, INPUT_CLS, MICRO_LABEL } from "./shell/theme.js";
5
+ const INPUT = `px-2 py-1 ${INPUT_CLS}`;
6
+ const MICRO = MICRO_LABEL;
7
+ const BTN = `${CONTROL} px-2 py-1 text-[10px] disabled:opacity-40`;
8
+ /** One-click archetype bundles: picking a preset writes its whole value bag as a single meta patch. */
9
+ function PresetRow({ schema, onMeta }) {
10
+ const presets = schema.presets ?? [];
11
+ if (presets.length === 0)
12
+ return null;
13
+ return (_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: MICRO, children: "preset" }), _jsxs("select", { className: `w-32 ${INPUT}`, value: "", onChange: (event) => {
14
+ const preset = presets.find((entry) => entry.id === event.target.value);
15
+ if (preset !== undefined)
16
+ onMeta({ ...preset.values }, `preset:${preset.id}`);
17
+ }, children: [_jsx("option", { value: "", disabled: true, children: "apply preset\u2026" }), presets.map((preset) => (_jsx("option", { value: preset.id, children: preset.label ?? preset.id }, preset.id)))] })] }));
18
+ }
6
19
  function labelOf(field) {
7
20
  return field.label ?? field.key;
8
21
  }
@@ -32,7 +45,7 @@ function FieldRow({ field, meta, onMeta }) {
32
45
  }
33
46
  case "color": {
34
47
  const value = params[field.key];
35
- return (_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: MICRO, children: labelOf(field) }), _jsx("input", { type: "color", className: "h-7 w-14 cursor-pointer rounded-md border border-white/10 bg-black/40", value: value, onChange: (event) => onMeta({ [field.key]: event.target.value }, coalesce) })] }));
48
+ return (_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: MICRO, children: labelOf(field) }), _jsx("input", { type: "color", className: `h-7 w-14 cursor-pointer rounded-[5px] border border-white/10 bg-black/40 ${FOCUS_RING}`, value: value, onChange: (event) => onMeta({ [field.key]: event.target.value }, coalesce) })] }));
36
49
  }
37
50
  case "text": {
38
51
  const value = params[field.key];
@@ -40,12 +53,12 @@ function FieldRow({ field, meta, onMeta }) {
40
53
  }
41
54
  case "seed": {
42
55
  const value = params[field.key];
43
- return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("label", { className: "flex min-w-0 flex-1 items-center gap-2", children: [_jsx("span", { className: MICRO, children: labelOf(field) }), _jsx("input", { className: `w-full min-w-0 ${INPUT}`, value: value, placeholder: "reroll\u2026", onChange: (event) => onMeta({ [field.key]: event.target.value }, coalesce) })] }), _jsx("button", { type: "button", className: "shrink-0 rounded-md bg-white/[0.04] px-2 py-1 text-neutral-300 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/10", title: "Reroll seed", onClick: () => onMeta({ [field.key]: `r${Math.abs(Math.round((value.length + 1) * 2654435761)).toString(36).slice(0, 6)}${value.length}` }, coalesce), children: "\u27F3" })] }));
56
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("label", { className: "flex min-w-0 flex-1 items-center gap-2", children: [_jsx("span", { className: MICRO, children: labelOf(field) }), _jsx("input", { className: `w-full min-w-0 ${INPUT}`, value: value, placeholder: "reroll\u2026", onChange: (event) => onMeta({ [field.key]: event.target.value }, coalesce) })] }), _jsx("button", { type: "button", className: BTN, title: "Reroll seed", onClick: () => onMeta({ [field.key]: `r${Math.abs(Math.round((value.length + 1) * 2654435761)).toString(36).slice(0, 6)}${value.length}` }, coalesce), children: "\u27F3" })] }));
44
57
  }
45
58
  case "weightedList": {
46
59
  const list = params[field.key];
47
60
  const set = (next) => onMeta({ [field.key]: next }, coalesce);
48
- return (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: MICRO, children: labelOf(field) }), list.map((entry, index) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { className: `min-w-0 flex-1 ${INPUT}`, value: entry.item, placeholder: field.itemLabel ?? "id", onChange: (event) => set(list.map((e, i) => (i === index ? { ...e, item: event.target.value } : e))) }), _jsx("input", { type: "number", step: 1, min: 0, className: `w-14 ${INPUT}`, value: entry.weight, onChange: (event) => set(list.map((e, i) => (i === index ? { ...e, weight: Math.max(0, Number(event.target.value)) } : e))) }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.04] px-1.5 py-1 text-neutral-400 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-rose-500/20 hover:text-rose-200", disabled: list.length <= 1, onClick: () => set(list.filter((_, i) => i !== index)), children: "\u00D7" })] }, index))), _jsx("button", { type: "button", className: "w-full rounded-md bg-white/[0.04] px-2 py-1 text-[10px] text-neutral-300 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/10", onClick: () => set([...list, { item: "", weight: 1 }]), children: "+ row" })] }));
61
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: MICRO, children: labelOf(field) }), list.map((entry, index) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { className: `min-w-0 flex-1 ${INPUT}`, value: entry.item, placeholder: field.itemLabel ?? "id", onChange: (event) => set(list.map((e, i) => (i === index ? { ...e, item: event.target.value } : e))) }), _jsx("input", { type: "number", step: 1, min: 0, className: `w-14 ${INPUT}`, value: entry.weight, onChange: (event) => set(list.map((e, i) => (i === index ? { ...e, weight: Math.max(0, Number(event.target.value)) } : e))) }), _jsx("button", { type: "button", className: `${CONTROL} px-1.5 py-1 text-neutral-400 hover:bg-rose-500/20 hover:text-rose-200 disabled:opacity-40`, disabled: list.length <= 1, onClick: () => set(list.filter((_, i) => i !== index)), children: "\u00D7" })] }, index))), _jsx("button", { type: "button", className: `w-full ${BTN}`, onClick: () => set([...list, { item: "", weight: 1 }]), children: "+ row" })] }));
49
62
  }
50
63
  case "action":
51
64
  return null; // Action buttons are rendered by the group section, which owns the whole schema.
@@ -54,12 +67,12 @@ function FieldRow({ field, meta, onMeta }) {
54
67
  /** A schema action button (randomize / reset a group) โ€” computed patches from the core helpers. */
55
68
  function ActionButton({ field, schema, onMeta }) {
56
69
  const run = () => onMeta(field.action === "randomize" ? randomizeGroupParams(schema, field.group, Math.random) : resetGroupParams(schema, field.group), `${field.action}:${field.group ?? "all"}`);
57
- return (_jsxs("button", { type: "button", className: "w-full rounded-md bg-white/[0.06] px-2 py-1 text-[10px] font-medium text-neutral-200 ring-1 ring-inset ring-white/[0.08] transition-colors hover:bg-white/12", onClick: run, children: [field.action === "randomize" ? "๐ŸŽฒ " : "โ†บ ", labelOf(field)] }));
70
+ return (_jsxs("button", { type: "button", className: `w-full ${BTN} font-medium`, onClick: run, children: [field.action === "randomize" ? "๐ŸŽฒ " : "โ†บ ", labelOf(field)] }));
58
71
  }
59
72
  /** A collapsible group section: header (click to toggle) + its fields + any action buttons. */
60
73
  function GroupSection({ title, accent, fields, schema, meta, onMeta, defaultCollapsed, }) {
61
74
  const [collapsed, setCollapsed] = useState(defaultCollapsed);
62
- return (_jsxs("div", { className: "rounded-md border border-white/[0.06]", children: [_jsxs("button", { type: "button", className: "flex w-full items-center gap-1 px-2 py-1 text-left text-[9px] font-semibold uppercase tracking-[0.12em] text-neutral-300 transition-colors hover:bg-white/[0.04]", onClick: () => setCollapsed((value) => !value), children: [_jsx("span", { className: "text-neutral-500", children: collapsed ? "โ–ธ" : "โ–พ" }), _jsx("span", { style: { color: accent }, children: title })] }), collapsed ? null : (_jsx("div", { className: "space-y-2 px-2 pb-2", children: fields.map((field) => field.type === "action" ? (_jsx(ActionButton, { field: field, schema: schema, onMeta: onMeta }, field.key)) : (_jsx(FieldRow, { field: field, meta: meta, onMeta: onMeta }, field.key))) }))] }));
75
+ return (_jsxs("div", { className: `rounded-[6px] border ${BORDER}`, children: [_jsxs("button", { type: "button", className: "flex w-full items-center gap-1 px-2 py-1 text-left text-[9px] font-semibold uppercase tracking-[0.12em] text-neutral-300 transition-colors hover:bg-white/[0.04]", onClick: () => setCollapsed((value) => !value), children: [_jsx("span", { className: "text-neutral-500", children: collapsed ? "โ–ธ" : "โ–พ" }), _jsx("span", { style: { color: accent }, children: title })] }), collapsed ? null : (_jsx("div", { className: "space-y-2 px-2 pb-2", children: fields.map((field) => field.type === "action" ? (_jsx(ActionButton, { field: field, schema: schema, onMeta: onMeta }, field.key)) : (_jsx(FieldRow, { field: field, meta: meta, onMeta: onMeta }, field.key))) }))] }));
63
76
  }
64
77
  /**
65
78
  * Renders any parameter schema as an inspector panel โ€” one row per field
@@ -74,5 +87,5 @@ export function SchemaInspector({ schema, label, accent = "#34d399", note, meta,
74
87
  const grouped = new Set(groups.map((group) => group.id));
75
88
  // Fields with no group (or a group not declared in schema.groups) render first, headerless.
76
89
  const ungrouped = schema.fields.filter((field) => field.group === undefined || !grouped.has(field.group));
77
- return (_jsxs("div", { className: "space-y-2 rounded-lg border p-2.5", style: { borderColor: `${accent}26`, backgroundColor: `${accent}10` }, children: [_jsx("div", { className: "text-[9px] font-semibold uppercase tracking-[0.14em]", style: { color: accent }, children: label }), ungrouped.map((field) => field.type === "action" ? (_jsx(ActionButton, { field: field, schema: schema, onMeta: onMeta }, field.key)) : (_jsx(FieldRow, { field: field, meta: meta, onMeta: onMeta }, field.key))), groups.map((group) => (_jsx(GroupSection, { title: group.label, accent: accent, fields: schema.fields.filter((field) => field.group === group.id), schema: schema, meta: meta, onMeta: onMeta, defaultCollapsed: group.collapsed ?? false }, group.id))), note !== undefined && note.length > 0 ? _jsx("div", { className: "text-[10px] text-neutral-500", children: note }) : null] }));
90
+ return (_jsxs("div", { className: "space-y-2 rounded-lg border p-2.5", style: { borderColor: `${accent}26`, backgroundColor: `${accent}10` }, children: [_jsx("div", { className: "text-[9px] font-semibold uppercase tracking-[0.14em]", style: { color: accent }, children: label }), _jsx(PresetRow, { schema: schema, onMeta: onMeta }), ungrouped.map((field) => field.type === "action" ? (_jsx(ActionButton, { field: field, schema: schema, onMeta: onMeta }, field.key)) : (_jsx(FieldRow, { field: field, meta: meta, onMeta: onMeta }, field.key))), groups.map((group) => (_jsx(GroupSection, { title: group.label, accent: accent, fields: schema.fields.filter((field) => field.group === group.id), schema: schema, meta: meta, onMeta: onMeta, defaultCollapsed: group.collapsed ?? false }, group.id))), note !== undefined && note.length > 0 ? _jsx("div", { className: "text-[10px] text-neutral-500", children: note }) : null] }));
78
91
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Scripting workspace: lists every document object with an authored behavior trigger, selects them
3
+ * for framing/inspection, and reuses {@link TriggerInspector} for edits. Writes go only through
4
+ * session `setMarker` / `setVolume` meta patches (same path as the inspector Components tab).
5
+ */
6
+ import type { EditorSession } from "@jgengine/core/editor/index";
7
+ import type { EditorHostApi } from "./session.js";
8
+ /**
9
+ * Left-dock scripting workspace: document-wide trigger list + selected-object editor.
10
+ * @internal โ€” mounted by `EditorChrome` when the scripting rail workspace is active.
11
+ */
12
+ export declare const ScriptingPanel: import("react").MemoExoticComponent<({ session, api, }: {
13
+ session: EditorSession;
14
+ api: EditorHostApi;
15
+ }) => import("react").JSX.Element>;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Scripting workspace: lists every document object with an authored behavior trigger, selects them
4
+ * for framing/inspection, and reuses {@link TriggerInspector} for edits. Writes go only through
5
+ * session `setMarker` / `setVolume` meta patches (same path as the inspector Components tab).
6
+ */
7
+ import { memo, useMemo } from "react";
8
+ import { listDocumentTriggers } from "./listDocumentTriggers.js";
9
+ import { TriggerInspector } from "./TriggerInspector.js";
10
+ import { Icon } from "./shell/icons.js";
11
+ import { BORDER, CONTROL, CONTROL_ACTIVE, FOCUS_RING, MICRO_LABEL, TEXT_MUTED } from "./shell/theme.js";
12
+ import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
13
+ function bindingSummary(entry) {
14
+ if (entry.bindings.length === 0)
15
+ return "โ€”";
16
+ return entry.bindings.map((binding) => `${binding.on} โ†’ ${binding.action}`).join(" ยท ");
17
+ }
18
+ /**
19
+ * Left-dock scripting workspace: document-wide trigger list + selected-object editor.
20
+ * @internal โ€” mounted by `EditorChrome` when the scripting rail workspace is active.
21
+ */
22
+ export const ScriptingPanel = memo(function ScriptingPanel({ session, api, }) {
23
+ const document = useStoreSelector(session, (state) => state.document);
24
+ const selection = useStoreSelector(session, (state) => state.selection, shallowArrayEqual);
25
+ const selectedId = selection[0];
26
+ const entries = useMemo(() => listDocumentTriggers(document), [document]);
27
+ const selectedEntry = selectedId === undefined ? undefined : entries.find((entry) => entry.sourceId === selectedId);
28
+ const selectedMarker = selectedEntry?.sourceKind === "marker" ? document.markers.find((marker) => marker.id === selectedId) : undefined;
29
+ const selectedVolume = selectedEntry?.sourceKind === "volume" ? document.volumes.find((volume) => volume.id === selectedId) : undefined;
30
+ const selectEntry = (entry) => {
31
+ session.dispatch({ type: "select", ids: [entry.sourceId] });
32
+ api.handle({ method: "camera_goto", id: entry.sourceId });
33
+ };
34
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [_jsxs("div", { className: "min-h-0 flex-1 space-y-1 overflow-auto p-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: MICRO_LABEL, children: "Authored triggers" }), _jsx("span", { className: `ml-auto tabular-nums ${TEXT_MUTED}`, children: entries.length })] }), entries.length === 0 ? (_jsxs("div", { className: "rounded-[6px] border border-dashed border-white/[0.08] bg-white/[0.02] px-3 py-4 text-[11px] leading-relaxed text-neutral-500", children: ["No behavior triggers on this scene yet. Select a marker or volume in the hierarchy, open the Components tab, and add a trigger \u2014 or place a zone and set ", _jsx("span", { className: "text-neutral-400", children: "on" }), " /", " ", _jsx("span", { className: "text-neutral-400", children: "action" }), " in the inspector."] })) : (entries.map((entry) => {
35
+ const selected = entry.sourceId === selectedId;
36
+ return (_jsxs("button", { type: "button", onClick: () => selectEntry(entry), className: `flex w-full flex-col gap-0.5 rounded-[6px] px-2.5 py-2 text-left transition-colors ${FOCUS_RING} ${selected ? CONTROL_ACTIVE : `${CONTROL} hover:bg-white/[0.06]`}`, children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Icon, { name: entry.sourceKind === "volume" ? "cube" : "target", size: 12 }), _jsx("span", { className: "min-w-0 flex-1 truncate text-[12px] text-neutral-100", children: entry.label }), _jsx("span", { className: `shrink-0 text-[10px] ${TEXT_MUTED}`, children: entry.sourceKind })] }), _jsxs("div", { className: `truncate pl-[18px] text-[10px] ${TEXT_MUTED}`, children: [entry.objectKind, " \u00B7 ", bindingSummary(entry)] })] }, `${entry.sourceKind}:${entry.sourceId}`));
37
+ }))] }), selectedMarker !== undefined ? (_jsxs("div", { className: `shrink-0 space-y-2 border-t ${BORDER} p-2.5`, children: [_jsxs("div", { className: MICRO_LABEL, children: ["Edit \u00B7 ", selectedMarker.label ?? selectedMarker.id] }), _jsx(TriggerInspector, { target: "marker", meta: selectedMarker.meta, onMeta: (patch, coalesce) => session.dispatch({
38
+ type: "setMarker",
39
+ id: selectedMarker.id,
40
+ patch: { meta: { ...selectedMarker.meta, ...patch } },
41
+ }, { coalesce: `${coalesce}:${selectedMarker.id}` }) })] })) : null, selectedVolume !== undefined ? (_jsxs("div", { className: `shrink-0 space-y-2 border-t ${BORDER} p-2.5`, children: [_jsxs("div", { className: MICRO_LABEL, children: ["Edit \u00B7 ", selectedVolume.label ?? selectedVolume.id] }), _jsx(TriggerInspector, { target: "volume", meta: selectedVolume.meta, onMeta: (patch, coalesce) => session.dispatch({
42
+ type: "setVolume",
43
+ id: selectedVolume.id,
44
+ patch: { meta: { ...selectedVolume.meta, ...patch } },
45
+ }, { coalesce: `${coalesce}:${selectedVolume.id}` }) })] })) : null, selectedId !== undefined && selectedEntry === undefined ? (_jsx("div", { className: `shrink-0 border-t ${BORDER} px-2.5 py-2 text-[10px] text-neutral-500`, children: "Selection has no authored trigger. Pick a row above, or add a trigger on this object in the inspector." })) : null] }));
46
+ });