@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,891 @@
1
+ /**
2
+ * CompositeAuthoringAdapter — merges N per-world {@link AuthoringAdapter}s into ONE
3
+ * authoring tree so the editor's hierarchy lists EVERY mounted world's entities and
4
+ * selection/inspection routes to the right backend. This is the
5
+ * editor-multi-root-selection seam, generalized to N-ary (T6.1 slice 3,
6
+ * docs/MULTI-WORLD-DESIGN.md §1.F — GAME-ROOT-DESIGN §6.4's v1 scope, verbatim):
7
+ * one top-level GROUP NODE per world (`world:<id> (<kind>)`), the child adapter's
8
+ * own roots nested beneath it. Routing is by GROUP-NODE OWNERSHIP (which child
9
+ * adapter's tree actually contains a given node id) — not the old 2-child `w2d:`
10
+ * id-prefix sniff, which cannot generalize past exactly one 2D surface.
11
+ *
12
+ * The editor still talks to a single {@link AuthoringAdapter}; selection +
13
+ * inspection work in every world; the 3D transform gizmo stays threejs-only (a
14
+ * viewport concern, outside this adapter); play/pause stays game-level (T7.6).
15
+ */
16
+
17
+ import type {
18
+ AssetDropProvider,
19
+ AuthoringAdapter,
20
+ AuthoringCapabilities,
21
+ ComponentsProvider,
22
+ EditorNode,
23
+ FileMapProvider,
24
+ HierarchyProvider,
25
+ InspectorProvider,
26
+ PersistenceProvider,
27
+ PropertyDescriptor,
28
+ RootGroup,
29
+ RootGroupsProvider,
30
+ SelectionProvider,
31
+ StoriesProvider,
32
+ StoryRef,
33
+ StructureProvider,
34
+ Transform,
35
+ TransformOwner,
36
+ TransformProvider,
37
+ } from '@engine/adapter';
38
+ import type * as THREE from 'three';
39
+ import { editorConsole } from '../editor-console';
40
+
41
+ /** One child world's authoring adapter, ordered as given to the constructor. */
42
+ export interface CompositeChild {
43
+ /** Manifest world id (T3.1) — becomes the group node id `world:<worldId>`. */
44
+ readonly worldId: string;
45
+ /** World render substrate kind — shown in the group node's label. */
46
+ readonly kind: string;
47
+ readonly adapter: AuthoringAdapter;
48
+ /**
49
+ * D12 (B4) — the world's INSTALL-TIME manifest zOrder (`world.zOrder ?? 0`),
50
+ * independent of this child's position in the constructor's array. Absent
51
+ * (every pre-B4 call site) ⇒ `childAdapters()` falls back to the array
52
+ * index, today's behavior unchanged. See `childAdapters()`'s own doc
53
+ * comment for why this stopped being "array order doubles as z-order" and
54
+ * what "install-time" means here.
55
+ */
56
+ readonly zOrder?: number;
57
+ /**
58
+ * C3 (docs/unified-world-editor/26-unified-editor-spec.md §4 C3) — nest
59
+ * this child's group node UNDER another child's group node instead of at
60
+ * the top level (directly under the synthetic `game` root, or as a bare
61
+ * top-level root when there is no `gameRoot`). Used by the scene-UI child
62
+ * (`edit-mode-authoring.ts`'s UI child, `kind: 'scene-ui'`) to appear
63
+ * "under its owning world" in the ONE hierarchy, per the C3 spec text.
64
+ * Absent ⇒ today's flat top-level shape, unchanged.
65
+ */
66
+ readonly parentWorldId?: string;
67
+ }
68
+
69
+ const GROUP_PREFIX = 'world:';
70
+ const GAME_ROOT_ID = 'game';
71
+
72
+ function groupNodeId(worldId: string): string {
73
+ return `${GROUP_PREFIX}${worldId}`;
74
+ }
75
+
76
+ /** True for a synthetic group-node id this Composite itself manufactures (never
77
+ * produced by a child adapter — the `world:` prefix is reserved for this use). */
78
+ function isGroupNodeId(id: string): boolean {
79
+ return id.startsWith(GROUP_PREFIX);
80
+ }
81
+
82
+ /**
83
+ * A4 (D8) — the game root's manifest-backed property surface, implemented by
84
+ * `edit-mode-authoring.ts`'s `ManifestAuthoring` over the RAW `vgai.game.json`
85
+ * (read-modify-write, never round-tripped through Zod). Defined here (not
86
+ * imported from `edit-mode-authoring.ts`) to avoid a circular import — that
87
+ * module already imports `CompositeAuthoringAdapter`.
88
+ */
89
+ export interface GameRootManifestProvider {
90
+ /** True while no `vgai.game.json` exists on disk yet (a synthesized
91
+ * in-memory manifest) — edits buffer in memory instead of writing. */
92
+ readonly synthesized: boolean;
93
+ getGameName(): string;
94
+ setGameName(name: string): void;
95
+ getWorldZOrder(worldId: string): number;
96
+ setWorldZOrder(worldId: string, value: number): void;
97
+ getWorldPausable(worldId: string): boolean;
98
+ setWorldPausable(worldId: string, value: boolean): void;
99
+ }
100
+
101
+ /** OR every child's capabilities together — a feature is offered if ANY
102
+ * child supports it (the editor gates per-node via the routed adapter
103
+ * anyway). Factored out of the constructor (B1) so {@link
104
+ * CompositeAuthoringAdapter.replaceChild} can recompute it after an
105
+ * in-place child swap without duplicating the reduce. */
106
+ function computeCapabilities(children: ReadonlyArray<CompositeChild>): AuthoringCapabilities {
107
+ return children.reduce<AuthoringCapabilities>(
108
+ (acc, c) => ({
109
+ transform: acc.transform || c.adapter.capabilities.transform,
110
+ material: acc.material || c.adapter.capabilities.material,
111
+ inspectorFields: acc.inspectorFields || c.adapter.capabilities.inspectorFields,
112
+ create: acc.create || c.adapter.capabilities.create,
113
+ delete: acc.delete || c.adapter.capabilities.delete,
114
+ reparent: acc.reparent || c.adapter.capabilities.reparent,
115
+ persist: acc.persist || c.adapter.capabilities.persist,
116
+ }),
117
+ {
118
+ transform: false,
119
+ material: false,
120
+ inspectorFields: false,
121
+ create: false,
122
+ delete: false,
123
+ reparent: false,
124
+ persist: false,
125
+ },
126
+ );
127
+ }
128
+
129
+ export class CompositeAuthoringAdapter implements AuthoringAdapter {
130
+ /** NOT `readonly` (interface-level `readonly` is a consumer contract, not a
131
+ * ban on internal reassignment) — B1's {@link replaceChild} recomputes
132
+ * this after an in-place child swap (e.g. a world's design-time layer
133
+ * mounting successfully upgrades its Boundary adapter to a live one). */
134
+ capabilities: AuthoringCapabilities;
135
+ /**
136
+ * NOT `ReadonlyArray` (B1): {@link replaceChild} mutates ONE element of
137
+ * this array in place, preserving both the array's own identity-adjacent
138
+ * invariants (length, index/z-order) and — critically — THIS composite's
139
+ * OWN object identity, which `ViewportPanel.tsx`'s teardown-identity
140
+ * capture, `exitEditModeAuthoring`'s installed-instance check, and the
141
+ * `markEditModeOverride` brand all key off. A REBUILT composite (a new
142
+ * `CompositeAuthoringAdapter` instance) would silently break all three —
143
+ * see this class's own doc comment and `edit-mode-authoring.ts`'s. Copied
144
+ * (`[...children]`) out of the constructor's `ReadonlyArray` parameter so
145
+ * mutating it here never aliases a caller's own array.
146
+ */
147
+ private readonly children: CompositeChild[];
148
+ /** A4 (D8) — present only when the caller wants a synthetic game-root node
149
+ * wrapping every world group (edit-mode's manifest-disclosing composite);
150
+ * absent for every other composite construction (play-mode's
151
+ * `installMultiWorldAuthoring`), which keeps `hierarchy.roots()` returning
152
+ * world group nodes directly, UNCHANGED from before A4. */
153
+ private readonly gameRoot: GameRootManifestProvider | null;
154
+
155
+ /**
156
+ * N-ary (T6.1 slice 3): an ordered `{ worldId, kind, adapter }[]` — one group
157
+ * node per entry, in array order.
158
+ *
159
+ * The 2-positional-arg legacy overload (`new CompositeAuthoringAdapter(world3d,
160
+ * world2d)`, normalizing to synthetic `'world3d'`/`'world2d'` ids) is REMOVED
161
+ * this slice (T6.1 slice 4, docs/MULTI-WORLD-DESIGN.md §1.H): its only
162
+ * production call site was `world2d-mode.ts`'s dev-console-only hook, deleted
163
+ * with the rest of the world2d silo. Every production caller now goes through
164
+ * the N-ary array constructor (`play-mode.ts`'s `installMultiWorldAuthoring`).
165
+ *
166
+ * `gameRoot` (A4, D8) is an opt-in second argument: `edit-mode-authoring.ts`
167
+ * passes a `ManifestAuthoring` instance to wrap every world group under one
168
+ * synthetic `game` root; every other caller omits it and gets today's
169
+ * shape (world groups as roots) byte-for-byte unchanged.
170
+ */
171
+ constructor(children: ReadonlyArray<CompositeChild>, gameRoot?: GameRootManifestProvider) {
172
+ this.children = [...children];
173
+ this.gameRoot = gameRoot ?? null;
174
+ if (children.length === 0) {
175
+ throw new Error('CompositeAuthoringAdapter: at least one child world is required.');
176
+ }
177
+ this.capabilities = computeCapabilities(this.children);
178
+ }
179
+
180
+ /**
181
+ * B1 — swap `worldId`'s child adapter IN PLACE (the design-time layer
182
+ * mount's "adapter upgrade": a world's read-only `BoundaryAuthoringAdapter`
183
+ * is replaced by a live one — react: `ReactWorldAuthoringAdapter` — once its
184
+ * layer mounts successfully; a layer that fails/throws rebuilds the
185
+ * Boundary with the caught reason, also through this method). Mutates the
186
+ * existing children array (preserving array order — z-order/`childAdapters()`
187
+ * index is unaffected) and recomputes `capabilities`, rather than
188
+ * constructing a new `CompositeAuthoringAdapter` — a rebuild would break
189
+ * `ViewportPanel.tsx`'s captured teardown identity, `exitEditModeAuthoring`'s
190
+ * installed-instance check, and the `markEditModeOverride` brand, all of
191
+ * which key off THIS instance's identity (see the class doc comment).
192
+ *
193
+ * No-op (loud warn, #18 discipline) when `worldId` doesn't name an existing
194
+ * child — never silently routes to the wrong world or grows the array.
195
+ * Callers are responsible for their own side effects around the swap
196
+ * (undo-timeline registration for a newly-live adapter,
197
+ * `store.notifyIngestEdit()` so panels re-render against the new child) —
198
+ * this method only owns the swap + capability recompute.
199
+ */
200
+ replaceChild(worldId: string, adapter: AuthoringAdapter): void {
201
+ const index = this.children.findIndex((c) => c.worldId === worldId);
202
+ if (index === -1) {
203
+ editorConsole.warn(
204
+ `[CompositeAuthoringAdapter] replaceChild: no child with worldId "${worldId}" — ignoring.`,
205
+ 'authoring',
206
+ );
207
+ return;
208
+ }
209
+ // D12 (B4) — carry the outgoing child's `zOrder` forward too (not just
210
+ // `kind`): a swap must not silently reset a world's install-time zOrder
211
+ // back to "no override" (array-index fallback). Built conditionally (not
212
+ // `{ ..., zOrder }`) because `zOrder` is an OPTIONAL property under this
213
+ // repo's `exactOptionalPropertyTypes` — an explicit `zOrder: undefined`
214
+ // is not the same as an absent property to the type checker.
215
+ const { kind, zOrder } = this.children[index]!;
216
+ this.children[index] =
217
+ zOrder === undefined ? { worldId, kind, adapter } : { worldId, kind, adapter, zOrder };
218
+ this.capabilities = computeCapabilities(this.children);
219
+ }
220
+
221
+ /** The group node id for a given child (exposed for callers building
222
+ * post-construction selection/expansion state — e.g. tests, e2e hooks). */
223
+ groupNodeId(worldId: string): string {
224
+ return groupNodeId(worldId);
225
+ }
226
+
227
+ /** The child that owns `id` — by ACTUAL ownership (its hierarchy resolves the
228
+ * id), not by any prefix convention. `null` for a group-node id or an id no
229
+ * child recognizes. O(children) per call; fine at editor-UI scale, and avoids
230
+ * a cache that could go stale as a live tree adds/removes nodes. */
231
+ private findOwnerChild(id: string): CompositeChild | null {
232
+ if (isGroupNodeId(id)) return null;
233
+ for (const child of this.children) {
234
+ if (child.adapter.hierarchy.node(id)) return child;
235
+ }
236
+ return null;
237
+ }
238
+
239
+ /** Route by ownership, falling back to the FIRST child for an unrecognized id
240
+ * (matches the old 2-child adapter's "everything else is world3d" floor —
241
+ * callers that pass a bogus id get inert/default responses, not a throw). */
242
+ private route(id: string): AuthoringAdapter {
243
+ return this.findOwnerChild(id)?.adapter ?? this.children[0]!.adapter;
244
+ }
245
+
246
+ /** PUBLIC ownership query (T0 — A3's inspector dispatch and B4's pick routing
247
+ * both need it): the `worldId` of the child that owns `id`, or `null` for a
248
+ * group-node id or an id no child recognizes. */
249
+ ownerOf(id: string): string | null {
250
+ return this.findOwnerChild(id)?.worldId ?? null;
251
+ }
252
+
253
+ /**
254
+ * PUBLIC read-only view of the wrapped children (T0), ordered as constructed.
255
+ *
256
+ * `zOrder` is EACH CHILD'S OWN `CompositeChild.zOrder` (the world's
257
+ * install-time manifest zOrder) when supplied, falling back to the array
258
+ * index otherwise. Array order is NOT z-order (D12/B4 correction of this
259
+ * doc comment's former claim): children are built in MANIFEST DECLARATION
260
+ * order (`edit-mode-authoring.ts`), not stacking order — paint/pick order is
261
+ * `stackOrder` over each child's manifest zOrder (mirroring
262
+ * `create-runtime.ts`'s real stacking pass and `design-time-layers.ts`'s
263
+ * layer z-index assignment), which is exactly why this field exists.
264
+ * "Install-time" — the zOrder captured when this composite/child was built,
265
+ * not a live subscription: a world's zOrder edited afterward (the generic
266
+ * inspector's `zOrder` property, `A4`/D8) does NOT retroactively reorder an
267
+ * already-mounted design-time layer stack or an already-computed pick walk
268
+ * order this call returns; both settle to the new order the next time the
269
+ * composite/layers are rebuilt. Callers that need the LIVE manifest value
270
+ * (e.g. `GameHierarchy.tsx`'s zOrder badge) read it from the manifest
271
+ * surface directly instead of trusting this field alone — see
272
+ * `compositeGroupBadge`'s own comment.
273
+ */
274
+ childAdapters(): ReadonlyArray<{
275
+ worldId: string;
276
+ adapter: AuthoringAdapter;
277
+ kind: string;
278
+ zOrder: number;
279
+ }> {
280
+ return this.children.map((c, index) => ({
281
+ worldId: c.worldId,
282
+ adapter: c.adapter,
283
+ kind: c.kind,
284
+ zOrder: c.zOrder ?? index,
285
+ }));
286
+ }
287
+
288
+ /** PUBLIC (A4, D8) — the synthetic game-root node's id when this composite
289
+ * was built with a `gameRoot` manifest surface, else `null`. Mirrors
290
+ * `groupNodeId()`'s "ask the adapter, don't hardcode the id" precedent —
291
+ * the shell (`GameHierarchy.tsx`) reads this instead of the literal
292
+ * string, so it never needs to know the reserved id itself. */
293
+ get gameRootId(): string | null {
294
+ return this.gameRoot ? GAME_ROOT_ID : null;
295
+ }
296
+
297
+ /**
298
+ * C3 — a child declared with `parentWorldId` nests its group node UNDER
299
+ * that parent's group node instead of at the top level: the parent's
300
+ * `parentId` resolution below picks up the nested group id automatically
301
+ * (`this.children.find(...).parentWorldId`), and this method also appends
302
+ * every child NESTED under `worldId` (in declaration order) to the END of
303
+ * `childIds`, after `worldId`'s own adapter roots — so e.g. a `scene-ui`
304
+ * child appears as a trailing "ui (scene-ui)" row under its owning world.
305
+ */
306
+ private groupNode(worldId: string, kind: string, childRoots: EditorNode[]): EditorNode {
307
+ const self = this.children.find((c) => c.worldId === worldId);
308
+ const parentId = self?.parentWorldId
309
+ ? groupNodeId(self.parentWorldId)
310
+ : this.gameRoot
311
+ ? GAME_ROOT_ID
312
+ : null;
313
+ const nestedGroupIds = this.children
314
+ .filter((c) => c.parentWorldId === worldId)
315
+ .map((c) => groupNodeId(c.worldId));
316
+ return {
317
+ id: groupNodeId(worldId),
318
+ label: `world:${worldId} (${kind})`,
319
+ kind: 'group',
320
+ parentId,
321
+ childIds: [...childRoots.map((r) => r.id), ...nestedGroupIds],
322
+ flags: { runtimeOnly: true },
323
+ };
324
+ }
325
+
326
+ /** C3 — every child NOT nested under another (`parentWorldId` unset): the
327
+ * top-level group set, either wrapped under the synthetic `game` root or
328
+ * returned directly (see callers). A nested child is reached only via its
329
+ * parent's `childIds` (`groupNode` above), never listed here too — a node
330
+ * in two parents' `childIds` would render twice / cycle the row walk. */
331
+ private topLevelChildren(): CompositeChild[] {
332
+ return this.children.filter((c) => !c.parentWorldId);
333
+ }
334
+
335
+ /** A4 (D8) — the synthetic `game` root wrapping every TOP-LEVEL world group
336
+ * node (C3: a nested child, e.g. a scene-UI world, is reached through its
337
+ * parent's own `childIds` instead — see `topLevelChildren`). Only ever
338
+ * constructed when `this.gameRoot` is set. */
339
+ private gameRootNode(): EditorNode {
340
+ return {
341
+ id: GAME_ROOT_ID,
342
+ label: this.gameRoot?.getGameName() || 'Game',
343
+ kind: 'group',
344
+ parentId: null,
345
+ childIds: this.topLevelChildren().map((c) => groupNodeId(c.worldId)),
346
+ flags: { runtimeOnly: true },
347
+ };
348
+ }
349
+
350
+ readonly hierarchy: HierarchyProvider = {
351
+ roots: (): EditorNode[] => {
352
+ // A4 (D8) — wrap under a synthetic `game` root ONLY when a manifest
353
+ // surface was supplied at construction (edit-mode's composite); every
354
+ // other composite (play-mode) returns world groups as roots directly,
355
+ // exactly as before A4. Either way, only TOP-LEVEL groups are roots
356
+ // (C3) — a nested child (e.g. scene-UI) is reached via its parent's
357
+ // own `childIds`, never listed as a root of its own.
358
+ if (this.gameRoot) return [this.gameRootNode()];
359
+ return this.topLevelChildren().map((c) =>
360
+ this.groupNode(c.worldId, c.kind, c.adapter.hierarchy.roots()),
361
+ );
362
+ },
363
+ node: (id) => {
364
+ if (id === GAME_ROOT_ID) {
365
+ return this.gameRoot ? this.gameRootNode() : null;
366
+ }
367
+ if (isGroupNodeId(id)) {
368
+ const worldId = id.slice(GROUP_PREFIX.length);
369
+ const child = this.children.find((c) => c.worldId === worldId);
370
+ if (!child) return null;
371
+ return this.groupNode(child.worldId, child.kind, child.adapter.hierarchy.roots());
372
+ }
373
+ const owner = this.findOwnerChild(id);
374
+ if (!owner) return null;
375
+ const node = owner.adapter.hierarchy.node(id);
376
+ if (!node) return null;
377
+ // A child's OWN root has `parentId: null` in its adapter's tree — rewrite
378
+ // it to point at this world's group node so the merged tree is one
379
+ // connected forest instead of the child roots looking parentless again.
380
+ return node.parentId === null ? { ...node, parentId: groupNodeId(owner.worldId) } : node;
381
+ },
382
+ object3D: (id) =>
383
+ id === GAME_ROOT_ID || isGroupNodeId(id) ? null : this.route(id).hierarchy.object3D(id),
384
+ idForObject3D: (o: THREE.Object3D) => {
385
+ for (const child of this.children) {
386
+ const id = child.adapter.hierarchy.idForObject3D(o);
387
+ if (id) return id;
388
+ }
389
+ return null;
390
+ },
391
+ };
392
+
393
+ readonly selection: SelectionProvider = {
394
+ // Union of every child's own selection.
395
+ get: () => {
396
+ const seen = new Set<string>();
397
+ for (const child of this.children) {
398
+ for (const id of child.adapter.selection?.get() ?? []) seen.add(id);
399
+ }
400
+ return [...seen];
401
+ },
402
+ set: (ids) => {
403
+ // A group-node id (or the game root) is not a real entity in any
404
+ // child's tree — route it specially rather than forwarding a bogus id
405
+ // nobody owns. Route each REAL id to its owning child.
406
+ //
407
+ // Every existing adapter (first-party/ingest/world2d) delegates
408
+ // selection to the SAME editor-global `EditorStore` — `selectMultiple`
409
+ // REPLACES the store's whole selected-id set, it does not merge. So
410
+ // calling `.set([])` on every non-owning child (the naive "clear the
411
+ // rest" approach) would call `selectMultiple` a second time on the
412
+ // SAME shared store and immediately clear out whatever the owning
413
+ // child's `.set()` call just wrote — a single click could never
414
+ // actually select anything. Only forward to children that own at
415
+ // least one of `ids`.
416
+ //
417
+ // A4 (D8): when NONE do (an empty selection, OR a selection that is
418
+ // PURELY a synthetic id), forward to exactly ONE child (any child
419
+ // sharing the backing store reaches every other child too) — but
420
+ // forward the SYNTHETIC ID ITSELF (not an empty clear) when one was
421
+ // given. This is why `RightPanel.tsx`'s `[...store.selectedEntityIds][0]`
422
+ // (and `Inspector.tsx`'s identical read) can show the game root's/
423
+ // a world group's own properties: every stock adapter shares the SAME
424
+ // `EditorStore`, so writing `'game'`/`'world:<id>'` into any one
425
+ // child's selection lands in that one shared `selectedEntityIds` set,
426
+ // which both those raw-store reads AND this composite's OWN `get()`
427
+ // (via the owning-adapter's `selection.get()`) then see identically —
428
+ // no separate synthetic-id bookkeeping needed on this class at all.
429
+ const byChild = new Map<AuthoringAdapter, string[]>();
430
+ let synthetic: string | null = null;
431
+ for (const id of ids) {
432
+ if (isGroupNodeId(id) || id === this.gameRootId) {
433
+ synthetic = id; // last one wins — only used when NOTHING else claims byChild
434
+ continue;
435
+ }
436
+ const owner = this.findOwnerChild(id);
437
+ if (!owner) continue;
438
+ const arr = byChild.get(owner.adapter);
439
+ if (arr) arr.push(id);
440
+ else byChild.set(owner.adapter, [id]);
441
+ }
442
+ if (byChild.size === 0) {
443
+ this.children[0]!.adapter.selection?.set(synthetic ? [synthetic] : []);
444
+ return;
445
+ }
446
+ for (const [adapter, childIds] of byChild) adapter.selection?.set(childIds);
447
+ },
448
+ };
449
+
450
+ readonly transforms: TransformProvider = {
451
+ get: (id): Transform =>
452
+ this.route(id).transforms?.get(id) ?? {
453
+ position: [0, 0, 0],
454
+ rotation: [0, 0, 0, 1],
455
+ scale: [1, 1, 1],
456
+ },
457
+ owner: (id): TransformOwner => this.route(id).transforms?.owner(id) ?? 'editor',
458
+ beginEdit: (id) => this.route(id).transforms?.beginEdit(id),
459
+ apply: (id, t) => this.route(id).transforms?.apply(id, t),
460
+ endEdit: (id) => this.route(id).transforms?.endEdit(id),
461
+ };
462
+
463
+ /**
464
+ * A4 (D8) — the game root's `name` and each world group's `zOrder`/
465
+ * `pausable`/`kind` are a MANIFEST concept, not something any child
466
+ * adapter's own inspector knows about — intercepted here, BEFORE `route()`,
467
+ * so a real per-world adapter is never asked about an id it doesn't own.
468
+ * `null` when `id` isn't the game root or a group-node id (or there is no
469
+ * `gameRoot` manifest surface at all), letting the caller fall through to
470
+ * the ordinary routed behavior unchanged.
471
+ */
472
+ private gameRootProperties(id: string): PropertyDescriptor[] | null {
473
+ if (!this.gameRoot) return null;
474
+ if (id === GAME_ROOT_ID) {
475
+ return [{ path: 'name', label: 'Name', type: 'string' }];
476
+ }
477
+ if (isGroupNodeId(id) && this.children.some((c) => groupNodeId(c.worldId) === id)) {
478
+ return [
479
+ { path: 'kind', label: 'Kind', type: 'string', readonly: true },
480
+ { path: 'zOrder', label: 'Z Order', type: 'number' },
481
+ { path: 'pausable', label: 'Pausable', type: 'boolean' },
482
+ ];
483
+ }
484
+ return null;
485
+ }
486
+
487
+ readonly inspector: InspectorProvider = {
488
+ properties: (id): PropertyDescriptor[] =>
489
+ this.gameRootProperties(id) ?? this.route(id).inspector?.properties(id) ?? [],
490
+ get: (id, path) => {
491
+ if (this.gameRoot) {
492
+ if (id === GAME_ROOT_ID) {
493
+ return path === 'name' ? this.gameRoot.getGameName() : undefined;
494
+ }
495
+ const worldId = isGroupNodeId(id) ? id.slice(GROUP_PREFIX.length) : null;
496
+ const child = worldId ? this.children.find((c) => c.worldId === worldId) : undefined;
497
+ if (child) {
498
+ if (path === 'kind') return child.kind;
499
+ if (path === 'zOrder') return this.gameRoot.getWorldZOrder(worldId!);
500
+ if (path === 'pausable') return this.gameRoot.getWorldPausable(worldId!);
501
+ return undefined;
502
+ }
503
+ }
504
+ return this.route(id).inspector?.get(id, path);
505
+ },
506
+ set: (id, path, value) => {
507
+ if (this.gameRoot) {
508
+ if (id === GAME_ROOT_ID) {
509
+ if (path === 'name') this.gameRoot.setGameName(String(value));
510
+ return;
511
+ }
512
+ const worldId = isGroupNodeId(id) ? id.slice(GROUP_PREFIX.length) : null;
513
+ const child = worldId ? this.children.find((c) => c.worldId === worldId) : undefined;
514
+ if (child) {
515
+ if (path === 'zOrder') this.gameRoot.setWorldZOrder(worldId!, Number(value));
516
+ else if (path === 'pausable') this.gameRoot.setWorldPausable(worldId!, Boolean(value));
517
+ return;
518
+ }
519
+ }
520
+ this.route(id).inspector?.set(id, path, value);
521
+ },
522
+ remove: (id, path) => {
523
+ // U2 — game-root/world-group synthetic props have no removable override;
524
+ // everything else routes to the owning child (which optional-omits
525
+ // `remove` unless it's the react-world adapter).
526
+ if (this.gameRoot && (id === GAME_ROOT_ID || isGroupNodeId(id))) return;
527
+ this.route(id).inspector?.remove?.(id, path);
528
+ },
529
+ };
530
+
531
+ /**
532
+ * A2 (map §5) — resolve a `create`/`creatableKinds` target: a group-node id
533
+ * (`world:<w>`) targets that world's OWN root (forwarded parentId
534
+ * `undefined`); a real id targets its owning child (forwarded as-is);
535
+ * `null`/`undefined` (no parent — the toolbar's root-level "Add") targets
536
+ * the FIRST child that actually has a structure provider — in edit mode
537
+ * that is the one focused, live `VgaiSceneAuthoringAdapter`; every other
538
+ * child is a read-only `BoundaryAuthoringAdapter` with no `structure` at
539
+ * all. `null` when nothing can serve the request (never silently routed to
540
+ * the wrong world — #18).
541
+ */
542
+ private resolveStructureTarget(
543
+ parentId: string | null | undefined,
544
+ ): { child: CompositeChild; forwardParentId: string | undefined } | null {
545
+ if (parentId != null && isGroupNodeId(parentId)) {
546
+ const worldId = parentId.slice(GROUP_PREFIX.length);
547
+ const child = this.children.find((c) => c.worldId === worldId);
548
+ return child ? { child, forwardParentId: undefined } : null;
549
+ }
550
+ if (parentId != null) {
551
+ const child = this.findOwnerChild(parentId);
552
+ return child ? { child, forwardParentId: parentId } : null;
553
+ }
554
+ const child = this.children.find((c) => c.adapter.structure);
555
+ return child ? { child, forwardParentId: undefined } : null;
556
+ }
557
+
558
+ /**
559
+ * A2 (map §5) — structural pass-through, routed by ownership. Never a
560
+ * silent no-op when a request can't be served: `create`/`remove`/
561
+ * `duplicate` log loudly via `editorConsole.warn` and return an honest
562
+ * empty/unchanged result (#18) rather than pretending to route to the
563
+ * wrong world. `reorder` is the one exception — it mirrors the base
564
+ * contract's OWN "absent means no reorder UI" convention, so a missing
565
+ * `reorder` on the owning child is a silent no-op, not a warning.
566
+ */
567
+ readonly structure: StructureProvider = {
568
+ create: (kind, parentId) => {
569
+ const target = this.resolveStructureTarget(parentId ?? null);
570
+ if (!target?.child.adapter.structure) {
571
+ editorConsole.warn(
572
+ `[CompositeAuthoringAdapter] create: no owning child with a structure ` +
573
+ `provider for parent "${parentId ?? '(root)'}"`,
574
+ 'authoring',
575
+ );
576
+ return '';
577
+ }
578
+ return target.child.adapter.structure.create(kind, target.forwardParentId);
579
+ },
580
+ remove: (id) => {
581
+ if (isGroupNodeId(id)) return; // a whole world isn't removable through this seam
582
+ const owner = this.findOwnerChild(id);
583
+ if (!owner?.adapter.structure) {
584
+ editorConsole.warn(
585
+ `[CompositeAuthoringAdapter] remove: no owner/structure for id "${id}"`,
586
+ 'authoring',
587
+ );
588
+ return;
589
+ }
590
+ // Propagate the owning child's return value (a react-world child
591
+ // returns an awaitable) so `deleteSelection` can still serialize a
592
+ // multi-delete THROUGH the composite the same way it does for a
593
+ // directly-active override adapter (see that adapter's own `remove`
594
+ // doc comment).
595
+ return owner.adapter.structure.remove(id);
596
+ },
597
+ duplicate: (id) => {
598
+ if (isGroupNodeId(id)) return id; // no-op — nothing to duplicate
599
+ const owner = this.findOwnerChild(id);
600
+ if (!owner?.adapter.structure) {
601
+ editorConsole.warn(
602
+ `[CompositeAuthoringAdapter] duplicate: no owner/structure for id "${id}"`,
603
+ 'authoring',
604
+ );
605
+ return id;
606
+ }
607
+ return owner.adapter.structure.duplicate(id);
608
+ },
609
+ reparent: (id, newParentId) => {
610
+ const owner = this.findOwnerChild(id);
611
+ if (!owner?.adapter.structure) {
612
+ editorConsole.warn(
613
+ `[CompositeAuthoringAdapter] reparent: no owner/structure for id "${id}"`,
614
+ 'authoring',
615
+ );
616
+ return;
617
+ }
618
+ if (newParentId === null || newParentId === groupNodeId(owner.worldId)) {
619
+ owner.adapter.structure.reparent(id, null); // -> a root of its own world
620
+ return;
621
+ }
622
+ const newOwner = this.findOwnerChild(newParentId);
623
+ if (!newOwner || newOwner.worldId !== owner.worldId) {
624
+ editorConsole.warn(
625
+ `[CompositeAuthoringAdapter] reparent: refusing to move "${id}" across worlds ` +
626
+ `(target parent "${newParentId}" is not in world "${owner.worldId}")`,
627
+ 'authoring',
628
+ );
629
+ return;
630
+ }
631
+ owner.adapter.structure.reparent(id, newParentId);
632
+ },
633
+ reorder: (id, beforeSiblingId) => {
634
+ if (isGroupNodeId(id)) return;
635
+ const owner = this.findOwnerChild(id);
636
+ if (!owner) return;
637
+ // Cross-world guard: a non-null sibling anchor MUST belong to the same
638
+ // child as `id`. Otherwise (e.g. a drag onto another world's row whose
639
+ // reparent was already refused) the child maps the foreign sibling id to
640
+ // "append to end" and silently moves the entity within its own list on an
641
+ // operation the composite just rejected. A null anchor (move to end) is
642
+ // always valid.
643
+ if (beforeSiblingId !== null && this.findOwnerChild(beforeSiblingId) !== owner) return;
644
+ owner.adapter.structure?.reorder?.(id, beforeSiblingId);
645
+ },
646
+ creatableKinds: (parentId) => {
647
+ const target = this.resolveStructureTarget(parentId);
648
+ return (
649
+ target?.child.adapter.structure?.creatableKinds?.(target.forwardParentId ?? null) ?? []
650
+ );
651
+ },
652
+ // D3.a (spec 27 §6) — forward wrap/unwrap to the owning child, same
653
+ // owner-lookup + loud-warn-on-miss shape as `duplicate`/`remove` above
654
+ // (unlike `reorder`, which mirrors the base contract's OWN "absent means
655
+ // no UI" silent-no-op convention — wrap/unwrap follow the LOUD group
656
+ // instead since they're triggered from an explicit, always-visible menu
657
+ // item, same as duplicate/delete).
658
+ wrap: (id, wrapperTag) => {
659
+ if (isGroupNodeId(id)) return;
660
+ const owner = this.findOwnerChild(id);
661
+ if (!owner?.adapter.structure?.wrap) {
662
+ editorConsole.warn(
663
+ `[CompositeAuthoringAdapter] wrap: no owner/structure.wrap for id "${id}"`,
664
+ 'authoring',
665
+ );
666
+ return;
667
+ }
668
+ owner.adapter.structure.wrap(id, wrapperTag);
669
+ },
670
+ unwrap: (id) => {
671
+ if (isGroupNodeId(id)) return;
672
+ const owner = this.findOwnerChild(id);
673
+ if (!owner?.adapter.structure?.unwrap) {
674
+ editorConsole.warn(
675
+ `[CompositeAuthoringAdapter] unwrap: no owner/structure.unwrap for id "${id}"`,
676
+ 'authoring',
677
+ );
678
+ return;
679
+ }
680
+ owner.adapter.structure.unwrap(id);
681
+ },
682
+ };
683
+
684
+ /**
685
+ * A2 (map §5) — asset-drop pass-through. FIXES the pre-existing mis-route: a
686
+ * group-node id (`world:<w>`) used to fall through `route()`'s "unknown id ->
687
+ * first child" floor, so dropping an asset onto a world's OWN group row
688
+ * silently targeted whatever child happened to be FIRST in the array,
689
+ * regardless of which world's row was actually under the cursor. Translate
690
+ * the group-node id to that world's child directly, forwarding `''` (the
691
+ * shell/`VgaiSceneAuthoringAdapter` root-drop sentinel — see that adapter's
692
+ * `assetDrop` doc comment) as the node id, since the synthetic `world:<w>`
693
+ * string means nothing to the child's own hierarchy.
694
+ */
695
+ private resolveAssetDropTarget(nodeId: string): { adapter: AuthoringAdapter; forwardId: string } {
696
+ if (isGroupNodeId(nodeId)) {
697
+ const worldId = nodeId.slice(GROUP_PREFIX.length);
698
+ const child = this.children.find((c) => c.worldId === worldId);
699
+ return child
700
+ ? { adapter: child.adapter, forwardId: '' }
701
+ : { adapter: this.children[0]!.adapter, forwardId: '' };
702
+ }
703
+ const owner = this.findOwnerChild(nodeId);
704
+ // Unknown real id: same "first child" floor `route()` uses elsewhere, forwarding
705
+ // the id unchanged (parity with the pre-existing bogus-id behavior).
706
+ return owner
707
+ ? { adapter: owner.adapter, forwardId: nodeId }
708
+ : { adapter: this.children[0]!.adapter, forwardId: nodeId };
709
+ }
710
+
711
+ /**
712
+ * D4 (B2) — resolves a `stories` call THE SAME WAY `resolveAssetDropTarget`
713
+ * (above) resolves an asset drop: a group-node id (`world:<w>`) used to
714
+ * fall through `route()`'s "unknown id -> first child" floor, so
715
+ * `storiesFor('world:hud')` silently asked whatever child happened to be
716
+ * FIRST in the array (never the actual owner), meaning the story picker
717
+ * could NEVER appear on the world's own group row — the one place B2's
718
+ * shell (`Inspector.tsx`) actually calls it. Translated to `<w>`'s own
719
+ * child adapter. Unlike the asset-drop translation, the node id is
720
+ * forwarded UNCHANGED (not blanked to `''`) — B2's `ReactWorldAuthoringAdapter.stories`
721
+ * is world-level and ignores `nodeId` entirely (see that class's doc
722
+ * comment), but a future per-node catalog (B3) will want the real id, so
723
+ * there is no sentinel to invent here.
724
+ */
725
+ private resolveStoriesTarget(nodeId: string): { adapter: AuthoringAdapter; forwardId: string } {
726
+ if (isGroupNodeId(nodeId)) {
727
+ const worldId = nodeId.slice(GROUP_PREFIX.length);
728
+ const child = this.children.find((c) => c.worldId === worldId);
729
+ return child
730
+ ? { adapter: child.adapter, forwardId: nodeId }
731
+ : { adapter: this.children[0]!.adapter, forwardId: nodeId };
732
+ }
733
+ const owner = this.findOwnerChild(nodeId);
734
+ return owner
735
+ ? { adapter: owner.adapter, forwardId: nodeId }
736
+ : { adapter: this.children[0]!.adapter, forwardId: nodeId };
737
+ }
738
+
739
+ /**
740
+ * D8 — union of every child's own {@link RootGroup}s, each group id prefixed
741
+ * `<worldId>:` so groups from different worlds never collide. `activate`
742
+ * strips the prefix back off to find the owning child and the child's own
743
+ * (unprefixed) group id.
744
+ */
745
+ readonly rootGroups: RootGroupsProvider = {
746
+ groups: (): RootGroup[] =>
747
+ this.children.flatMap(
748
+ (c) =>
749
+ c.adapter.rootGroups?.groups().map((g) => ({ ...g, id: `${c.worldId}:${g.id}` })) ?? [],
750
+ ),
751
+ activate: (groupId, rootId) => {
752
+ const sep = groupId.indexOf(':');
753
+ if (sep < 0) return undefined;
754
+ const worldId = groupId.slice(0, sep);
755
+ const childGroupId = groupId.slice(sep + 1);
756
+ const child = this.children.find((c) => c.worldId === worldId);
757
+ return child?.adapter.rootGroups?.activate(childGroupId, rootId);
758
+ },
759
+ };
760
+
761
+ /** First NON-NULL child answer, in child order. */
762
+ readonly files: FileMapProvider = {
763
+ rootForFile: (path) => {
764
+ for (const child of this.children) {
765
+ const id = child.adapter.files?.rootForFile(path);
766
+ if (id) return id;
767
+ }
768
+ return null;
769
+ },
770
+ };
771
+
772
+ /** D4 — storybook stories, routed by node ownership (group-node ids
773
+ * translated — see `resolveStoriesTarget` above, the same fix
774
+ * `resolveAssetDropTarget` applied for asset drops). A node whose owning
775
+ * adapter has no `stories` provider reports/does nothing (empty list, null
776
+ * active, no-op apply/isolate) rather than throwing. */
777
+ readonly stories: StoriesProvider = {
778
+ storiesFor: (nodeId): StoryRef[] => {
779
+ const { adapter, forwardId } = this.resolveStoriesTarget(nodeId);
780
+ return adapter.stories?.storiesFor(forwardId) ?? [];
781
+ },
782
+ active: (nodeId) => {
783
+ const { adapter, forwardId } = this.resolveStoriesTarget(nodeId);
784
+ return adapter.stories?.active(forwardId) ?? null;
785
+ },
786
+ apply: (nodeId, storyId) => {
787
+ const { adapter, forwardId } = this.resolveStoriesTarget(nodeId);
788
+ adapter.stories?.apply(forwardId, storyId);
789
+ },
790
+ isolate: (nodeId, storyId) => {
791
+ // A null nodeId (exit isolation) has no owner to route by — forward the
792
+ // exit to every child that implements `isolate` so nothing is left stuck
793
+ // in an isolated state.
794
+ if (nodeId === null) {
795
+ for (const child of this.children) child.adapter.stories?.isolate?.(null, storyId);
796
+ return;
797
+ }
798
+ const { adapter, forwardId } = this.resolveStoriesTarget(nodeId);
799
+ adapter.stories?.isolate?.(forwardId, storyId);
800
+ },
801
+ };
802
+
803
+ /** D6 (S-C) — GameComponent management, routed by node ownership. */
804
+ readonly components: ComponentsProvider = {
805
+ available: (nodeId): string[] => this.route(nodeId).components?.available(nodeId) ?? [],
806
+ list: (nodeId): { type: string }[] => this.route(nodeId).components?.list(nodeId) ?? [],
807
+ add: (nodeId, type) => this.route(nodeId).components?.add(nodeId, type),
808
+ remove: (nodeId, type) => this.route(nodeId).components?.remove(nodeId, type),
809
+ config: (nodeId, type) => this.route(nodeId).components?.config?.(nodeId, type) ?? null,
810
+ };
811
+
812
+ /** Asset drop, routed by node ownership (group-node ids translated — see
813
+ * `resolveAssetDropTarget` above). */
814
+ readonly assetDrop: AssetDropProvider = {
815
+ accepts: (nodeId, assetPath) => {
816
+ const target = this.resolveAssetDropTarget(nodeId);
817
+ return target.adapter.assetDrop?.accepts(target.forwardId, assetPath) ?? false;
818
+ },
819
+ drop: (nodeId, assetPath) => {
820
+ const target = this.resolveAssetDropTarget(nodeId);
821
+ target.adapter.assetDrop?.drop(target.forwardId, assetPath);
822
+ },
823
+ };
824
+
825
+ // NOTE: `pickable` is deliberately NOT implemented here — layered viewport
826
+ // picking (D12) across children is shell policy (B4), not something this
827
+ // composite merges. Callers needing per-layer pick should go through
828
+ // `childAdapters()` and pick each child's own `pickable` themselves.
829
+
830
+ subscribe(listener: () => void): () => void {
831
+ const unsubs = this.children
832
+ .map((c) => c.adapter.subscribe?.(listener))
833
+ .filter((u): u is () => void => !!u);
834
+ return () => {
835
+ for (const u of unsubs) u();
836
+ };
837
+ }
838
+
839
+ /**
840
+ * Persistence (T3.2 slice 3, generalized 2→N unchanged in semantics): `isDirty`
841
+ * = OR of children that actually have a provider (a child without one, e.g. an
842
+ * embed-only adapter, is simply skipped — it has nothing to be dirty about);
843
+ * `save()` saves the dirty children SEQUENTIALLY, logging loudly per-child on
844
+ * failure without aborting the rest; `destination` joins child destinations
845
+ * with `' + '`.
846
+ *
847
+ * No own undo: the global timeline (`undo-timeline.ts`) already dispatches
848
+ * per-adapter, and each child that supports undo registers ITSELF — wrapping
849
+ * one in a Composite changes nothing about that registration.
850
+ */
851
+ get persistence(): PersistenceProvider {
852
+ // Always a REAL (never `undefined`) provider — see composite-authoring-adapter's
853
+ // 2-child precedent / `ephemeral-persistence.ts` for the same pattern.
854
+ const persistable = this.children.filter(
855
+ (
856
+ c,
857
+ ): c is CompositeChild & {
858
+ adapter: AuthoringAdapter & { persistence: PersistenceProvider };
859
+ } => c.adapter.capabilities.persist && !!c.adapter.persistence,
860
+ );
861
+ return {
862
+ isDirty: () => persistable.some((c) => c.adapter.persistence.isDirty()),
863
+ save: async () => {
864
+ for (const c of persistable) {
865
+ const p = c.adapter.persistence;
866
+ if (!p.isDirty()) continue;
867
+ try {
868
+ await p.save();
869
+ } catch (err) {
870
+ console.error(
871
+ `[CompositeAuthoringAdapter] save failed for world "${c.worldId}" ` +
872
+ `(destination: ${p.destination}) — continuing with remaining worlds:`,
873
+ err,
874
+ );
875
+ }
876
+ }
877
+ },
878
+ // A labeled record keyed by world id — the N-ary generalization of the
879
+ // 2-child `{world3d, world2d}` shape (§4/§6.5); report if a different
880
+ // shape (e.g. a flat merge) is actually wanted.
881
+ serialize: () =>
882
+ Object.fromEntries(
883
+ this.children.map((c) => [c.worldId, c.adapter.persistence?.serialize()]),
884
+ ),
885
+ destination:
886
+ persistable.length > 0
887
+ ? persistable.map((c) => c.adapter.persistence.destination).join(' + ')
888
+ : '(no persistable child)',
889
+ };
890
+ }
891
+ }