@volter/editor-game 0.5.65 → 0.5.67

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 (281) hide show
  1. package/contributions/audio-unlock.service.ts +2 -2
  2. package/contributions/autoplay.service.ts +1 -1
  3. package/contributions/bridge.command.ts +3 -3
  4. package/contributions/canvas/component-board.service.ts +16 -0
  5. package/contributions/canvas/design-time-mount.service.ts +45 -0
  6. package/contributions/canvas-story-capture.service.ts +12 -0
  7. package/contributions/edit-mode-audio.service.ts +2 -2
  8. package/contributions/edit-mode-networking.service.ts +1 -1
  9. package/contributions/gameplay.command.ts +4 -4
  10. package/contributions/generation.service.ts +3 -3
  11. package/contributions/generations.status.tsx +1 -1
  12. package/contributions/godot.palette.json +99 -0
  13. package/contributions/godot.style.ts +82 -0
  14. package/contributions/godot.view.ts +75 -0
  15. package/contributions/ingest.service.ts +4 -4
  16. package/contributions/instances.command.ts +1 -1
  17. package/contributions/navmesh.menu.ts +1 -1
  18. package/contributions/network-observer.service.ts +14 -0
  19. package/contributions/play.command.ts +11 -5
  20. package/contributions/react/component-board.service.ts +1 -1
  21. package/contributions/react/design-time-mount.service.ts +1 -1
  22. package/contributions/react/react-inspector.service.ts +2 -2
  23. package/contributions/scene-document.service.ts +5 -5
  24. package/contributions/state-watch.menu.ts +1 -1
  25. package/contributions/state-watch.utility.tsx +1 -1
  26. package/contributions/team-playtest.service.ts +4 -4
  27. package/contributions/three/component-board.service.ts +1 -1
  28. package/contributions/three/component-verbs.command.ts +8 -8
  29. package/contributions/three/three-authoring.service.ts +8 -5
  30. package/contributions/three-story-capture.service.ts +12 -0
  31. package/contributions/unity.palette.json +102 -0
  32. package/contributions/unity.style.ts +72 -0
  33. package/contributions/unity.view.ts +82 -0
  34. package/contributions/unreal.palette.json +105 -0
  35. package/contributions/unreal.style.ts +66 -0
  36. package/contributions/unreal.view.ts +93 -0
  37. package/dist-node/serving.mjs +16 -0
  38. package/package.json +33 -16
  39. package/serving/index.ts +27 -0
  40. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  41. package/src/asset-budget/asset-budget-model.ts +4 -4
  42. package/src/asset-budget/optimize-apply.ts +7 -7
  43. package/src/audio/AudioDebuggerPanel.tsx +1 -1
  44. package/src/bridge/dispatch.ts +16 -16
  45. package/src/bridge/live-frames.ts +1 -1
  46. package/src/bridge/screenshot.ts +6 -6
  47. package/src/build/BuildProfilesPanel.tsx +3 -3
  48. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -0
  49. package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
  50. package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
  51. package/src/canvas/canvas-design-mount.ts +524 -0
  52. package/src/canvas/design-time-canvas-mount.ts +79 -0
  53. package/src/coverage/live-authoring-surface.ts +6 -6
  54. package/src/coverage/live-project-verbs.ts +2 -2
  55. package/src/coverage/native-system-coverage.ts +6 -6
  56. package/src/coverage/root-coverage.ts +1 -1
  57. package/src/coverage/session-coverage.ts +5 -5
  58. package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
  59. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +13 -53
  60. package/src/edit-mode/edit-mode-audio.ts +4 -4
  61. package/src/edit-mode/edit-mode-networking.ts +2 -2
  62. package/src/game-document/GameCaptureFrameButton.tsx +2 -2
  63. package/src/game-document/GameDocument.tsx +2 -2
  64. package/src/game-document/GamePanel.tsx +8 -8
  65. package/src/game-document/InstanceInspectorPicker.tsx +2 -2
  66. package/src/game-document/crowd-debug.ts +1 -1
  67. package/src/game-document/device-preview.ts +10 -11
  68. package/src/game-document/physics-debug.ts +1 -1
  69. package/src/generation/GenerationActivity.tsx +6 -8
  70. package/src/generation/generation-documents.tsx +8 -8
  71. package/src/generation/generation-jobs.ts +2 -2
  72. package/src/generation/generation-presentation.ts +1 -1
  73. package/src/host/adapter-reach.ts +1 -1
  74. package/src/host/adapter-runtime-bindings.ts +97 -6
  75. package/src/host/api/configurations.ts +2 -2
  76. package/src/host/authoring/babylon-authoring-adapter.ts +8 -8
  77. package/src/host/authoring/contract-hierarchy-authoring.ts +1 -1
  78. package/src/host/authoring/contract-scenes-stories.ts +1 -1
  79. package/src/host/authoring/creation-site-related.ts +2 -2
  80. package/src/host/authoring/ephemeral-persistence.ts +1 -1
  81. package/src/host/authoring/gesture-persist.ts +2 -2
  82. package/src/host/authoring/ingest-data-writer.ts +1 -1
  83. package/src/host/authoring/ingest-source-persistence.ts +8 -8
  84. package/src/host/authoring/mount-isolated-pixi-screen.ts +1 -1
  85. package/src/host/authoring/mounted-authoring.ts +4 -4
  86. package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
  87. package/src/host/authoring/pixi-authoring-adapter.ts +42 -17
  88. package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
  89. package/src/host/authoring/pixi-live-write-target.ts +7 -7
  90. package/src/host/authoring/pixi-source-identity.ts +3 -3
  91. package/src/host/authoring/pixi-source-write-target.ts +1614 -0
  92. package/src/host/authoring/pixi-still-presentation.ts +2 -2
  93. package/src/host/authoring/pixi-structure-history.ts +3 -3
  94. package/src/host/authoring/pixi-transform-channels.ts +16 -14
  95. package/src/host/authoring/source-persistence-backend.ts +6 -6
  96. package/src/host/authoring/struct-write-pipe.ts +3 -3
  97. package/src/host/binding-resolver.ts +8 -9
  98. package/src/host/browser-transpile.ts +2 -2
  99. package/src/host/canvas-entry-runtime.ts +59 -48
  100. package/src/host/canvas-preview-frames.ts +482 -0
  101. package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
  102. package/src/host/components/HeaderTelemetry.tsx +9 -16
  103. package/src/host/components/PixiIsolationSceneContent.tsx +17 -16
  104. package/src/host/components/ThreeIsolationSceneContent.tsx +5 -5
  105. package/src/host/components/frame-debugger-model.ts +2 -2
  106. package/src/host/components/header-telemetry-model.ts +2 -2
  107. package/src/host/components/scene-document.tsx +29 -61
  108. package/src/host/components/utility-view-state.ts +1 -1
  109. package/src/host/components/world-root-stage-binding.tsx +17 -26
  110. package/src/host/components/world-root-stage.ts +130 -63
  111. package/src/host/coverage/capability-coverage.ts +13 -1
  112. package/src/host/coverage/game-contract-seam-evidence.ts +1 -1
  113. package/src/host/coverage/project-verb-coverage.ts +2 -17
  114. package/src/host/coverage/system-adapter-coverage.ts +4 -5
  115. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +14 -8
  116. package/src/host/document-preview-three.ts +2 -2
  117. package/src/host/entry-adjudication.ts +6 -6
  118. package/src/host/game-css-scope-transform.ts +4 -0
  119. package/src/host/game-module-access.ts +1 -1
  120. package/src/host/game-realm-page.ts +16 -8
  121. package/src/host/game-realm-reclaim.ts +1 -1
  122. package/src/host/gameplay-export.ts +25 -14
  123. package/src/host/gameplay-recording.ts +5 -5
  124. package/src/host/gated-globals.ts +6 -6
  125. package/src/host/history/json-history-resource.ts +3 -3
  126. package/src/host/instance-extract-actions.ts +1 -1
  127. package/src/host/instance-fork-actions.ts +1 -1
  128. package/src/host/projection/dom.ts +2 -2
  129. package/src/host/projection/pixi.ts +25 -3
  130. package/src/host/r3f-entry-runtime.ts +65 -34
  131. package/src/host/react-mount-runtime.ts +8 -49
  132. package/src/host/realm-services.ts +2 -2
  133. package/src/host/roots/canvas-root.tsx +361 -0
  134. package/src/host/roots/module-root.ts +1 -1
  135. package/src/host/roots/r3f-root.tsx +473 -0
  136. package/src/host/roots/react-root.ts +9 -43
  137. package/src/host/scene-view-drawability.ts +2 -2
  138. package/src/host/served-bundle-runtime-modules.ts +1 -19
  139. package/src/host/server-log-bridge.ts +3 -3
  140. package/src/host/stories/mounted-story-viewport-source.ts +1 -1
  141. package/src/host/stories/pixi-story-model.ts +30 -0
  142. package/src/host/stories/story-media-captures.ts +46 -0
  143. package/src/host/stories/story-media-presence.ts +3 -3
  144. package/src/host/stories/story-pixi-preview.ts +408 -0
  145. package/src/host/stories/story-three-preview.ts +806 -0
  146. package/src/host/stories/three-story-captures.ts +35 -0
  147. package/src/host/story-three-preview-runtime.ts +56 -0
  148. package/src/host/three-ingest-runtime.ts +1 -1
  149. package/src/host/use-active-performance-source.ts +3 -3
  150. package/src/host/viewport-pose-memory.ts +1 -1
  151. package/src/host/viewport-root-presentation.ts +6 -5
  152. package/src/ingest/active-ingest.ts +2 -2
  153. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  154. package/src/ingest/authoring/ingest-root-adapter.ts +13 -13
  155. package/src/ingest/capture-wait-report.ts +1 -1
  156. package/src/ingest/deferred-ingest-play.ts +11 -10
  157. package/src/ingest/discovery-public-ingest.ts +2 -2
  158. package/src/ingest/ingest-boot-viewport.ts +6 -8
  159. package/src/ingest/ingest-canvas-scene-document.tsx +17 -16
  160. package/src/ingest/ingest-canvas-scene.ts +5 -8
  161. package/src/ingest/ingest-evidence-hook.ts +1 -1
  162. package/src/ingest/ingest-frame-snapshot.ts +1 -1
  163. package/src/ingest/ingest-play-control.ts +4 -4
  164. package/src/ingest/ingest-render-debug.ts +11 -11
  165. package/src/ingest/ingest-siblings.ts +16 -28
  166. package/src/ingest/module-mode.ts +18 -19
  167. package/src/ingest/mount-canvas-ingest-root.ts +27 -27
  168. package/src/ingest/mount-coverage.ts +4 -4
  169. package/src/ingest/mount-dom-ingest-root.ts +16 -16
  170. package/src/ingest/mount-ingest-root.ts +14 -14
  171. package/src/ingest/mount-three-ingest-root.ts +12 -12
  172. package/src/ingest/resolve-canvas.ts +1 -1
  173. package/src/ingest/served-html-boot.ts +1 -1
  174. package/src/ingest/surface-canvas.ts +1 -1
  175. package/src/ingest/unmount-ingest-root.ts +7 -7
  176. package/src/navmesh/navmesh-handler.ts +24 -16
  177. package/src/network/NetworkInspectorPanel.tsx +449 -14
  178. package/src/network/network-inspector-model.ts +14 -2
  179. package/src/play/play-log-events.ts +1 -1
  180. package/src/play/play-mode.ts +99 -142
  181. package/src/play/play-recording.ts +2 -2
  182. package/src/play/react-play-live-authoring.ts +3 -3
  183. package/src/play/run-selection.ts +93 -0
  184. package/src/play-bar/PlayBar.tsx +20 -39
  185. package/src/profiler/FrameDebuggerPanel.tsx +1 -1
  186. package/src/profiler/PerformancePanel.tsx +4 -4
  187. package/src/react/design-time-react-mount.ts +37 -85
  188. package/src/react/dom-authoring-adapter.ts +11 -11
  189. package/src/react/react-inspector-section.tsx +13 -13
  190. package/src/react/react-world-authoring-adapter.ts +31 -143
  191. package/src/react/story-documents/story-documents.tsx +18 -22
  192. package/src/react/ui-board-document.tsx +10 -11
  193. package/src/react/ui-component-board.ts +5 -5
  194. package/src/runtime/adapter/audio-meter.ts +21 -0
  195. package/src/runtime/adapter/first-party-audio-system.ts +230 -0
  196. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
  197. package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
  198. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
  199. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
  200. package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
  201. package/src/runtime/adapter/native-debug-module.ts +498 -0
  202. package/src/runtime/audio/bus-mixer.ts +161 -0
  203. package/src/runtime/audio/pose-guard.ts +80 -0
  204. package/src/runtime/core/frame-pacing.ts +126 -0
  205. package/src/runtime/core/game-loop.ts +225 -0
  206. package/src/runtime/core/game-scoped-slot.ts +28 -0
  207. package/src/runtime/core/seeded-random.ts +162 -0
  208. package/src/runtime/core/sim-clock.ts +391 -0
  209. package/src/runtime/core/system-runner.ts +269 -0
  210. package/src/runtime/core/types.ts +104 -0
  211. package/src/runtime/create-runtime.ts +1128 -0
  212. package/src/runtime/debug-bridge.ts +570 -0
  213. package/src/runtime/debug-registry.ts +899 -0
  214. package/src/runtime/dev/chrome-trace.ts +153 -0
  215. package/src/runtime/dev/instruments.ts +403 -0
  216. package/src/runtime/dev/logger.ts +119 -0
  217. package/src/runtime/dev/performance-profiler.ts +367 -0
  218. package/src/runtime/dev/register-render-vitals.ts +276 -0
  219. package/src/runtime/dev/render-census.ts +354 -0
  220. package/src/runtime/dev/render-debug-adapter.ts +218 -0
  221. package/src/runtime/dev/render-memory.ts +226 -0
  222. package/src/runtime/dev/render-vitals.ts +338 -0
  223. package/src/runtime/dev/static-batch-advisor.ts +188 -0
  224. package/src/runtime/dev/webgl-frame-capture.ts +366 -0
  225. package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
  226. package/src/runtime/dev-build.ts +47 -0
  227. package/src/runtime/game.ts +1636 -0
  228. package/src/runtime/gameplay-rng-trap.ts +135 -0
  229. package/src/runtime/host-context.ts +64 -0
  230. package/src/runtime/input-router.ts +169 -0
  231. package/src/runtime/mount-manifest.ts +480 -0
  232. package/src/runtime/pixi/authoring.ts +706 -0
  233. package/src/runtime/pixi/ingest.ts +116 -0
  234. package/src/runtime/pixi/physics-registry.ts +49 -0
  235. package/src/runtime/pixi/render-pass-bracket.ts +117 -0
  236. package/src/runtime/pixi/scene-capture.ts +179 -0
  237. package/src/runtime/pixi/system-adapters.ts +69 -0
  238. package/src/runtime/playtest.ts +22 -0
  239. package/src/runtime/presentation.ts +141 -0
  240. package/src/runtime/render-control.ts +642 -0
  241. package/src/runtime/render-seed.ts +77 -0
  242. package/src/runtime/run-ticks-settled.ts +73 -0
  243. package/src/runtime/setup/setup-audio.ts +72 -0
  244. package/src/services/audio-pose-guard.ts +2 -2
  245. package/src/services/game-audio.ts +152 -0
  246. package/src/services/game-network.ts +657 -0
  247. package/src/services/game-physics.ts +334 -0
  248. package/src/state-watch/StateWatchPanel.tsx +1 -1
  249. package/src/three/authoring/camera-runtime-inspector-section.tsx +4 -3
  250. package/src/three/authoring/constraint-inspector-section.tsx +8 -7
  251. package/src/three/authoring/model-asset-inspector-section.tsx +14 -17
  252. package/src/three/authoring/oid-source-persistence.ts +13 -13
  253. package/src/three/authoring/r3f-design-session.ts +79 -61
  254. package/src/three/authoring/r3f-source-authoring-adapter.ts +112 -108
  255. package/src/three/authoring/reflection-probe-inspector-section.tsx +5 -4
  256. package/src/three/authoring/spatial-joint-handles.ts +1 -1
  257. package/src/three/authoring/spatial-lod-handles.ts +1 -1
  258. package/src/three/authoring/spatial-particle-handles.ts +1 -1
  259. package/src/three/authoring/three-authoring-adapter.ts +33 -33
  260. package/src/three/component-verbs/extract-menu.ts +8 -7
  261. package/src/three/component-verbs/fork-menu.ts +8 -7
  262. package/src/three/component-verbs/internals-menu.ts +3 -3
  263. package/src/three/drei-asset-caches.ts +13 -0
  264. package/src/three/story-documents/three-story-documents.tsx +24 -34
  265. package/src/three/three-board/ThreeBoardDocument.tsx +25 -25
  266. package/src/three/three-board/board-scene.ts +7 -7
  267. package/src/three/three-board/three-component-board.ts +5 -5
  268. package/contributions/react/pasteboard.action.ts +0 -41
  269. package/contributions/xstate-behavior.action.ts +0 -42
  270. package/contributions/xstate-behavior.document.tsx +0 -73
  271. package/contributions/xstate-behavior.inspector.tsx +0 -28
  272. package/contributions/xstate-behavior.menu.ts +0 -23
  273. package/src/react/pasteboard-materialize.ts +0 -154
  274. package/src/services/game-audio-unlock.ts +0 -48
  275. package/src/xstate/XStateBehaviorSection.tsx +0 -130
  276. package/src/xstate/XStateMachineInspector.tsx +0 -566
  277. package/src/xstate/character-animation-machine.fixture.ts +0 -74
  278. package/src/xstate/live-behaviors.ts +0 -106
  279. package/src/xstate/use-live-actor-state.ts +0 -44
  280. package/src/xstate/xstate-graph.ts +0 -235
  281. package/src/xstate/xstate-layout.ts +0 -76
@@ -0,0 +1,407 @@
1
+ /**
2
+ * WHAT IS ON THE 2D BOARD — the canvas board's membership and its mount pass,
3
+ * kept off the React tree so both are readable (and testable) without a
4
+ * document.
5
+ *
6
+ * The board itself is DOM: `CanvasBoardDocument.tsx` lays these rows out with
7
+ * the shared story-board substrate (`authoring/react-story-board.ts`), the same
8
+ * one the `UI` board uses. This module answers only WHICH stories are on it,
9
+ * WHAT each frame is, and how big the frame must be.
10
+ *
11
+ * ## Membership — one exhibit per COMPONENT
12
+ *
13
+ * The unit is the component, not the story VARIANT: a prefab with five
14
+ * design-time states appears once, in the state `pickComponentPreviewStory`
15
+ * selects, and its other states live in its own story frames elsewhere. That is
16
+ * the 3D board's rule and the 3D board's join, so no surface can disagree with
17
+ * another about which state represents a component.
18
+ *
19
+ * ## Membership — declared `canvas`, then mounted as an exhibit
20
+ *
21
+ * A story is a candidate iff `declaredStoryMedium` says `canvas`. An
22
+ * undeclared story is a named gap and is never a candidate — the board does
23
+ * not mount to guess a medium. A declared canvas story is then mounted
24
+ * (`stories/story-pixi-preview.ts`) as the exhibit; a mount that yields no
25
+ * content, or throws, lands in {@link CanvasBoard.skipped} with its reason.
26
+ *
27
+ * Mounts run ONE AT A TIME, on the process-wide turn every other story mount
28
+ * takes (`stories/story-mount-turn.ts`): a story's loaders touch process-global
29
+ * state — Pixi's `Assets` cache and the `@pixi/react` component catalogue are
30
+ * both module-global — so overlapping two mounts interleaves their setup.
31
+ *
32
+ * ## Ghost slots — the project's story-LESS canvas components
33
+ *
34
+ * A `canvas`-surface component with NO story must be VISIBLE AS MISSING rather
35
+ * than silently absent. Discovery reuses the Content gallery's own machinery,
36
+ * never a second scan: the caller hands in the `listProjectComponents()` index
37
+ * and the story↔component join is `pickComponentPreviewStory`. A ghost gets a
38
+ * reserved, named frame and deliberately NO fabricated render — an empty frame
39
+ * is honest and a guessed sprite is not.
40
+ *
41
+ * ## Resource ownership (stated here, once)
42
+ *
43
+ * Each exhibit owns ONE story mount, and {@link disposeCanvasBoard} is the ONE
44
+ * teardown path for the set. A mount owns its own react root, its Application,
45
+ * its renderer and its host element; nothing here destroys any of those
46
+ * directly. A build superseded mid-flight frees what it already mounted inside
47
+ * its own turn, so a caller never receives a board it would have to tear down
48
+ * out of turn.
49
+ */
50
+
51
+ import type { ProjectComponentEntry } from '@volter/editor-sdk/kit/asset-workflow/project-content';
52
+ import { getProjectStoryRegions } from '@volter/editor-sdk/kit/stories/project-story-regions';
53
+ import {
54
+ declaredStoryMedium,
55
+ reportUndeclaredStoryMedium,
56
+ } from '@volter/editor-sdk/kit/stories/story-declared-medium';
57
+ import type { StoryPreviewComponent } from '@volter/editor-sdk/kit/stories/story-preview-component';
58
+ import { mountedStoryHasPixiContent } from '../../host/stories/pixi-story-model';
59
+ import { deriveStoryGroupPath, storyGroupKey } from '@volter/editor-sdk/kit/stories/story-grouping';
60
+ import {
61
+ type MountedStoryPixi,
62
+ type PixiStoryMountInTurn,
63
+ withPixiStoryMountTurn,
64
+ } from '../../host/stories/story-pixi-preview';
65
+ import { type ProjectStoryModule, pickComponentPreviewStory } from '@volter/editor-sdk/kit/stories/story-registry';
66
+
67
+ /** The district every ghost slot belongs to, so the storyless components
68
+ * gather in one trailing block instead of scattering through the museum. */
69
+ export const GHOST_DISTRICT = 'No story yet';
70
+
71
+ /** The square a ghost frame reserves, in px. Not a claim about the component's
72
+ * size — the frame is empty — just enough room to read as a reserved place. */
73
+ export const GHOST_FRAME_SIZE = { width: 160, height: 160 } as const;
74
+
75
+ /** The cell a canvas story gets when its content reports no usable bounds. */
76
+ const FALLBACK_FRAME_SIZE = { width: 240, height: 180 } as const;
77
+ /** Small sprites still need a selectable frame and a non-colliding label. */
78
+ const MIN_CONTENT_FRAME_SIZE = 96;
79
+
80
+ export interface CanvasBoardCrop {
81
+ readonly x: number;
82
+ readonly y: number;
83
+ readonly width: number;
84
+ readonly height: number;
85
+ }
86
+
87
+ /** Who an exhibit is — the story it composes and what its frame is named. */
88
+ export interface CanvasBoardIdentity {
89
+ /** `<modulePath>#<storyName>` — stable within one project. */
90
+ readonly id: string;
91
+ readonly storyName: string;
92
+ readonly modulePath: string;
93
+ /**
94
+ * The DISTRICT this exhibit stands in — the story group path's ancestors
95
+ * (`Gameplay` for a `Gameplay/Hero` story, `prefabs` for an untitled
96
+ * `src/prefabs/Hero.stories.tsx`), empty when the story is ungrouped.
97
+ *
98
+ * The board's unit is the COMPONENT — one exhibit each — so clustering by
99
+ * component would give a column of one-frame blocks. The shelf a museum
100
+ * wants is the group: every Gameplay prefab side by side, comparable at a
101
+ * glance. That is what the district is, and it is derived with the shared
102
+ * grouping model rather than a second one.
103
+ */
104
+ readonly district: string;
105
+ /** What the frame's name prints. The CSF export name alone is NOT it:
106
+ * `Default` is the conventional export, so a district of five components
107
+ * would name five different things `Default`. The group's leaf names the
108
+ * component; the story's own label is appended only where one leaf
109
+ * contributes more than one exhibit, the only case where it disambiguates. */
110
+ readonly label: string;
111
+ }
112
+
113
+ /** One placed exhibit: who it is, its live mount, and the content frame the
114
+ * board presents around that mount. */
115
+ export interface CanvasBoardExhibit extends CanvasBoardIdentity {
116
+ readonly mounted: MountedStoryPixi;
117
+ readonly frameSize: { readonly width: number; readonly height: number };
118
+ /** Portion of the mounting canvas the frame presents. The Application is a
119
+ * harness; only a standard Storybook `layout: 'fullscreen'` story presents
120
+ * its entire declared canvas. */
121
+ readonly crop: CanvasBoardCrop;
122
+ }
123
+
124
+ /** One storyless canvas component's reserved slot. */
125
+ export interface CanvasBoardGhost {
126
+ /** `<path>#<name>` — the frame id. */
127
+ readonly key: string;
128
+ readonly name: string;
129
+ /** Project-root-relative source file. */
130
+ readonly path: string;
131
+ readonly line: number;
132
+ }
133
+
134
+ /** A story that could not become an exhibit, and why. */
135
+ export interface CanvasBoardSkip {
136
+ readonly id: string;
137
+ readonly modulePath: string;
138
+ readonly storyName: string;
139
+ readonly reason: string;
140
+ }
141
+
142
+ /** Everything one build produced. */
143
+ export interface CanvasBoard {
144
+ readonly exhibits: readonly CanvasBoardExhibit[];
145
+ readonly ghosts: readonly CanvasBoardGhost[];
146
+ readonly skipped: readonly CanvasBoardSkip[];
147
+ }
148
+
149
+ /**
150
+ * The skip tally at the granularity that matches reality: a skip is per STORY,
151
+ * but the cause is almost always per MODULE, so a bare count reads as N
152
+ * independent failures when it is one module that simply is not a canvas one.
153
+ */
154
+ export function summarizeCanvasBoardSkips(skipped: readonly CanvasBoardSkip[]): {
155
+ readonly stories: number;
156
+ readonly modules: number;
157
+ } {
158
+ return {
159
+ stories: skipped.length,
160
+ modules: new Set(skipped.map((entry) => entry.modulePath)).size,
161
+ };
162
+ }
163
+
164
+ interface CanvasBoardCandidate {
165
+ readonly identity: CanvasBoardIdentity;
166
+ readonly Component: StoryPreviewComponent;
167
+ readonly layout: 'centered' | 'padded' | 'fullscreen';
168
+ }
169
+
170
+ function storyLayout(parameters: Record<string, unknown>): CanvasBoardCandidate['layout'] {
171
+ const layout = parameters['layout'];
172
+ return layout === 'fullscreen' || layout === 'padded' || layout === 'centered'
173
+ ? layout
174
+ : 'padded';
175
+ }
176
+
177
+ /**
178
+ * ONE EXHIBIT PER PREFAB — is this story the one that represents its component
179
+ * on the board? The 3D board's rule and the 3D board's join. A story whose CSF
180
+ * declares no `meta.component` is joined to nothing and keeps its own exhibit.
181
+ */
182
+ function isComponentPreviewStory(
183
+ modules: readonly ProjectStoryModule[],
184
+ modulePath: string,
185
+ story: { name: string; componentName?: string },
186
+ ): boolean {
187
+ if (!story.componentName) return true;
188
+ const picked = pickComponentPreviewStory(modules, story.componentName, modulePath);
189
+ return !picked || (picked.modulePath === modulePath && picked.name === story.name);
190
+ }
191
+
192
+ /** The composed stories that REPRESENT a component, named through the shared
193
+ * story-grouping model. */
194
+ export function collectCanvasBoardCandidates(
195
+ modules: readonly ProjectStoryModule[],
196
+ ): CanvasBoardCandidate[] {
197
+ const draft: {
198
+ identity: Omit<CanvasBoardIdentity, 'label'>;
199
+ leafKey: string;
200
+ leaf: string;
201
+ storyLabel: string;
202
+ Component: StoryPreviewComponent;
203
+ layout: 'centered' | 'padded' | 'fullscreen';
204
+ }[] = [];
205
+ const perLeaf = new Map<string, number>();
206
+ const regions = getProjectStoryRegions();
207
+ for (const module_ of modules) {
208
+ if (!module_.ok) continue;
209
+ for (const story of module_.stories) {
210
+ const declared = declaredStoryMedium({ modulePath: module_.modulePath, regions });
211
+ if (declared.medium !== 'canvas') {
212
+ if (declared.via === 'undeclared') {
213
+ reportUndeclaredStoryMedium(module_.modulePath, declared.reason);
214
+ }
215
+ continue;
216
+ }
217
+ if (!isComponentPreviewStory(modules, module_.modulePath, story)) continue;
218
+ const group = deriveStoryGroupPath({
219
+ modulePath: module_.modulePath,
220
+ ...(story.title === undefined ? {} : { title: story.title }),
221
+ });
222
+ const district = storyGroupKey(group);
223
+ const leafKey = `${district}/${group.leaf}`;
224
+ perLeaf.set(leafKey, (perLeaf.get(leafKey) ?? 0) + 1);
225
+ draft.push({
226
+ identity: {
227
+ id: `${module_.modulePath}#${story.name}`,
228
+ storyName: story.name,
229
+ modulePath: module_.modulePath,
230
+ district,
231
+ },
232
+ leafKey,
233
+ leaf: group.leaf,
234
+ storyLabel: story.label,
235
+ Component: story.Component as unknown as StoryPreviewComponent,
236
+ layout: storyLayout(story.parameters),
237
+ });
238
+ }
239
+ }
240
+ return draft.map(({ identity, leafKey, leaf, storyLabel, Component, layout }) => ({
241
+ identity: {
242
+ ...identity,
243
+ label: (perLeaf.get(leafKey) ?? 0) > 1 ? `${leaf} · ${storyLabel}` : leaf,
244
+ },
245
+ Component,
246
+ layout,
247
+ }));
248
+ }
249
+
250
+ /**
251
+ * The `canvas`-surface components no story represents — the ghost-slot set.
252
+ * The join is the Content gallery's own, so the two surfaces can never disagree
253
+ * about which components "have" a story. Inline-SVG entries are image assets,
254
+ * not components (`contentKind`), and other surfaces belong to other boards.
255
+ */
256
+ export function collectStorylessCanvasComponents(
257
+ modules: readonly ProjectStoryModule[],
258
+ components: readonly ProjectComponentEntry[],
259
+ ): CanvasBoardGhost[] {
260
+ const seen = new Set<string>();
261
+ const storyless: CanvasBoardGhost[] = [];
262
+ for (const component of components) {
263
+ if (component.surface !== 'canvas') continue;
264
+ if (component.contentKind === 'image') continue;
265
+ const key = `${component.path}#${component.name}`;
266
+ if (seen.has(key)) continue;
267
+ seen.add(key);
268
+ if (pickComponentPreviewStory(modules, component.name, component.path)) continue;
269
+ storyless.push({ key, name: component.name, path: component.path, line: component.line });
270
+ }
271
+ return storyless;
272
+ }
273
+
274
+ /** The frame is the story CONTENT, not the `<Application>` needed to mount it.
275
+ * Storybook's standard `fullscreen` layout is the explicit exception. */
276
+ function presentationOf(
277
+ mounted: MountedStoryPixi,
278
+ layout: CanvasBoardCandidate['layout'],
279
+ ): { frameSize: { width: number; height: number }; crop: CanvasBoardCrop } {
280
+ const source = layout === 'fullscreen' ? mounted.app.screen : mounted.stage.getBounds();
281
+ const padding = layout === 'fullscreen' ? 0 : 8;
282
+ const crop: { x: number; y: number; width: number; height: number } = {
283
+ x: Math.floor(source.x - padding),
284
+ y: Math.floor(source.y - padding),
285
+ width: Math.ceil(source.width + padding * 2),
286
+ height: Math.ceil(source.height + padding * 2),
287
+ };
288
+ if (
289
+ !Number.isFinite(crop.width) ||
290
+ !Number.isFinite(crop.height) ||
291
+ crop.width < 1 ||
292
+ crop.height < 1
293
+ ) {
294
+ return {
295
+ frameSize: { ...FALLBACK_FRAME_SIZE },
296
+ crop: { x: 0, y: 0, ...FALLBACK_FRAME_SIZE },
297
+ };
298
+ }
299
+ if (layout !== 'fullscreen') {
300
+ const width = Math.max(MIN_CONTENT_FRAME_SIZE, crop.width);
301
+ const height = Math.max(MIN_CONTENT_FRAME_SIZE, crop.height);
302
+ crop.x -= (width - crop.width) / 2;
303
+ crop.y -= (height - crop.height) / 2;
304
+ crop.width = width;
305
+ crop.height = height;
306
+ }
307
+ return {
308
+ frameSize: { width: crop.width, height: crop.height },
309
+ crop,
310
+ };
311
+ }
312
+
313
+ function describeError(error: unknown): string {
314
+ return error instanceof Error ? error.message : String(error);
315
+ }
316
+
317
+ async function mountCandidates(
318
+ candidates: readonly CanvasBoardCandidate[],
319
+ signal: AbortSignal | undefined,
320
+ mount: PixiStoryMountInTurn,
321
+ ): Promise<{ exhibits: CanvasBoardExhibit[]; skipped: CanvasBoardSkip[] }> {
322
+ const exhibits: CanvasBoardExhibit[] = [];
323
+ const skipped: CanvasBoardSkip[] = [];
324
+ for (const { identity, Component, layout } of candidates) {
325
+ if (signal?.aborted) break;
326
+ const skip = (reason: string): void => {
327
+ skipped.push({
328
+ id: identity.id,
329
+ modulePath: identity.modulePath,
330
+ storyName: identity.storyName,
331
+ reason,
332
+ });
333
+ };
334
+ try {
335
+ const mounted = await mount(Component);
336
+ if (!mountedStoryHasPixiContent(mounted.stage)) {
337
+ // Mounted, but rendered nothing — the honest "not a canvas story"
338
+ // outcome for a story that reconciles to an empty stage.
339
+ mounted.dispose();
340
+ skip('The story mounted no Pixi content.');
341
+ continue;
342
+ }
343
+ // The Application is the story's mounting harness, not an implicit
344
+ // artboard. Default/padded/centered stories expose only the native stage;
345
+ // prefab stories that want checkerboard transparency author Pixi's own
346
+ // `backgroundAlpha={0}` (as the shipped examples do). `fullscreen` is
347
+ // the explicit Storybook spelling for presenting the whole output
348
+ // surface, native background and all.
349
+ exhibits.push({ ...identity, mounted, ...presentationOf(mounted, layout) });
350
+ } catch (error) {
351
+ skip(describeError(error));
352
+ }
353
+ }
354
+ return { exhibits, skipped };
355
+ }
356
+
357
+ /**
358
+ * Build the whole board from a story-registry snapshot plus the project's
359
+ * component index. Resolves once every story has either become an exhibit or
360
+ * been recorded as skipped, and every storyless `canvas` component has its slot.
361
+ *
362
+ * Builds never overlap. A `signal` supersedes this build: it stops mounting
363
+ * further stories, frees whatever it already mounted INSIDE its own turn, and
364
+ * rejects.
365
+ */
366
+ export function buildCanvasBoard(
367
+ modules: readonly ProjectStoryModule[],
368
+ components: readonly ProjectComponentEntry[] = [],
369
+ signal?: AbortSignal,
370
+ ): Promise<CanvasBoard> {
371
+ return withPixiStoryMountTurn(async (mount) => {
372
+ const { exhibits, skipped } = await mountCandidates(
373
+ collectCanvasBoardCandidates(modules),
374
+ signal,
375
+ mount,
376
+ );
377
+ const board: CanvasBoard = {
378
+ exhibits,
379
+ ghosts: collectStorylessCanvasComponents(modules, components),
380
+ skipped,
381
+ };
382
+ if (signal?.aborted) {
383
+ disposeInTurn(board);
384
+ throw new Error('The 2D board build was superseded by a newer one.');
385
+ }
386
+ return board;
387
+ });
388
+ }
389
+
390
+ /** Tear a built board down inside a story-mount turn, so its stories' effect
391
+ * cleanups never run in the middle of another build's mounts. */
392
+ export function disposeCanvasBoard(board: CanvasBoard): Promise<void> {
393
+ return withPixiStoryMountTurn(async () => {
394
+ disposeInTurn(board);
395
+ });
396
+ }
397
+
398
+ function disposeInTurn(board: CanvasBoard): void {
399
+ for (const exhibit of board.exhibits) {
400
+ try {
401
+ exhibit.mounted.dispose();
402
+ } catch (error) {
403
+ // biome-ignore lint/suspicious/noConsole: a failed unmount must stay diagnosable without stranding the remaining mounts
404
+ console.error('[canvas-board] a story mount failed to dispose.', error);
405
+ }
406
+ }
407
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * THE `2D` BOARD's registration — one medium's answer to
3
+ * `@volter/editor-sdk/kit/component-board-registry`.
4
+ *
5
+ * The board stays behind a dynamic `import()`: the 2D board is the only board
6
+ * whose renderer initializes Pixi,
7
+ * so a project whose census says it has no canvas content never loads it. The
8
+ * asynchronous `install` contract exists for exactly this board.
9
+ */
10
+
11
+ import type { ComponentBoard } from '@volter/editor-sdk/kit/component-board-registry';
12
+ import { editorConsole } from '@volter/editor-sdk/kit/editor-console';
13
+ import { CANVAS_COMPONENTS_DOCUMENT_ID } from '@volter/editor-sdk/kit/workspace-document-ids';
14
+ import {
15
+ getStoryMediaPresence,
16
+ subscribeStoryMediaPresence,
17
+ } from '../../host/stories/story-media-presence';
18
+ import { projectStoriesReady, subscribeProjectStoryModules } from '@volter/editor-sdk/kit/stories/story-registry';
19
+
20
+ /** The tab reads `2D`, the peer of `Scene` and `UI` in the center strip. */
21
+ const CANVAS_COMPONENTS_TITLE = '2D';
22
+
23
+ export const canvasComponentBoard: ComponentBoard = {
24
+ medium: 'canvas',
25
+ owner: '@volter/editor-game/canvas/canvas-board',
26
+ documentId: CANVAS_COMPONENTS_DOCUMENT_ID,
27
+ title: CANVAS_COMPONENTS_TITLE,
28
+ presence: () =>
29
+ !projectStoriesReady() ? null : getStoryMediaPresence().canvas ? 'present' : 'absent',
30
+ subscribe: (listener) => {
31
+ const stopPresence = subscribeStoryMediaPresence(listener);
32
+ const stopRegistry = subscribeProjectStoryModules(listener);
33
+ return () => {
34
+ stopPresence();
35
+ stopRegistry();
36
+ };
37
+ },
38
+ install: async () => {
39
+ try {
40
+ const { installCanvasBoardDocument } = await import('./CanvasBoardDocument');
41
+ installCanvasBoardDocument();
42
+ } catch (error: unknown) {
43
+ editorConsole.error(
44
+ `Failed to load the ${CANVAS_COMPONENTS_TITLE} board: ${String(error)}`,
45
+ 'scene',
46
+ );
47
+ throw error;
48
+ }
49
+ },
50
+ absentReason: () =>
51
+ `This project has no ${CANVAS_COMPONENTS_TITLE} board (${CANVAS_COMPONENTS_DOCUMENT_ID}): ` +
52
+ "board presence keys on STORIES, not on roots, and none of this project's stories declares " +
53
+ 'the "canvas" medium. Declare it — a manifest root entry naming the story\'s module, or a ' +
54
+ '`vgai.adapter.ts` regionInclude that covers it — and the board appears. `vgai console` ' +
55
+ 'names every story it could not place.',
56
+ };