@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,373 @@
1
+ /**
2
+ * The PERSISTENCE BACKEND seam for a LIVE authoring adapter, on ANY surface:
3
+ * where a closed gesture's value goes, and what a surface says about that
4
+ * before the gesture happens.
5
+ *
6
+ * IT IS KEYED ON NEITHER SURFACE NOR PROVENANCE — a per-substrate copy is
7
+ * the fork ARCHITECTURE-CORE §Rules names, wearing a filename: an ingested Pixi
8
+ * game needs the SAME ownership gate, the SAME per-edit refusals and the SAME
9
+ * one-transaction-carries-both-halves rule as an ingested three game, and a seam
10
+ * named after one substrate is how a second copy gets written instead of reused.
11
+ * `ThreeAuthoringAdapter` and the canvas write target
12
+ * (`pixi-live-write-target.ts`) are both its consumers; nothing in this file
13
+ * mentions `Object3D` or `Container`, because the currency is an entity id, a
14
+ * property path, an anchor and two values.
15
+ *
16
+ * The adapter owns the gesture and the session journal; it never decides the
17
+ * destination. It hands a backend the creation-site anchor for the property
18
+ * being edited plus the before/after values, and journals the edit live-only
19
+ * whenever the backend declines — which is the DEFAULT, not the exception.
20
+ *
21
+ * ABSENCE IS A BACKEND. An adapter constructed with no backend at all persists
22
+ * nothing and journals nothing: its edits live on the running object for the
23
+ * session and the host injects `createEphemeralPersistence` over them. That is
24
+ * what a play-mode adoption wants — pushing an undo entry there would write
25
+ * play state into the edit-mode history.
26
+ *
27
+ * {@link createCreationSitePersistence} is the other shipped backend, and it
28
+ * covers two of the three destinations the ownership doctrine names:
29
+ * - the CREATION-SITE LITERAL WRITER, when this tier can reach a dev server to
30
+ * write the world's own source through (`/__ingest-source/*`);
31
+ * - an HONEST LIVE-ONLY-WITH-REASON, when it cannot — inventing a fallback
32
+ * that writes somewhere else is exactly the sidecar the doctrine forbids.
33
+ * Every refusal still names its gate.
34
+ *
35
+ * The third shipped backend is `oid-source-persistence.ts`, for a world whose
36
+ * own source the serve-time OID transform DID reach — a vendored R3F game is
37
+ * the case that forces it to exist. There, no `new` expression in the game's
38
+ * source constructs anything (fiber does, inside `node_modules`), so
39
+ * creation-site anchoring is structurally empty and its writer can never fire;
40
+ * the JSX callsite the OID stamp names is the object's real source address.
41
+ */
42
+
43
+ import type { NodeCreationSite, WriteAnchorKind } from '@volter/editor-project/adapter';
44
+ import type {
45
+ ChannelValue,
46
+ CreationSiteLiteralReport,
47
+ CreationSiteSurface,
48
+ CreationSiteWriteScope,
49
+ } from '@volter/editor-core/creation-site-edit';
50
+ import { editorConsole } from '@volter/editor-core/editor-console';
51
+ import { editorIsAuthoring } from '@volter/editor-core/editor-session-mode';
52
+ import type { HistoryService } from '@volter/editor-core/history/history-service';
53
+ import { ingestSourceWritesRecordedIfPrimed } from '@volter/editor-core/ui-source/tier-source-write-backend';
54
+ import type { IngestInspectRequest } from './ingest-source-persistence';
55
+ import {
56
+ IngestSourcePersistence,
57
+ ingestOwnershipNow,
58
+ reportIngestSourceRefusal,
59
+ } from './ingest-source-persistence';
60
+ import { LIVE_ONLY_DESTINATION } from '@volter/editor-core/authoring/write-pipe';
61
+
62
+ /**
63
+ * The adapter's own channel read/write, handed to a backend so the live half of
64
+ * a persisted edit can be restored by undo through the SAME writers an ordinary
65
+ * edit uses.
66
+ */
67
+ export interface LiveChannelAccess {
68
+ /** The value in force for one property, or `undefined` when the node is gone. */
69
+ read(entityId: string, property: string): ChannelValue | undefined;
70
+ /** Put a value back on the live object (undo/redo). */
71
+ apply(entityId: string, property: string, value: ChannelValue): void;
72
+ }
73
+
74
+ /**
75
+ * WHAT is being edited — everything a backend needs to decide, before any value
76
+ * exists. One object rather than a positional list because the two shipped
77
+ * backends read DIFFERENT members of it: the creation-site backend answers from
78
+ * `anchor`/`instances`, the OID backend from `sourceOid`, and a positional
79
+ * signature would have grown a fourth parameter nobody reading the call could
80
+ * name.
81
+ */
82
+ export interface SourceWriteSubject {
83
+ readonly entityId: string;
84
+ /** An editor property path (a key of the surface's channel table). */
85
+ readonly property: string;
86
+ /**
87
+ * Which surface's vocabulary {@link SourceWriteSubject.property} is in.
88
+ * Absent ⇒ `three`. It travels with the subject rather than being fixed at
89
+ * backend construction because it is a fact about the PROPERTY NAME, and the
90
+ * server — which owns the channel tables — is the only thing that resolves it.
91
+ */
92
+ readonly surface?: CreationSiteSurface | undefined;
93
+ /** Where the value being edited LIVES: the creation site of the object that
94
+ * owns the property (for a material colour, the material — not the mesh), or
95
+ * the record in the game's own data file that addresses this object. */
96
+ readonly anchor: NodeCreationSite;
97
+ /**
98
+ * WHICH LANE this subject's write would travel, in the pinned vocabulary
99
+ * (`WriteAnchorKind`) — planned by whoever built the subject, since that is
100
+ * the code that chose the branch, and carried here so a caller does not have
101
+ * to re-derive it from the anchor's shape (which cannot distinguish a JSX
102
+ * prop from a construction literal, nor either from a body-placed spawn).
103
+ */
104
+ readonly anchorKind: WriteAnchorKind;
105
+ /** How many objects that anchor has constructed. A SOURCE-anchor fact only —
106
+ * a data record addresses exactly one object by construction, so the count
107
+ * is 1 there and nothing reads it. */
108
+ readonly instances: number;
109
+ /** Ordinary gestures omit this and address one live object. The explicit
110
+ * component-default gesture carries `creation-site`, authorizing a rewrite
111
+ * whose declared effect is all `instances` objects. */
112
+ readonly writeScope?: CreationSiteWriteScope | undefined;
113
+ /**
114
+ * The serve-time OID stamp of the object that OWNS the property (the same
115
+ * owner hop `anchor` follows), when the served source carried one. Absent for
116
+ * every unstamped world, which is what makes the two backends' domains
117
+ * disjoint rather than overlapping.
118
+ */
119
+ readonly sourceOid?: string | undefined;
120
+ }
121
+
122
+ /** One property edit, fully planned, offered to the backend. */
123
+ export interface SourceWriteRequest extends SourceWriteSubject {
124
+ readonly baseline: ChannelValue;
125
+ readonly next: ChannelValue;
126
+ /** History label for the transaction ("Transform Sun"). */
127
+ readonly label: string;
128
+ }
129
+
130
+ /**
131
+ * One REMOVAL, fully planned, offered to the backend — the same subject a write
132
+ * carries, minus the values. The missing `baseline`/`next` IS the difference:
133
+ * absence is not a value, and a door that took one would be a value write
134
+ * wearing another name.
135
+ */
136
+ export interface SourceRemoveRequest extends SourceWriteSubject {
137
+ /** History label for the transaction ("Remove Sun position"). */
138
+ readonly label: string;
139
+ }
140
+
141
+ /**
142
+ * THE REMOVAL DOOR — drop the authored value at a subject's address entirely,
143
+ * rather than writing a value into it.
144
+ *
145
+ * WHY A BACKEND NEEDS ONE AT ALL: every dialect writer here appends
146
+ * (`addIfMissing`), so a gesture on a callsite that authored no `position`
147
+ * ADDS one, and writing the old numbers back leaves that attribute standing —
148
+ * the file ends one attribute heavier than it started and no byte-level
149
+ * edit/revert round trip can close. Only removal expresses that absence.
150
+ *
151
+ * ABSENT ⇒ this backend cannot express byte-absence at all, and the adapter
152
+ * above it declares no `TransformProvider.remove`, so the protocol's removal
153
+ * door refuses BY NAME (`REMOVAL_UNAVAILABLE`) instead of a value write
154
+ * pretending to be a revert. The two members are ONE optional group rather
155
+ * than two optional methods precisely so "there is a door" and "here is who
156
+ * may walk through it" cannot come apart.
157
+ */
158
+ export interface SourceRemovalDoor {
159
+ /**
160
+ * Is there a door for THIS subject's property, right now?
161
+ *
162
+ * SYNCHRONOUS, because its caller is `TransformProvider.editability`, which
163
+ * has to answer before the gesture. So it may only read what the backend
164
+ * already holds — the same rule {@link SourcePersistenceBackend.gate} states
165
+ * for the write side, and for the same reason.
166
+ */
167
+ available(subject: SourceWriteSubject): boolean;
168
+ /**
169
+ * Attempt the removal. `true` ⇒ the authored value is gone and this
170
+ * backend's own transaction carries the undo. `false` ⇒ NOTHING was dropped
171
+ * — the callsite carried no such attribute, or the writer refused it — and
172
+ * the backend has already reported why, so the caller acks the live-only
173
+ * floor rather than a revert nobody performed.
174
+ */
175
+ perform(request: SourceRemoveRequest): Promise<boolean>;
176
+ }
177
+
178
+ export type SourceWriteVerdict = { ok: true } | { ok: false; reason: string };
179
+
180
+ export interface SourcePersistenceBackend {
181
+ /** Called once by the adapter, before any gesture. */
182
+ attach(live: LiveChannelAccess): void;
183
+ /**
184
+ * The source address this backend would write `sourceOid` at, when it knows
185
+ * one — the adapter prefers it over the creation-site registry's answer, so a
186
+ * stamped object's "created at" line names its real JSX callsite instead of
187
+ * the registry's honest-but-useless "constructed outside project source".
188
+ *
189
+ * ABSENT on a backend with no OID index (the creation-site one), and `null`
190
+ * for an oid this backend's index has not resolved. Never a guess.
191
+ */
192
+ anchor?(sourceOid: string): NodeCreationSite | null;
193
+ /**
194
+ * Is the value at `sourceOid`'s anchor read by a PHYSICS BODY BINDING — i.e.
195
+ * would a write here land at a spawn a simulation re-poses from, rather than
196
+ * at a prop the element itself keeps?
197
+ *
198
+ * Only a backend holding the game's own source index can answer, which is why
199
+ * it lives here rather than on the adapter: the fact is `physicsBinding` /
200
+ * `r3fAuthoring.bodyForwarded` on the served `OidEntry`
201
+ * (`ui-source/r3f-physics-binding.ts`). ABSENT ⇒ this backend indexes no
202
+ * source and every anchor it plans is classified by its own branch alone.
203
+ */
204
+ physicsPlaced?(sourceOid: string, property: string): boolean;
205
+ /** The sentence `transforms.editability` cites for this node + property. */
206
+ describe(subject: SourceWriteSubject): string;
207
+ /** Synchronous "is a write even on the table?", with the reason when not.
208
+ * Must not await: the live-only path stays as synchronous as it was before
209
+ * any of this existed, so an `endEdit(); undo()` sequence still works. */
210
+ gate(subject: SourceWriteSubject): SourceWriteVerdict;
211
+ /**
212
+ * Could this backend write at all right now — a reachable writer, and a
213
+ * session that is AUTHORING rather than playing? Decides only whether a
214
+ * CHEAP-gate refusal is worth telling the user about: "that property has no
215
+ * source anchor" is honesty during an authoring gesture and noise on every
216
+ * frame of a game somebody is playing.
217
+ */
218
+ armed(): boolean;
219
+ /** Report a refusal where the user can see it. */
220
+ report(label: string, reason: string): void;
221
+ /**
222
+ * READ what the construction statement says these properties are — the
223
+ * component-instance diff's other half ("the site's literal is the default,
224
+ * the live object is the instance").
225
+ *
226
+ * ABSENT ⇒ this backend has no readable source behind its subjects, and the
227
+ * adapter reports no component-instance description at all rather than one
228
+ * whose defaults it made up. Present ⇒ every property gets an answer or is
229
+ * simply missing from the map, which is the honest "the site names none".
230
+ */
231
+ readSiteLiterals?(
232
+ request: IngestInspectRequest,
233
+ ): Promise<Record<string, CreationSiteLiteralReport>>;
234
+ /** Attempt the write. `true` ⇒ persisted, and the adapter skips its journal
235
+ * because the backend's own transaction already carries both halves. */
236
+ write(request: SourceWriteRequest): Promise<boolean>;
237
+ /**
238
+ * The other half of `write` for a backend that can express ABSENCE — see
239
+ * {@link SourceRemovalDoor}. Absent on a backend that cannot, which is how a
240
+ * lane honestly reports that byte-absence is unreachable through it rather
241
+ * than reverting with a value write.
242
+ */
243
+ readonly removal?: SourceRemovalDoor;
244
+ /** What `AuthoringAdapter.persistence.destination` reports. */
245
+ destination(): string;
246
+ dispose(): void;
247
+ }
248
+
249
+ export interface CreationSitePersistenceOptions {
250
+ readonly history: HistoryService | null;
251
+ /**
252
+ * Injectable so a unit test can drive the write path without a dev server.
253
+ * Absent in production: the backend builds its own when the tier can reach
254
+ * one, and stays at the honest live-only floor when it cannot.
255
+ */
256
+ readonly writer?: IngestSourcePersistence | undefined;
257
+ }
258
+
259
+ export function createCreationSitePersistence(
260
+ options: CreationSitePersistenceOptions,
261
+ ): SourcePersistenceBackend {
262
+ let writer: IngestSourcePersistence | null = null;
263
+ return {
264
+ attach(live) {
265
+ // Whether an ingest edit is RECORDED is what this session's host serves
266
+ // (`/__ingest-source/*`, from `creationSiteWritePlugin`), never how the
267
+ // editor shell was built — the packaged editor runs a production bundle
268
+ // and serves that route, so `import.meta.env.DEV` refused the write on the
269
+ // one tier a registry install has. See
270
+ // `../ui-source/tier-source-write-backend.ts`.
271
+ writer =
272
+ options.writer ??
273
+ (ingestSourceWritesRecordedIfPrimed('An ingest root’s source persistence')
274
+ ? new IngestSourcePersistence({ history: options.history, live })
275
+ : null);
276
+ },
277
+ describe({ anchor, instances, property, writeScope }) {
278
+ if (writer) return writer.describe(anchor, instances, property, writeScope);
279
+ if (!anchor.anchored) return `Live-only edit — ${anchor.reason}.`;
280
+ return anchor.kind === 'data'
281
+ ? `Live-only edit — this object is ${anchor.display}.`
282
+ : `Live-only edit — this object is created at ${anchor.display}.`;
283
+ },
284
+ gate({ anchor, instances, property, writeScope }) {
285
+ if (!writer) return { ok: false, reason: 'this editor tier cannot write project source' };
286
+ return writer.gate(anchor, instances, property, writeScope);
287
+ },
288
+ armed: () => writer !== null && editorIsAuthoring(),
289
+ readSiteLiterals: (request) => (writer ? writer.inspect(request) : Promise.resolve({})),
290
+ report: (label, reason) => reportIngestSourceRefusal(label, reason),
291
+ async write(request) {
292
+ if (!writer) return false;
293
+ try {
294
+ const outcome = await writer.persist(request);
295
+ if (outcome.persisted) {
296
+ editorConsole.log(
297
+ `[ingest] ${request.label} written to the game's own source — ${outcome.detail}`,
298
+ );
299
+ return true;
300
+ }
301
+ reportIngestSourceRefusal(request.label, outcome.reason);
302
+ return false;
303
+ } catch (error) {
304
+ reportIngestSourceRefusal(
305
+ request.label,
306
+ `the write failed: ${error instanceof Error ? error.message : String(error)}`,
307
+ );
308
+ return false;
309
+ }
310
+ },
311
+ /**
312
+ * THE REMOVAL DOOR on the creation-site lane — the byte-absence half the
313
+ * insertion arm makes necessary: an authored transform can ADD a
314
+ * `receiver.member.axis = value;` statement for an axis the source never
315
+ * named, and no value write can revert an added property. The plan is
316
+ * `planCreationSiteRemoval` (server-side, same `prepare`/`apply` route and
317
+ * checksum guard as a value write, so a vendored game's lock rides along
318
+ * in the same gesture): the ONE deletable shape is the whole own-line
319
+ * assignment statement insertion writes; everything else — constructor
320
+ * literals, `.set(…)` arguments, alias writes, computed expressions —
321
+ * refuses in the planner's own words.
322
+ *
323
+ * `available` answers for the DOOR (a writer exists and the gate is open
324
+ * for this subject), the same honesty split the OID lane records: whether
325
+ * the site currently AUTHORS such a statement is the file's fact, and
326
+ * `perform` — which returns the server plan's own verdict — answers it.
327
+ */
328
+ removal: {
329
+ available(subject) {
330
+ if (!writer) return false;
331
+ return writer.gate(subject.anchor, subject.instances, subject.property, subject.writeScope)
332
+ .ok;
333
+ },
334
+ async perform(request) {
335
+ if (!writer) return false;
336
+ try {
337
+ const outcome = await writer.remove({
338
+ property: request.property,
339
+ surface: request.surface,
340
+ anchor: request.anchor,
341
+ instances: request.instances,
342
+ writeScope: request.writeScope,
343
+ label: request.label,
344
+ });
345
+ if (outcome.persisted) {
346
+ editorConsole.log(
347
+ `[ingest] ${request.label} — dropped from the game's own source (${outcome.detail})`,
348
+ );
349
+ return true;
350
+ }
351
+ reportIngestSourceRefusal(request.label, outcome.reason);
352
+ return false;
353
+ } catch (error) {
354
+ reportIngestSourceRefusal(
355
+ request.label,
356
+ `the removal failed: ${error instanceof Error ? error.message : String(error)}`,
357
+ );
358
+ return false;
359
+ }
360
+ },
361
+ },
362
+ destination() {
363
+ const owned = ingestOwnershipNow();
364
+ return editorIsAuthoring() && owned?.writable && writer
365
+ ? "the game's own source (creation-site write-back)"
366
+ : LIVE_ONLY_DESTINATION;
367
+ },
368
+ dispose() {
369
+ writer?.dispose();
370
+ writer = null;
371
+ },
372
+ };
373
+ }
@@ -0,0 +1,81 @@
1
+ /** Match source revisions to completed Vite updates, irrespective of channel
2
+ * order. A websocket message alone is not evidence that an update applied.
3
+ * Entries are bounded by current file paths, never by edit count.
4
+ */
5
+ export interface RefreshSource {
6
+ file: string;
7
+ path: string;
8
+ sha: string;
9
+ timestamp?: number;
10
+ /** Accepted importer URLs for a changed non-component dependency. */
11
+ boundaries?: readonly string[];
12
+ }
13
+
14
+ function updateFile(path: string): string {
15
+ return decodeURIComponent(path.split('?')[0]!)
16
+ .replace(/^\/@fs\/(?=[A-Za-z]:\/)/, '')
17
+ .replace(/^\/@fs/, '');
18
+ }
19
+
20
+ export class SourceRefreshRevisions {
21
+ private readonly sources = new Map<string, RefreshSource>();
22
+ private readonly applied = new Map<string, string>();
23
+ private readonly expected = new Map<string, string | null>();
24
+ private missingRevision = false;
25
+
26
+ source(source: RefreshSource): void {
27
+ this.sources.set(source.file, source);
28
+ }
29
+
30
+ complete(updates: readonly { acceptedPath: string; timestamp: number }[]): void {
31
+ for (const [file, source] of this.sources) {
32
+ const current = updates.filter((update) => source.timestamp === update.timestamp);
33
+ const complete = source.boundaries
34
+ ? source.boundaries.every((boundary) =>
35
+ current.some((update) => update.acceptedPath === boundary),
36
+ )
37
+ : current.some((update) => updateFile(update.acceptedPath) === file);
38
+ if (!complete) continue;
39
+ this.applied.set(source.path, source.sha);
40
+ this.sources.delete(file);
41
+ }
42
+ }
43
+
44
+ matches(update: { acceptedPath: string; timestamp: number }): boolean {
45
+ const file = updateFile(update.acceptedPath);
46
+ return [...this.sources.values()].some(
47
+ (source) =>
48
+ source.timestamp === update.timestamp &&
49
+ (source.boundaries
50
+ ? source.boundaries.includes(update.acceptedPath)
51
+ : source.file === file),
52
+ );
53
+ }
54
+
55
+ revision(resources: readonly { path: string; sha: string | null }[]): void {
56
+ for (const resource of resources) {
57
+ this.expected.set(resource.path, resource.sha);
58
+ if (resource.sha === null) this.applied.delete(resource.path);
59
+ }
60
+ }
61
+
62
+ needsRemount(): boolean {
63
+ if (this.missingRevision) return true;
64
+ for (const [path, sha] of this.expected) {
65
+ if (sha === null || this.applied.get(path) !== sha) return true;
66
+ }
67
+ return false;
68
+ }
69
+
70
+ missing(): void {
71
+ this.missingRevision = true;
72
+ }
73
+
74
+ /** A completed cold mount consumed these revisions through ordinary source. */
75
+ mounted(): void {
76
+ this.expected.clear();
77
+ this.applied.clear();
78
+ this.sources.clear();
79
+ this.missingRevision = false;
80
+ }
81
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * THE STRUCTURAL WRITE PIPE — the ONE producer of the `source-structure`
3
+ * resolution and its live-only floor, shared by every source-document lane.
4
+ *
5
+ * Extracted from three near-verbatim copies (owner-directed overlap audit,
6
+ * 2026-08-22): `R3fSourceAuthoringAdapter` (`structOp`/`removeMany`/
7
+ * `groupMany`/`pipedStructWrite`/`structRefusal`), `ReactRootAuthoringAdapter`
8
+ * (`structOp`/`removeManyElements`/`pipedStructWrite`/`structRefusal`) and
9
+ * `pixi-source-write-target` (`structOp`/`structMany`/`pipedStruct`/
10
+ * `structRefusal`). The copies had already drifted in the small ways their own
11
+ * doc comments warn about — refusal spellings, destination defaults — and one
12
+ * module makes the drift impossible rather than policed.
13
+ *
14
+ * WHY THE STRUCT DIALECT RESOLVES ON ITS OWN VERB (kept from every copy's
15
+ * header): `writeStruct`/`writeStructMany` is a DIFFERENT DOOR from the
16
+ * attribute writer, so it acks `source-structure`, never `source-prop` —
17
+ * resolving a delete on `writeProp`'s presence is how a lane comes to name a
18
+ * value lane that never carried the bytes. `record` is a no-op because the
19
+ * backend is wrapped in `withProjectSourceHistory`: the sha-guarded whole-file
20
+ * transaction that carries the bytes IS the undo entry, and journaling here
21
+ * would make one gesture two undos.
22
+ *
23
+ * WHAT STAYS PER-LANE, deliberately: OID RESOLUTION (identity is the lane's
24
+ * own scheme — a caller hands in the resolved oid), the DESTINATION sentence,
25
+ * and the ON-CHANGED hook (react marks dirty and queues a source reconcile;
26
+ * the canvas target refreshes its index; r3f notifies the store). A lane hands
27
+ * those in; this module never guesses them.
28
+ */
29
+
30
+ import { showTransientHint } from '@volter/editor-core/transient-hint';
31
+ import type { SourceWriteBackend } from '@volter/editor-core/ui-source/source-write-backend';
32
+ import { resolvesLiveOnly, runWritePipe, type WriteAck, type WriteResolution } from '@volter/editor-core/authoring/write-pipe';
33
+
34
+ /** The `writeStruct` options bag, spelled once (the backend's own shape). */
35
+ export type StructOpOptions = Parameters<SourceWriteBackend['writeStruct']>[2];
36
+
37
+ export interface StructWritePipeConfig {
38
+ /** The lane's own refusal/no-op reporter — prefix included, e.g.
39
+ * `` (m) => console.warn(`[R3fSourceAuthoringAdapter] ${m}`) ``. */
40
+ report(message: string): void;
41
+ /** The live-only floor's reason when no struct door is bound. */
42
+ readonly noWriterReason: string;
43
+ /** The lane's backend, read at call time (a session can gain or lose one). */
44
+ backend(): SourceWriteBackend | null | undefined;
45
+ /** Ran after a LANDED structural write, before the ack resolves — the
46
+ * lane's own reconcile/notify choreography. */
47
+ onChanged(): void | Promise<void>;
48
+ }
49
+
50
+ export interface StructWritePipe {
51
+ /** Every structural source write, through the pipe on the struct verb. */
52
+ pipedStruct(
53
+ write: () => Promise<boolean>,
54
+ op: string,
55
+ bound: boolean,
56
+ destination: string,
57
+ ): Promise<WriteAck>;
58
+ /** A structural verb that never reaches a writer, answered THROUGH the pipe
59
+ * so the lane has exactly ONE producer of the live-only floor. `hint`, when
60
+ * given, is the sentence the author who made the gesture also sees. */
61
+ structRefusal(reason: string, hint?: string): Promise<WriteAck>;
62
+ /** One `writeStruct` op against a resolved oid; `null` oid refuses by name. */
63
+ structOp(
64
+ oid: string | null | undefined,
65
+ subjectLabel: string,
66
+ op: string,
67
+ opts: StructOpOptions,
68
+ destination: string,
69
+ ): Promise<WriteAck>;
70
+ /** One `writeStructMany` batch. The CALLER owns oid collection and any
71
+ * every-node-addressable refusal — batching policy is lane policy. */
72
+ structMany(
73
+ oids: readonly string[],
74
+ op: string,
75
+ opts: { wrapperTag?: string } | undefined,
76
+ destination: string,
77
+ ): Promise<WriteAck>;
78
+ }
79
+
80
+ export function createStructWritePipe(config: StructWritePipeConfig): StructWritePipe {
81
+ const pipedStruct: StructWritePipe['pipedStruct'] = (write, op, bound, destination) =>
82
+ runWritePipe({
83
+ resolve: (): WriteResolution =>
84
+ bound
85
+ ? { reaches: 'writer', anchorKind: 'source-structure', destination, write }
86
+ : resolvesLiveOnly(config.noWriterReason),
87
+ record: () => undefined,
88
+ report: (reason) => config.report(`"${op}" stays live-only — ${reason}.`),
89
+ });
90
+
91
+ const structRefusal: StructWritePipe['structRefusal'] = (reason, hint) =>
92
+ runWritePipe({
93
+ resolve: () => resolvesLiveOnly(reason),
94
+ record: () => undefined,
95
+ report: (said) => {
96
+ config.report(said);
97
+ if (hint) showTransientHint(hint);
98
+ },
99
+ });
100
+
101
+ return {
102
+ pipedStruct,
103
+ structRefusal,
104
+ structOp: (oid, subjectLabel, op, opts, destination) => {
105
+ if (!oid) {
106
+ return structRefusal(`"${op}" refused: "${subjectLabel}" has no source stamp.`);
107
+ }
108
+ return pipedStruct(
109
+ async () => {
110
+ const backend = config.backend()!;
111
+ const res = await backend.writeStruct(oid, op, opts);
112
+ if (!res.changed) {
113
+ config.report(
114
+ `struct "${op}" refused/no-op for oid "${oid}": ${res.error ?? 'no change'}`,
115
+ );
116
+ return false;
117
+ }
118
+ await config.onChanged();
119
+ return true;
120
+ },
121
+ op,
122
+ config.backend()?.writeStruct !== undefined,
123
+ destination,
124
+ );
125
+ },
126
+ structMany: (oids, op, opts, destination) =>
127
+ pipedStruct(
128
+ async () => {
129
+ const backend = config.backend()!;
130
+ const res = await backend.writeStructMany!(oids, op, opts);
131
+ if (!res.changed) {
132
+ config.report(`batch "${op}" refused/no-op: ${res.error ?? 'no change'}`);
133
+ return false;
134
+ }
135
+ await config.onChanged();
136
+ return true;
137
+ },
138
+ `${op} (batch)`,
139
+ config.backend()?.writeStructMany !== undefined,
140
+ destination,
141
+ ),
142
+ };
143
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * What is still allowed to move the editor camera after a world is adopted,
3
+ * and when that stops.
4
+ *
5
+ * Opening the Scene tab on an adopted world is not one action, because the
6
+ * world is not one event: a game captures on its first drawn frame and keeps
7
+ * building itself for seconds afterwards (a level streams in; an R3F game's
8
+ * own camera appears only when its `<Suspense>` content resolves). So there is
9
+ * a WINDOW, and two things may fire inside it — the game's own camera, which
10
+ * wins outright, and a measured framing, which lands once as the interim
11
+ * answer. `scene-framing.ts` is what each of those means.
12
+ *
13
+ * The rule that outranks both: **the reader's first camera gesture ends the
14
+ * window** (#1706). Yanking the view out from under someone already looking is
15
+ * worse than never framing at all, so `cancel()` is total — after it, nothing
16
+ * in here touches the camera again, whatever is still loading.
17
+ */
18
+
19
+ /** How long a requested auto-frame keeps waiting for a world that is still
20
+ * building itself, and how often it re-checks. */
21
+ export const AUTO_FRAME_WINDOW_MS = 20_000;
22
+ export const AUTO_FRAME_RETRY_MS = 250;
23
+ /** How long an adopted game gets to produce its OWN camera before the editor
24
+ * settles for its measured framing. Shorter than the framing window on
25
+ * purpose: replacing the view is welcome while the reader is still watching
26
+ * the world appear, and an intrusion long after that. */
27
+ export const AUTO_SEED_WINDOW_MS = 5_000;
28
+
29
+ /** The two things that may open the reader on the world. Each returns whether
30
+ * it actually moved the camera. */
31
+ export interface AutoFrameSteps {
32
+ /** Adopt the adopted game's own camera; false when it has none yet, or none
33
+ * whose view is usable. */
34
+ seedFromGameCamera(): boolean;
35
+ /** Frame the content that exists right now; false when there is none yet. */
36
+ frameContent(): boolean;
37
+ }
38
+
39
+ export class AutoFrameWindow {
40
+ #framingDeadline = 0;
41
+ #seedDeadline = 0;
42
+ #nextTry = 0;
43
+ readonly #steps: AutoFrameSteps;
44
+
45
+ constructor(steps: AutoFrameSteps) {
46
+ this.#steps = steps;
47
+ }
48
+
49
+ /** True while something in here may still move the camera. */
50
+ get pending(): boolean {
51
+ return this.#framingDeadline > 0 || this.#seedDeadline > 0;
52
+ }
53
+
54
+ /**
55
+ * Open the window and take the first attempt immediately.
56
+ * `watchForGameCamera` is false for a world that has no game camera to wait
57
+ * for — a first-party scene never gets one.
58
+ */
59
+ begin(now: number, watchForGameCamera: boolean): void {
60
+ this.#framingDeadline = now + AUTO_FRAME_WINDOW_MS;
61
+ this.#seedDeadline = watchForGameCamera ? now + AUTO_SEED_WINDOW_MS : 0;
62
+ this.#nextTry = 0;
63
+ this.tick(now);
64
+ }
65
+
66
+ /** Ride the caller's frame loop. Cheap and self-rate-limiting: a no-op
67
+ * unless the window is open and the retry cadence has come round. */
68
+ tick(now: number): void {
69
+ if (!this.pending || now < this.#nextTry) return;
70
+ this.#nextTry = now + AUTO_FRAME_RETRY_MS;
71
+ // The game's own camera outranks any measurement, and ends the window: it
72
+ // is the answer both other paths were approximating.
73
+ if (this.#seedDeadline > 0 && this.#steps.seedFromGameCamera()) {
74
+ this.cancel();
75
+ return;
76
+ }
77
+ // The framing lands ONCE — re-framing every retry would drag the view
78
+ // around for the whole window while a level streams in.
79
+ if (this.#framingDeadline > 0 && this.#steps.frameContent()) this.#framingDeadline = 0;
80
+ if (now >= this.#framingDeadline) this.#framingDeadline = 0;
81
+ if (now >= this.#seedDeadline) this.#seedDeadline = 0;
82
+ }
83
+
84
+ /** The reader took the camera. Nothing here moves it again. */
85
+ cancel(): void {
86
+ this.#framingDeadline = 0;
87
+ this.#seedDeadline = 0;
88
+ }
89
+ }