@volter/editor-game 0.5.65

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 (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * "Reveal Internals" / "Hide Internals" — the instance row's toggle,
3
+ * registered through the host's hierarchy contribution seam.
4
+ *
5
+ * Same split as `extract-menu.ts`: `GameHierarchy` stays contract-only and
6
+ * never learns that a three world has hidden internals; THIS module probes the
7
+ * active adapter for the optional capability and owns the session state. The
8
+ * projection math and the read-only rule are pure and stay in the HIERARCHY
9
+ * PANEL's own modules (`@editor/hierarchy-internals`,
10
+ * `@editor/hierarchy-row-model`), which the panel itself reads while rendering
11
+ * — `components/GameHierarchy.tsx:158` imports `internalsProjection` and
12
+ * `applyRevealExpansionDefaults` directly. The projection is the panel's; only
13
+ * the MENU that toggles it is this lane's.
14
+ *
15
+ * NO OWNER WALK IS NEEDED HERE (unlike the source menu's
16
+ * `instanceSourceLocatorFor`): the composite ROUTES
17
+ * `hasInternals`/`internalChildren` to the owning child by `route(id)`, so
18
+ * probing the active adapter is already asking the right one.
19
+ */
20
+
21
+ import { getActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
22
+ import {
23
+ type InternalsSource,
24
+ internalsProjection,
25
+ isInternalsRevealed,
26
+ revealedInternalsIds,
27
+ toggleInternalsRevealed,
28
+ } from '@volter/editor-core/hierarchy-internals';
29
+ import { registerHierarchyMenuItems } from '@volter/editor-core/hierarchy-menu-registry';
30
+ import type { AuthoringAdapter } from '@volter/editor-project/adapter';
31
+
32
+ /** Menu capitalization follows the panel's Title Case idiom (`Rename`,
33
+ * `Go to Callsite`). */
34
+ export const REVEAL_INTERNALS_LABEL = 'Reveal Internals';
35
+ export const HIDE_INTERNALS_LABEL = 'Hide Internals';
36
+
37
+ /**
38
+ * Whether THIS row may be revealed/hidden.
39
+ *
40
+ * Two conditions, and the second is the one that is easy to miss: the row must
41
+ * hide something (`hasInternals`), and the row must not ITSELF be a revealed
42
+ * internal. An internal row's own children are already on screen — the reveal
43
+ * walk is transitive — so offering "Reveal Internals" there would be a toggle
44
+ * with no effect and a second, meaningless piece of state.
45
+ */
46
+ export function internalsToggleFor(
47
+ adapter: AuthoringAdapter,
48
+ nodeId: string,
49
+ ): { label: string; toggle: () => void } | null {
50
+ const source = adapter as unknown as InternalsSource;
51
+ if (typeof source.hasInternals !== 'function' || !source.hasInternals(nodeId)) return null;
52
+ if (internalsProjection(source, revealedInternalsIds()).isInternal(nodeId)) return null;
53
+ return {
54
+ label: isInternalsRevealed(nodeId) ? HIDE_INTERNALS_LABEL : REVEAL_INTERNALS_LABEL,
55
+ toggle: () => {
56
+ toggleInternalsRevealed(nodeId);
57
+ },
58
+ };
59
+ }
60
+
61
+ let unregister: (() => void) | null = null;
62
+
63
+ /** Idempotent install of the internals item (see `component-verbs.service.ts`). */
64
+ export function ensureInstanceInternalsMenuRegistered(): void {
65
+ if (unregister) return;
66
+ unregister = registerHierarchyMenuItems(
67
+ ({ nodeId, store }) => internalsToggleFor(getActiveAuthoring(store), nodeId) !== null,
68
+ ({ nodeId, store }) => {
69
+ const entry = internalsToggleFor(getActiveAuthoring(store), nodeId);
70
+ if (!entry) return [];
71
+ return [
72
+ {
73
+ label: entry.label,
74
+ action: () => {
75
+ entry.toggle();
76
+ // The revealed set is module state the panel reads while rendering;
77
+ // this is the notify every source-write path already uses to make
78
+ // the hierarchy re-read its world.
79
+ store.notifyIngestEdit();
80
+ },
81
+ },
82
+ ];
83
+ },
84
+ );
85
+ }
86
+
87
+ /** Drop the registration — the service contribution's stop. */
88
+ export function unregisterInstanceInternalsMenu(): void {
89
+ unregister?.();
90
+ unregister = null;
91
+ }
@@ -0,0 +1,607 @@
1
+ /**
2
+ * The per-story 3D document the `3D` board opens: ONE mounted `three`
3
+ * story, presented on the editor's existing Object3D document surface
4
+ * (`StageHost.tsx`'s `Object3DDocumentViewport` — real
5
+ * EditorViewport, orbit camera, source-backed R3F hierarchy and transforms,
6
+ * the shared `Object3DDocumentToolbar`). There is deliberately no
7
+ * new viewport class here: a mounted story IS an `Object3D`, so it opens on the
8
+ * surface every other native `Object3D` opens on.
9
+ *
10
+ * ## What this module actually owns
11
+ *
12
+ * This module owns each asynchronous story mount. A viewport borrows it until
13
+ * its content replacement has settled; retiring a source releases the fiber
14
+ * root only after the last viewport borrow returns. A source revision never
15
+ * closes the document or resets its editor state.
16
+ *
17
+ * ## Args → Inspector
18
+ *
19
+ * A registered Inspector section (the same `inspector-section-registry` seam
20
+ * `story-documents.tsx`'s `Story Args` uses), whose editable set and widget
21
+ * types come from the SHARED `storyArgPropertyDescriptors`
22
+ * (`stories/story-arg-descriptors.ts`, extracted from
23
+ * `react-world-authoring-adapter.ts`'s `portableStoryArgDescriptors`) — so a
24
+ * story arg is editable in exactly one place's opinion, whichever surface shows
25
+ * it.
26
+ */
27
+
28
+ import { Object3DDocumentViewport } from '@volter/editor-core/components/Object3DDocumentViewport';
29
+ import type { SourceDocumentAuthoringFactory } from '@volter/editor-core/components/StageHost';
30
+ import { SurfaceStateOverlay } from '@volter/editor-core/components/SurfaceStateOverlay';
31
+ import { STANDARD_COMPONENT_CAMERA_DIRECTION } from '@volter/editor-core/components/standard-viewport-dressing';
32
+ import { registerDocumentOpener } from '@volter/editor-core/document-open-registry';
33
+ import type { ViewportTab } from '@volter/editor-core/editor-shell-store';
34
+ import { createHmrRegistrationGroup } from '@volter/editor-core/hmr-registration-group';
35
+ import {
36
+ CONTRIBUTED_SECTION_ORDER,
37
+ STORY_ARGS_SECTION_ID,
38
+ STORY_ARGS_SECTION_TITLE,
39
+ } from '@volter/editor-core/inspection/model';
40
+ import { registerInspectorSections } from '@volter/editor-core/inspector-section-registry';
41
+ import type { ComposedProjectStory } from '@volter/editor-core/stories/compose-project-stories';
42
+ import {
43
+ type MountedStoryViewportSource,
44
+ mountedStoryViewportSource,
45
+ } from '../../host/stories/mounted-story-viewport-source';
46
+ import { getProjectStoryRegions } from '@volter/editor-core/stories/project-story-regions';
47
+ import { storyArgPropertyDescriptors } from '../../host/stories/story-arg-descriptors';
48
+ import { declaredStoryMedium } from '@volter/editor-core/stories/story-declared-medium';
49
+ import {
50
+ type StoryDocumentOpenRequest,
51
+ THREE_STORY_DOCUMENT_OPENER,
52
+ } from '@volter/editor-core/stories/story-document-openers';
53
+ import {
54
+ getProjectStoryModules,
55
+ subscribeProjectStoryModules,
56
+ } from '@volter/editor-core/stories/story-registry';
57
+ import {
58
+ disposeStoryObject3D,
59
+ mountStoryObject3D,
60
+ type StoryPreviewComponent,
61
+ } from '@volter/editor-core/stories/story-three-preview';
62
+ import { mountedStoryHasThreeContent } from '@volter/editor-core/stories/three-story-model';
63
+ import { explainSurface } from '@volter/editor-core/surface-state';
64
+ import { sourceWriteBackendIfPrimed } from '@volter/editor-core/ui-source/tier-source-write-backend';
65
+ import {
66
+ activeWorkspaceDocument,
67
+ closeWorkspaceDocument,
68
+ openWorkspaceDocument,
69
+ type WorkspaceDocumentContentProps,
70
+ } from '@volter/editor-core/workspace-document-registry';
71
+ import { registerWorkspaceDocumentRestorer } from '@volter/editor-core/workspace-document-restore';
72
+ import { STORY_ARGS_SECTION_ICON } from '../../react/story-documents/story-args-section';
73
+ import {
74
+ prepareStoryDocumentRestore,
75
+ storyDocumentId,
76
+ } from '../../react/story-documents/story-documents';
77
+ import { Checkbox, TextInput, themeVars } from '@volter/editor-sdk/widgets';
78
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
79
+ import { R3fSourceAuthoringAdapter } from '../authoring/r3f-source-authoring-adapter';
80
+
81
+ /** Inspector path namespace for this document's story args. Its own, because
82
+ * the path namespace belongs to the node the values are read back through. */
83
+ const THREE_STORY_ARG_PATH_PREFIX = 'threeStory.arg.';
84
+
85
+ /** The narrow store surface a three-story document needs — the T6.3 input-gate
86
+ * hand-off, identical to `story-documents.tsx`'s `StoryDocumentStore`. */
87
+ export interface ThreeStoryDocumentStore {
88
+ setActiveViewportTab(tab: ViewportTab): void;
89
+ }
90
+
91
+ interface ThreeStoryDocumentState {
92
+ readonly modulePath: string;
93
+ readonly storyName: string;
94
+ argOverrides: Record<string, unknown>;
95
+ }
96
+
97
+ const _states = new Map<string, ThreeStoryDocumentState>();
98
+ let _version = 0;
99
+ const _listeners = new Set<() => void>();
100
+
101
+ function notifyChanged(): void {
102
+ _version++;
103
+ for (const fn of _listeners) fn();
104
+ }
105
+
106
+ function subscribeThreeStoryDocuments(fn: () => void): () => void {
107
+ _listeners.add(fn);
108
+ return () => {
109
+ _listeners.delete(fn);
110
+ };
111
+ }
112
+
113
+ function threeStoryDocumentsVersion(): number {
114
+ return _version;
115
+ }
116
+
117
+ /** Stable identity for a portable story rendered on the declared Three medium. */
118
+ export function threeStoryDocumentId(modulePath: string, storyName: string): string {
119
+ return `three-story:${modulePath}#${storyName}`;
120
+ }
121
+
122
+ /** The ACTIVE center document's three-story state, or `null`. */
123
+ function activeThreeStoryDocument(): (ThreeStoryDocumentState & { id: string }) | null {
124
+ const active = activeWorkspaceDocument();
125
+ if (!active) return null;
126
+ const state = _states.get(active.descriptor.id);
127
+ return state ? { ...state, id: active.descriptor.id } : null;
128
+ }
129
+
130
+ /** Set one arg override — remounts this story's Object3D. `undefined` CLEARS
131
+ * the override (the `delete`-the-key convention `setStoryArgOverride` uses,
132
+ * for the same reason: an absent key falls back to the authored arg, a stored
133
+ * `NaN` would win over it). */
134
+ export function setThreeStoryArgOverride(id: string, key: string, value: unknown): void {
135
+ const state = _states.get(id);
136
+ if (!state) return;
137
+ if (value === undefined) {
138
+ const next = { ...state.argOverrides };
139
+ delete next[key];
140
+ state.argOverrides = next;
141
+ } else {
142
+ state.argOverrides = { ...state.argOverrides, [key]: value };
143
+ }
144
+ notifyChanged();
145
+ }
146
+
147
+ /**
148
+ * Open (or activate) the 3D document for one `three` story. The board's
149
+ * exhibit click-through target.
150
+ */
151
+ export function openThreeStoryDocument(
152
+ store: ThreeStoryDocumentStore,
153
+ modulePath: string,
154
+ storyName: string,
155
+ title = storyName,
156
+ // A click-through OPENS AND FOCUSES, which is why activation is the default.
157
+ // The Edit tab row (`scene-documents.tsx`) opens a story-reached scene as a
158
+ // STANDING tab instead: it is offered, never imposed, so it passes `false`
159
+ // for the same reason the boards do.
160
+ options: { activate?: boolean } = {},
161
+ ): string {
162
+ ensureThreeStoryContributionsRegistered();
163
+ // One portable story has one preview document. Close a stale DOM preview
164
+ // of the same story before opening its resolved Object3D medium; otherwise
165
+ // the hidden DOM renderer still mounts the R3F component and reports a
166
+ // misleading "Hooks can only be used within Canvas" crash.
167
+ closeWorkspaceDocument(storyDocumentId(modulePath, storyName));
168
+ const id = threeStoryDocumentId(modulePath, storyName);
169
+ if (!_states.has(id)) {
170
+ _states.set(id, { modulePath, storyName, argOverrides: {} });
171
+ notifyChanged();
172
+ }
173
+ return openWorkspaceDocument(
174
+ {
175
+ id,
176
+ title,
177
+ kind: 'world',
178
+ workspaceRole: 'authored-subject',
179
+ provenance: { sourcePath: modulePath },
180
+ Content: ThreeStoryDocumentContent,
181
+ closeable: true,
182
+ presentation: () => ({ kind: 'story', modulePath, storyName, mode: 'preview' }),
183
+ // A per-story 3D document is SESSION-CREATED — nothing at boot
184
+ // re-derives it from the composite, unlike a manifest world — so it
185
+ // writes itself down or comes back as a titled panel with no content.
186
+ // The LIVE tab text goes with it: the opening board names an exhibit by
187
+ // a board-scoped rule (`board-scene.ts`) that one story cannot
188
+ // re-derive.
189
+ persist: ({ title: liveTitle }) => ({ modulePath, storyName, title: liveTitle }),
190
+ onActivate: () => store.setActiveViewportTab('edit'),
191
+ onDispose: (documentId) => {
192
+ if (_states.delete(documentId)) notifyChanged();
193
+ },
194
+ },
195
+ { activate: options.activate !== false },
196
+ );
197
+ }
198
+
199
+ /** One open three-story document's identity, or `undefined`. A story is a
200
+ * STABLE, ADDRESSABLE state (module path + story name), so this is everything
201
+ * `workspace-state-persistence` needs to write the document down and reopen
202
+ * it on the next boot — the same shape, and the same reason, as
203
+ * `story-documents.tsx`'s `storyDocumentState`. */
204
+ export function threeStoryDocumentState(
205
+ id: string,
206
+ ): { readonly modulePath: string; readonly storyName: string } | undefined {
207
+ const state = _states.get(id);
208
+ return state ? { modulePath: state.modulePath, storyName: state.storyName } : undefined;
209
+ }
210
+
211
+ /**
212
+ * Reopen a persisted three-story document at boot, or refuse. VERIFIES first
213
+ * (the persistence module's "persist+restore only documents that can
214
+ * truthfully reopen" rule): a module that no longer loads, or no longer
215
+ * exports that story, restores NOTHING and the stale document is dropped
216
+ * by the workspace's ordinary reconciliation. Callers must have settled the
217
+ * story registry (`refreshProjectStories`) first — this reads it synchronously.
218
+ */
219
+ export function restoreThreeStoryDocument(
220
+ store: ThreeStoryDocumentStore,
221
+ modulePath: string,
222
+ storyName: string,
223
+ title?: string,
224
+ ): boolean {
225
+ const { story } = findComposedStory(modulePath, storyName);
226
+ if (!story) return false;
227
+ if (declaredStoryMedium({ modulePath, regions: getProjectStoryRegions() }).medium !== 'three') {
228
+ return false;
229
+ }
230
+ // `title` is the tab's saved text. The board names an exhibit by a
231
+ // BOARD-SCOPED rule (`board-scene.ts`: the component leaf, disambiguated
232
+ // with the story label only where one leaf contributes several exhibits),
233
+ // which no single story can re-derive — so the tab is restored with the text
234
+ // it had, falling back to the registry's own label for a record that
235
+ // predates this or came from another opener.
236
+ openThreeStoryDocument(store, modulePath, storyName, title ?? story.label);
237
+ return true;
238
+ }
239
+
240
+ /** The turntable's address (`document-open-registry.ts`) — a story mounted as
241
+ * an Object3D on the standard 3D document surface. */
242
+ registerDocumentOpener<StoryDocumentOpenRequest>({
243
+ id: THREE_STORY_DOCUMENT_OPENER,
244
+ owner: 'three-story-documents',
245
+ open: (store, request) =>
246
+ openThreeStoryDocument(
247
+ store,
248
+ request.modulePath,
249
+ request.storyName,
250
+ request.title ?? request.storyName,
251
+ request.activate === undefined ? {} : { activate: request.activate },
252
+ ),
253
+ });
254
+
255
+ /** Reopen a persisted 3D story document. `restoreThreeStoryDocument` is the
256
+ * verification (module loads, story still exported, medium still `three`)
257
+ * and answers `false` when any of it has moved. */
258
+ registerWorkspaceDocumentRestorer({
259
+ kind: 'world',
260
+ owner: 'three-story-documents',
261
+ prepare: prepareStoryDocumentRestore,
262
+ restore: ({ state, store }) => {
263
+ const record = state as
264
+ | { modulePath?: unknown; storyName?: unknown; title?: unknown }
265
+ | null
266
+ | undefined;
267
+ const modulePath = typeof record?.modulePath === 'string' ? record.modulePath : null;
268
+ const storyName = typeof record?.storyName === 'string' ? record.storyName : null;
269
+ if (!modulePath || !storyName) return false;
270
+ const title = typeof record?.title === 'string' ? record.title : undefined;
271
+ return restoreThreeStoryDocument(store, modulePath, storyName, title);
272
+ },
273
+ });
274
+
275
+ /** Test-only reset — drops all three-story document state. */
276
+ export function __resetThreeStoryDocumentsForTest(): void {
277
+ _states.clear();
278
+ notifyChanged();
279
+ }
280
+
281
+ function findComposedStory(
282
+ modulePath: string,
283
+ storyName: string,
284
+ ): { story: ComposedProjectStory | undefined; moduleError: string | undefined } {
285
+ const module_ = getProjectStoryModules().find((m) => m.modulePath === modulePath);
286
+ if (!module_) return { story: undefined, moduleError: undefined };
287
+ if (!module_.ok) return { story: undefined, moduleError: module_.error };
288
+ return { story: module_.stories.find((s) => s.name === storyName), moduleError: undefined };
289
+ }
290
+
291
+ // --- Document content -----------------------------------------------------
292
+
293
+ function ThreeStoryDocumentContent({ documentId, active }: WorkspaceDocumentContentProps) {
294
+ useSyncExternalStore(subscribeThreeStoryDocuments, threeStoryDocumentsVersion);
295
+ // Re-resolve on every registry refresh (project change, `story-files-changed`
296
+ // SSE) — the same live-rescan physics every other story surface uses.
297
+ useSyncExternalStore(subscribeProjectStoryModules, getProjectStoryModules);
298
+ const state = _states.get(documentId);
299
+ const [source, setSource] = useState<MountedStoryViewportSource | null>(null);
300
+ const [error, setError] = useState<string | null>(null);
301
+ const ownedSources = useRef(new Set<MountedStoryViewportSource>());
302
+ // Keep the displayed mount alive while its replacement compiles and settles.
303
+ // Retire old mounts only after React has committed the new viewport source.
304
+ useEffect(() => {
305
+ if (!source) return;
306
+ for (const owned of ownedSources.current) {
307
+ // Set insertion order is publication order. A newer result may already
308
+ // be queued for React; never retire it from an older commit's effect.
309
+ if (owned === source) break;
310
+ owned.dispose();
311
+ ownedSources.current.delete(owned);
312
+ }
313
+ }, [source]);
314
+ useEffect(
315
+ () => () => {
316
+ for (const owned of ownedSources.current) owned.dispose();
317
+ ownedSources.current.clear();
318
+ },
319
+ [],
320
+ );
321
+ // The layout host mounts every open document, so a session
322
+ // restored with several story tabs would otherwise mount every one of their
323
+ // fiber roots at boot. Mount on FIRST activation and retain the mount
324
+ // afterwards — latching rather than tracking `active` is the point: a mount
325
+ // freed on deactivation is exactly the black-panel defect
326
+ // `mountedStoryViewportSource` documents.
327
+ const [visited, setVisited] = useState(active);
328
+ // This document is a mounted project-owned R3F composition whose stamped
329
+ // JSX source is its write authority, so THIS caller supplies the source
330
+ // adapter — the Object3D document constructs no source lane itself.
331
+ // Memoized on the identity it closes over: the factory is an activation
332
+ // dependency of the viewport and must not change per render.
333
+ const modulePath = state?.modulePath;
334
+ const sourceAuthoring = useMemo<SourceDocumentAuthoringFactory | undefined>(
335
+ () =>
336
+ modulePath
337
+ ? ({ store, scene, hierarchyRoots }) => {
338
+ const adapter = new R3fSourceAuthoringAdapter(store, scene, {
339
+ worldId: documentId,
340
+ entryPath: modulePath,
341
+ writeBackend: sourceWriteBackendIfPrimed('The Asset Editor'),
342
+ hierarchyMode: 'definition',
343
+ ...(hierarchyRoots ? { hierarchyRoots } : {}),
344
+ });
345
+ return { adapter, dispose: () => adapter.dispose() };
346
+ }
347
+ : undefined,
348
+ [documentId, modulePath],
349
+ );
350
+ useEffect(() => {
351
+ if (active) setVisited(true);
352
+ }, [active]);
353
+ const { story, moduleError } = state
354
+ ? findComposedStory(state.modulePath, state.storyName)
355
+ : { story: undefined, moduleError: undefined };
356
+ const overridesKey = state ? JSON.stringify(state.argOverrides) : '';
357
+
358
+ useEffect(() => {
359
+ if (!visited || !story || !state) return;
360
+ let cancelled = false;
361
+ setError(null);
362
+ void mountStoryObject3D(story.Component as StoryPreviewComponent, {
363
+ ...state.argOverrides,
364
+ })
365
+ .then((mounted) => {
366
+ if (cancelled) {
367
+ void disposeStoryObject3D(mounted);
368
+ return;
369
+ }
370
+ // A story that reconciles to an EMPTY subtree is the one remaining way
371
+ // this document could paint a black viewport with nothing to say. Same
372
+ // predicate every three surface qualifies on, so they cannot disagree
373
+ // about what "mounted as three" means — and here it
374
+ // becomes a reported failure rather than an empty picture.
375
+ if (!mountedStoryHasThreeContent(mounted.root)) {
376
+ void disposeStoryObject3D(mounted);
377
+ setError(
378
+ 'The story mounted with no three content — its first R3F commit rendered an empty ' +
379
+ 'subtree. A <Suspense> boundary around three content is the usual cause: the ' +
380
+ 'boundary commits its fallback first, so the real children arrive on a later ' +
381
+ 'commit this document never sees.',
382
+ );
383
+ return;
384
+ }
385
+ const next = mountedStoryViewportSource(mounted);
386
+ ownedSources.current.add(next);
387
+ setSource(next);
388
+ })
389
+ .catch((reason: unknown) => {
390
+ if (cancelled) return;
391
+ setError(reason instanceof Error ? reason.message : String(reason));
392
+ });
393
+ return () => {
394
+ cancelled = true;
395
+ // Cancel obsolete builds. The displayed source remains owned until a
396
+ // replacement commits or the document closes; HMR is not a close.
397
+ };
398
+ // `story.Component` identity changes on registry refresh, `overridesKey` on
399
+ // an arg edit — both remount, which is exactly the wanted behavior.
400
+ // `visited` only ever goes false → true, so it never tears a mount down.
401
+ }, [visited, story, state, overridesKey]);
402
+
403
+ if (!state) {
404
+ return (
405
+ <div style={{ padding: 12, fontSize: 12, color: themeVars.content.muted }}>
406
+ This story document is no longer available.
407
+ </div>
408
+ );
409
+ }
410
+
411
+ if ((!story || error) && !source) {
412
+ const rootId = `${state.modulePath}#${state.storyName}`;
413
+ return (
414
+ <div
415
+ data-testid="three-story-document-error"
416
+ style={{ position: 'relative', flex: 1, minHeight: 0 }}
417
+ >
418
+ <SurfaceStateOverlay
419
+ testId="three-story-surface-status"
420
+ explanation={{
421
+ tone: 'error',
422
+ title: `Story ${rootId} failed`,
423
+ description:
424
+ error ?? moduleError ?? 'It may have been renamed or removed; close this document.',
425
+ }}
426
+ />
427
+ </div>
428
+ );
429
+ }
430
+
431
+ if (!source) {
432
+ return (
433
+ <div
434
+ data-testid="three-story-document-loading"
435
+ style={{ position: 'relative', flex: 1, minHeight: 0 }}
436
+ >
437
+ <SurfaceStateOverlay
438
+ testId="three-story-surface-status"
439
+ explanation={explainSurface({
440
+ surface: 'Story',
441
+ rootIds: [`${state.modulePath}#${state.storyName}`],
442
+ phase: 'loading',
443
+ content: 'unknown',
444
+ })}
445
+ />
446
+ </div>
447
+ );
448
+ }
449
+
450
+ return (
451
+ <div style={{ position: 'relative', display: 'flex', flex: 1, minHeight: 0 }}>
452
+ <Object3DDocumentViewport
453
+ documentId={documentId}
454
+ sourcePath={state.modulePath}
455
+ build={source.build}
456
+ displayName={story?.label ?? state.storyName}
457
+ active={active}
458
+ assetType="component"
459
+ {...(sourceAuthoring ? { sourceAuthoring } : {})}
460
+ cameraDirection={STANDARD_COMPONENT_CAMERA_DIRECTION}
461
+ />
462
+ {(!story || error) && (
463
+ <SurfaceStateOverlay
464
+ testId="three-story-surface-status"
465
+ explanation={{
466
+ tone: 'error',
467
+ title: 'Source update failed',
468
+ description:
469
+ error ??
470
+ moduleError ??
471
+ 'This story is unavailable. The last working view is retained.',
472
+ }}
473
+ />
474
+ )}
475
+ </div>
476
+ );
477
+ }
478
+
479
+ // --- Inspector "Story Args" section for a three-story document -------------
480
+
481
+ function ThreeStoryArgsSection() {
482
+ useSyncExternalStore(subscribeThreeStoryDocuments, threeStoryDocumentsVersion);
483
+ useSyncExternalStore(subscribeProjectStoryModules, getProjectStoryModules);
484
+ const active = activeThreeStoryDocument();
485
+ if (!active) return null;
486
+ const { story } = findComposedStory(active.modulePath, active.storyName);
487
+ const args = story ? { ...story.args, ...active.argOverrides } : {};
488
+ // One owner for "which args are editable, and as what" — shared with the
489
+ // React world adapter's story nodes.
490
+ const descriptors = storyArgPropertyDescriptors(args, THREE_STORY_ARG_PATH_PREFIX);
491
+
492
+ return (
493
+ <div
494
+ data-testid="inspector-story-args-three"
495
+ style={{
496
+ pointerEvents: 'auto',
497
+ padding: 8,
498
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
499
+ }}
500
+ >
501
+ {/* No mini-label of its own: the projection heads this section with its
502
+ title and icon (`components/InspectionProjection.tsx`). */}
503
+ <div
504
+ style={{ fontSize: 11, color: themeVars.content.primary, fontWeight: 600, marginBottom: 4 }}
505
+ >
506
+ {active.storyName}
507
+ </div>
508
+ {descriptors.length === 0 && (
509
+ <div style={{ fontSize: 11, color: themeVars.content.dim }}>
510
+ {story ? 'This story declares no editable args.' : 'Story source unavailable.'}
511
+ </div>
512
+ )}
513
+ {descriptors.map((descriptor) => (
514
+ <div
515
+ key={descriptor.path}
516
+ style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '3px 0' }}
517
+ >
518
+ <span
519
+ title={descriptor.label}
520
+ style={{
521
+ width: 90,
522
+ flexShrink: 0,
523
+ fontSize: 11,
524
+ color: themeVars.content.muted,
525
+ overflow: 'hidden',
526
+ textOverflow: 'ellipsis',
527
+ }}
528
+ >
529
+ {descriptor.label}
530
+ </span>
531
+ {descriptor.type === 'boolean' ? (
532
+ <Checkbox
533
+ className="vgai-checkbox"
534
+ data-testid={`three-story-arg-${descriptor.label}`}
535
+ checked={args[descriptor.label] === true}
536
+ onChange={(e) =>
537
+ setThreeStoryArgOverride(active.id, descriptor.label, e.target.checked)
538
+ }
539
+ />
540
+ ) : descriptor.type === 'number' ? (
541
+ <TextInput
542
+ type="number"
543
+ className="vgai-input"
544
+ data-testid={`three-story-arg-${descriptor.label}`}
545
+ value={args[descriptor.label] as number}
546
+ onChange={(e) => {
547
+ // Empty/unparseable clears the override rather than storing
548
+ // NaN — same reason as `setThreeStoryArgOverride`'s doc.
549
+ const raw = e.target.value;
550
+ const parsed = Number.parseFloat(raw);
551
+ setThreeStoryArgOverride(
552
+ active.id,
553
+ descriptor.label,
554
+ raw === '' || Number.isNaN(parsed) ? undefined : parsed,
555
+ );
556
+ }}
557
+ style={{ width: 80 }}
558
+ />
559
+ ) : descriptor.type === 'string' ? (
560
+ <TextInput
561
+ type="text"
562
+ className="vgai-input"
563
+ data-testid={`three-story-arg-${descriptor.label}`}
564
+ value={args[descriptor.label] as string}
565
+ onChange={(e) =>
566
+ setThreeStoryArgOverride(active.id, descriptor.label, e.target.value)
567
+ }
568
+ style={{ flex: 1, minWidth: 0 }}
569
+ />
570
+ ) : (
571
+ <span
572
+ data-testid={`three-story-arg-${descriptor.label}`}
573
+ style={{ fontSize: 11, color: themeVars.content.dim }}
574
+ >
575
+ {JSON.stringify(args[descriptor.label])}
576
+ </span>
577
+ )}
578
+ </div>
579
+ ))}
580
+ </div>
581
+ );
582
+ }
583
+
584
+ const registrationGroup = createHmrRegistrationGroup(import.meta.hot, 'three-story-contributions');
585
+
586
+ /** Idempotent one-time registration of the three-story Inspector section.
587
+ * Called from `openThreeStoryDocument` so merely importing this module in a
588
+ * test does not mutate shared registries. */
589
+ export function ensureThreeStoryContributionsRegistered(): void {
590
+ registrationGroup.ensure((track) => {
591
+ track(
592
+ registerInspectorSections({
593
+ match: () => activeThreeStoryDocument() !== null,
594
+ id: STORY_ARGS_SECTION_ID,
595
+ title: STORY_ARGS_SECTION_TITLE,
596
+ icon: STORY_ARGS_SECTION_ICON,
597
+ order: CONTRIBUTED_SECTION_ORDER,
598
+ Section: ThreeStoryArgsSection,
599
+ }),
600
+ );
601
+ });
602
+ }
603
+
604
+ /** Test-only: allow re-registration after a registry reset. */
605
+ export function __resetThreeStoryContributionsForTest(): void {
606
+ registrationGroup.reset();
607
+ }