@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,509 @@
1
+ import { resolveUrl } from '@engine/loader';
2
+ import { parsePrefabFile, parseSceneFile } from '@engine/scene/parse';
3
+ import type { PrefabFile, SceneFile } from '@engine/scene/scene-types';
4
+ import { use, useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
5
+ import { createPortal } from 'react-dom';
6
+ import { connectAssetEvents, connectSceneEvents } from '../asset-events';
7
+ import { getActiveAuthoring, hasAuthoringOverride } from '../authoring/active-adapter';
8
+ import {
9
+ findOwningRootGroupId,
10
+ ROOT_ACTIVATION_REQUEST_EVENT,
11
+ type RootActivationRequest,
12
+ requestRootActivation,
13
+ } from '../authoring/root-activation';
14
+ import { watchExternalSceneChanges } from '../browser-external-watch';
15
+ import { useEditorInit, useEditorStore } from '../EditorContext';
16
+ import { readProjectTextFile, saveFile } from '../editor-api';
17
+ import { editorConsole } from '../editor-console';
18
+ import { getEditorMode } from '../editor-mode';
19
+ import { resizeGame, setGameContainer } from '../play-mode';
20
+ import { SceneUIPalettePanel } from '../scene-ui-mode/scene-ui-palette-panel';
21
+ import { UIPrefabDialogHost } from '../scene-ui-mode/ui-prefab-dialog-host';
22
+ import { getStorageBackend } from '../storage';
23
+ import { BottomPanel } from './BottomPanel';
24
+ import { GameHierarchy } from './GameHierarchy';
25
+ import { MountFailureBanner } from './MountFailureBanner';
26
+ import { NetworkSection } from './NetworkSection';
27
+ import { PlayBar, type Resolution } from './PlayBar';
28
+ import { PrefabSaveDialog } from './PrefabSaveDialog';
29
+ import { ProjectHeader } from './ProjectHeader';
30
+ import { RightPanel } from './RightPanel';
31
+ import { RootActivateDialog } from './RootActivateDialog';
32
+ import { ViewportPanel } from './ViewportPanel';
33
+
34
+ /** Load a scene from a URL into the store. */
35
+ export async function openSceneFromAsset(
36
+ store: { loadDocument: (doc: SceneFile, path: string) => void },
37
+ assetPath: string,
38
+ ): Promise<boolean> {
39
+ try {
40
+ const res = await fetch(resolveUrl(assetPath));
41
+ if (!res.ok) return false;
42
+ const doc = parseSceneFile(await res.json());
43
+ const savePath = assetPath.replace(/^\//, '');
44
+ store.loadDocument(doc, savePath);
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /** Save a MaterialFile to disk via the editor server API. */
52
+ export async function saveMaterialToServer(
53
+ material: import('@engine/scene/scene-types').MaterialFile,
54
+ path: string,
55
+ ): Promise<boolean> {
56
+ const content = JSON.stringify(material, null, 2);
57
+ return saveFile(path, content);
58
+ }
59
+
60
+ /** Save a PrefabFile to disk via the editor server API. */
61
+ export async function savePrefabToServer(prefab: PrefabFile, path: string): Promise<boolean> {
62
+ const content = JSON.stringify(prefab, null, 2);
63
+ return saveFile(path, content);
64
+ }
65
+
66
+ /** Load a prefab from a URL into the store for editing. */
67
+ export async function openPrefabFromAsset(
68
+ store: { loadPrefab: (prefab: PrefabFile, path: string) => void },
69
+ assetPath: string,
70
+ ): Promise<boolean> {
71
+ try {
72
+ const text = await readProjectTextFile(assetPath);
73
+ if (text === null) return false;
74
+ const prefab = parsePrefabFile(JSON.parse(text));
75
+ const savePath = assetPath.replace(/^\//, '');
76
+ store.loadPrefab(prefab, savePath);
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /** Fetch a PrefabFile (for unpack operations) — storage-first so a just-saved prefab resolves. */
84
+ export async function fetchPrefabFile(url: string): Promise<PrefabFile | null> {
85
+ try {
86
+ const text = await readProjectTextFile(url);
87
+ if (text === null) return null;
88
+ return parsePrefabFile(JSON.parse(text));
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /** Game panel — a container div that play-mode.ts fills with a canvas. */
95
+ function GamePanel({ style }: { style?: React.CSSProperties }) {
96
+ const store = useEditorStore();
97
+ const outerRef = useRef<HTMLDivElement>(null);
98
+ const [resolution, setResolution] = useState<Resolution>({
99
+ label: 'Fill',
100
+ width: null,
101
+ height: null,
102
+ });
103
+ const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
104
+
105
+ // Callback ref so setGameContainer is called even when the element swaps (fill ↔ device)
106
+ const prevRef = useRef<HTMLDivElement | null>(null);
107
+ const canvasRef = useCallback((el: HTMLDivElement | null) => {
108
+ if (el === prevRef.current) return;
109
+ // Move game canvas to new container
110
+ if (prevRef.current && el) {
111
+ while (prevRef.current.firstChild) el.appendChild(prevRef.current.firstChild);
112
+ }
113
+ prevRef.current = el;
114
+ setGameContainer(el);
115
+ }, []);
116
+
117
+ // Track outer container size for scale calculation
118
+ useEffect(() => {
119
+ const el = outerRef.current;
120
+ if (!el) return;
121
+ const observer = new ResizeObserver(([entry]) => {
122
+ if (!entry) return;
123
+ setContainerSize({ width: entry.contentRect.width, height: entry.contentRect.height });
124
+ });
125
+ observer.observe(el);
126
+ return () => observer.disconnect();
127
+ }, []);
128
+
129
+ // Resize the game renderer when resolution or container size changes
130
+ const isFill = resolution.width === null;
131
+ useEffect(() => {
132
+ if (isFill) {
133
+ // Fill mode: resize to match available container space
134
+ if (containerSize.width > 0 && containerSize.height > 0) {
135
+ resizeGame(containerSize.width, containerSize.height);
136
+ }
137
+ } else {
138
+ resizeGame(resolution.width!, resolution.height!);
139
+ }
140
+ }, [isFill, resolution.width, resolution.height, containerSize.width, containerSize.height]);
141
+
142
+ // Scale: fit the device resolution into the available space, never upscale
143
+ let scale = 1;
144
+ if (!isFill && containerSize.width > 0 && containerSize.height > 0) {
145
+ const scaleX = containerSize.width / resolution.width!;
146
+ const scaleY = containerSize.height / resolution.height!;
147
+ scale = Math.min(scaleX, scaleY, 1);
148
+ }
149
+
150
+ return (
151
+ <div style={{ overflow: 'hidden', position: 'relative', flexDirection: 'column', ...style }}>
152
+ <div
153
+ ref={outerRef}
154
+ style={{
155
+ flex: 1,
156
+ overflow: 'hidden',
157
+ display: 'flex',
158
+ alignItems: 'center',
159
+ justifyContent: 'center',
160
+ position: 'relative',
161
+ }}
162
+ >
163
+ {isFill ? (
164
+ <div ref={canvasRef} aria-hidden="true" style={{ position: 'absolute', inset: 0 }} />
165
+ ) : (
166
+ /* Wrapper sized to the scaled dimensions so flexbox centering works.
167
+ The inner canvasRef is full device resolution, CSS-scaled down. */
168
+ <div
169
+ style={{
170
+ width: resolution.width! * scale,
171
+ height: resolution.height! * scale,
172
+ overflow: 'hidden',
173
+ }}
174
+ >
175
+ <div
176
+ ref={canvasRef}
177
+ aria-hidden="true"
178
+ style={{
179
+ width: resolution.width!,
180
+ height: resolution.height!,
181
+ transformOrigin: 'top left',
182
+ transform: `scale(${scale})`,
183
+ }}
184
+ />
185
+ </div>
186
+ )}
187
+ {/* Same overlay ViewportPanel renders on the Scene tab (T-R1a #6) — the
188
+ Game tab is where play auto-switches to, so it must render here too,
189
+ not only on the Scene-tab viewport it would otherwise be hidden behind. */}
190
+ {store.playState !== 'stopped' && <NetworkSection />}
191
+ </div>
192
+ <PlayBar resolution={resolution} onResolutionChange={setResolution} scale={scale} />
193
+ </div>
194
+ );
195
+ }
196
+
197
+ function ViewportTabBar({
198
+ active,
199
+ onChange,
200
+ showGame,
201
+ }: {
202
+ active: 'scene' | 'game';
203
+ onChange: (tab: 'scene' | 'game') => void;
204
+ showGame: boolean;
205
+ }) {
206
+ const tabStyle = (isActive: boolean): React.CSSProperties => ({
207
+ background: 'none',
208
+ border: 'none',
209
+ padding: '4px 8px',
210
+ cursor: 'pointer',
211
+ fontSize: 11,
212
+ fontWeight: 600,
213
+ textTransform: 'uppercase',
214
+ color: isActive ? '#c5c8ce' : '#666',
215
+ borderBottom: isActive ? '2px solid #579eff' : '2px solid transparent',
216
+ marginBottom: -1,
217
+ pointerEvents: 'auto',
218
+ });
219
+ return (
220
+ <div
221
+ style={{
222
+ display: 'flex',
223
+ gap: 2,
224
+ background: '#2c2c2c',
225
+ borderBottom: '1px solid #333',
226
+ flexShrink: 0,
227
+ pointerEvents: 'auto',
228
+ }}
229
+ >
230
+ <button type="button" style={tabStyle(active === 'scene')} onClick={() => onChange('scene')}>
231
+ Viewport
232
+ </button>
233
+ {showGame && (
234
+ <button type="button" style={tabStyle(active === 'game')} onClick={() => onChange('game')}>
235
+ Game
236
+ </button>
237
+ )}
238
+ </div>
239
+ );
240
+ }
241
+
242
+ export function DefaultEditorLayout({
243
+ onReturnToProjectScreen,
244
+ }: {
245
+ onReturnToProjectScreen?: (() => void) | undefined;
246
+ }) {
247
+ const store = useEditorStore();
248
+ use(useEditorInit()); // suspends until project is detected
249
+
250
+ // Subscribe to store changes
251
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
252
+ const isPlaying = store.playState !== 'stopped';
253
+ const [prefabSaveState, setPrefabSaveState] = useState<{ entityId: string; name: string } | null>(
254
+ null,
255
+ );
256
+ // A4 (D8) — a root-activation request whose adapter was dirty, awaiting the
257
+ // user's Cancel/Save-&-Switch choice. `requestRootActivation` (shared by
258
+ // GameHierarchy's radio dblclick and `handleOpenScene` below) dispatches
259
+ // this via a window event instead of rendering anything itself (adapters —
260
+ // and the hierarchy panel driven purely by them — can't own a portal).
261
+ const [pendingRootActivation, setPendingRootActivation] = useState<RootActivationRequest | null>(
262
+ null,
263
+ );
264
+
265
+ useEffect(() => {
266
+ const handler = (e: Event) => {
267
+ setPendingRootActivation((e as CustomEvent<RootActivationRequest>).detail);
268
+ };
269
+ window.addEventListener(ROOT_ACTIVATION_REQUEST_EVENT, handler);
270
+ return () => window.removeEventListener(ROOT_ACTIVATION_REQUEST_EVENT, handler);
271
+ }, []);
272
+
273
+ // Listen for "save as prefab" events from the hierarchy context menu
274
+ useEffect(() => {
275
+ const handler = (e: Event) => {
276
+ const detail = (e as CustomEvent).detail as { entityId: string; name: string };
277
+ setPrefabSaveState(detail);
278
+ };
279
+ window.addEventListener('editor:save-as-prefab', handler);
280
+ return () => window.removeEventListener('editor:save-as-prefab', handler);
281
+ }, []);
282
+
283
+ const handlePrefabSaveConfirm = useCallback(
284
+ (path: string) => {
285
+ if (!prefabSaveState) return;
286
+ const prefab = store.extractAsPrefab(prefabSaveState.entityId);
287
+ if (!prefab) {
288
+ setPrefabSaveState(null);
289
+ return;
290
+ }
291
+ savePrefabToServer(prefab, path).then((ok) => {
292
+ if (ok) {
293
+ const servingPath = `/${path}`;
294
+ store.convertToPrefabInstance(prefabSaveState.entityId, servingPath);
295
+ }
296
+ setPrefabSaveState(null);
297
+ });
298
+ },
299
+ [store, prefabSaveState],
300
+ );
301
+
302
+ // A4 (D8) — the SINGLE choke point for the asset-browser's scene/prefab
303
+ // double-click (and the `editor:open-asset` event below). A scene file
304
+ // that is one of the active adapter's OWN root-group members reroutes
305
+ // through the shared dirty-gated `requestRootActivation` (radio activation)
306
+ // instead of the legacy `openSceneFromAsset` — it must NOT go through both
307
+ // paths, or the two could disagree about which scene is "loaded". Prefabs,
308
+ // and any scene the active adapter doesn't map to a root of its own
309
+ // (`files.rootForFile` returns null — e.g. no adapter installed, or a
310
+ // scene living outside this project's own root-group tracking), keep the
311
+ // legacy path unchanged.
312
+ const handleOpenScene = useCallback(
313
+ (assetPath: string) => {
314
+ if (assetPath.endsWith('.prefab.json')) {
315
+ openPrefabFromAsset(store, assetPath);
316
+ return;
317
+ }
318
+ const adapter = getActiveAuthoring(store);
319
+ const rootId = adapter.files?.rootForFile(assetPath) ?? null;
320
+ const groupId = rootId ? findOwningRootGroupId(adapter, rootId) : null;
321
+ if (rootId && groupId) {
322
+ requestRootActivation(adapter, groupId, rootId);
323
+ return;
324
+ }
325
+ openSceneFromAsset(store, assetPath);
326
+ },
327
+ [store],
328
+ );
329
+
330
+ // Listen for "open asset" events (e.g. "Open Prefab" from context menu)
331
+ useEffect(() => {
332
+ const handler = (e: Event) => {
333
+ const detail = (e as CustomEvent).detail as { path: string };
334
+ if (detail.path) handleOpenScene(detail.path);
335
+ };
336
+ window.addEventListener('editor:open-asset', handler);
337
+ return () => window.removeEventListener('editor:open-asset', handler);
338
+ }, [handleOpenScene]);
339
+
340
+ // Subscribe to server-sent asset move events (file watcher)
341
+ useEffect(() => {
342
+ return connectAssetEvents((event) => {
343
+ store.applyAssetMove(event.oldPath, event.newPath);
344
+ window.dispatchEvent(new CustomEvent('editor:assets-changed'));
345
+ });
346
+ }, [store]);
347
+
348
+ // Subscribe to server-sent scene file changes (live sync from disk). This channel
349
+ // is scoped to the currently-OPEN first-party `.vscn` (`eventPath === currentPath`
350
+ // below, `currentPath` = `store.savePath`) — the SSE transport itself is unchanged
351
+ // (design docs/PERSISTENCE-PROVIDER-DESIGN.md §5). When no override is active, the
352
+ // active adapter IS the first-party one, so applying the update is routed through
353
+ // its `persistence.applyExternal` (the existing `applyExternalUpdate` diff-apply,
354
+ // `event.content` forwarded as `rawContent` so own-echo detection is unaffected)
355
+ // instead of reaching into the store directly. When a non-first-party override
356
+ // (ingest overlay / UI / world2d / ephemeral) IS active, it has its OWN persisted
357
+ // artifact + reload contract, unrelated to this event — routing THIS doc through
358
+ // ITS `applyExternal` would feed a foreign `SceneFile` into e.g. an ingest
359
+ // overlay's apply, so fall back to `store.applyExternalUpdate` directly there,
360
+ // preserving its existing play-state-guarded queueing behavior unchanged.
361
+ const onSceneChanged = useCallback(
362
+ (event: { path: string; content: string }) => {
363
+ const currentPath = store.savePath;
364
+ if (!currentPath) return;
365
+ const eventPath = event.path.startsWith('/') ? event.path.slice(1) : event.path;
366
+ if (eventPath !== currentPath) return;
367
+ try {
368
+ const doc = parseSceneFile(JSON.parse(event.content));
369
+ if (hasAuthoringOverride()) {
370
+ store.applyExternalUpdate(doc, event.content);
371
+ } else {
372
+ getActiveAuthoring(store).persistence?.applyExternal?.(doc, event.content);
373
+ }
374
+ } catch (err) {
375
+ if (err instanceof Error) {
376
+ editorConsole.error(`Scene validation failed: ${err.message}`, 'scene');
377
+ }
378
+ }
379
+ },
380
+ [store],
381
+ );
382
+
383
+ useEffect(() => connectSceneEvents(onSceneChanged), [onSceneChanged]);
384
+
385
+ // A4 — external-change detection for the open scene. A2's watch only sees the
386
+ // editor's OWN writes; a real on-disk (FSA) folder can be edited out-of-band
387
+ // (agent apply-diff, another editor, git). Poll it and deliver through the
388
+ // SAME handler — the store's own-echo guard drops self-writes, so only genuine
389
+ // external edits reload. OPFS is a private sandbox with no external writer, so
390
+ // this is scoped to the FSA backend; server mode keeps SSE.
391
+ useEffect(() => {
392
+ if (getEditorMode() !== 'browser') return;
393
+ if (getStorageBackend().id !== 'fsa') return;
394
+ return watchExternalSceneChanges(() => store.savePath, onSceneChanged);
395
+ }, [store, onSceneChanged]);
396
+
397
+ const bottomEl = document.getElementById('editor-bottom');
398
+
399
+ return (
400
+ <>
401
+ <div
402
+ style={{
403
+ position: 'absolute',
404
+ top: 0,
405
+ left: 0,
406
+ width: '100%',
407
+ height: '100%',
408
+ display: 'flex',
409
+ flexDirection: 'column',
410
+ pointerEvents: 'none',
411
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
412
+ fontSize: '13px',
413
+ color: '#c5c8ce',
414
+ }}
415
+ >
416
+ <ProjectHeader onReturnToProjectScreen={onReturnToProjectScreen} />
417
+ {/* D-W3 (F9): rendered here, OUTSIDE `GameHierarchy` — a manifest world
418
+ that failed to mount entirely never installs an override, so a
419
+ banner living only INSIDE the hierarchy panel wouldn't help: this
420
+ one must be visible over the default Untitled scene the editor
421
+ falls back to in exactly that case. (`GameHierarchy` separately
422
+ renders `OverlayReportBanner` internally, unconditionally, for the
423
+ ingest/composite overlay-report case — see that component.) */}
424
+ <MountFailureBanner />
425
+ <div
426
+ style={{
427
+ display: 'flex',
428
+ flex: 1,
429
+ minHeight: 0,
430
+ }}
431
+ >
432
+ {/* A2 — ONE contract-only hierarchy panel drives every session shape
433
+ (bare single-scene edit, ingested game, multi-world composite):
434
+ `hasAuthoringOverride()` no longer selects between two panels. */}
435
+ <GameHierarchy />
436
+ {/* E1.a (spec-27 §7 E1) — the scene-UI palette (+ prefab
437
+ instantiate list), visible only while a scene-UI child is
438
+ installed (renders null otherwise — the component's own gate). */}
439
+ <SceneUIPalettePanel />
440
+ {/* Viewport + Game area — two independent panels, tab-switched */}
441
+ <div
442
+ style={{
443
+ flex: 1,
444
+ display: 'flex',
445
+ flexDirection: 'column',
446
+ position: 'relative',
447
+ pointerEvents: 'none',
448
+ }}
449
+ >
450
+ <ViewportTabBar
451
+ active={store.activeViewportTab}
452
+ onChange={(tab) => store.setActiveViewportTab(tab)}
453
+ showGame={isPlaying}
454
+ />
455
+ {/* Viewport panel (editor) — all overlays are inside ViewportPanel */}
456
+ <div
457
+ style={{
458
+ flex: 1,
459
+ position: 'relative',
460
+ display: store.activeViewportTab === 'scene' ? 'block' : 'none',
461
+ }}
462
+ >
463
+ <ViewportPanel />
464
+ </div>
465
+ {/* Game panel — always mounted, hidden when not playing/active */}
466
+ <GamePanel
467
+ style={{
468
+ flex: 1,
469
+ position: 'relative',
470
+ display: isPlaying && store.activeViewportTab === 'game' ? 'flex' : 'none',
471
+ flexDirection: 'column',
472
+ pointerEvents: 'auto',
473
+ ...(isPlaying && store.playState === 'playing'
474
+ ? { outline: '2px solid #4ec9b0', outlineOffset: -2 }
475
+ : isPlaying
476
+ ? { outline: '2px solid #dcdcaa', outlineOffset: -2 }
477
+ : {}),
478
+ }}
479
+ />
480
+ </div>
481
+ <RightPanel />
482
+ </div>
483
+ {prefabSaveState && (
484
+ <PrefabSaveDialog
485
+ defaultName={prefabSaveState.name}
486
+ onSave={handlePrefabSaveConfirm}
487
+ onCancel={() => setPrefabSaveState(null)}
488
+ />
489
+ )}
490
+ {/* E1.c (spec-27 §7 E1) — the scene-UI "Save as Prefab" dialog
491
+ (hierarchy context-menu, `ui-prefab-hierarchy-menu.ts`); a
492
+ self-contained host, renders nothing until that menu item fires. */}
493
+ <UIPrefabDialogHost />
494
+ {pendingRootActivation && (
495
+ <RootActivateDialog
496
+ label={pendingRootActivation.label}
497
+ onConfirm={() => {
498
+ const { groupId, rootId } = pendingRootActivation;
499
+ setPendingRootActivation(null);
500
+ void getActiveAuthoring(store).rootGroups?.activate(groupId, rootId);
501
+ }}
502
+ onCancel={() => setPendingRootActivation(null)}
503
+ />
504
+ )}
505
+ </div>
506
+ {bottomEl && createPortal(<BottomPanel onOpenScene={handleOpenScene} />, bottomEl)}
507
+ </>
508
+ );
509
+ }
@@ -0,0 +1,179 @@
1
+ import { faChevronDown, faChevronRight, faFolder } from '@fortawesome/free-solid-svg-icons';
2
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
+ import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
4
+ import { listAssets } from '../editor-api';
5
+
6
+ // --- Types ---
7
+
8
+ interface FolderTreeNode {
9
+ id: string;
10
+ name: string;
11
+ root: string;
12
+ path: string;
13
+ children: FolderTreeNode[];
14
+ }
15
+
16
+ interface FolderTreeProps {
17
+ currentPath: string;
18
+ currentRoot: string;
19
+ onNavigate: (root: string, path: string) => void;
20
+ }
21
+
22
+ interface FolderRow {
23
+ node: FolderTreeNode;
24
+ depth: number;
25
+ hasChildren: boolean;
26
+ isOpen: boolean;
27
+ }
28
+
29
+ // --- Helpers ---
30
+
31
+ function makeNodeId(root: string, path: string): string {
32
+ return `${root}:${path}`;
33
+ }
34
+
35
+ async function fetchSubfolders(root: string, dir: string): Promise<FolderTreeNode[]> {
36
+ const entries = await listAssets(root, dir);
37
+ return entries
38
+ .filter((e) => e.type === 'directory')
39
+ .map((e) => {
40
+ const path = dir ? `${dir}/${e.name}` : e.name;
41
+ return {
42
+ id: makeNodeId(root, path),
43
+ name: e.name,
44
+ root,
45
+ path,
46
+ children: [],
47
+ };
48
+ });
49
+ }
50
+
51
+ /** Recursively replace children of a target node by ID. */
52
+ function updateNodeChildren(
53
+ nodes: FolderTreeNode[],
54
+ targetId: string,
55
+ newChildren: FolderTreeNode[],
56
+ ): FolderTreeNode[] {
57
+ return nodes.map((node) => {
58
+ if (node.id === targetId) return { ...node, children: newChildren };
59
+ if (node.children.length > 0) {
60
+ const updated = updateNodeChildren(node.children, targetId, newChildren);
61
+ if (updated !== node.children) return { ...node, children: updated };
62
+ }
63
+ return node;
64
+ });
65
+ }
66
+
67
+ function flattenFolderRows(nodes: FolderTreeNode[], openIds: Set<string>, depth = 0): FolderRow[] {
68
+ const rows: FolderRow[] = [];
69
+ for (const node of nodes) {
70
+ const isOpen = openIds.has(node.id);
71
+ // Show as having children if explicitly opened (children may be loading)
72
+ // or if children array is non-empty
73
+ const hasChildren = node.children.length > 0 || isOpen;
74
+ rows.push({ node, depth, hasChildren, isOpen });
75
+ if (isOpen && node.children.length > 0) {
76
+ rows.push(...flattenFolderRows(node.children, openIds, depth + 1));
77
+ }
78
+ }
79
+ return rows;
80
+ }
81
+
82
+ // --- Main Component ---
83
+
84
+ export function FolderTree({ currentPath, currentRoot, onNavigate }: FolderTreeProps) {
85
+ const [treeData, setTreeData] = useState<FolderTreeNode[]>([]);
86
+ const openIdsRef = useRef(new Set<string>());
87
+ const loadedRef = useRef(new Set<string>());
88
+ const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
89
+
90
+ // Load top-level folders directly (no wrapper "public" node)
91
+ useEffect(() => {
92
+ fetchSubfolders('public', '').then((topFolders) => {
93
+ setTreeData(topFolders);
94
+ });
95
+ }, []);
96
+
97
+ const loadChildren = useCallback((id: string, root: string, path: string) => {
98
+ if (loadedRef.current.has(id)) return;
99
+ loadedRef.current.add(id);
100
+ fetchSubfolders(root, path).then((children) => {
101
+ setTreeData((prev) => updateNodeChildren(prev, id, children));
102
+ });
103
+ }, []);
104
+
105
+ const selectedId = makeNodeId(currentRoot, currentPath);
106
+ const rows = flattenFolderRows(treeData, openIdsRef.current);
107
+
108
+ return (
109
+ <div style={{ flex: 1, minHeight: 0, overflow: 'auto', paddingTop: 4 }}>
110
+ {rows.map((row) => {
111
+ const data = row.node;
112
+ const isActive = data.id === selectedId;
113
+
114
+ return (
115
+ <div
116
+ key={data.id}
117
+ style={{
118
+ display: 'flex',
119
+ alignItems: 'center',
120
+ height: 24,
121
+ gap: 4,
122
+ paddingLeft: row.depth * 14,
123
+ cursor: 'pointer',
124
+ background: isActive ? '#579EFF33' : 'transparent',
125
+ color: isActive ? '#fff' : '#c5c8ce',
126
+ borderRadius: 2,
127
+ whiteSpace: 'nowrap',
128
+ overflow: 'hidden',
129
+ fontSize: 12,
130
+ }}
131
+ onClick={() => {
132
+ onNavigate(data.root, data.path);
133
+ if (!row.isOpen) {
134
+ openIdsRef.current.add(data.id);
135
+ loadChildren(data.id, data.root, data.path);
136
+ forceUpdate();
137
+ }
138
+ }}
139
+ onMouseEnter={(e) => {
140
+ if (!isActive) (e.currentTarget as HTMLDivElement).style.background = '#333';
141
+ }}
142
+ onMouseLeave={(e) => {
143
+ if (!isActive) (e.currentTarget as HTMLDivElement).style.background = 'transparent';
144
+ }}
145
+ >
146
+ {/* Expand/collapse arrow */}
147
+ <span
148
+ onClick={(e) => {
149
+ e.stopPropagation();
150
+ const set = openIdsRef.current;
151
+ if (set.has(data.id)) {
152
+ set.delete(data.id);
153
+ } else {
154
+ set.add(data.id);
155
+ loadChildren(data.id, data.root, data.path);
156
+ }
157
+ forceUpdate();
158
+ }}
159
+ style={{
160
+ width: 10,
161
+ fontSize: 8,
162
+ textAlign: 'center',
163
+ flexShrink: 0,
164
+ color: '#8c8c8c',
165
+ userSelect: 'none',
166
+ }}
167
+ >
168
+ <FontAwesomeIcon icon={row.isOpen ? faChevronDown : faChevronRight} />
169
+ </span>
170
+ <span style={{ fontSize: 12, color: '#e8a740' }}>
171
+ <FontAwesomeIcon icon={faFolder} />
172
+ </span>
173
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{data.name}</span>
174
+ </div>
175
+ );
176
+ })}
177
+ </div>
178
+ );
179
+ }