@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,700 @@
1
+ /**
2
+ * Editor server SDK.
3
+ *
4
+ * All frontend code should use this module instead of calling fetch()/EventSource
5
+ * with raw endpoint URLs.
6
+ *
7
+ * The project's `public/` file operations (`listAssets`, `saveFile`) are routed
8
+ * through the backend-agnostic `StorageBackend` (see ./storage): locally that is
9
+ * `HttpStorage` over the same `/__editor/*` routes (behavior unchanged); in a
10
+ * server-less browser build it is `OpfsStorage`/`FsaStorage`, so the editor's
11
+ * file I/O works with no Node server. The remaining endpoints are NOT `public/`
12
+ * file ops, and split three ways in browser mode (Phase A cleanup — see
13
+ * `docs/FRONTEND-FIRST-ROADMAP.md` Phase A's "retire the remaining Node-only
14
+ * `/__editor/*` ops" bullet):
15
+ * - `loadEditorState`/`saveEditorState` route through `getStorageBackend()`
16
+ * directly (`.vgai/editor-state.json`, project-root-relative — same
17
+ * namespace `authoring/world-overlay.ts` already established), because
18
+ * remembering view/tool state is genuine authoring-loop parity.
19
+ * - Background, non-actionable reads with no independent browser-mode value
20
+ * degrade to an honest empty result with NO fetch attempted at all:
21
+ * `listRecentProjects`/`listExamples` (browser mode has no server-tracked
22
+ * project history), `saveThumbnail` (its only consumer is the Node-only
23
+ * recent-projects list), `startLogSession`/`endLogSession`/
24
+ * `flushLogEntries` (agent/CLI-tooling introspection — bucketed with the
25
+ * CLI control channel, which is already fully dropped in browser mode by
26
+ * `command-listener.ts`), and the read-only asset-library lookups.
27
+ * - User-triggered actions with no browser equivalent throw a clear, honest
28
+ * `Error` instead of doing a doomed fetch: `createProject`, `browseFolder`,
29
+ * `openProjectOnServer`, `revealInFinder`, `startExport`,
30
+ * `downloadOnlineAsset` — the browser-mode UI either hides the affordance
31
+ * (Build/Library tabs, "Show in Finder") or catches the throw into an
32
+ * existing error banner (`ProjectScreen.tsx`).
33
+ */
34
+
35
+ import { resolveUrl } from '@engine/loader';
36
+ import { getEditorMode } from './editor-mode';
37
+ import { getStorageBackend } from './storage';
38
+
39
+ const BASE = '/__editor';
40
+
41
+ /** Browser mode has no Node dev server — every `/__editor/*` op below either
42
+ * routes through `getStorageBackend()` or degrades explicitly; this helper
43
+ * just names the check so each function reads the same way. */
44
+ function isBrowserMode(): boolean {
45
+ return getEditorMode() === 'browser';
46
+ }
47
+
48
+ /**
49
+ * Read a project text file, storage-backend first, then static HTTP fallback.
50
+ *
51
+ * A prefab/asset the user just saved lives in the storage backend (OPFS in the
52
+ * browser build); a prefab shipped with the project is a static file served over
53
+ * HTTP. Reading storage-first makes both work — critically, it lets a
54
+ * just-created prefab be resolved/unpacked in the server-less browser editor,
55
+ * where a plain `fetch()` of the OPFS path would 404. Returns null if neither has it.
56
+ */
57
+ export async function readProjectTextFile(path: string): Promise<string | null> {
58
+ const key = path.replace(/^\//, '');
59
+ try {
60
+ const be = getStorageBackend();
61
+ if (await be.exists(key)) return await be.read(key);
62
+ } catch {
63
+ /* fall through to HTTP */
64
+ }
65
+ try {
66
+ const res = await fetch(resolveUrl(path));
67
+ if (res.ok) return await res.text();
68
+ } catch {
69
+ /* ignore */
70
+ }
71
+ return null;
72
+ }
73
+
74
+ export interface AssetEntry {
75
+ name: string;
76
+ type: 'file' | 'directory';
77
+ size: number;
78
+ }
79
+
80
+ /** List files and folders in a directory under public/. */
81
+ export async function listAssets(root: string, dir: string): Promise<AssetEntry[]> {
82
+ void root; // backend is project-rooted; `root` kept for signature stability
83
+ try {
84
+ const entries = await getStorageBackend().list(dir);
85
+ return entries.map((e) => ({
86
+ name: e.name,
87
+ type: e.type === 'dir' ? 'directory' : 'file',
88
+ size: e.size ?? 0,
89
+ }));
90
+ } catch {
91
+ return [];
92
+ }
93
+ }
94
+
95
+ /** Save a file to public/. Returns true on success. */
96
+ export async function saveFile(
97
+ path: string,
98
+ content: string,
99
+ encoding?: 'base64',
100
+ ): Promise<boolean> {
101
+ try {
102
+ // base64 content → raw bytes; the backend re-encodes per its transport.
103
+ const data: string | Uint8Array =
104
+ encoding === 'base64' ? Uint8Array.from(atob(content), (c) => c.charCodeAt(0)) : content;
105
+ await getStorageBackend().write(path, data);
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ /** Start a build/export. Returns the raw Response for SSE streaming. */
113
+ export async function startExport(target: string, signal?: AbortSignal | null): Promise<Response> {
114
+ if (isBrowserMode()) {
115
+ throw new Error(
116
+ 'Export is not available in browser mode — building a game requires the Node dev server ' +
117
+ '(`npm run dev`) or CLI (`vgai edit`).',
118
+ );
119
+ }
120
+ const init: RequestInit = {
121
+ method: 'POST',
122
+ headers: { 'Content-Type': 'application/json' },
123
+ body: JSON.stringify({ target }),
124
+ };
125
+ if (signal) init.signal = signal;
126
+ return fetch(`${BASE}/export`, init);
127
+ }
128
+
129
+ /** Build a download URL for a build artifact. */
130
+ export function getDownloadUrl(file: string): string {
131
+ return `${BASE}/download?file=${encodeURIComponent(file)}`;
132
+ }
133
+
134
+ /**
135
+ * The subset of `EventSource`'s surface every caller here actually uses.
136
+ * `close()` on the object `connectEvents()` returns detaches only the CALLING
137
+ * consumer (see below) — it is deliberately not a real `EventSource`, so
138
+ * nothing can reach in and close the shared socket out from under a sibling
139
+ * consumer.
140
+ */
141
+ export interface EditorEventSource {
142
+ addEventListener(type: string, listener: (event: MessageEvent) => void): void;
143
+ removeEventListener(type: string, listener: (event: MessageEvent) => void): void;
144
+ close(): void;
145
+ }
146
+
147
+ let sharedSource: EventSource | null = null;
148
+ let sharedRefCount = 0;
149
+
150
+ /**
151
+ * Connect to the editor's SSE event stream.
152
+ *
153
+ * SHARED and ref-counted across every caller (W6a field note,
154
+ * docs/DATA-TOOLS-DESIGN.md §7): this used to hand back a brand-new
155
+ * `EventSource` per call, and by the time W6a added its 2 new push
156
+ * channels (tool/data-file add+unlink) this editor had **6** independent
157
+ * long-lived consumers (asset-moved + scene-changed in
158
+ * `DefaultEditorLayout`, the CLI command channel, console server-logs, and
159
+ * the 2 new ones) — landing EXACTLY on Chromium's hard 6-connections-
160
+ * per-origin cap for HTTP/1.1 and permanently starving every OTHER fetch
161
+ * to this origin (module chunks, `/__editor/tool-files`, everything) once
162
+ * all 6 slots were claimed forever. Verified live: with 6 real
163
+ * `EventSource`s already open, a 7th never left `readyState 0`
164
+ * (CONNECTING) — nor did ordinary `fetch()` calls issued after it.
165
+ *
166
+ * The fix is ONE real connection, reference-counted: `addEventListener`/
167
+ * `removeEventListener` proxy straight through to it, and `close()` only
168
+ * removes THIS caller's own listeners and decrements the count — the
169
+ * underlying socket closes for real only once the last caller releases it.
170
+ * However many editor features want server push, the origin only ever
171
+ * pays for one connection.
172
+ */
173
+ export function connectEvents(): EditorEventSource {
174
+ if (!sharedSource) {
175
+ sharedSource = new EventSource(`${BASE}/events`);
176
+ }
177
+ const real = sharedSource;
178
+ sharedRefCount++;
179
+
180
+ const ownListeners: Array<{ type: string; listener: EventListener }> = [];
181
+ let released = false;
182
+
183
+ return {
184
+ addEventListener(type, listener) {
185
+ const wrapped = listener as EventListener;
186
+ real.addEventListener(type, wrapped);
187
+ ownListeners.push({ type, listener: wrapped });
188
+ },
189
+ removeEventListener(type, listener) {
190
+ const wrapped = listener as EventListener;
191
+ real.removeEventListener(type, wrapped);
192
+ const idx = ownListeners.findIndex((l) => l.type === type && l.listener === wrapped);
193
+ if (idx >= 0) ownListeners.splice(idx, 1);
194
+ },
195
+ close() {
196
+ if (released) return;
197
+ released = true;
198
+ for (const { type, listener } of ownListeners) real.removeEventListener(type, listener);
199
+ ownListeners.length = 0;
200
+ sharedRefCount--;
201
+ if (sharedRefCount <= 0) {
202
+ real.close();
203
+ if (sharedSource === real) sharedSource = null;
204
+ sharedRefCount = 0;
205
+ }
206
+ },
207
+ };
208
+ }
209
+
210
+ /** Report the current editor state to the server (called by command-listener). */
211
+ export async function reportEditorState(state: Record<string, unknown>): Promise<void> {
212
+ try {
213
+ await fetch(`${BASE}/state`, {
214
+ method: 'POST',
215
+ headers: { 'Content-Type': 'application/json' },
216
+ body: JSON.stringify(state),
217
+ });
218
+ } catch {
219
+ /* best-effort */
220
+ }
221
+ }
222
+
223
+ /** Report a command result back to the server so the SDK/CLI gets the response.
224
+ * `data` carries an optional payload for commands that answer with data
225
+ * (e.g. `capture-viewport`'s base64 PNG) — passed through verbatim to the
226
+ * SDK/CLI caller's response body. */
227
+ export async function reportCommandResult(
228
+ requestId: string,
229
+ ok: boolean,
230
+ error?: string,
231
+ data?: Record<string, unknown>,
232
+ ): Promise<void> {
233
+ try {
234
+ await fetch(`${BASE}/command-result`, {
235
+ method: 'POST',
236
+ headers: { 'Content-Type': 'application/json' },
237
+ body: JSON.stringify({ _requestId: requestId, ok, error, data }),
238
+ });
239
+ } catch {
240
+ /* best-effort */
241
+ }
242
+ }
243
+
244
+ export interface RecentProject {
245
+ name: string;
246
+ path: string;
247
+ lastOpened: string;
248
+ thumbnail?: string;
249
+ }
250
+
251
+ /** List recently opened projects. Always empty in browser mode — there is no
252
+ * Node-tracked project history there (A1's persisted-root mechanism
253
+ * remembers exactly one folder, not a list); an empty result is the honest
254
+ * state, not a degraded one. */
255
+ export async function listRecentProjects(): Promise<RecentProject[]> {
256
+ if (isBrowserMode()) return [];
257
+ try {
258
+ const res = await fetch(`${BASE}/recent-projects`);
259
+ if (!res.ok) return [];
260
+ const data = (await res.json()) as { projects: RecentProject[] };
261
+ return data.projects;
262
+ } catch {
263
+ return [];
264
+ }
265
+ }
266
+
267
+ export interface ExampleInfo {
268
+ id: string;
269
+ name: string;
270
+ }
271
+
272
+ /** List `examples/<id>/` projects available to scaffold from (Track E3, §1.F).
273
+ * Always empty in browser mode — this is the on-disk scaffold-a-new-project
274
+ * flow; bundled examples are reached in browser mode via the separate
275
+ * `?project=<id>` hosted deep-link (`hosted-example.ts`) instead. */
276
+ export async function listExamples(): Promise<ExampleInfo[]> {
277
+ if (isBrowserMode()) return [];
278
+ try {
279
+ const res = await fetch(`${BASE}/examples`);
280
+ if (!res.ok) return [];
281
+ const data = (await res.json()) as { examples: ExampleInfo[] };
282
+ return data.examples;
283
+ } catch {
284
+ return [];
285
+ }
286
+ }
287
+
288
+ /** Create a new project from a template. */
289
+ export async function createProject(
290
+ name: string,
291
+ location: string,
292
+ template: 'starter' | 'empty' | 'example',
293
+ exampleId?: string,
294
+ ): Promise<{ ok: boolean; path?: string; config?: { name: string }; error?: string }> {
295
+ if (isBrowserMode()) {
296
+ throw new Error(
297
+ 'Creating a project on disk from a template needs the Node dev server. In browser mode, ' +
298
+ 'use "Open Folder…" to author a real on-disk project via File System Access, or just start ' +
299
+ 'editing — a Starter project is already seeded in your browser storage.',
300
+ );
301
+ }
302
+ const res = await fetch(`${BASE}/create-project`, {
303
+ method: 'POST',
304
+ headers: { 'Content-Type': 'application/json' },
305
+ body: JSON.stringify({ name, location, template, ...(exampleId ? { exampleId } : {}) }),
306
+ });
307
+ return (await res.json()) as {
308
+ ok: boolean;
309
+ path?: string;
310
+ config?: { name: string };
311
+ error?: string;
312
+ };
313
+ }
314
+
315
+ /** Open a native folder picker dialog. Returns the selected path, or null if cancelled. */
316
+ export async function browseFolder(title?: string): Promise<string | null> {
317
+ if (isBrowserMode()) {
318
+ throw new Error(
319
+ 'The native folder picker needs the Node dev server. In browser mode, use "Open Folder…" ' +
320
+ '(above) instead — it picks a real on-disk folder via File System Access.',
321
+ );
322
+ }
323
+ try {
324
+ const res = await fetch(`${BASE}/browse-folder`, {
325
+ method: 'POST',
326
+ headers: { 'Content-Type': 'application/json' },
327
+ body: JSON.stringify({ title }),
328
+ });
329
+ if (!res.ok) return null;
330
+ const data = (await res.json()) as { path: string | null };
331
+ return data.path;
332
+ } catch {
333
+ return null;
334
+ }
335
+ }
336
+
337
+ /** Reveal a file or folder in the OS file manager. */
338
+ export async function revealInFinder(targetPath: string): Promise<void> {
339
+ if (isBrowserMode()) {
340
+ throw new Error(
341
+ 'Revealing a file in the OS file manager needs the Node dev server — a browser-mode project ' +
342
+ 'has no OS path to reveal (File System Access does not expose one).',
343
+ );
344
+ }
345
+ await fetch(`${BASE}/reveal`, {
346
+ method: 'POST',
347
+ headers: { 'Content-Type': 'application/json' },
348
+ body: JSON.stringify({ path: targetPath }),
349
+ });
350
+ }
351
+
352
+ /** Save a viewport thumbnail for the current project. Always a no-op in
353
+ * browser mode — its only consumer is the Node-only recent-projects list
354
+ * (`listRecentProjects`), which has no browser-mode equivalent to attach a
355
+ * thumbnail to; this is background bookkeeping, not a user-facing promise. */
356
+ export async function saveThumbnail(dataUrl: string): Promise<boolean> {
357
+ if (isBrowserMode()) return false;
358
+ try {
359
+ const res = await fetch(`${BASE}/save-thumbnail`, {
360
+ method: 'POST',
361
+ headers: { 'Content-Type': 'application/json' },
362
+ body: JSON.stringify({ dataUrl }),
363
+ });
364
+ return res.ok;
365
+ } catch {
366
+ return false;
367
+ }
368
+ }
369
+
370
+ /** Query the server for the currently active project (if any). */
371
+ export async function getCurrentServerProject(): Promise<{
372
+ path: string;
373
+ config: { name: string };
374
+ /** Whether the project ships src/ui/registry.ts (scene-UI project mode, D9). */
375
+ hasUiRegistry?: boolean;
376
+ /** Whether this server is the no-monorepo-checkout packaged runtime
377
+ * (`packages/editor/server/packaged.ts`) — see `adapter-resolver.ts`'s
378
+ * `isPackagedRuntime` for why a react world's mount needs this. */
379
+ packaged?: boolean;
380
+ } | null> {
381
+ // Browser mode has no Node dev server to ask — same honest no-fetch
382
+ // degrade as the other background reads in this file (Phase A).
383
+ if (isBrowserMode()) return null;
384
+ try {
385
+ const res = await fetch(`${BASE}/project`);
386
+ if (!res.ok) return null;
387
+ const data = (await res.json()) as {
388
+ project: {
389
+ path: string;
390
+ config: { name: string };
391
+ hasUiRegistry?: boolean;
392
+ packaged?: boolean;
393
+ } | null;
394
+ };
395
+ return data.project;
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+
401
+ /** Open/switch to a project by its filesystem path. */
402
+ export async function openProjectOnServer(
403
+ projectPath: string,
404
+ ): Promise<{ ok: boolean; config?: { name: string } }> {
405
+ if (isBrowserMode()) {
406
+ throw new Error(
407
+ 'Opening a project by filesystem path needs the Node dev server. In browser mode, use ' +
408
+ '"Open Folder…" instead — it picks a real on-disk folder via File System Access.',
409
+ );
410
+ }
411
+ const res = await fetch(`${BASE}/open-project`, {
412
+ method: 'POST',
413
+ headers: { 'Content-Type': 'application/json' },
414
+ body: JSON.stringify({ path: projectPath }),
415
+ });
416
+ return (await res.json()) as { ok: boolean; config?: { name: string } };
417
+ }
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // Persistent editor state (.vgai/editor-state.json)
421
+ // ---------------------------------------------------------------------------
422
+
423
+ /** Project-root-relative path — same `.vgai/` namespace as
424
+ * `authoring/world-overlay.ts`'s overlays, and the exact file the server's
425
+ * `/__editor/editor-state` route reads/writes via `node:fs`. */
426
+ const EDITOR_STATE_PATH = '.vgai/editor-state.json';
427
+
428
+ /** Load persisted editor state. Returns {} if none exists. Browser mode goes
429
+ * straight through `getStorageBackend()` (OPFS/FSA root at the true project
430
+ * folder, no `public/` insertion) instead of the Node-only route — this is
431
+ * genuine authoring-loop parity (remembered transform mode/snap/last
432
+ * scene/camera), not a background nicety, so it is ROUTED, not dropped. */
433
+ export async function loadEditorState(): Promise<Record<string, unknown>> {
434
+ try {
435
+ if (isBrowserMode()) {
436
+ const backend = getStorageBackend();
437
+ if (!(await backend.exists(EDITOR_STATE_PATH))) return {};
438
+ return JSON.parse(await backend.read(EDITOR_STATE_PATH)) as Record<string, unknown>;
439
+ }
440
+ const res = await fetch(`${BASE}/editor-state`);
441
+ if (!res.ok) return {};
442
+ return (await res.json()) as Record<string, unknown>;
443
+ } catch {
444
+ return {};
445
+ }
446
+ }
447
+
448
+ /** Save editor state (persisted in .vgai/editor-state.json — see
449
+ * `loadEditorState`'s doc comment for the browser-mode routing). */
450
+ export async function saveEditorState(state: Record<string, unknown>): Promise<boolean> {
451
+ try {
452
+ if (isBrowserMode()) {
453
+ await getStorageBackend().write(EDITOR_STATE_PATH, JSON.stringify(state, null, 2));
454
+ return true;
455
+ }
456
+ const res = await fetch(`${BASE}/editor-state`, {
457
+ method: 'POST',
458
+ headers: { 'Content-Type': 'application/json' },
459
+ body: JSON.stringify(state),
460
+ });
461
+ return res.ok;
462
+ } catch {
463
+ return false;
464
+ }
465
+ }
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // Play-mode console log persistence
469
+ // ---------------------------------------------------------------------------
470
+
471
+ export interface LogEntry {
472
+ t: number;
473
+ level: string;
474
+ source?: string;
475
+ sub?: string;
476
+ msg: string;
477
+ meta?: Record<string, unknown>;
478
+ }
479
+
480
+ /** Start a new log session. Returns the filename, or null on failure. Always
481
+ * `null` in browser mode with NO fetch attempted — play-mode console → JSONL
482
+ * persistence is `vgai apply-diff`/agent-tooling introspection, bucketed
483
+ * with the CLI control channel (already fully dropped in browser mode by
484
+ * `command-listener.ts`): no browser-mode consumer reads this file. */
485
+ export async function startLogSession(): Promise<string | null> {
486
+ if (isBrowserMode()) return null;
487
+ try {
488
+ const res = await fetch(`${BASE}/log-session`, {
489
+ method: 'POST',
490
+ headers: { 'Content-Type': 'application/json' },
491
+ body: JSON.stringify({ action: 'start' }),
492
+ });
493
+ if (!res.ok) return null;
494
+ const data = (await res.json()) as { ok: boolean; file: string };
495
+ return data.file;
496
+ } catch {
497
+ return null;
498
+ }
499
+ }
500
+
501
+ /** End the current log session. No-op in browser mode (see `startLogSession`). */
502
+ export async function endLogSession(): Promise<void> {
503
+ if (isBrowserMode()) return;
504
+ try {
505
+ await fetch(`${BASE}/log-session`, {
506
+ method: 'POST',
507
+ headers: { 'Content-Type': 'application/json' },
508
+ body: JSON.stringify({ action: 'end' }),
509
+ });
510
+ } catch {
511
+ /* best-effort */
512
+ }
513
+ }
514
+
515
+ /** Flush a batch of log entries to the server. Returns true on success. No-op
516
+ * in browser mode (see `startLogSession`) — critically, this is called on a
517
+ * `setInterval` while Play is running, so without this guard it would hammer
518
+ * a dead endpoint every 200ms for the whole play session (the same
519
+ * retry-loop anti-pattern A2's SSE guards exist to avoid, just via
520
+ * `setInterval`+`fetch` instead of `EventSource`). */
521
+ export async function flushLogEntries(entries: LogEntry[]): Promise<boolean> {
522
+ if (entries.length === 0) return true;
523
+ if (isBrowserMode()) return true;
524
+ try {
525
+ const res = await fetch(`${BASE}/log-entries`, {
526
+ method: 'POST',
527
+ headers: { 'Content-Type': 'application/json' },
528
+ body: JSON.stringify({ entries }),
529
+ });
530
+ return res.ok;
531
+ } catch {
532
+ return false;
533
+ }
534
+ }
535
+
536
+ // ---------------------------------------------------------------------------
537
+ // Asset Library (online browsing + download)
538
+ //
539
+ // The whole feature is a Node-only proxy to Poly Haven/ambientcg (CORS +
540
+ // SSRF-allowlist reasons to stay server-side) that writes into
541
+ // `<project>/public/asset-library/...` via raw `node:fs`, not
542
+ // `StorageBackend` — there is no browser-mode equivalent. `BottomPanel.tsx`
543
+ // hides the whole "Library" tab in browser mode; the functions below are a
544
+ // defensive backstop for any other caller: read-only lookups degrade to an
545
+ // honest empty result (no fetch attempted), the one mutating call
546
+ // (`downloadOnlineAsset`) throws instead.
547
+ // ---------------------------------------------------------------------------
548
+
549
+ export interface OnlineAsset {
550
+ id: string;
551
+ source: 'polyhaven' | 'ambientcg' | 'local';
552
+ name: string;
553
+ type: 'model' | 'animation' | 'source' | 'hdri' | 'texture' | 'material';
554
+ thumbnailUrl: string;
555
+ categories: string[];
556
+ tags: string[];
557
+ downloadCount?: number;
558
+ license?: string;
559
+ author?: string;
560
+ sourceUrl?: string;
561
+ cloudHosted?: boolean;
562
+ }
563
+
564
+ export interface AssetFileOption {
565
+ label: string;
566
+ format: string;
567
+ url: string;
568
+ sizeBytes?: number;
569
+ includes?: { relativePath: string; url: string; size: number }[];
570
+ }
571
+
572
+ // ---------------------------------------------------------------------------
573
+ // Downloaded-asset cache (client-side, keyed by "source:slug")
574
+ // ---------------------------------------------------------------------------
575
+
576
+ /** Maps "source:slug" → local serving path (e.g. "/asset-library/polyhaven/arm-chair-01/..."). */
577
+ const downloadedAssetPaths = new Map<string, string>();
578
+
579
+ /** Listeners notified when the download cache changes. */
580
+ const downloadListeners = new Set<() => void>();
581
+
582
+ /** Same slug logic as the server's download endpoint. */
583
+ function toSlug(nameOrId: string): string {
584
+ return nameOrId
585
+ .toLowerCase()
586
+ .replace(/[^a-z0-9]+/g, '-')
587
+ .replace(/^-|-$/g, '');
588
+ }
589
+
590
+ function assetCacheKey(source: string, nameOrId: string): string {
591
+ return `${source}:${toSlug(nameOrId)}`;
592
+ }
593
+
594
+ /** Check if an online asset has already been downloaded. Returns local path or null. */
595
+ export function getDownloadedAssetPath(source: string, nameOrId: string): string | null {
596
+ return downloadedAssetPaths.get(assetCacheKey(source, nameOrId)) ?? null;
597
+ }
598
+
599
+ /** Subscribe to download cache changes. Returns unsubscribe function. */
600
+ export function onAssetDownloaded(cb: () => void): () => void {
601
+ downloadListeners.add(cb);
602
+ return () => downloadListeners.delete(cb);
603
+ }
604
+
605
+ /** Fetch already-downloaded assets from the server and seed the cache.
606
+ * No-op in browser mode — see the Asset Library section header comment. */
607
+ export async function loadDownloadedAssets(): Promise<void> {
608
+ if (isBrowserMode()) return;
609
+ try {
610
+ const res = await fetch(`${BASE}/asset-library/downloaded`);
611
+ if (!res.ok) return;
612
+ const data = (await res.json()) as {
613
+ downloaded: { source: string; slug: string; path: string }[];
614
+ };
615
+ for (const entry of data.downloaded) {
616
+ downloadedAssetPaths.set(`${entry.source}:${entry.slug}`, `/${entry.path}`);
617
+ }
618
+ for (const cb of downloadListeners) cb();
619
+ } catch {
620
+ /* best-effort */
621
+ }
622
+ }
623
+
624
+ /** Search for online assets via the editor server proxy. */
625
+ export async function searchOnlineAssets(params: {
626
+ source: string;
627
+ type: string;
628
+ q?: string;
629
+ category?: string;
630
+ offset?: number;
631
+ limit?: number;
632
+ }): Promise<{ assets: OnlineAsset[]; total: number }> {
633
+ if (isBrowserMode()) return { assets: [], total: 0 };
634
+ const qs = new URLSearchParams();
635
+ qs.set('source', params.source);
636
+ qs.set('type', params.type);
637
+ if (params.q) qs.set('q', params.q);
638
+ if (params.category) qs.set('category', params.category);
639
+ if (params.offset != null) qs.set('offset', String(params.offset));
640
+ if (params.limit != null) qs.set('limit', String(params.limit));
641
+
642
+ const res = await fetch(`${BASE}/asset-library/search?${qs}`);
643
+ if (!res.ok) return { assets: [], total: 0 };
644
+ return (await res.json()) as { assets: OnlineAsset[]; total: number };
645
+ }
646
+
647
+ /** Get available file options (resolutions/formats) for an online asset. */
648
+ export async function getOnlineAssetFiles(source: string, id: string): Promise<AssetFileOption[]> {
649
+ if (isBrowserMode()) return [];
650
+ const qs = new URLSearchParams({ source, id });
651
+ const res = await fetch(`${BASE}/asset-library/files?${qs}`);
652
+ if (!res.ok) return [];
653
+ const data = (await res.json()) as { files: AssetFileOption[] };
654
+ return data.files;
655
+ }
656
+
657
+ /** Download an online asset into the project's public/asset-library/ folder. */
658
+ export async function downloadOnlineAsset(params: {
659
+ source: string;
660
+ id: string;
661
+ name: string;
662
+ url: string;
663
+ format: string;
664
+ includes?: { relativePath: string; url: string; size: number }[];
665
+ }): Promise<{ ok: boolean; path?: string; alreadyExists?: boolean; error?: string }> {
666
+ if (isBrowserMode()) {
667
+ throw new Error(
668
+ 'Downloading online assets needs the Node dev server (Poly Haven/ambientcg access is proxied ' +
669
+ 'server-side). Not available in browser mode.',
670
+ );
671
+ }
672
+ const res = await fetch(`${BASE}/asset-library/download`, {
673
+ method: 'POST',
674
+ headers: { 'Content-Type': 'application/json' },
675
+ body: JSON.stringify(params),
676
+ });
677
+ const result = (await res.json()) as {
678
+ ok: boolean;
679
+ path?: string;
680
+ alreadyExists?: boolean;
681
+ error?: string;
682
+ };
683
+ // Cache the local path on success
684
+ if (result.ok && result.path) {
685
+ downloadedAssetPaths.set(assetCacheKey(params.source, params.name), `/${result.path}`);
686
+ for (const cb of downloadListeners) cb();
687
+ }
688
+ return result;
689
+ }
690
+
691
+ /** Get Poly Haven categories for a given asset type. */
692
+ export async function getAssetLibraryCategories(type: string, source?: string): Promise<string[]> {
693
+ if (isBrowserMode()) return [];
694
+ const params = new URLSearchParams({ type });
695
+ if (source) params.set('source', source);
696
+ const res = await fetch(`${BASE}/asset-library/categories?${params}`);
697
+ if (!res.ok) return [];
698
+ const data = (await res.json()) as { categories: string[] };
699
+ return data.categories;
700
+ }