@vgai/editor 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (519) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +35 -0
  3. package/dist/a/b/c/test-nested.json +1 -0
  4. package/dist/assets/CCDIKSolver-Dz-ysxZF.js +1 -0
  5. package/dist/assets/SelectionBox-BXsv_6aH.js +1 -0
  6. package/dist/assets/browser-project-scripts-DOru4L1x.js +2 -0
  7. package/dist/assets/browserAll-CEkWYlFk.js +1 -0
  8. package/dist/assets/collision-world-DniMB0qq.glb +0 -0
  9. package/dist/assets/create-runtime-DYIWr3va.js +1 -0
  10. package/dist/assets/data-asset-Bb9sIM7i.js +5 -0
  11. package/dist/assets/esbuild-BHljloGq.wasm +0 -0
  12. package/dist/assets/fake-three-BMTQ9rl_.js +1 -0
  13. package/dist/assets/game-8SDZPPgH.js +1 -0
  14. package/dist/assets/game-BI111o5f.js +10 -0
  15. package/dist/assets/game-CevorxyV.js +1 -0
  16. package/dist/assets/game-Di1H0gFj.js +1 -0
  17. package/dist/assets/game-Di7XlywD.js +1 -0
  18. package/dist/assets/game-DjnCaw8R.js +1 -0
  19. package/dist/assets/game-Q9A0aXSV.js +1 -0
  20. package/dist/assets/game-pt_1lQLv.js +1 -0
  21. package/dist/assets/game-source-CvidN7AB.js +1 -0
  22. package/dist/assets/game-xsOho_wg.js +83 -0
  23. package/dist/assets/index-Bp5cecD6.js +12 -0
  24. package/dist/assets/index-CPVV6pjw.js +1 -0
  25. package/dist/assets/index-CpOnYyeD.js +213 -0
  26. package/dist/assets/index-Cs8xwv3e.js +1 -0
  27. package/dist/assets/index-DL7RcQ0n.js +27 -0
  28. package/dist/assets/index-Dr8H_6Rl.js +1 -0
  29. package/dist/assets/index-_bCaipUk.js +13650 -0
  30. package/dist/assets/ingest-siblings-Dz8eEqrV.js +1 -0
  31. package/dist/assets/mount-manifest-Bhk0ohsl.js +1 -0
  32. package/dist/assets/recast-navigation.wasm-compat-CvP4UGK6.js +1 -0
  33. package/dist/assets/scene-query-BivAHhJu.js +1 -0
  34. package/dist/assets/stats.module--VATS4Kh.js +1 -0
  35. package/dist/assets/webworkerAll-BLrH0F6F.js +1 -0
  36. package/dist/assets/xstate-animation-binding-1NGLmw6y.js +1 -0
  37. package/dist/assets/xstate.esm-BgUD33nl.js +3 -0
  38. package/dist/index.html +37 -0
  39. package/dist/ingest/racing-game/UPSTREAM.md +68 -0
  40. package/dist/ingest/racing-game/a/Inter-Black.woff +0 -0
  41. package/dist/ingest/racing-game/a/Inter-Black.woff2 +0 -0
  42. package/dist/ingest/racing-game/a/Inter-BlackItalic.woff +0 -0
  43. package/dist/ingest/racing-game/a/Inter-BlackItalic.woff2 +0 -0
  44. package/dist/ingest/racing-game/a/Inter-Bold.woff +0 -0
  45. package/dist/ingest/racing-game/a/Inter-Bold.woff2 +0 -0
  46. package/dist/ingest/racing-game/a/Inter-BoldItalic.woff +0 -0
  47. package/dist/ingest/racing-game/a/Inter-BoldItalic.woff2 +0 -0
  48. package/dist/ingest/racing-game/a/Inter-ExtraBold.woff +0 -0
  49. package/dist/ingest/racing-game/a/Inter-ExtraBold.woff2 +0 -0
  50. package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff +0 -0
  51. package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff2 +0 -0
  52. package/dist/ingest/racing-game/a/Inter-ExtraLight.woff +0 -0
  53. package/dist/ingest/racing-game/a/Inter-ExtraLight.woff2 +0 -0
  54. package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff +0 -0
  55. package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff2 +0 -0
  56. package/dist/ingest/racing-game/a/Inter-Italic.woff +0 -0
  57. package/dist/ingest/racing-game/a/Inter-Italic.woff2 +0 -0
  58. package/dist/ingest/racing-game/a/Inter-Light.woff +0 -0
  59. package/dist/ingest/racing-game/a/Inter-Light.woff2 +0 -0
  60. package/dist/ingest/racing-game/a/Inter-LightItalic.woff +0 -0
  61. package/dist/ingest/racing-game/a/Inter-LightItalic.woff2 +0 -0
  62. package/dist/ingest/racing-game/a/Inter-Medium.woff +0 -0
  63. package/dist/ingest/racing-game/a/Inter-Medium.woff2 +0 -0
  64. package/dist/ingest/racing-game/a/Inter-MediumItalic.woff +0 -0
  65. package/dist/ingest/racing-game/a/Inter-MediumItalic.woff2 +0 -0
  66. package/dist/ingest/racing-game/a/Inter-Regular.woff +0 -0
  67. package/dist/ingest/racing-game/a/Inter-Regular.woff2 +0 -0
  68. package/dist/ingest/racing-game/a/Inter-SemiBold.woff +0 -0
  69. package/dist/ingest/racing-game/a/Inter-SemiBold.woff2 +0 -0
  70. package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff +0 -0
  71. package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff2 +0 -0
  72. package/dist/ingest/racing-game/a/Inter-Thin.woff +0 -0
  73. package/dist/ingest/racing-game/a/Inter-Thin.woff2 +0 -0
  74. package/dist/ingest/racing-game/a/Inter-ThinItalic.woff +0 -0
  75. package/dist/ingest/racing-game/a/Inter-ThinItalic.woff2 +0 -0
  76. package/dist/ingest/racing-game/a/Inter-italic.var.woff2 +0 -0
  77. package/dist/ingest/racing-game/a/Inter-roman.var.woff2 +0 -0
  78. package/dist/ingest/racing-game/a/Inter.var.woff2 +0 -0
  79. package/dist/ingest/racing-game/a/index.css +1 -0
  80. package/dist/ingest/racing-game/images/bronze.png +0 -0
  81. package/dist/ingest/racing-game/images/gold.png +0 -0
  82. package/dist/ingest/racing-game/images/silver.png +0 -0
  83. package/dist/ingest/racing-game/index.html +15 -0
  84. package/dist/ingest/racing-game/models/chassis-draco.glb +0 -0
  85. package/dist/ingest/racing-game/models/track-draco.glb +0 -0
  86. package/dist/ingest/racing-game/models/wheel-draco.glb +0 -0
  87. package/dist/ingest/racing-game/rg-entry.js +1 -0
  88. package/dist/ingest/racing-game/sounds/accelerate.mp3 +0 -0
  89. package/dist/ingest/racing-game/sounds/boost.mp3 +0 -0
  90. package/dist/ingest/racing-game/sounds/crash.mp3 +0 -0
  91. package/dist/ingest/racing-game/sounds/engine.mp3 +0 -0
  92. package/dist/ingest/racing-game/sounds/honk.mp3 +0 -0
  93. package/dist/ingest/racing-game/sounds/tire-brake.mp3 +0 -0
  94. package/dist/ingest/racing-game/sounds/train.mp3 +0 -0
  95. package/dist/ingest/racing-game/sounds/water.mp3 +0 -0
  96. package/dist/ingest/racing-game/textures/cursor.svg +3 -0
  97. package/dist/ingest/racing-game/textures/dikhololo_night_1k.hdr +0 -0
  98. package/dist/ingest/racing-game/textures/heightmap_1024.png +0 -0
  99. package/dist/ingest/racing-game/textures/mask.svg +11 -0
  100. package/dist/ingest/racing-game/textures/no_sound.png +0 -0
  101. package/dist/ingest/racing-game/three-r139.module.js +50617 -0
  102. package/dist/ingest/racing-game/vendor.js +179 -0
  103. package/dist/ingest/racing-game/vgai.game.json +20 -0
  104. package/dist/ingest/simcity/UPSTREAM.md +65 -0
  105. package/dist/ingest/simcity/favicon.ico +0 -0
  106. package/dist/ingest/simcity/fonts/Bebas-Regular.otf +0 -0
  107. package/dist/ingest/simcity/fonts/Digitalt.otf +0 -0
  108. package/dist/ingest/simcity/fonts/Stanberry.ttf +0 -0
  109. package/dist/ingest/simcity/icons/bulldozer-color.png +0 -0
  110. package/dist/ingest/simcity/icons/bulldozer.png +0 -0
  111. package/dist/ingest/simcity/icons/calendar.png +0 -0
  112. package/dist/ingest/simcity/icons/factory-color.png +0 -0
  113. package/dist/ingest/simcity/icons/factory.png +0 -0
  114. package/dist/ingest/simcity/icons/house-color.png +0 -0
  115. package/dist/ingest/simcity/icons/house.png +0 -0
  116. package/dist/ingest/simcity/icons/job.png +0 -0
  117. package/dist/ingest/simcity/icons/office.png +0 -0
  118. package/dist/ingest/simcity/icons/pause-color.png +0 -0
  119. package/dist/ingest/simcity/icons/pause.png +0 -0
  120. package/dist/ingest/simcity/icons/person.png +0 -0
  121. package/dist/ingest/simcity/icons/play-color.png +0 -0
  122. package/dist/ingest/simcity/icons/play.png +0 -0
  123. package/dist/ingest/simcity/icons/power-color.png +0 -0
  124. package/dist/ingest/simcity/icons/power-line-color.png +0 -0
  125. package/dist/ingest/simcity/icons/power.png +0 -0
  126. package/dist/ingest/simcity/icons/road-color.png +0 -0
  127. package/dist/ingest/simcity/icons/road.png +0 -0
  128. package/dist/ingest/simcity/icons/select-color.png +0 -0
  129. package/dist/ingest/simcity/icons/select.png +0 -0
  130. package/dist/ingest/simcity/icons/store-color.png +0 -0
  131. package/dist/ingest/simcity/index.html +94 -0
  132. package/dist/ingest/simcity/index.js +58 -0
  133. package/dist/ingest/simcity/main.css +233 -0
  134. package/dist/ingest/simcity/models/armored-truck.glb +0 -0
  135. package/dist/ingest/simcity/models/building-block-4floor-corner.glb +0 -0
  136. package/dist/ingest/simcity/models/building-block-4floor-short.glb +0 -0
  137. package/dist/ingest/simcity/models/building-block-5floor.glb +0 -0
  138. package/dist/ingest/simcity/models/building-burger-joint.glb +0 -0
  139. package/dist/ingest/simcity/models/building-cafe.glb +0 -0
  140. package/dist/ingest/simcity/models/building-casino.glb +0 -0
  141. package/dist/ingest/simcity/models/building-cinema.glb +0 -0
  142. package/dist/ingest/simcity/models/building-house-block-big.glb +0 -0
  143. package/dist/ingest/simcity/models/building-house-family-large.glb +0 -0
  144. package/dist/ingest/simcity/models/building-house-family-small.glb +0 -0
  145. package/dist/ingest/simcity/models/building-office-balcony.glb +0 -0
  146. package/dist/ingest/simcity/models/building-office-big.glb +0 -0
  147. package/dist/ingest/simcity/models/building-office-pyramid.glb +0 -0
  148. package/dist/ingest/simcity/models/building-office-tall.glb +0 -0
  149. package/dist/ingest/simcity/models/building-office.glb +0 -0
  150. package/dist/ingest/simcity/models/building-restaurant.glb +0 -0
  151. package/dist/ingest/simcity/models/building-skyscraper.glb +0 -0
  152. package/dist/ingest/simcity/models/car-ambulance-pickup.glb +0 -0
  153. package/dist/ingest/simcity/models/car-baywatch.glb +0 -0
  154. package/dist/ingest/simcity/models/car-hippie-van.glb +0 -0
  155. package/dist/ingest/simcity/models/car-passenger-race.glb +0 -0
  156. package/dist/ingest/simcity/models/car-passenger.glb +0 -0
  157. package/dist/ingest/simcity/models/car-police.glb +0 -0
  158. package/dist/ingest/simcity/models/car-taxi.glb +0 -0
  159. package/dist/ingest/simcity/models/car-tow-truck.glb +0 -0
  160. package/dist/ingest/simcity/models/car-truck-dump.glb +0 -0
  161. package/dist/ingest/simcity/models/car-veteran.glb +0 -0
  162. package/dist/ingest/simcity/models/construction-small.glb +0 -0
  163. package/dist/ingest/simcity/models/data-center.glb +0 -0
  164. package/dist/ingest/simcity/models/industry-factory-old.glb +0 -0
  165. package/dist/ingest/simcity/models/industry-factory.glb +0 -0
  166. package/dist/ingest/simcity/models/industry-refinery.glb +0 -0
  167. package/dist/ingest/simcity/models/industry-warehouse.glb +0 -0
  168. package/dist/ingest/simcity/models/power_line_pole_modified.glb +0 -0
  169. package/dist/ingest/simcity/models/tile-plain_grass.glb +0 -0
  170. package/dist/ingest/simcity/models/tile-road-curve.glb +0 -0
  171. package/dist/ingest/simcity/models/tile-road-end.glb +0 -0
  172. package/dist/ingest/simcity/models/tile-road-intersection-t.glb +0 -0
  173. package/dist/ingest/simcity/models/tile-road-intersection.glb +0 -0
  174. package/dist/ingest/simcity/models/tile-road-straight.glb +0 -0
  175. package/dist/ingest/simcity/models/truck.glb +0 -0
  176. package/dist/ingest/simcity/statusIcons/no-power.png +0 -0
  177. package/dist/ingest/simcity/statusIcons/no-road-access.png +0 -0
  178. package/dist/ingest/simcity/textures/base.png +0 -0
  179. package/dist/ingest/simcity/textures/grid.png +0 -0
  180. package/dist/ingest/simcity/textures/specular.png +0 -0
  181. package/dist/ingest/simcity/three-r155.module.js +51466 -0
  182. package/dist/ingest/simcity/vgai.game.json +20 -0
  183. package/dist/ingest/tanks/UPSTREAM.md +69 -0
  184. package/dist/ingest/tanks/index.html +214 -0
  185. package/dist/ingest/tanks/models/pixelTank.fbx +0 -0
  186. package/dist/ingest/tanks/tanks-entry.js +92 -0
  187. package/dist/ingest/tanks/three-r170.module.js +54571 -0
  188. package/dist/ingest/tanks/vgai.game.json +20 -0
  189. package/dist/inputmaps/default.inputmap.json +41 -0
  190. package/dist/jsm/libs/draco/gltf/draco_decoder.js +33 -0
  191. package/dist/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
  192. package/dist/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
  193. package/dist/scenes/untitled.vscn.json +29 -0
  194. package/dist/textures/waternormals.jpg +0 -0
  195. package/dist-server/packaged.mjs +18155 -0
  196. package/package.json +36 -0
  197. package/server/asset-library-routes.ts +833 -0
  198. package/server/canonical-path.ts +50 -0
  199. package/server/cloud-asset-catalog.ts +101 -0
  200. package/server/dev.ts +350 -0
  201. package/server/editor-server.ts +1537 -0
  202. package/server/editor-sse.ts +73 -0
  203. package/server/game-globals-shadow.ts +58 -0
  204. package/server/local-asset-catalog.ts +163 -0
  205. package/server/packaged.ts +571 -0
  206. package/server/prod.ts +75 -0
  207. package/server/project-optimize-deps-entries.ts +149 -0
  208. package/server/project-view.ts +65 -0
  209. package/server/server-utils.ts +430 -0
  210. package/server/session-registry.ts +93 -0
  211. package/src/EditorContext.tsx +87 -0
  212. package/src/achieved-tier.ts +126 -0
  213. package/src/action-registry.ts +268 -0
  214. package/src/adapter-resolver.ts +1858 -0
  215. package/src/asset-events.ts +100 -0
  216. package/src/authoring/active-adapter.ts +98 -0
  217. package/src/authoring/active-systems.ts +50 -0
  218. package/src/authoring/boundary-authoring-adapter.ts +178 -0
  219. package/src/authoring/composite-authoring-adapter.ts +891 -0
  220. package/src/authoring/design-time-layers.ts +776 -0
  221. package/src/authoring/edit-mode-authoring.ts +643 -0
  222. package/src/authoring/embed-only-adapter.ts +57 -0
  223. package/src/authoring/ephemeral-persistence.ts +26 -0
  224. package/src/authoring/eyedropper-session.ts +69 -0
  225. package/src/authoring/first-party-hierarchy-menu.ts +74 -0
  226. package/src/authoring/first-party-inspector/AddSectionMenu.tsx +118 -0
  227. package/src/authoring/first-party-inspector/EnvironmentSection.tsx +550 -0
  228. package/src/authoring/first-party-inspector/HeaderSection.tsx +329 -0
  229. package/src/authoring/first-party-inspector/is-first-party-selection.ts +44 -0
  230. package/src/authoring/first-party-inspector/register.tsx +108 -0
  231. package/src/authoring/ingest-authoring-adapter.ts +475 -0
  232. package/src/authoring/ingest-capture.ts +94 -0
  233. package/src/authoring/ingest-game-adapter.ts +291 -0
  234. package/src/authoring/ingest-iframe-adapter.ts +38 -0
  235. package/src/authoring/ingest-iframe-reachable-adapter.ts +401 -0
  236. package/src/authoring/ingest-overlay.ts +193 -0
  237. package/src/authoring/layered-pick.ts +87 -0
  238. package/src/authoring/mount-failure-report.ts +112 -0
  239. package/src/authoring/overlay-report.ts +52 -0
  240. package/src/authoring/project-scenes.ts +111 -0
  241. package/src/authoring/react-dom-authoring-adapter.ts +795 -0
  242. package/src/authoring/react-inspector-section.tsx +1041 -0
  243. package/src/authoring/react-world-authoring-adapter.ts +2400 -0
  244. package/src/authoring/root-activation.ts +61 -0
  245. package/src/authoring/scene-ui-layer.ts +485 -0
  246. package/src/authoring/story-overlay.ts +48 -0
  247. package/src/authoring/ui-authoring-adapter.ts +325 -0
  248. package/src/authoring/ui-hierarchy-menu.ts +40 -0
  249. package/src/authoring/ui-inspector-sections.tsx +156 -0
  250. package/src/authoring/undo-timeline.ts +145 -0
  251. package/src/authoring/vgai-scene-authoring-adapter.ts +746 -0
  252. package/src/authoring/viewport-pick-context.ts +31 -0
  253. package/src/authoring/viewport-raycast.ts +80 -0
  254. package/src/authoring/world-overlay.ts +76 -0
  255. package/src/authoring/world-pan-state.ts +115 -0
  256. package/src/authoring/world-session-state.ts +102 -0
  257. package/src/authoring/world2d-authoring-adapter.ts +313 -0
  258. package/src/browser-external-watch.ts +98 -0
  259. package/src/browser-play-entry.ts +112 -0
  260. package/src/browser-project-scripts.ts +112 -0
  261. package/src/browser-transpile.ts +246 -0
  262. package/src/browser-watch-reload.ts +100 -0
  263. package/src/command-listener.ts +326 -0
  264. package/src/components/AlignToolbar.tsx +189 -0
  265. package/src/components/AppRoot.tsx +73 -0
  266. package/src/components/AssetBrowser.tsx +502 -0
  267. package/src/components/BottomPanel.tsx +326 -0
  268. package/src/components/BuildPanel.tsx +455 -0
  269. package/src/components/CameraInfo.tsx +56 -0
  270. package/src/components/CommandPalette.tsx +265 -0
  271. package/src/components/ConsolePanel.tsx +272 -0
  272. package/src/components/DataPanel.tsx +1268 -0
  273. package/src/components/DefaultEditorLayout.tsx +509 -0
  274. package/src/components/FolderTree.tsx +179 -0
  275. package/src/components/GameHierarchy.tsx +1349 -0
  276. package/src/components/Inspector.tsx +671 -0
  277. package/src/components/InspectorToolSection.tsx +74 -0
  278. package/src/components/MountFailureBanner.tsx +110 -0
  279. package/src/components/NetworkSection.tsx +109 -0
  280. package/src/components/OnlineAssetBrowser.tsx +448 -0
  281. package/src/components/OverlayReportBanner.tsx +122 -0
  282. package/src/components/PlayBar.tsx +356 -0
  283. package/src/components/PrefabSaveDialog.tsx +201 -0
  284. package/src/components/ProjectHeader.tsx +174 -0
  285. package/src/components/ProjectScreen.tsx +1004 -0
  286. package/src/components/RightPanel.tsx +166 -0
  287. package/src/components/RootActivateDialog.tsx +105 -0
  288. package/src/components/SaveDialog.tsx +197 -0
  289. package/src/components/SaveStatus.tsx +69 -0
  290. package/src/components/StatsOverlay.tsx +35 -0
  291. package/src/components/ToolHost.tsx +80 -0
  292. package/src/components/Toolbar.tsx +307 -0
  293. package/src/components/ViewportOverlay.tsx +234 -0
  294. package/src/components/ViewportPanel.tsx +369 -0
  295. package/src/components/WorldContextMenu.tsx +257 -0
  296. package/src/components/WorldSelectionOverlay.tsx +2003 -0
  297. package/src/components/WorldTextEditor.tsx +86 -0
  298. package/src/components/asset-viewers/AssetViewer.tsx +38 -0
  299. package/src/components/asset-viewers/AudioViewer.tsx +149 -0
  300. package/src/components/asset-viewers/GenericJsonViewer.tsx +77 -0
  301. package/src/components/asset-viewers/ImageViewer.tsx +65 -0
  302. package/src/components/asset-viewers/InputMapViewer.tsx +206 -0
  303. package/src/components/asset-viewers/ModelViewer.tsx +180 -0
  304. package/src/components/asset-viewers/OnlineAssetDetail.tsx +236 -0
  305. package/src/components/data-csv.ts +301 -0
  306. package/src/components/entity-name-commit.ts +18 -0
  307. package/src/components/inspector-property-grouping.ts +65 -0
  308. package/src/components/inspector-widgets/AlignmentGrid.tsx +188 -0
  309. package/src/components/inspector-widgets/BorderEditor.tsx +305 -0
  310. package/src/components/inspector-widgets/ColorPicker.tsx +548 -0
  311. package/src/components/inspector-widgets/FilterEditor.tsx +108 -0
  312. package/src/components/inspector-widgets/FontPicker.tsx +196 -0
  313. package/src/components/inspector-widgets/GradientEditor.tsx +277 -0
  314. package/src/components/inspector-widgets/ScrubbableInput.tsx +152 -0
  315. package/src/components/inspector-widgets/ShadowEditor.tsx +332 -0
  316. package/src/components/inspector-widgets/color-utils.ts +202 -0
  317. package/src/components/inspector-widgets/gallery-main.tsx +167 -0
  318. package/src/components/inspector-widgets/index.ts +22 -0
  319. package/src/components/inspector-widgets/shared.tsx +135 -0
  320. package/src/components/inspectors/AnimationSection.tsx +143 -0
  321. package/src/components/inspectors/AudioSection.tsx +215 -0
  322. package/src/components/inspectors/BoneAttachmentSection.tsx +82 -0
  323. package/src/components/inspectors/CameraSection.tsx +105 -0
  324. package/src/components/inspectors/ComponentsSection.tsx +358 -0
  325. package/src/components/inspectors/JointsSection.tsx +255 -0
  326. package/src/components/inspectors/LightSection.tsx +116 -0
  327. package/src/components/inspectors/MaterialSection.tsx +1189 -0
  328. package/src/components/inspectors/MeshSection.tsx +99 -0
  329. package/src/components/inspectors/NavigationSection.tsx +58 -0
  330. package/src/components/inspectors/ParticleSection.tsx +681 -0
  331. package/src/components/inspectors/PhysicsSection.tsx +326 -0
  332. package/src/components/inspectors/PostProcessingSection.tsx +1290 -0
  333. package/src/components/inspectors/RenderingSection.tsx +109 -0
  334. package/src/components/inspectors/ShadowSection.tsx +84 -0
  335. package/src/components/inspectors/SplineSection.tsx +191 -0
  336. package/src/components/inspectors/TransformSection.tsx +191 -0
  337. package/src/components/inspectors/json-schema-fields.ts +281 -0
  338. package/src/components/inspectors/multi-edit-utils.ts +49 -0
  339. package/src/components/inspectors/zod-fields.ts +184 -0
  340. package/src/components/primitives/ColorInput.tsx +109 -0
  341. package/src/components/primitives/JsonInput.tsx +98 -0
  342. package/src/components/primitives/NumberInput.tsx +164 -0
  343. package/src/components/primitives/Panel.tsx +68 -0
  344. package/src/components/primitives/SectionHeader.tsx +45 -0
  345. package/src/components/primitives/Tooltip.tsx +82 -0
  346. package/src/components/primitives/Vec3Input.tsx +62 -0
  347. package/src/components/world-overlay-gestures.ts +1438 -0
  348. package/src/doctor/data-checks.ts +285 -0
  349. package/src/doctor/report.ts +386 -0
  350. package/src/editor-api.ts +700 -0
  351. package/src/editor-console.ts +194 -0
  352. package/src/editor-hotkeys.ts +441 -0
  353. package/src/editor-layers.ts +2 -0
  354. package/src/editor-mode.ts +28 -0
  355. package/src/editor-store.ts +2225 -0
  356. package/src/editor-viewport.ts +1924 -0
  357. package/src/entity-factory.ts +221 -0
  358. package/src/game-globals-prelude.ts +27 -0
  359. package/src/gated-globals.ts +131 -0
  360. package/src/generated/setup-by-example.generated.ts +53 -0
  361. package/src/gizmo-registry.ts +608 -0
  362. package/src/handle-system.ts +221 -0
  363. package/src/hierarchy-menu-registry.ts +70 -0
  364. package/src/hierarchy-rows.ts +73 -0
  365. package/src/hosted-example.ts +160 -0
  366. package/src/hotkeys.ts +142 -0
  367. package/src/ingest/discovery-react.ts +132 -0
  368. package/src/ingest/discovery.ts +205 -0
  369. package/src/ingest/discovery2d.ts +204 -0
  370. package/src/ingest/esm-entry-resolution.ts +102 -0
  371. package/src/ingest/games/bloom-composer/game.js +99 -0
  372. package/src/ingest/games/bloom-composer/vgai.game.json +21 -0
  373. package/src/ingest/games/embed-bundle/embed.html +10 -0
  374. package/src/ingest/games/embed-bundle/vgai.game.json +21 -0
  375. package/src/ingest/games/games-fps/assets/collision-world.glb +0 -0
  376. package/src/ingest/games/games-fps/game.js +479 -0
  377. package/src/ingest/games/games-fps/vgai.game.json +24 -0
  378. package/src/ingest/games/iframe-reachable/game-source.js +58 -0
  379. package/src/ingest/games/iframe-reachable/vgai.game.json +21 -0
  380. package/src/ingest/games/interactive-cubes/game.js +139 -0
  381. package/src/ingest/games/interactive-cubes/vgai.game.json +21 -0
  382. package/src/ingest/games/mismatched-three/fake-three.js +38 -0
  383. package/src/ingest/games/mismatched-three/game.js +10 -0
  384. package/src/ingest/games/mismatched-three/vgai.game.json +22 -0
  385. package/src/ingest/games/own-three-bundled/game.js +43 -0
  386. package/src/ingest/games/own-three-bundled/vgai.game.json +21 -0
  387. package/src/ingest/games/pointerlock-maze/game.js +288 -0
  388. package/src/ingest/games/pointerlock-maze/vgai.game.json +22 -0
  389. package/src/ingest/games-2d/_fixtures/first-party-scene.scn2d.json +124 -0
  390. package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +25 -0
  391. package/src/ingest/games-2d/bunnymark/game.ts +88 -0
  392. package/src/ingest/games-2d/bunnymark/vgai.game.json +21 -0
  393. package/src/ingest/games-2d/flappy/game.ts +148 -0
  394. package/src/ingest/games-2d/flappy/vgai.game.json +21 -0
  395. package/src/ingest/games-2d/flappy-pixi/vgai.game.json +25 -0
  396. package/src/ingest/games-2d/puzzling-potions/vgai.game.json +25 -0
  397. package/src/ingest/games-2d/tilemap-camera/game.ts +115 -0
  398. package/src/ingest/games-2d/tilemap-camera/vgai.game.json +21 -0
  399. package/src/ingest/games-react/react-rpg/entry.tsx +185 -0
  400. package/src/ingest/games-react/react-rpg/shims.d.ts +26 -0
  401. package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +129 -0
  402. package/src/ingest/games-react/react-rpg/vgai.game.json +21 -0
  403. package/src/ingest/host-namespace-registry.ts +196 -0
  404. package/src/ingest/types.ts +127 -0
  405. package/src/ingest-hierarchy-rows.ts +207 -0
  406. package/src/ingest-mode.ts +2092 -0
  407. package/src/ingest-siblings.ts +466 -0
  408. package/src/initial-scene.ts +194 -0
  409. package/src/inspector-section-registry.ts +117 -0
  410. package/src/interactive-edit-scope.ts +34 -0
  411. package/src/main.tsx +23 -0
  412. package/src/manifest-project.ts +43 -0
  413. package/src/model-thumbnail.ts +156 -0
  414. package/src/module-mode.ts +513 -0
  415. package/src/navmesh-handler.ts +94 -0
  416. package/src/packaged-runtime.ts +49 -0
  417. package/src/play-mode.ts +1148 -0
  418. package/src/playground/main.ts +206 -0
  419. package/src/project-manager.ts +173 -0
  420. package/src/scene-diff.ts +112 -0
  421. package/src/scene-serializer.ts +144 -0
  422. package/src/scene-sync.ts +587 -0
  423. package/src/scene-ui-mode/active-scene-ui.ts +58 -0
  424. package/src/scene-ui-mode/scene-ui-palette-panel.tsx +152 -0
  425. package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +29 -0
  426. package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +74 -0
  427. package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +49 -0
  428. package/src/scene-ui-mode/ui-prefab-io.ts +76 -0
  429. package/src/script-loader.ts +143 -0
  430. package/src/storage/fsa-storage.ts +55 -0
  431. package/src/storage/handle-storage.ts +150 -0
  432. package/src/storage/handle-store.ts +116 -0
  433. package/src/storage/http-storage.ts +137 -0
  434. package/src/storage/index.ts +147 -0
  435. package/src/storage/mem-storage.ts +135 -0
  436. package/src/storage/opfs-storage.ts +25 -0
  437. package/src/storage/paths.ts +26 -0
  438. package/src/storage/seed.ts +106 -0
  439. package/src/storage/types.ts +110 -0
  440. package/src/tool-loader.ts +291 -0
  441. package/src/ui-editor/inspector.tsx +563 -0
  442. package/src/ui-editor/node-ops.ts +266 -0
  443. package/src/ui-editor/overlay.tsx +380 -0
  444. package/src/ui-editor/palette.tsx +114 -0
  445. package/src/ui-editor/react-store.ts +78 -0
  446. package/src/ui-editor/timeline-panel.tsx +443 -0
  447. package/src/ui-editor/ui-edit-store.ts +552 -0
  448. package/src/ui-editor/ui-prefab.ts +95 -0
  449. package/src/ui-source/inspect.ts +544 -0
  450. package/src/ui-source/oid-transform.ts +133 -0
  451. package/src/ui-source/source-write-backend.ts +203 -0
  452. package/src/ui-source/tailwind-map.ts +174 -0
  453. package/src/ui-source/writer.ts +1175 -0
  454. package/src/widgets/index.ts +73 -0
  455. package/template/.claude/skills/editor/SKILL.md +252 -0
  456. package/template/CLAUDE.md +512 -0
  457. package/template/LICENSE +30 -0
  458. package/template/emit-schemas.ts +42 -0
  459. package/template/index.html +17 -0
  460. package/template/package.json +53 -0
  461. package/template/project.json +9 -0
  462. package/template/public/audio/ambient-arena.wav +0 -0
  463. package/template/public/audio/ambient-music.wav +0 -0
  464. package/template/public/audio/ambient-nature.wav +0 -0
  465. package/template/public/audio/ambient-wind.wav +0 -0
  466. package/template/public/inputmaps/default.inputmap.json +41 -0
  467. package/template/public/jsm/libs/draco/gltf/draco_decoder.js +33 -0
  468. package/template/public/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
  469. package/template/public/jsm/libs/draco/gltf/draco_encoder.js +33 -0
  470. package/template/public/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
  471. package/template/public/scenes/default.vscn.json +43 -0
  472. package/template/public/scenes/untitled.vscn.json +29 -0
  473. package/template/public/textures/waternormals.jpg +0 -0
  474. package/template/server/colyseus-setup.ts +85 -0
  475. package/template/server/main.ts +34 -0
  476. package/template/server/rooms/arena-room.ts +420 -0
  477. package/template/server/rooms/game-room.ts +162 -0
  478. package/template/server/rooms.ts +15 -0
  479. package/template/server/tsconfig.json +19 -0
  480. package/template/src/data/README.md +40 -0
  481. package/template/src/data/assets.ts +31 -0
  482. package/template/src/data/tuning.data.json +5 -0
  483. package/template/src/data/tuning.schema.json +21 -0
  484. package/template/src/data/tuning.schema.ts +34 -0
  485. package/template/src/data/tuning.ts +24 -0
  486. package/template/src/main.ts +157 -0
  487. package/template/src/scripts/components/data-spinner.ts +45 -0
  488. package/template/src/scripts/components/scene-camera.ts +40 -0
  489. package/template/src/scripts/main.ts +94 -0
  490. package/template/src/scripts/registry.ts +46 -0
  491. package/template/src/tools/README.md +14 -0
  492. package/template/src/tools/example.tool.tsx +91 -0
  493. package/template/src/tools/spin.tool.tsx +101 -0
  494. package/template/src/ui/Button.tsx +27 -0
  495. package/template/src/ui/DefaultHud.tsx +40 -0
  496. package/template/src/ui/KeyHint.tsx +25 -0
  497. package/template/src/ui/Panel.tsx +52 -0
  498. package/template/src/ui/RebindPanel.tsx +129 -0
  499. package/template/src/ui/Slider.tsx +28 -0
  500. package/template/src/ui/StatusDot.tsx +22 -0
  501. package/template/src/ui/Toggle.tsx +19 -0
  502. package/template/src/ui/game-state.tsx +37 -0
  503. package/template/src/ui/index.ts +7 -0
  504. package/template/src/ui/mount-default-hud.tsx +19 -0
  505. package/template/src/ui/mount-ui.ts +15 -0
  506. package/template/tsconfig.json +36 -0
  507. package/template/validate-asset-content.ts +78 -0
  508. package/template/validate-manifest.ts +55 -0
  509. package/template/validate-scenes.ts +211 -0
  510. package/template/vgai.game.json +21 -0
  511. package/template/vite.config.ts +128 -0
  512. package/vite-plugin-example-assets.ts +60 -0
  513. package/vite-plugin-game-globals.ts +46 -0
  514. package/vite-plugin-game-static.ts +71 -0
  515. package/vite-plugin-project-game-static.ts +84 -0
  516. package/vite-plugin-react-game-static.ts +98 -0
  517. package/vite-plugin-react-world-provider.ts +81 -0
  518. package/vite-plugin-react-world-runtime.ts +76 -0
  519. package/vite-plugin-ui-oid.ts +561 -0
@@ -0,0 +1,776 @@
1
+ /**
2
+ * Design-time layer stack (B1, docs/unified-world-editor/26-unified-editor-spec.md
3
+ * §3 B1 · D3/D9) — mounts a DESIGN-TIME layer for every non-focused react/pixi
4
+ * world so edit mode composites the viewport exactly like the runtime does
5
+ * (`@engine/runtime/create-runtime.ts`'s `createWorldsGameRuntime`, mirrored
6
+ * below): a z-ordered DOM stack, the threejs canvas keeping its existing,
7
+ * UNTOUCHED slot, each react/pixi world getting an absolutely-positioned
8
+ * layer above it. Layers default `pointer-events:none`; the eye
9
+ * (`world-session-state.ts`'s `hidden`) hides a layer; `interactive` forwards
10
+ * real pointer events for design-time hover/press testing; `pickLock` is
11
+ * stored only (B4 consumes it in the pick walk). A layer that throws on
12
+ * mount degrades to its A1 `BoundaryAuthoringAdapter` error node (#18) via
13
+ * `mount-failure-report.ts` — never a blank editor; every OTHER world (and
14
+ * the focused threejs world in particular) keeps working regardless.
15
+ *
16
+ * On a successful mount, the world's read-only Boundary adapter is REPLACED
17
+ * in the composite by a live one (`CompositeAuthoringAdapter.replaceChild`,
18
+ * B1) — react: the EXISTING, fully-featured `ReactWorldAuthoringAdapter`
19
+ * (spec-27's work; this file only produces the DOM it reflects, it never
20
+ * implements a second react adapter). Pixi stays Boundary-grade for the
21
+ * WHOLE S-B effort — display layer + (B4) stage hit-test only, no
22
+ * hierarchy/stories — see `mountPixiLayer`'s doc comment.
23
+ *
24
+ * Only NON-focused `react`/`pixijs` children are candidates for a layer: the
25
+ * focused threejs world already renders through `ViewportPanel`'s own
26
+ * `<canvas>` (its existing slot, deliberately untouched here — a plain,
27
+ * un-z-indexed, `position:static` element always paints below every
28
+ * POSITIONED layer this module adds, "canvas keeps its slot / accept
29
+ * canvas-bottom" per this task's own allowance). A non-focused THREEJS world
30
+ * (there is no live scene to show it, and switching focus is out of B1's
31
+ * scope — see `edit-mode-authoring.ts`'s `switchFocusedWorld` stub) simply
32
+ * keeps its existing Boundary node, untouched by this module.
33
+ */
34
+
35
+ import type { AuthoringAdapter, PickProvider } from '@engine/adapter';
36
+ import { createAssetCache } from '@engine/assets';
37
+ import { createGameLoop } from '@engine/core/game-loop';
38
+ import type { ResolvedWorldEntry } from '@engine/manifest/load';
39
+ import { createGame, type Game } from '@engine/runtime/game';
40
+ import { stackOrder } from '@engine/runtime/input-router';
41
+ import { PixiSceneGameAdapter } from '@engine/world2d/pixi-game-adapter';
42
+ import { createDataSource } from '@vgai/scene-ui/data-source';
43
+ import { storiesFromRegistry, type UIStory } from '@vgai/scene-ui/registry';
44
+ import { UIDataProvider } from '@vgai/scene-ui/ui-data-context';
45
+ import { Component, type ReactNode } from 'react';
46
+ import {
47
+ fetchScene2DFile,
48
+ loadProjectUIRegistry,
49
+ resolveGameProviderForProject,
50
+ resolveReactWorldEntryComponent,
51
+ resolveReactWorldMountRuntime,
52
+ } from '../adapter-resolver';
53
+ import { editorConsole } from '../editor-console';
54
+ import type { EditorStore } from '../editor-store';
55
+ import { getCurrentProject } from '../project-manager';
56
+ import { createHttpSourceWriteBackend } from '../ui-source/source-write-backend';
57
+ import { BoundaryAuthoringAdapter, type BoundaryWorldInfo } from './boundary-authoring-adapter';
58
+ import type { CompositeAuthoringAdapter } from './composite-authoring-adapter';
59
+ import { registerEditModeChild } from './edit-mode-authoring';
60
+ import { addMountFailureReport, formatMountFailureMessage } from './mount-failure-report';
61
+ import { ReactWorldAuthoringAdapter } from './react-world-authoring-adapter';
62
+ import { mountSceneUILayer } from './scene-ui-layer';
63
+ import { getStoryOverlay } from './story-overlay';
64
+ import { UIAuthoringAdapter } from './ui-authoring-adapter';
65
+ import {
66
+ getWorldPan,
67
+ panTransformValue,
68
+ resetWorldPan,
69
+ subscribeWorldPan,
70
+ } from './world-pan-state';
71
+ import { isWorldHidden, isWorldInteractive } from './world-session-state';
72
+
73
+ /** Bottom-to-top layer z-index base — strictly below `PlayBar`'s z100/
74
+ * `ToolStrip`+`ViewportOverlay`'s z1000, so gizmo/orbit-controls/marquee
75
+ * chrome is never occluded by a design-time layer. */
76
+ const BASE_Z_INDEX = 1;
77
+
78
+ /** One candidate world for a design-time layer — read entirely off its
79
+ * existing `BoundaryAuthoringAdapter` (kind/path/zOrder/pausable are already
80
+ * exposed there, independent of whether the composite has a game-root
81
+ * manifest surface — see that adapter's own inspector). */
82
+ interface LayerCandidate {
83
+ readonly worldId: string;
84
+ readonly kind: 'react' | 'pixijs';
85
+ readonly path: string | undefined;
86
+ readonly zOrder: number;
87
+ readonly pausable: boolean;
88
+ }
89
+
90
+ function readCandidates(composite: CompositeAuthoringAdapter): LayerCandidate[] {
91
+ const out: LayerCandidate[] = [];
92
+ for (const { worldId, adapter } of composite.childAdapters()) {
93
+ if (!(adapter instanceof BoundaryAuthoringAdapter)) continue; // the focused (live) world
94
+ const kind = adapter.inspector.get(worldId, 'kind') as string;
95
+ if (kind !== 'react' && kind !== 'pixijs') continue; // B1 scope: react/pixi layers only
96
+ const reason = adapter.inspector.get(worldId, 'reason');
97
+ if (reason !== undefined) continue; // already an error node — nothing new to attempt
98
+ out.push({
99
+ worldId,
100
+ kind,
101
+ path: adapter.inspector.get(worldId, 'path') as string | undefined,
102
+ zOrder: Number(adapter.inspector.get(worldId, 'zOrder') ?? 0),
103
+ pausable: adapter.inspector.get(worldId, 'pausable') !== false,
104
+ });
105
+ }
106
+ return out;
107
+ }
108
+
109
+ /**
110
+ * The inert design-time GameContext stand-in (B1's one genuine design
111
+ * decision). A REAL `Game` — never a hand-typed duck-typed literal, mirroring
112
+ * this repo's own test convention (`packages/editor/test/adapter-conformance-kit.ts`'s
113
+ * `headlessGame()`) — built with a loop that is constructed but NEVER
114
+ * started/ticked, and with NO worlds ever registered. This satisfies
115
+ * `useGame`/`useGameState` (`@engine/react/game-state`, and any project's own
116
+ * re-export of it) without throwing, genuinely inertly:
117
+ * - `state.subscribe` never fires — nothing ever calls
118
+ * `GameStateBridgeInternal.bump()` (only `GameInternal.runFrame` does, and
119
+ * this Game's loop never runs a frame);
120
+ * - `state.frameVersion` stays `0` forever;
121
+ * - `queryByComponent`/`world()`/`worlds` are empty/`null` because `worlds`
122
+ * stays `[]` (nothing ever calls `registerWorld`).
123
+ * `defaultWorld`/`components`/`input`/`audio` throw descriptively if touched
124
+ * (same as any fresh `createGame()` before a world registers) — a HUD that
125
+ * reaches for those is out of scope for a design-time stand-in and SHOULD
126
+ * throw loudly, caught by `DesignTimeErrorBoundary` below, degrading to the
127
+ * #18 error node rather than silently faking a value.
128
+ *
129
+ * B2 swaps this stand-in's DATA for story-sourced data (a `data`/services
130
+ * seam) — this function is the seam B2 replaces/extends, not a permanent
131
+ * fixture; it never constructs a live Game/GameComponents/sockets.
132
+ */
133
+ function createDesignTimeGame(): Game {
134
+ const loop = createGameLoop({ fixedTimestep: 1 / 60, maxSubSteps: 8, update: () => {} });
135
+ return createGame({ loop, assets: createAssetCache() });
136
+ }
137
+
138
+ /** Minimal class-component error boundary — the only way React lets a host
139
+ * catch a synchronous render-time throw (there is no hook equivalent).
140
+ * Mirrors `ingest-siblings.ts`'s `SiblingErrorBoundary` (not exported from
141
+ * that file, so re-declared here rather than reached into across an
142
+ * unrelated module). */
143
+ class DesignTimeErrorBoundary extends Component<
144
+ { onCaught: (error: unknown) => void; children: ReactNode },
145
+ { crashed: boolean }
146
+ > {
147
+ override state = { crashed: false };
148
+ static getDerivedStateFromError(): { crashed: boolean } {
149
+ return { crashed: true };
150
+ }
151
+ override componentDidCatch(error: unknown): void {
152
+ this.props.onCaught(error);
153
+ }
154
+ override render(): ReactNode {
155
+ return this.state.crashed ? null : this.props.children;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * A minimal `ResolvedWorldEntry` satisfying `resolveReactWorldEntryComponent`
161
+ * (`../adapter-resolver.ts`, which reads only `id`/`kind`/`scene`/`entry` off
162
+ * it) — the fields below it (`adapter`/`capabilities`/`loop`/`description`)
163
+ * are unused by that function but required by the type; filled with the
164
+ * honest "this is a native default-react world" values.
165
+ */
166
+ function buildResolvedWorldEntryStub(worldId: string, entry: string): ResolvedWorldEntry {
167
+ return {
168
+ id: worldId,
169
+ kind: 'react',
170
+ description: undefined,
171
+ adapter: { type: 'default', identity: 'default-react' },
172
+ scene: undefined,
173
+ entry,
174
+ zOrder: 0,
175
+ pausable: true,
176
+ loop: 'gated',
177
+ capabilities: { local: 'first-party', hosted: 'first-party' },
178
+ };
179
+ }
180
+
181
+ /** Re-applies session-local layer CSS (eye + interactive) — cheap, called on
182
+ * every store notification, never triggers a remount. */
183
+ function applySessionStyle(layer: HTMLElement, worldId: string): void {
184
+ layer.style.display = isWorldHidden(worldId) ? 'none' : 'block';
185
+ layer.style.pointerEvents = isWorldInteractive(worldId) ? 'auto' : 'none';
186
+ }
187
+
188
+ function createLayerElement(container: HTMLElement, worldId: string, zIndex: number): HTMLElement {
189
+ const layer = document.createElement('div');
190
+ layer.dataset['worldId'] = worldId;
191
+ layer.style.position = 'absolute';
192
+ layer.style.top = '0';
193
+ layer.style.left = '0';
194
+ layer.style.width = '100%';
195
+ layer.style.height = '100%';
196
+ layer.style.zIndex = String(zIndex);
197
+ applySessionStyle(layer, worldId);
198
+ // D4 (spec27 §8 "space-pan" row) — seed this layer with whatever pan is
199
+ // currently in effect (normally none — `mountDesignTimeLayers` resets pan
200
+ // on every fresh install, see its own doc comment — but re-applying here
201
+ // too keeps this function correct standalone). See `world-pan-state.ts`'s
202
+ // doc comment for the full lockstep-translate soundness argument: THIS
203
+ // element is exactly `this.root`/`hostRect()` in
204
+ // `react-world-authoring-adapter.ts`, i.e. the host `RectProvider.rect(id)`
205
+ // subtracts — panning it is what keeps `rect(id)` pan-invariant.
206
+ applyPanTransform(layer);
207
+ container.appendChild(layer);
208
+ return layer;
209
+ }
210
+
211
+ /** Re-applies the current shared world-pan transform to one layer element —
212
+ * called at creation and by `mountDesignTimeLayers`' pan subscription on
213
+ * every subsequent pan change (see `world-pan-state.ts`). */
214
+ function applyPanTransform(layer: HTMLElement): void {
215
+ layer.style.transform = panTransformValue(getWorldPan()) ?? '';
216
+ }
217
+
218
+ interface LayerMountResult {
219
+ readonly adapter?: AuthoringAdapter;
220
+ /**
221
+ * D12 (B4) — a stage hit-test over this layer's mounted content, in CLIENT
222
+ * (browser) px, `PickProvider['pick']`-shaped. Only `mountPixiLayer`
223
+ * produces one (react layers get their pick via the FULL
224
+ * `ReactWorldAuthoringAdapter.pickable` the `adapter` field above already
225
+ * carries) — see the success handler below for how a pixi layer's `pick`
226
+ * (with no `adapter` of its own) gets wired onto its still-Boundary-grade
227
+ * authoring adapter.
228
+ */
229
+ readonly pick?: PickProvider['pick'];
230
+ dispose(): void;
231
+ }
232
+
233
+ /**
234
+ * B3 — the layer's current render target: EITHER the world's `entry`
235
+ * (the world's own entry component, against whatever story `stories.apply`
236
+ * last applied — B1/B2's original render), OR a single catalog component
237
+ * `isolate`d against a story (the storybook canvas, §0/§3 B3). Exactly one
238
+ * of these is "current" at a time; `apply`/`isolate` write into their OWN
239
+ * slot independently (`entryStory`/`isolation` below) so applying an
240
+ * entry-level story WHILE isolated updates the entry's story in the
241
+ * background WITHOUT restoring the entry render (RISKS: "apply-while-isolated
242
+ * must NOT restore entry") — the render picks `isolation` over `entryStory`
243
+ * whenever `isolation` is set.
244
+ */
245
+ interface LayerRenderState {
246
+ entryStory: (UIStory & { id: string }) | null;
247
+ isolation: { componentKey: string; story: (UIStory & { id: string }) | null } | null;
248
+ }
249
+
250
+ /**
251
+ * React design-time mount (B1, story wiring B2 · D4, catalog B3) — the entry
252
+ * via the EXISTING #31 loader (`resolveReactWorldEntryComponent`, reused
253
+ * verbatim), wrapped in the project's own `GameProvider`
254
+ * (`resolveGameProviderForProject` — reused verbatim so a project shipping
255
+ * its own `src/ui/game-state.tsx` is wrapped by ITS OWN provider, never
256
+ * split-context — see that function's doc comment) around the inert
257
+ * `createDesignTimeGame()` stand-in (UNTOUCHED by B2/B3 — see that function's
258
+ * own doc comment), inside a `UIDataProvider` (B2 — seeded from the active
259
+ * story's `data`, or `{}` with none applied) and a React error boundary.
260
+ * `flushSync` forces the initial render/commit/error-boundary-recovery to
261
+ * finish before this call returns (mirrors `ingest-siblings.ts`'s
262
+ * `mountDefaultReactSibling`), so a throwing entry is caught HERE —
263
+ * synchronously — rather than discovered later on react's own schedule.
264
+ *
265
+ * B2 — stories: `storySources` is `[storiesFromRegistry(registry)`, the
266
+ * project's `src/ui/registry.ts` (`states` folded in as an alias) — `,
267
+ * getStoryOverlay(worldId) ?? {}]`, an editor-side overlay (Q2 decoupling,
268
+ * `story-overlay.ts`) — the overlay LAST so it wins on an id collision. No
269
+ * story is applied at mount (`render(…)`, matching B1's original empty-data
270
+ * render byte-for-byte when no story exists yet); `stories.apply` (routed
271
+ * here from the story picker via `onStoryApplied`) RE-RENDERS this SAME root
272
+ * in place against the applied story's `data`/`props` — never a remount,
273
+ * never a second `createDesignTimeGame()`/`GameProvider` (the inert Game
274
+ * stand-in stays the SAME instance across every story switch AND every
275
+ * isolate/exit, exactly as B1 left it — story data rides `UIDataProvider`/
276
+ * entry-or-component props ONLY, never `game.state`/`queryByComponent`, see
277
+ * this module's own crux doc comment — the `30-composition-render` "Ticks: 0"
278
+ * tripwire is the proof).
279
+ *
280
+ * B3 — `catalogComponents: registry?.components` gates
281
+ * `ReactWorldAuthoringAdapter`'s catalog hierarchy ON for this world (§0's
282
+ * reconciliation: THIS is the one site that turns it on — `play-mode.ts`'s
283
+ * `installMultiWorldAuthoring` never passes it, so play mode's hierarchy is
284
+ * untouched by construction). `onIsolate` re-renders the SAME root against
285
+ * the isolated component (alone) instead of the entry — `render()` below is
286
+ * `renderStory`'s generalization: it picks `isolation` over `entryStory`
287
+ * whenever an isolation is active (see {@link LayerRenderState}'s doc
288
+ * comment).
289
+ *
290
+ * On success, mints the world's live `ReactWorldAuthoringAdapter` (the SAME
291
+ * construction recipe as `play-mode.ts`'s `installMultiWorldAuthoring`, plus
292
+ * the new `storySources`/`onStoryApplied`/`catalogComponents`/`onIsolate`/
293
+ * `worldId` options) over this layer's now-mounted DOM — the adapter reflects
294
+ * an already-mounted root, it never mounts one itself; this function is the
295
+ * design-time MOUNT B1/B2/B3 add, the adapter class itself is wholesale
296
+ * reused.
297
+ */
298
+ async function mountReactLayer(
299
+ candidate: LayerCandidate,
300
+ layer: HTMLElement,
301
+ projectRoot: string,
302
+ store: EditorStore,
303
+ ): Promise<LayerMountResult> {
304
+ if (!candidate.path) {
305
+ throw new Error(
306
+ `design-time react layer "${candidate.worldId}": no \`entry\` declared — nothing to mount.`,
307
+ );
308
+ }
309
+ const worldStub = buildResolvedWorldEntryStub(candidate.worldId, candidate.path);
310
+ const Entry = await resolveReactWorldEntryComponent(worldStub, projectRoot);
311
+ const GameProvider = await resolveGameProviderForProject(
312
+ projectRoot,
313
+ candidate.worldId,
314
+ 'default-react',
315
+ );
316
+ // C1 (phase-b/c1-react-mount): this layer's own react-world mount is a
317
+ // SECOND call site with the identical packaged-runtime dual-React-instance
318
+ // exposure `adapter-resolver.ts`'s `ReactWorldAdapter.mount()` has — under
319
+ // the packaged runtime, `createElement`/`createRoot`/`flushSync` must come
320
+ // from the PROJECT's own react, not this module's (removed) static
321
+ // imports. See `resolveReactWorldMountRuntime`'s doc comment.
322
+ const { createElement, createRoot, flushSync } = await resolveReactWorldMountRuntime();
323
+ const game = createDesignTimeGame();
324
+
325
+ // D4 (B2) — story sources: the project's registry (states folded in as
326
+ // stories) first, an editor-side overlay last (wins on an id collision).
327
+ // `loadProjectUIRegistry` stays silent for the common no-registry case
328
+ // (its own `hasUiRegistry` probe) — awaited HERE (not the async cache slot
329
+ // `scene-ui-layer.ts`'s own registry load populates, which can still be
330
+ // settling when this layer mounts, since the two mounts race).
331
+ const registry = await loadProjectUIRegistry(projectRoot);
332
+ const storySources: ReadonlyArray<Record<string, UIStory>> = [
333
+ storiesFromRegistry(registry ?? {}),
334
+ getStoryOverlay(candidate.worldId) ?? {},
335
+ ];
336
+ // B3 — the catalog gate: absent/empty `components` (the common project,
337
+ // and every pre-B3 fixture) keeps `ReactWorldAuthoringAdapter`'s hierarchy
338
+ // the plain DOM walk, byte-identical (§1's gate — "components NON-EMPTY
339
+ // specifically").
340
+ const catalogComponents = registry?.components;
341
+
342
+ const root = createRoot(layer);
343
+ const renderState: LayerRenderState = { entryStory: null, isolation: null };
344
+
345
+ /** Render the CURRENT layer state (entry or isolated — see
346
+ * {@link LayerRenderState}) against the mounted root. Shared by the
347
+ * initial mount and every subsequent `stories.apply`/`stories.isolate` —
348
+ * React reconciles in place, never a remount (the inert `game`/
349
+ * `GameProvider` stay the SAME instances — the inert-Game invariant). */
350
+ const render = (onCaught: (error: unknown) => void): void => {
351
+ const { entryStory, isolation } = renderState;
352
+ const story = isolation ? isolation.story : entryStory;
353
+ // An isolated key the catalog doesn't actually have is defensive-only —
354
+ // the adapter itself refuses to `isolate` an unregistered key — so
355
+ // `Component` falling through to `null` here should never observably
356
+ // happen; it's a safe degrade rather than a crash if it somehow did.
357
+ const Component = isolation ? catalogComponents?.[isolation.componentKey] : undefined;
358
+ const view = isolation
359
+ ? Component
360
+ ? createElement(Component, { ...(story?.props ?? {}) })
361
+ : null
362
+ : createElement(Entry, { ...(story?.props ?? {}) });
363
+ root.render(
364
+ createElement(DesignTimeErrorBoundary, {
365
+ // Key by the current view + applied story so switching stories, OR
366
+ // switching entry<->isolated, REMOUNTS the boundary (React unmounts a
367
+ // differently-keyed element at the same position), resetting its
368
+ // `crashed` latch — otherwise a story/isolation whose data makes the
369
+ // render throw would stick `crashed` and blank the world for the rest
370
+ // of the session even after picking a good story/exiting again.
371
+ key: isolation
372
+ ? `isolate:${isolation.componentKey}:${isolation.story?.id ?? ''}`
373
+ : story
374
+ ? `story:${story.id}`
375
+ : '__none__',
376
+ onCaught,
377
+ children: createElement(
378
+ UIDataProvider,
379
+ { data: createDataSource(story?.data ?? {}) },
380
+ createElement(GameProvider, { game }, view),
381
+ ),
382
+ }),
383
+ );
384
+ };
385
+
386
+ let caught: unknown = null;
387
+ flushSync(() => {
388
+ render((error: unknown) => {
389
+ caught = error;
390
+ });
391
+ });
392
+ if (caught !== null) {
393
+ root.unmount();
394
+ throw caught instanceof Error ? caught : new Error(String(caught));
395
+ }
396
+
397
+ const adapter = new ReactWorldAuthoringAdapter(layer, store, {
398
+ ...(import.meta.env.DEV ? { writeBackend: createHttpSourceWriteBackend() } : {}),
399
+ storySources,
400
+ worldId: candidate.worldId,
401
+ catalogComponents,
402
+ onStoryApplied: (story) => {
403
+ renderState.entryStory = story;
404
+ render((error: unknown) => {
405
+ editorConsole.error(
406
+ `[design-time-layers] world "${candidate.worldId}" crashed applying story ` +
407
+ `"${story?.id ?? '(cleared)'}": ${formatMountFailureMessage(error)}`,
408
+ 'authoring',
409
+ );
410
+ });
411
+ },
412
+ onIsolate: (v) => {
413
+ renderState.isolation = v;
414
+ render((error: unknown) => {
415
+ editorConsole.error(
416
+ `[design-time-layers] world "${candidate.worldId}" crashed isolating ` +
417
+ `"${v?.componentKey ?? '(cleared)'}": ${formatMountFailureMessage(error)}`,
418
+ 'authoring',
419
+ );
420
+ });
421
+ },
422
+ });
423
+ return {
424
+ adapter,
425
+ dispose: () => {
426
+ // A4 (spec 27 §3) — release the adapter's `vite:afterUpdate` echo-reconcile
427
+ // subscription before dropping its DOM, so listeners don't accumulate across
428
+ // layer remounts (a leaked one is harmless — it early-returns on an empty echo —
429
+ // but this keeps teardown clean).
430
+ adapter.disposeReactWorldAdapter();
431
+ root.unmount();
432
+ },
433
+ };
434
+ }
435
+
436
+ /** Best-effort layer pixel size — the container should already be laid out
437
+ * by the time this runs (mounted after `loadInitialScene` + the manifest
438
+ * fetch both resolve), but falls back to a fixed floor rather than handing
439
+ * Pixi/WebGL a degenerate 0×0 surface if it somehow isn't yet. */
440
+ function measure(container: HTMLElement): { width: number; height: number } {
441
+ const rect = container.getBoundingClientRect();
442
+ return {
443
+ width: Math.max(1, Math.round(rect.width) || container.clientWidth || 300),
444
+ height: Math.max(1, Math.round(rect.height) || container.clientHeight || 200),
445
+ };
446
+ }
447
+
448
+ /**
449
+ * Pixi display-only design-time mount (B1) — `.scn2d` load
450
+ * (`fetchScene2DFile`, reused verbatim) + a ONE-SHOT render (no ongoing
451
+ * simulation/ticking at design time: this is a static disclosure, not a live
452
+ * preview). Reuses the exact production recipe (`PixiSceneGameAdapter.mount`,
453
+ * `transparent:true`, `autoStart:false` — the adapter's own default) rather
454
+ * than hand-rolling a leaner physics-free path: `loadScene2DFromData` still
455
+ * requires a full, real `World2DContext` (a real Rapier `World` +
456
+ * `ComponentManager` + `SystemRunner` — `packages/engine/src/world2d/types.ts`
457
+ * has no optional/physics-free fields), so there is no genuinely cheaper
458
+ * primitive available — this pays the one-time Rapier-wasm-init +
459
+ * component-attach cost once per pixi world, at design-time-layer-mount time.
460
+ * This is the explicitly chosen display-only depth for B1.
461
+ *
462
+ * Pixi is NOT upgraded to a live authoring adapter here: it stays
463
+ * Boundary-grade for the WHOLE S-B effort (display layer + B4's stage
464
+ * hit-test only, no hierarchy/stories) — an explicit, out-of-scope-by-design
465
+ * non-goal, not an oversight.
466
+ *
467
+ * B1 supports `scene`-declared pixi worlds only (the `.scn2d` display-only
468
+ * case named in the spec); an `entry`-based (imperative-setup) pixi world has
469
+ * no scene file to fetch and fails loudly here rather than being silently
470
+ * skipped — degrading to the same #18 error node as any other mount failure.
471
+ */
472
+ async function mountPixiLayer(
473
+ candidate: LayerCandidate,
474
+ layer: HTMLElement,
475
+ container: HTMLElement,
476
+ ): Promise<LayerMountResult> {
477
+ if (!candidate.path) {
478
+ throw new Error(
479
+ `design-time pixi layer "${candidate.worldId}": no \`scene\` declared — nothing to mount ` +
480
+ '(B1 display-only mount supports scene-based pixi worlds only, not `entry`).',
481
+ );
482
+ }
483
+ const sceneData = await fetchScene2DFile(candidate.path);
484
+ const canvas = document.createElement('canvas');
485
+ canvas.style.width = '100%';
486
+ canvas.style.height = '100%';
487
+ canvas.style.display = 'block';
488
+ layer.appendChild(canvas);
489
+
490
+ const { width, height } = measure(container);
491
+ const adapter = new PixiSceneGameAdapter({ id: candidate.worldId, sceneData });
492
+ const mounted = await adapter.mount({ canvas, width, height, transparent: true });
493
+ mounted.update?.(0); // one-shot render — see doc comment above
494
+
495
+ // D12 (B4) — stage hit-test: client (browser) px -> the stage's own
496
+ // coordinate space (pixi's logical/CSS-pixel `renderer.screen`, NOT the
497
+ // resolution-scaled device-pixel framebuffer size) via the canvas's OWN
498
+ // rect, so a post-mount CSS resize (this layer's canvas is styled
499
+ // 100%/100%, `measure()` only runs once at mount time) is scaled correctly
500
+ // rather than assuming a 1:1 client-px-to-stage-unit mapping forever. A
501
+ // world with no live rect (e.g. torn down between the event and this call)
502
+ // degrades to `null` — never a stale/garbage hit.
503
+ const pick: PickProvider['pick'] = (clientX, clientY) => {
504
+ const rect = canvas.getBoundingClientRect();
505
+ if (rect.width === 0 || rect.height === 0) return null;
506
+ const screen = mounted.ctx.app.renderer.screen;
507
+ const localX = ((clientX - rect.left) / rect.width) * screen.width;
508
+ const localY = ((clientY - rect.top) / rect.height) * screen.height;
509
+ for (const child of mounted.stage.children) {
510
+ if (child.getBounds().containsPoint(localX, localY)) return candidate.worldId;
511
+ }
512
+ return null;
513
+ };
514
+
515
+ return { dispose: () => mounted.dispose(), pick };
516
+ }
517
+
518
+ /**
519
+ * Mount a design-time layer for every non-focused react/pixi world declared
520
+ * by `composite` (already installed via `installEditModeAuthoringForProject`)
521
+ * into `container` (`ViewportPanel`'s own containerRef div — the SAME element
522
+ * the editor's threejs `<canvas>` lives in). Returns a disposer; call it from
523
+ * the SAME effect cleanup that tears down the composite
524
+ * (`exitEditModeAuthoring`) — see `ViewportPanel.tsx`'s wiring.
525
+ *
526
+ * A no-op (returns a no-op disposer) when there is nothing to layer: a
527
+ * single-world project (back-compat floor, matches `installEditModeAuthoring`'s
528
+ * own single-world floor) or no resolvable current project.
529
+ *
530
+ * Play-mode handoff: the editor's Scene and Game tabs are BOTH always
531
+ * mounted (only one is `display`-visible at a time — the Game tab hosts
532
+ * `play-mode.ts`'s OWN, entirely separate composite/mount over a REAL,
533
+ * ticking `Game`). The instant play starts, this module tears its
534
+ * design-time layers down for the rest of THIS edit session: two
535
+ * independent DOM trees for the SAME react world (this inert stand-in,
536
+ * play-mode's live one) would otherwise carry the SAME `data-testid`s
537
+ * (whatever the entry's own JSX declares, e.g. tri-world's `hud-count`)
538
+ * simultaneously in one document — not just visually confusing, but a
539
+ * genuine test/automation hazard (two elements answering to one testid).
540
+ * Design-time layers do not remount when play stops again — reload picks
541
+ * them back up, exactly like any other edit-mode-only state (undo history,
542
+ * panel layout) that a play session doesn't attempt to preserve. This never
543
+ * touches the FOCUSED threejs world or the rest of the editor, which stay
544
+ * fully functional regardless of play state.
545
+ */
546
+ export function mountDesignTimeLayers(
547
+ container: HTMLElement,
548
+ composite: CompositeAuthoringAdapter,
549
+ store: EditorStore,
550
+ ): () => void {
551
+ // D4 (spec27 §8 "space-pan" row) — a fresh layer stack starts un-panned:
552
+ // every call here is a "world switch" (initial edit-mode install, or an
553
+ // `'editor:edit-mode-rebuild'` re-install — see `ViewportPanel.tsx`'s
554
+ // `installAll`), so a stale pan offset from a PREVIOUS layer stack must
555
+ // never carry over onto a different one.
556
+ resetWorldPan();
557
+ const projectRoot = getCurrentProject()?.rootPath;
558
+ const candidates = readCandidates(composite);
559
+ // C3 — the scene-UI child (at most one, the focused world's): already a
560
+ // LIVE `UIAuthoringAdapter` at composite-install time (`edit-mode-
561
+ // authoring.ts` — no Boundary-to-live upgrade dance needed here, unlike
562
+ // react/pixi), so it's a mount candidate in its own right rather than one
563
+ // of `readCandidates`'s Boundary-only entries.
564
+ const uiChildEntry = composite.childAdapters().find((c) => c.kind === 'scene-ui');
565
+ const uiAdapter =
566
+ uiChildEntry && uiChildEntry.adapter instanceof UIAuthoringAdapter
567
+ ? uiChildEntry.adapter
568
+ : null;
569
+ if ((candidates.length === 0 && !uiAdapter) || !projectRoot) {
570
+ return () => {};
571
+ }
572
+
573
+ // Bottom-to-top paint order — mirrors `create-runtime.ts`'s own stacking
574
+ // pass, over exactly the worlds this module lays out (react/pixi
575
+ // candidates only; the focused threejs world keeps its untouched canvas
576
+ // slot below all of these — see the module doc comment).
577
+ const sorted = stackOrder(candidates.map((c) => ({ id: c.worldId, zOrder: c.zOrder })));
578
+ const orderedCandidates = sorted.map((s) => candidates.find((c) => c.worldId === s.id)!);
579
+
580
+ const candidateByWorld = new Map(orderedCandidates.map((c) => [c.worldId, c]));
581
+ const layers = new Map<string, HTMLElement>();
582
+ const disposers: Array<() => void> = [];
583
+ // Worlds whose Boundary child we upgraded to a LIVE design-time adapter
584
+ // (successful react mounts). Tracked so the one-way play handoff can put an
585
+ // HONEST disclosure node back in the composite instead of leaving a live
586
+ // adapter over torn-down DOM (which renders silently empty — a #18 violation).
587
+ const upgraded = new Set<string>();
588
+ let torndown = false; // the whole-module disposer (ViewportPanel unmount) ran
589
+ let playTeardownDone = false; // the one-way play-mode handoff (see doc comment) ran
590
+
591
+ // C3 — the scene-UI layer's own disposer, tracked separately from
592
+ // `disposers`/`layers` (react/pixi's Boundary-upgrade bookkeeping doesn't
593
+ // apply to it — the UI child's adapter stays live throughout; only its DOM
594
+ // layer needs tearing down, here and on the play handoff below).
595
+ let disposeUILayer: (() => void) | undefined;
596
+ if (uiAdapter && uiChildEntry) {
597
+ disposeUILayer = mountSceneUILayer(
598
+ container,
599
+ uiChildEntry.worldId,
600
+ uiAdapter,
601
+ store,
602
+ BASE_Z_INDEX + orderedCandidates.length,
603
+ );
604
+ }
605
+
606
+ function disposeEverything(): void {
607
+ for (const dispose of disposers.splice(0)) {
608
+ try {
609
+ dispose();
610
+ } catch (err) {
611
+ editorConsole.error(`[design-time-layers] dispose failed: ${err}`, 'authoring');
612
+ }
613
+ }
614
+ for (const layer of layers.values()) layer.remove();
615
+ layers.clear();
616
+ }
617
+
618
+ /**
619
+ * One-way play handoff: dispose the design-time layers AND revert every
620
+ * upgraded world back to a `BoundaryAuthoringAdapter` disclosure node
621
+ * (#18 — never silently empty). Not called on the whole-module disposer
622
+ * (`torndown`, ViewportPanel unmount), where the composite is going away
623
+ * anyway. Design-time layers do not remount when play stops again — reload
624
+ * restores them (see the module doc comment). The scene-UI layer's DOM is
625
+ * torn down the same way (play mode renders scene-UI through the real
626
+ * engine's HUD mount instead — two live DOM trees for the same testids
627
+ * would be the same hazard `design-time-layers.ts`'s own doc comment
628
+ * already calls out for react worlds); its adapter is left alone (still
629
+ * live, just without a mounted layer) since there is no Boundary form of it.
630
+ */
631
+ function suspendForPlay(): void {
632
+ disposeEverything();
633
+ disposeUILayer?.();
634
+ disposeUILayer = undefined;
635
+ for (const worldId of upgraded) {
636
+ const c = candidateByWorld.get(worldId);
637
+ if (!c) continue;
638
+ const info: BoundaryWorldInfo = {
639
+ id: c.worldId,
640
+ kind: c.kind,
641
+ entryOrScenePath: c.path,
642
+ zOrder: c.zOrder,
643
+ pausable: c.pausable,
644
+ };
645
+ composite.replaceChild(
646
+ worldId,
647
+ new BoundaryAuthoringAdapter(
648
+ store,
649
+ info,
650
+ 'Design-time layer suspended by play mode — reload the editor to restore it.',
651
+ ),
652
+ );
653
+ }
654
+ upgraded.clear();
655
+ store.notifyIngestEdit();
656
+ }
657
+
658
+ orderedCandidates.forEach((candidate, i) => {
659
+ const layer = createLayerElement(container, candidate.worldId, BASE_Z_INDEX + i);
660
+ layers.set(candidate.worldId, layer);
661
+
662
+ const mountPromise: Promise<LayerMountResult> =
663
+ candidate.kind === 'react'
664
+ ? mountReactLayer(candidate, layer, projectRoot, store)
665
+ : mountPixiLayer(candidate, layer, container);
666
+
667
+ mountPromise
668
+ .then((result) => {
669
+ if (torndown || playTeardownDone) {
670
+ result.dispose();
671
+ return;
672
+ }
673
+ if (result.adapter) {
674
+ composite.replaceChild(candidate.worldId, result.adapter);
675
+ registerEditModeChild(result.adapter);
676
+ upgraded.add(candidate.worldId);
677
+ } else if (result.pick) {
678
+ // D12 (B4) — a pixi layer produces a `pick`, not a full adapter
679
+ // (pixi stays Boundary-grade for the whole S-B effort). Re-wrap the
680
+ // existing Boundary with the pick attached, and track it in
681
+ // `upgraded` too: `suspendForPlay` must revert THIS one back to a
682
+ // plain pick-less Boundary same as a react upgrade — a pick closing
683
+ // over a disposed pixi stage must not survive into play mode.
684
+ const info: BoundaryWorldInfo = {
685
+ id: candidate.worldId,
686
+ kind: candidate.kind,
687
+ entryOrScenePath: candidate.path,
688
+ zOrder: candidate.zOrder,
689
+ pausable: candidate.pausable,
690
+ };
691
+ composite.replaceChild(
692
+ candidate.worldId,
693
+ new BoundaryAuthoringAdapter(store, info, undefined, result.pick),
694
+ );
695
+ upgraded.add(candidate.worldId);
696
+ }
697
+ disposers.push(result.dispose);
698
+ store.notifyIngestEdit();
699
+ })
700
+ .catch((err: unknown) => {
701
+ layer.remove();
702
+ layers.delete(candidate.worldId);
703
+ if (torndown || playTeardownDone) return;
704
+ const message = formatMountFailureMessage(err);
705
+ editorConsole.error(
706
+ `[design-time-layers] world "${candidate.worldId}" (${candidate.kind}) failed to ` +
707
+ `mount: ${message}`,
708
+ 'authoring',
709
+ );
710
+ const info: BoundaryWorldInfo = {
711
+ id: candidate.worldId,
712
+ kind: candidate.kind,
713
+ entryOrScenePath: candidate.path,
714
+ zOrder: candidate.zOrder,
715
+ pausable: candidate.pausable,
716
+ };
717
+ composite.replaceChild(
718
+ candidate.worldId,
719
+ new BoundaryAuthoringAdapter(store, info, message),
720
+ );
721
+ addMountFailureReport({
722
+ worldId: candidate.worldId,
723
+ kind: candidate.kind,
724
+ identity: candidate.kind === 'react' ? 'default-react' : 'default-pixi',
725
+ message,
726
+ });
727
+ store.notifyIngestEdit();
728
+ });
729
+ });
730
+
731
+ // D4 (spec27 §8 "space-pan" row) — the lockstep half of the pan feature:
732
+ // re-apply the current shared transform to every currently-mounted layer
733
+ // the instant it changes (`WorldSelectionOverlay`'s own outer wrapper binds
734
+ // the SAME `subscribeWorldPan`/`panTransformValue` pair independently — see
735
+ // `world-pan-state.ts`'s doc comment for why applying the identical value
736
+ // at both mount points is what keeps `rect(id)` pan-invariant). A cheap
737
+ // style write, never a remount.
738
+ const unsubPan = subscribeWorldPan(() => {
739
+ for (const layer of layers.values()) applyPanTransform(layer);
740
+ });
741
+
742
+ const unsubStore = store.subscribe(() => {
743
+ if (!playTeardownDone && store.playState !== 'stopped') {
744
+ playTeardownDone = true;
745
+ suspendForPlay();
746
+ return;
747
+ }
748
+ // Session toggles (eye/interactive) change via `store.notifyIngestEdit()`
749
+ // (`GameHierarchy.tsx`'s row affordances) — this is a cheap style
750
+ // re-apply, never a remount.
751
+ if (!playTeardownDone) {
752
+ for (const [worldId, layer] of layers) applySessionStyle(layer, worldId);
753
+ // B3 — "select elsewhere restores the entry": any-panel selection
754
+ // change (hierarchy row click, viewport pick, …) that no longer
755
+ // includes the isolated node exits isolation. Every store notification
756
+ // runs this (cheap — one hierarchy.node lookup per upgraded react
757
+ // world), covering every selection surface uniformly rather than
758
+ // wiring each one individually.
759
+ for (const { worldId, adapter } of composite.childAdapters()) {
760
+ if (!upgraded.has(worldId) || !(adapter instanceof ReactWorldAuthoringAdapter)) continue;
761
+ const isolatedId = adapter.isolatedNodeId;
762
+ if (isolatedId && !store.selectedEntityIds.has(isolatedId)) {
763
+ adapter.stories.isolate?.(null);
764
+ }
765
+ }
766
+ }
767
+ });
768
+
769
+ return () => {
770
+ torndown = true;
771
+ unsubStore();
772
+ unsubPan();
773
+ disposeEverything();
774
+ disposeUILayer?.();
775
+ };
776
+ }