@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,412 @@
1
+ /**
2
+ * The 3D board's FRAMING MATH — pure, three-free, and the sibling of
3
+ * `board-layout.ts`.
4
+ *
5
+ * Layout answers *where each exhibit stands* at true scale. This module
6
+ * answers *what the default camera should fit*. Those are different
7
+ * questions the moment one exhibit carries a helper volume — a light-beam
8
+ * cone, an unbounded ground plane, a skybox-scale mesh — whose authored
9
+ * AABB is orders of magnitude larger than the body a reader is trying to
10
+ * identify. Framing the union then zooms the camera out until every
11
+ * exhibit is a few pixels in a mostly-black frame.
12
+ *
13
+ * True scale is inviolable: nothing here scales an exhibit, and a helper
14
+ * volume is still in the scene at its authored size. What changes is
15
+ * which box the DEFAULT view (and the Frame button / view presets) fits:
16
+ *
17
+ * 1. **Per-exhibit presence.** Each exhibit's node boxes are reduced to
18
+ * the body worth standing next to its neighbours. A named helper —
19
+ * an authored overlay volume, a BackSide enclosure, or an unbounded
20
+ * plane — is dropped from that body when a body remains. A
21
+ * single-node exhibit is never trimmed: it *is* the exhibit, even
22
+ * when that exhibit is a 28 m cone or a 120 m tower.
23
+ * 2. **Layout uses presence**, so a helper cannot reserve a floor slot
24
+ * the size of itself and push the next district out to its tip. The
25
+ * helper still extends through space at true scale.
26
+ * 3. **The default frame is the union of placed presence boxes**, not
27
+ * the union of every authored vertex. Frame-selection still frames
28
+ * whatever is picked (the cone, if that is what was clicked).
29
+ *
30
+ * Size is the wrong axis for "volume vs body." An 8× sliver cut missed
31
+ * the reported 16 × 16 × 28 m cone (1.75×). A relative-to-own-median cut
32
+ * ate the subject of any exhibit that is one large mesh plus small
33
+ * details (a hull with rivets; a building with a door). Mass distribution
34
+ * cannot save it either: the same cone is 91 % of the lighthouse's mesh
35
+ * surface (so a 90 % area box IS the cone) and still sits in the 90 %
36
+ * triangle-count set. The discriminator that exists in the data is the
37
+ * material flag the author set because the thing is a volume:
38
+ * `depthWrite === false` (the cone: MeshBasicMaterial, transparent,
39
+ * opacity 0.16, depthWrite false; every tower / hull / rivet mesh:
40
+ * opaque standard material, depthWrite true). That flag arrives here as
41
+ * {@link BoardNode.overlay}. Emissive intensity is not a volume mark —
42
+ * the lantern house is emissive and is body.
43
+ *
44
+ * A 0.2 m prop next to a 30 m building is two compact exhibits, and the
45
+ * union of those bodies is exactly the true-scale comparison the board
46
+ * exists to show.
47
+ */
48
+
49
+ import {
50
+ type BoardBounds,
51
+ type BoardItemPlacement,
52
+ type BoardLayout,
53
+ type BoardLayoutItem,
54
+ layoutThreeBoard,
55
+ } from './board-layout';
56
+
57
+ export type { BoardBounds };
58
+
59
+ /** Why a node box was kept out of an exhibit's presence / default frame. */
60
+ export type BoardHelperKind = 'volume' | 'unbounded-plane' | 'environment-scale';
61
+
62
+ export interface BoardHelperVolume {
63
+ readonly kind: BoardHelperKind;
64
+ readonly bounds: BoardBounds;
65
+ readonly size: readonly [number, number, number];
66
+ }
67
+
68
+ /**
69
+ * One drawing node of an exhibit. The two flags are authored material
70
+ * marks collected by `collectContentNodeRecords` — this module never
71
+ * looks at a mesh.
72
+ *
73
+ * - `overlay`: `depthWrite === false` (a light cone, trigger, AoE)
74
+ * - `enclosure`: `side === BackSide` (a skybox)
75
+ */
76
+ export interface BoardNode {
77
+ readonly bounds: BoardBounds;
78
+ readonly overlay?: boolean;
79
+ readonly enclosure?: boolean;
80
+ }
81
+
82
+ export interface ExhibitPresence {
83
+ /** Union of every finite node box — the authored extent, true scale. */
84
+ readonly full: BoardBounds;
85
+ /** The body the board stands and frames. Equal to {@link full} when
86
+ * nothing was defensible to drop. */
87
+ readonly presence: BoardBounds;
88
+ readonly helpers: readonly BoardHelperVolume[];
89
+ }
90
+
91
+ export interface BoardExhibitMeasure {
92
+ readonly id: string;
93
+ readonly groupKey: string;
94
+ readonly nodes: readonly (BoardBounds | BoardNode)[];
95
+ }
96
+
97
+ export interface BoardFramedExhibit {
98
+ readonly id: string;
99
+ readonly groupKey: string;
100
+ readonly full: BoardBounds;
101
+ readonly presence: BoardBounds;
102
+ readonly helpers: readonly BoardHelperVolume[];
103
+ readonly placement: BoardItemPlacement;
104
+ }
105
+
106
+ export interface BoardFrame {
107
+ readonly layout: BoardLayout;
108
+ readonly exhibits: readonly BoardFramedExhibit[];
109
+ /** Union of placed presence boxes — what the default camera fits. */
110
+ readonly frame: BoardBounds;
111
+ /** Union of placed FULL boxes — what a naive frame-all would fit. */
112
+ readonly fullFrame: BoardBounds;
113
+ }
114
+
115
+ const ZERO_BOUNDS: BoardBounds = { min: [0, 0, 0], max: [0, 0, 0] };
116
+
117
+ /** A plane is thinner than this on one axis, and larger than
118
+ * {@link PLANE_SPAN_METRES} on the other two. A shape of the box itself
119
+ * — not a comparison to its siblings — so a building with handles cannot
120
+ * classify as a plane. */
121
+ const PLANE_THIN_METRES = 0.35;
122
+ const PLANE_SPAN_METRES = 20;
123
+
124
+ function isFiniteBounds(bounds: BoardBounds): boolean {
125
+ return [...bounds.min, ...bounds.max].every((value) => Number.isFinite(value));
126
+ }
127
+
128
+ function normalizeBounds(bounds: BoardBounds): BoardBounds {
129
+ if (!isFiniteBounds(bounds)) return ZERO_BOUNDS;
130
+ return {
131
+ min: [
132
+ Math.min(bounds.min[0], bounds.max[0]),
133
+ Math.min(bounds.min[1], bounds.max[1]),
134
+ Math.min(bounds.min[2], bounds.max[2]),
135
+ ],
136
+ max: [
137
+ Math.max(bounds.min[0], bounds.max[0]),
138
+ Math.max(bounds.min[1], bounds.max[1]),
139
+ Math.max(bounds.min[2], bounds.max[2]),
140
+ ],
141
+ };
142
+ }
143
+
144
+ export function boundsSize(bounds: BoardBounds): readonly [number, number, number] {
145
+ const normalized = normalizeBounds(bounds);
146
+ return [
147
+ normalized.max[0] - normalized.min[0],
148
+ normalized.max[1] - normalized.min[1],
149
+ normalized.max[2] - normalized.min[2],
150
+ ];
151
+ }
152
+
153
+ export function boundsSpan(bounds: BoardBounds): number {
154
+ return Math.max(...boundsSize(bounds));
155
+ }
156
+
157
+ export function unionBounds(boxes: readonly BoardBounds[]): BoardBounds {
158
+ const finite = boxes.map(normalizeBounds).filter((box) => isFiniteBounds(box));
159
+ if (finite.length === 0) return ZERO_BOUNDS;
160
+ const first = finite[0]!;
161
+ const min: [number, number, number] = [first.min[0], first.min[1], first.min[2]];
162
+ const max: [number, number, number] = [first.max[0], first.max[1], first.max[2]];
163
+ for (let i = 1; i < finite.length; i++) {
164
+ const box = finite[i]!;
165
+ min[0] = Math.min(min[0], box.min[0]);
166
+ min[1] = Math.min(min[1], box.min[1]);
167
+ min[2] = Math.min(min[2], box.min[2]);
168
+ max[0] = Math.max(max[0], box.max[0]);
169
+ max[1] = Math.max(max[1], box.max[1]);
170
+ max[2] = Math.max(max[2], box.max[2]);
171
+ }
172
+ return { min, max };
173
+ }
174
+
175
+ export function translateBounds(
176
+ bounds: BoardBounds,
177
+ translation: readonly [number, number, number],
178
+ ): BoardBounds {
179
+ const normalized = normalizeBounds(bounds);
180
+ return {
181
+ min: [
182
+ normalized.min[0] + translation[0],
183
+ normalized.min[1] + translation[1],
184
+ normalized.min[2] + translation[2],
185
+ ],
186
+ max: [
187
+ normalized.max[0] + translation[0],
188
+ normalized.max[1] + translation[1],
189
+ normalized.max[2] + translation[2],
190
+ ],
191
+ };
192
+ }
193
+
194
+ /** Accept either a raw box or a {@link BoardNode} so layout ghosts (boxes
195
+ * only) and collected exhibits (boxes + overlay) share one call. */
196
+ export function asBoardNode(input: BoardBounds | BoardNode): BoardNode {
197
+ if ('min' in input) return { bounds: input };
198
+ return {
199
+ bounds: input.bounds,
200
+ ...(input.overlay === true ? { overlay: true as const } : {}),
201
+ ...(input.enclosure === true ? { enclosure: true as const } : {}),
202
+ };
203
+ }
204
+
205
+ /** Named kind for a helper volume, or `null` when the box is the exhibit's body.
206
+ *
207
+ * `overlay` / `enclosure` are authored material marks. Size never names a
208
+ * volume or a skybox: a sibling ratio is what ate the hull-with-rivets
209
+ * and the building-with-handles. */
210
+ export function classifyHelper(
211
+ bounds: BoardBounds,
212
+ flags: { overlay?: boolean; enclosure?: boolean } = {},
213
+ ): BoardHelperKind | null {
214
+ if (flags.overlay) return 'volume';
215
+ if (flags.enclosure) return 'environment-scale';
216
+ const size = boundsSize(bounds);
217
+ const rankedSize = [...size].sort((a, b) => b - a);
218
+ const longest = rankedSize[0] ?? 0;
219
+ const mid = rankedSize[1] ?? 0;
220
+ const shortest = rankedSize[2] ?? 0;
221
+ if (shortest < PLANE_THIN_METRES && mid >= PLANE_SPAN_METRES && longest >= PLANE_SPAN_METRES) {
222
+ return 'unbounded-plane';
223
+ }
224
+ return null;
225
+ }
226
+
227
+ /**
228
+ * The body of one exhibit, given one box per content node.
229
+ *
230
+ * A node is dropped only when it has a named helper kind — overlay volume,
231
+ * enclosure / skybox, or unbounded plane — AND at least one body node
232
+ * remains. A stacked building (drum + crown) is all body. A hull with
233
+ * rivets is all body. A single-node exhibit is never trimmed: it *is* the
234
+ * exhibit, even if that exhibit is a plane or a cone.
235
+ */
236
+ export function exhibitPresence(nodes: readonly (BoardBounds | BoardNode)[]): ExhibitPresence {
237
+ const content = nodes
238
+ .map(asBoardNode)
239
+ .map((node) => ({
240
+ bounds: normalizeBounds(node.bounds),
241
+ overlay: node.overlay === true,
242
+ enclosure: node.enclosure === true,
243
+ }))
244
+ .filter((node) => isFiniteBounds(node.bounds));
245
+ const boxes = content.map((node) => node.bounds);
246
+ const full = unionBounds(boxes);
247
+ if (content.length < 2) return { full, presence: full, helpers: [] };
248
+
249
+ const kinds = content.map((node) =>
250
+ classifyHelper(node.bounds, { overlay: node.overlay, enclosure: node.enclosure }),
251
+ );
252
+ const body = content.filter((_, index) => kinds[index] === null).map((node) => node.bounds);
253
+ if (body.length === 0) return { full, presence: full, helpers: [] };
254
+
255
+ const helpers: BoardHelperVolume[] = [];
256
+ for (let i = 0; i < content.length; i++) {
257
+ const kind = kinds[i] ?? null;
258
+ if (kind === null) continue;
259
+ helpers.push({ kind, bounds: content[i]!.bounds, size: boundsSize(content[i]!.bounds) });
260
+ }
261
+ return { full, presence: unionBounds(body), helpers };
262
+ }
263
+
264
+ /**
265
+ * Lay every exhibit out by its PRESENCE box and compute the default frame.
266
+ *
267
+ * `fullFrame` is carried so a test (and a doctor) can name the camera the
268
+ * old union would have produced without reconstructing it.
269
+ */
270
+ export function frameThreeBoard(exhibits: readonly BoardExhibitMeasure[]): BoardFrame {
271
+ const measured = exhibits.map((exhibit) => ({
272
+ exhibit,
273
+ presence: exhibitPresence(exhibit.nodes),
274
+ }));
275
+
276
+ const layoutItems: BoardLayoutItem[] = measured.map(({ exhibit, presence }) => ({
277
+ id: exhibit.id,
278
+ groupKey: exhibit.groupKey,
279
+ bounds: presence.presence,
280
+ }));
281
+ const layout = layoutThreeBoard(layoutItems);
282
+ const placementById = new Map<string, BoardItemPlacement>();
283
+ for (const district of layout.districts) {
284
+ for (const item of district.items) placementById.set(item.id, item);
285
+ }
286
+
287
+ const framed: BoardFramedExhibit[] = [];
288
+ const placedPresence: BoardBounds[] = [];
289
+ const placedFull: BoardBounds[] = [];
290
+ for (const { exhibit, presence } of measured) {
291
+ const placement = placementById.get(exhibit.id);
292
+ if (!placement) continue;
293
+ framed.push({
294
+ id: exhibit.id,
295
+ groupKey: exhibit.groupKey,
296
+ full: presence.full,
297
+ presence: presence.presence,
298
+ helpers: presence.helpers,
299
+ placement,
300
+ });
301
+ placedPresence.push(translateBounds(presence.presence, placement.translation));
302
+ placedFull.push(translateBounds(presence.full, placement.translation));
303
+ }
304
+
305
+ return {
306
+ layout,
307
+ exhibits: framed,
308
+ frame: unionBounds(placedPresence),
309
+ fullFrame: unionBounds(placedFull),
310
+ };
311
+ }
312
+
313
+ export interface BoardCameraSpec {
314
+ readonly fovDeg: number;
315
+ readonly aspect: number;
316
+ readonly padding?: number;
317
+ }
318
+
319
+ export interface BoardCameraPose {
320
+ readonly target: readonly [number, number, number];
321
+ readonly position: readonly [number, number, number];
322
+ readonly distance: number;
323
+ }
324
+
325
+ /**
326
+ * Perspective fit of `frame` along `direction` (centre → camera). Same
327
+ * geometry as `perspectiveDistanceToFitBox`, kept three-free so the unit
328
+ * test is bounds-in / pose-out.
329
+ */
330
+ export function boardCameraPose(
331
+ frame: BoardBounds,
332
+ direction: readonly [number, number, number],
333
+ camera: BoardCameraSpec,
334
+ ): BoardCameraPose {
335
+ const normalized = normalizeBounds(frame);
336
+ const target = [
337
+ (normalized.min[0] + normalized.max[0]) / 2,
338
+ (normalized.min[1] + normalized.max[1]) / 2,
339
+ (normalized.min[2] + normalized.max[2]) / 2,
340
+ ] as const;
341
+ const length = Math.hypot(direction[0], direction[1], direction[2]) || 1;
342
+ const view: readonly [number, number, number] = [
343
+ direction[0] / length,
344
+ direction[1] / length,
345
+ direction[2] / length,
346
+ ];
347
+ const fallbackUp: readonly [number, number, number] =
348
+ Math.abs(view[1]) > 0.999 ? [0, 0, 1] : [0, 1, 0];
349
+ const rightRaw: readonly [number, number, number] = [
350
+ fallbackUp[1] * view[2] - fallbackUp[2] * view[1],
351
+ fallbackUp[2] * view[0] - fallbackUp[0] * view[2],
352
+ fallbackUp[0] * view[1] - fallbackUp[1] * view[0],
353
+ ];
354
+ const rightLen = Math.hypot(...rightRaw) || 1;
355
+ const right: readonly [number, number, number] = [
356
+ rightRaw[0] / rightLen,
357
+ rightRaw[1] / rightLen,
358
+ rightRaw[2] / rightLen,
359
+ ];
360
+ const up: readonly [number, number, number] = [
361
+ view[1] * right[2] - view[2] * right[1],
362
+ view[2] * right[0] - view[0] * right[2],
363
+ view[0] * right[1] - view[1] * right[0],
364
+ ];
365
+
366
+ let halfDepth = 0;
367
+ let halfHeight = 0;
368
+ let halfWidth = 0;
369
+ for (const x of [normalized.min[0], normalized.max[0]]) {
370
+ for (const y of [normalized.min[1], normalized.max[1]]) {
371
+ for (const z of [normalized.min[2], normalized.max[2]]) {
372
+ const ox = x - target[0];
373
+ const oy = y - target[1];
374
+ const oz = z - target[2];
375
+ halfWidth = Math.max(halfWidth, Math.abs(ox * right[0] + oy * right[1] + oz * right[2]));
376
+ halfHeight = Math.max(halfHeight, Math.abs(ox * up[0] + oy * up[1] + oz * up[2]));
377
+ halfDepth = Math.max(halfDepth, Math.abs(ox * view[0] + oy * view[1] + oz * view[2]));
378
+ }
379
+ }
380
+ }
381
+
382
+ const padding = camera.padding ?? 1.15;
383
+ const verticalHalfFov = (camera.fovDeg * 0.5 * Math.PI) / 180;
384
+ const horizontalHalfFov = Math.atan(Math.tan(verticalHalfFov) * Math.max(camera.aspect, 0.01));
385
+ const fitDistance = Math.max(
386
+ halfHeight / Math.tan(verticalHalfFov),
387
+ halfWidth / Math.tan(horizontalHalfFov),
388
+ );
389
+ const distance = Math.max(fitDistance * padding + halfDepth, 3);
390
+ return {
391
+ target,
392
+ distance,
393
+ position: [
394
+ target[0] + view[0] * distance,
395
+ target[1] + view[1] * distance,
396
+ target[2] + view[2] * distance,
397
+ ],
398
+ };
399
+ }
400
+
401
+ /** Approximate on-screen height of a world-space span at `distance`, as a
402
+ * fraction of the viewport. Used by tests to pin "identifiable", not by
403
+ * the board itself. */
404
+ export function viewportHeightFraction(
405
+ worldHeight: number,
406
+ distance: number,
407
+ fovDeg: number,
408
+ ): number {
409
+ if (!(distance > 0)) return 0;
410
+ const half = Math.tan((fovDeg * 0.5 * Math.PI) / 180) * distance;
411
+ return worldHeight / (2 * half);
412
+ }