@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,150 @@
1
+ /**
2
+ * Shared `StorageBackend` implementation over a `FileSystemDirectoryHandle`.
3
+ *
4
+ * Both browser backends are the same code over a different root handle:
5
+ * - `OpfsStorage` — root is `navigator.storage.getDirectory()` (sandboxed, no
6
+ * prompt; the zero-friction default for a server-less editor).
7
+ * - `FsaStorage` — root is a handle from `showDirectoryPicker()`, i.e. a REAL
8
+ * on-disk folder the user chose (File System Access API).
9
+ *
10
+ * The handle API is identical for both, so all path logic lives here; subclasses
11
+ * only supply the root handle (and, for FSA, a permission gate).
12
+ *
13
+ * No OS file-change notifications exist for either, so `watch` derives events
14
+ * from this backend's own mutations — in a server-less editor the editor is the
15
+ * sole writer, which is exactly the change signal HMR needs.
16
+ */
17
+
18
+ import { dirAndBase, normalize, segments } from './paths';
19
+ import type { DirEntry, Stat, StorageBackend, WatchCallback, WatchEvent } from './types';
20
+
21
+ export abstract class HandleStorage implements StorageBackend {
22
+ abstract readonly id: string;
23
+
24
+ private readonly watchers = new Set<WatchCallback>();
25
+
26
+ /** Resolve the backend's root directory handle (subclass-supplied). */
27
+ protected abstract rootHandle(): Promise<FileSystemDirectoryHandle>;
28
+
29
+ private emit(event: WatchEvent): void {
30
+ for (const cb of this.watchers) cb(event);
31
+ }
32
+
33
+ /** Walk to a directory handle, optionally creating intermediate dirs. */
34
+ private async dirHandle(
35
+ path: string,
36
+ create: boolean,
37
+ ): Promise<FileSystemDirectoryHandle | null> {
38
+ let handle = await this.rootHandle();
39
+ for (const seg of segments(path)) {
40
+ try {
41
+ handle = await handle.getDirectoryHandle(seg, { create });
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ return handle;
47
+ }
48
+
49
+ /** Resolve a file handle from its parent directory + base name. */
50
+ private async fileHandle(path: string, create: boolean): Promise<FileSystemFileHandle | null> {
51
+ const [dir, base] = dirAndBase(path);
52
+ if (!base) return null;
53
+ const parent = await this.dirHandle(dir, create);
54
+ if (!parent) return null;
55
+ try {
56
+ return await parent.getFileHandle(base, { create });
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ async read(path: string): Promise<string> {
63
+ const bytes = await this.readBytes(path);
64
+ return new TextDecoder().decode(bytes);
65
+ }
66
+
67
+ async readBytes(path: string): Promise<Uint8Array> {
68
+ const handle = await this.fileHandle(path, false);
69
+ if (!handle) throw new Error(`${this.id}.read: no such file '${normalize(path)}'`);
70
+ const file = await handle.getFile();
71
+ return new Uint8Array(await file.arrayBuffer());
72
+ }
73
+
74
+ async write(path: string, data: string | Uint8Array): Promise<void> {
75
+ const existed = await this.exists(path);
76
+ const handle = await this.fileHandle(path, true);
77
+ if (!handle) throw new Error(`${this.id}.write: cannot create '${normalize(path)}'`);
78
+ const writable = await handle.createWritable();
79
+ // `new Uint8Array(data)` copies into an ArrayBuffer-backed view, satisfying
80
+ // the DOM BufferSource type (TS 5.9's default generic is ArrayBufferLike).
81
+ await writable.write(typeof data === 'string' ? data : new Uint8Array(data));
82
+ await writable.close();
83
+ this.emit({ type: existed ? 'update' : 'create', path: normalize(path) });
84
+ }
85
+
86
+ async list(dir: string): Promise<DirEntry[]> {
87
+ const handle = await this.dirHandle(dir, false);
88
+ if (!handle) throw new Error(`${this.id}.list: no such directory '${normalize(dir)}'`);
89
+ const base = normalize(dir);
90
+ const entries: DirEntry[] = [];
91
+ // FileSystemDirectoryHandle is an async iterable of [name, handle].
92
+ for await (const [name, child] of handle as AsyncIterable<[string, FileSystemHandle]> &
93
+ FileSystemDirectoryHandle) {
94
+ const isDir = child.kind === 'directory';
95
+ const entry: DirEntry = {
96
+ name,
97
+ path: base ? `${base}/${name}` : name,
98
+ type: isDir ? 'dir' : 'file',
99
+ };
100
+ if (!isDir) entry.size = (await (child as FileSystemFileHandle).getFile()).size;
101
+ entries.push(entry);
102
+ }
103
+ // Directories first, then alphabetical — matches the server's /assets route.
104
+ entries.sort((a, b) => {
105
+ if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
106
+ return a.name.localeCompare(b.name);
107
+ });
108
+ return entries;
109
+ }
110
+
111
+ async stat(path: string): Promise<Stat | null> {
112
+ const norm = normalize(path);
113
+ const file = await this.fileHandle(path, false);
114
+ if (file) {
115
+ const f = await file.getFile();
116
+ return { path: norm, type: 'file', size: f.size, mtime: f.lastModified };
117
+ }
118
+ const dir = await this.dirHandle(path, false);
119
+ if (dir) return { path: norm, type: 'dir', size: 0, mtime: 0 };
120
+ return null;
121
+ }
122
+
123
+ async exists(path: string): Promise<boolean> {
124
+ return (await this.stat(path)) !== null;
125
+ }
126
+
127
+ async mkdir(path: string): Promise<void> {
128
+ const existed = await this.exists(path);
129
+ await this.dirHandle(path, true);
130
+ if (!existed) this.emit({ type: 'create', path: normalize(path) });
131
+ }
132
+
133
+ async remove(path: string): Promise<void> {
134
+ const [dir, base] = dirAndBase(path);
135
+ if (!base) return;
136
+ const parent = await this.dirHandle(dir, false);
137
+ if (!parent) return;
138
+ try {
139
+ await parent.removeEntry(base, { recursive: true });
140
+ this.emit({ type: 'remove', path: normalize(path) });
141
+ } catch {
142
+ // Already gone — treat remove as idempotent.
143
+ }
144
+ }
145
+
146
+ watch(callback: WatchCallback): () => void {
147
+ this.watchers.add(callback);
148
+ return () => this.watchers.delete(callback);
149
+ }
150
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Persist the picked project-root directory handle across reloads.
3
+ *
4
+ * The File System Access API hands back a `FileSystemDirectoryHandle` that is
5
+ * structured-cloneable, so it round-trips through IndexedDB verbatim. Persisting
6
+ * it is what lets the browser-first editor REOPEN the same on-disk folder after
7
+ * a reload instead of silently dropping back into the OPFS sandbox — the
8
+ * difference between a toy and a real local-authoring editor.
9
+ *
10
+ * Permission is a separate axis from persistence: the handle survives a reload,
11
+ * but the browser may still require a fresh user gesture (`requestPermission`)
12
+ * to re-grant read/write. Callers `queryHandlePermission()` (never prompts) on
13
+ * boot and, when it comes back `'prompt'`, surface a one-click reopen affordance
14
+ * that calls `requestHandlePermission()` from inside a click handler.
15
+ *
16
+ * All IndexedDB calls degrade to no-ops / `null` where `indexedDB` is absent
17
+ * (SSR, the Node test env, privacy modes) so importing this module is always
18
+ * safe.
19
+ */
20
+
21
+ const DB_NAME = 'vgai-editor';
22
+ const STORE = 'handles';
23
+ /** Single well-known key — the editor tracks one active project root at a time. */
24
+ const KEY = 'project-root';
25
+
26
+ /** Subset of the FSA permission API (not in older TS DOM lib versions). */
27
+ interface PermissionedHandle {
28
+ queryPermission?(d: { mode: 'read' | 'readwrite' }): Promise<PermissionState>;
29
+ requestPermission?(d: { mode: 'read' | 'readwrite' }): Promise<PermissionState>;
30
+ }
31
+
32
+ function hasIdb(): boolean {
33
+ return typeof indexedDB !== 'undefined';
34
+ }
35
+
36
+ function openDb(): Promise<IDBDatabase> {
37
+ return new Promise((resolve, reject) => {
38
+ const req = indexedDB.open(DB_NAME, 1);
39
+ req.onupgradeneeded = () => {
40
+ if (!req.result.objectStoreNames.contains(STORE)) req.result.createObjectStore(STORE);
41
+ };
42
+ req.onsuccess = () => resolve(req.result);
43
+ req.onerror = () => reject(req.error);
44
+ });
45
+ }
46
+
47
+ /** Remember the picked directory handle (no-op where IndexedDB is unavailable). */
48
+ export async function saveProjectHandle(handle: FileSystemDirectoryHandle): Promise<void> {
49
+ if (!hasIdb()) return;
50
+ const db = await openDb();
51
+ try {
52
+ await new Promise<void>((resolve, reject) => {
53
+ const tx = db.transaction(STORE, 'readwrite');
54
+ tx.objectStore(STORE).put(handle, KEY);
55
+ tx.oncomplete = () => resolve();
56
+ tx.onerror = () => reject(tx.error);
57
+ });
58
+ } finally {
59
+ db.close();
60
+ }
61
+ }
62
+
63
+ /** Load the remembered directory handle, or `null` if none / no IndexedDB. */
64
+ export async function loadProjectHandle(): Promise<FileSystemDirectoryHandle | null> {
65
+ if (!hasIdb()) return null;
66
+ const db = await openDb();
67
+ try {
68
+ return await new Promise<FileSystemDirectoryHandle | null>((resolve, reject) => {
69
+ const tx = db.transaction(STORE, 'readonly');
70
+ const req = tx.objectStore(STORE).get(KEY);
71
+ req.onsuccess = () => resolve((req.result as FileSystemDirectoryHandle | undefined) ?? null);
72
+ req.onerror = () => reject(req.error);
73
+ });
74
+ } finally {
75
+ db.close();
76
+ }
77
+ }
78
+
79
+ /** Forget the remembered handle (e.g. after permission is permanently denied). */
80
+ export async function clearProjectHandle(): Promise<void> {
81
+ if (!hasIdb()) return;
82
+ const db = await openDb();
83
+ try {
84
+ await new Promise<void>((resolve, reject) => {
85
+ const tx = db.transaction(STORE, 'readwrite');
86
+ tx.objectStore(STORE).delete(KEY);
87
+ tx.oncomplete = () => resolve();
88
+ tx.onerror = () => reject(tx.error);
89
+ });
90
+ } finally {
91
+ db.close();
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Query the current read/write permission for a handle WITHOUT prompting.
97
+ * A handle with no permission API (e.g. OPFS) is treated as already granted.
98
+ */
99
+ export async function queryHandlePermission(
100
+ handle: FileSystemDirectoryHandle,
101
+ ): Promise<PermissionState> {
102
+ const h = handle as FileSystemDirectoryHandle & PermissionedHandle;
103
+ if (!h.queryPermission) return 'granted';
104
+ return h.queryPermission({ mode: 'readwrite' });
105
+ }
106
+
107
+ /**
108
+ * Request read/write permission for a handle — MUST be called from within a
109
+ * user gesture (it may prompt). Resolves `true` once permission is granted.
110
+ */
111
+ export async function requestHandlePermission(handle: FileSystemDirectoryHandle): Promise<boolean> {
112
+ const h = handle as FileSystemDirectoryHandle & PermissionedHandle;
113
+ if (!h.requestPermission) return true;
114
+ if ((await queryHandlePermission(handle)) === 'granted') return true;
115
+ return (await h.requestPermission({ mode: 'readwrite' })) === 'granted';
116
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Server-backed `StorageBackend` for the local (Node) editor.
3
+ *
4
+ * A thin adapter over the editor server's existing routes — it changes nothing
5
+ * about how the local editor talks to disk, it just presents that surface behind
6
+ * the shared interface. The backend is rooted at the project's `public/` folder,
7
+ * matching the server's `/__editor/assets` (list) and `/__editor/save-file`
8
+ * (write) routes; reads come from Vite's `/@fs/` static serving.
9
+ *
10
+ * Route mapping (paths are relative to `public/`):
11
+ * list → GET /__editor/assets?dir → { entries: [{name,type,size}] }
12
+ * write → POST /__editor/save-file ({path, content, encoding?})
13
+ * read → GET /@fs/{projectRoot}/public/{path}
14
+ * watch → EventSource /__editor/events (chokidar-backed)
15
+ *
16
+ * The CLI control channel, native dialogs, `export`, online asset library, and
17
+ * project/editor-state persistence are NOT file operations and live outside this
18
+ * interface (see types.ts and editor-api.ts).
19
+ */
20
+
21
+ import { dirAndBase, normalize } from './paths';
22
+ import type { DirEntry, Stat, StorageBackend, WatchCallback, WatchEvent } from './types';
23
+
24
+ const BASE = '/__editor';
25
+
26
+ interface ServerAssetEntry {
27
+ name: string;
28
+ type: 'file' | 'directory';
29
+ size: number;
30
+ }
31
+
32
+ export class HttpStorage implements StorageBackend {
33
+ readonly id = 'http';
34
+
35
+ /** Absolute project root on the server (used for /@fs reads). */
36
+ constructor(private readonly projectRoot: string) {}
37
+
38
+ async read(path: string): Promise<string> {
39
+ const res = await fetch(`/@fs/${this.projectRoot}/public/${normalize(path)}`);
40
+ if (!res.ok) throw new Error(`HttpStorage.read '${normalize(path)}': HTTP ${res.status}`);
41
+ return res.text();
42
+ }
43
+
44
+ async readBytes(path: string): Promise<Uint8Array> {
45
+ const res = await fetch(`/@fs/${this.projectRoot}/public/${normalize(path)}`);
46
+ if (!res.ok) throw new Error(`HttpStorage.readBytes '${normalize(path)}': HTTP ${res.status}`);
47
+ return new Uint8Array(await res.arrayBuffer());
48
+ }
49
+
50
+ async write(path: string, data: string | Uint8Array): Promise<void> {
51
+ const body =
52
+ typeof data === 'string'
53
+ ? { path: normalize(path), content: data }
54
+ : { path: normalize(path), content: bytesToBase64(data), encoding: 'base64' as const };
55
+ const res = await fetch(`${BASE}/save-file`, {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify(body),
59
+ });
60
+ if (!res.ok) throw new Error(`HttpStorage.write '${normalize(path)}': HTTP ${res.status}`);
61
+ }
62
+
63
+ async list(dir: string): Promise<DirEntry[]> {
64
+ const res = await fetch(`${BASE}/assets?${new URLSearchParams({ dir: normalize(dir) })}`);
65
+ if (!res.ok) throw new Error(`HttpStorage.list '${normalize(dir)}': HTTP ${res.status}`);
66
+ const { entries } = (await res.json()) as { entries: ServerAssetEntry[] };
67
+ return entries.map((e) => {
68
+ const out: DirEntry = {
69
+ name: e.name,
70
+ path: normalize(dir ? `${dir}/${e.name}` : e.name),
71
+ type: e.type === 'directory' ? 'dir' : 'file',
72
+ };
73
+ if (e.type === 'file') out.size = e.size;
74
+ return out;
75
+ });
76
+ }
77
+
78
+ async stat(path: string): Promise<Stat | null> {
79
+ // The server exposes listings, not a per-path stat. Derive from the parent
80
+ // listing — sufficient for the editor's needs (existence + type + size).
81
+ const [dir, base] = dirAndBase(path);
82
+ if (!base) return { path: '', type: 'dir', size: 0, mtime: 0 };
83
+ let entries: DirEntry[];
84
+ try {
85
+ entries = await this.list(dir);
86
+ } catch {
87
+ return null;
88
+ }
89
+ const hit = entries.find((e) => e.name === base);
90
+ return hit ? { path: hit.path, type: hit.type, size: hit.size ?? 0, mtime: 0 } : null;
91
+ }
92
+
93
+ async exists(path: string): Promise<boolean> {
94
+ return (await this.stat(path)) !== null;
95
+ }
96
+
97
+ async mkdir(path: string): Promise<void> {
98
+ // The server has no empty-dir route; directories materialize when their
99
+ // first file is written (save-file creates parents). No-op here.
100
+ void path;
101
+ }
102
+
103
+ async remove(path: string): Promise<void> {
104
+ const res = await fetch(`${BASE}/save-file`, {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify({ path: normalize(path), delete: true }),
108
+ });
109
+ if (!res.ok) throw new Error(`HttpStorage.remove '${normalize(path)}': HTTP ${res.status}`);
110
+ }
111
+
112
+ watch(callback: WatchCallback): () => void {
113
+ const source = new EventSource(`${BASE}/events`);
114
+ const handler = (ev: MessageEvent) => {
115
+ try {
116
+ const data = JSON.parse(ev.data) as { type?: string; file?: string; path?: string };
117
+ const path = data.path ?? data.file;
118
+ if (!path) return;
119
+ callback({ type: (data.type as WatchEvent['type']) ?? 'update', path: normalize(path) });
120
+ } catch {
121
+ // Ignore non-JSON keepalive frames.
122
+ }
123
+ };
124
+ source.addEventListener('message', handler);
125
+ return () => {
126
+ source.removeEventListener('message', handler);
127
+ source.close();
128
+ };
129
+ }
130
+ }
131
+
132
+ /** Encode bytes as base64 for the save-file route's `encoding: 'base64'` mode. */
133
+ function bytesToBase64(bytes: Uint8Array): string {
134
+ let binary = '';
135
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!);
136
+ return btoa(binary);
137
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Storage backend selection.
3
+ *
4
+ * One backend-agnostic file interface, three implementations:
5
+ * - `HttpStorage` — the local (Node server) editor; wraps `/__editor/*`.
6
+ * - `OpfsStorage` — server-less browser editor; sandboxed OPFS.
7
+ * - `FsaStorage` — server-less browser editor over a REAL on-disk folder
8
+ * (File System Access API; Chromium).
9
+ *
10
+ * Upstream editor/engine code should depend only on `StorageBackend` from
11
+ * `./types` and obtain an instance via `getStorageBackend()` — never branch on
12
+ * the environment itself.
13
+ */
14
+
15
+ import { FsaStorage } from './fsa-storage';
16
+ import {
17
+ clearProjectHandle,
18
+ loadProjectHandle,
19
+ queryHandlePermission,
20
+ requestHandlePermission,
21
+ saveProjectHandle,
22
+ } from './handle-store';
23
+ import { HttpStorage } from './http-storage';
24
+ import { OpfsStorage } from './opfs-storage';
25
+ import type { HostCapabilities, StorageBackend } from './types';
26
+
27
+ export { FsaStorage } from './fsa-storage';
28
+ export { HttpStorage } from './http-storage';
29
+ export { MemStorage } from './mem-storage';
30
+ export { OpfsStorage } from './opfs-storage';
31
+ export type { DirEntry, HostCapabilities, Stat, StorageBackend, WatchEvent } from './types';
32
+
33
+ import { getEditorMode, getProjectDefinePath } from '../editor-mode';
34
+
35
+ let _backend: StorageBackend | null = null;
36
+
37
+ /**
38
+ * Get the active storage backend, defaulted by build mode:
39
+ * - server-first → `HttpStorage` (edits the on-disk project via the server)
40
+ * - browser-first → `OpfsStorage` (in-browser OPFS VFS, no server)
41
+ * Call `setStorageBackend()` to override (e.g. a File System Access root the
42
+ * user picked, or a test double).
43
+ */
44
+ export function getStorageBackend(): StorageBackend {
45
+ if (_backend) return _backend;
46
+ _backend =
47
+ getEditorMode() === 'server' ? new HttpStorage(getProjectDefinePath()) : new OpfsStorage();
48
+ return _backend;
49
+ }
50
+
51
+ /** Override the active backend (FSA-picked root, tests, etc.). */
52
+ export function setStorageBackend(backend: StorageBackend): void {
53
+ _backend = backend;
54
+ }
55
+
56
+ /** Subset of the File System Access API used for picking a project root. */
57
+ interface DirectoryPickerWindow {
58
+ showDirectoryPicker?(opts?: { mode?: 'read' | 'readwrite' }): Promise<FileSystemDirectoryHandle>;
59
+ }
60
+
61
+ /**
62
+ * Host capabilities for the current environment — the non-file operations that
63
+ * do not belong on `StorageBackend` (native dialogs, reveal-in-OS).
64
+ *
65
+ * `pickProjectRoot` is present only where the File System Access API exists
66
+ * (Chromium): it prompts for a real on-disk folder, makes an `FsaStorage` rooted
67
+ * there the active backend, and returns it. `reveal` is omitted in the browser.
68
+ */
69
+ export function getHostCapabilities(): HostCapabilities {
70
+ const picker = (window as unknown as DirectoryPickerWindow).showDirectoryPicker;
71
+ if (!picker) return {};
72
+ return {
73
+ async pickProjectRoot(): Promise<StorageBackend | null> {
74
+ try {
75
+ const handle = await picker.call(window, { mode: 'readwrite' });
76
+ const backend = new FsaStorage(handle);
77
+ setStorageBackend(backend);
78
+ // Remember the folder so a reload reopens it instead of falling back to
79
+ // the OPFS sandbox (see restorePersistedProjectRoot). Never blocks the
80
+ // pick: a storage failure here only forfeits the reopen-on-reload nicety.
81
+ await saveProjectHandle(handle).catch((err) =>
82
+ console.warn('[vgai] could not persist project folder handle:', err),
83
+ );
84
+ return backend;
85
+ } catch {
86
+ // User cancelled the picker.
87
+ return null;
88
+ }
89
+ },
90
+ };
91
+ }
92
+
93
+ /** A remembered on-disk project folder and whether it is usable right now. */
94
+ export interface PersistedProjectRoot {
95
+ /** The folder's display name (its base name). */
96
+ name: string;
97
+ /**
98
+ * `'granted'` — permission survived the reload; this call already installed
99
+ * an `FsaStorage` over it as the active backend. `'prompt'` — the folder is
100
+ * remembered but needs a fresh user gesture to re-grant read/write, so it is
101
+ * NOT installed; call `reopenPersistedProjectRoot()` from a click handler.
102
+ */
103
+ status: 'granted' | 'prompt';
104
+ }
105
+
106
+ /**
107
+ * On boot (browser mode), try to silently reopen the last-picked on-disk folder.
108
+ *
109
+ * The File System Access spec forbids prompting outside a user gesture, so this
110
+ * only *queries* permission: when it is still `'granted'`, it installs an
111
+ * `FsaStorage` over the remembered handle as the active backend and reports
112
+ * `'granted'`; when it is `'prompt'`, it reports `'prompt'` without installing
113
+ * (the caller shows a reopen affordance); a permanently-`'denied'` handle is
114
+ * forgotten. Returns `null` when nothing is remembered.
115
+ */
116
+ export async function restorePersistedProjectRoot(): Promise<PersistedProjectRoot | null> {
117
+ const handle = await loadProjectHandle();
118
+ if (!handle) return null;
119
+ const status = await queryHandlePermission(handle);
120
+ if (status === 'denied') {
121
+ await clearProjectHandle();
122
+ return null;
123
+ }
124
+ if (status === 'granted') setStorageBackend(new FsaStorage(handle));
125
+ return { name: handle.name, status };
126
+ }
127
+
128
+ /**
129
+ * Re-grant permission to the remembered on-disk folder and install it as the
130
+ * active backend. MUST be called from within a user gesture — it may prompt.
131
+ * Resolves to the installed backend, or `null` if nothing is remembered or the
132
+ * user denied the prompt.
133
+ */
134
+ export async function reopenPersistedProjectRoot(): Promise<StorageBackend | null> {
135
+ const handle = await loadProjectHandle();
136
+ if (!handle) return null;
137
+ if (!(await requestHandlePermission(handle))) return null;
138
+ const backend = new FsaStorage(handle);
139
+ setStorageBackend(backend);
140
+ return backend;
141
+ }
142
+
143
+ /** Display name of the remembered on-disk folder, or `null` if none. */
144
+ export async function peekPersistedProjectRootName(): Promise<string | null> {
145
+ const handle = await loadProjectHandle();
146
+ return handle?.name ?? null;
147
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Pure in-memory `StorageBackend` — a virtual filesystem backed by a `Map`.
3
+ *
4
+ * Nothing touches disk or OPFS; files live as bytes in JS and vanish on reload.
5
+ * The most "virtual" of the backends, and the simplest. Useful for:
6
+ * - tests (deterministic, instant, no OPFS cleanup),
7
+ * - ephemeral "try it" playgrounds (open, hack, close — nothing persisted),
8
+ * - seeding (load a bundled template into memory, then optionally flush to an
9
+ * `FsaStorage`/`OpfsStorage` to persist).
10
+ *
11
+ * Implements the same interface as every other backend, so it is a drop-in via
12
+ * `setStorageBackend(new MemStorage())`.
13
+ */
14
+
15
+ import { dirAndBase, normalize } from './paths';
16
+ import type { DirEntry, Stat, StorageBackend, WatchCallback, WatchEvent } from './types';
17
+
18
+ type Node = { type: 'file'; data: Uint8Array; mtime: number } | { type: 'dir'; mtime: number };
19
+
20
+ export class MemStorage implements StorageBackend {
21
+ readonly id = 'mem';
22
+
23
+ /** Normalized path → node. The root (`''`) is implicit. */
24
+ private readonly nodes = new Map<string, Node>();
25
+ private readonly watchers = new Set<WatchCallback>();
26
+
27
+ /** Optionally seed the VFS from a `{ path: contents }` map. */
28
+ constructor(seed?: Record<string, string | Uint8Array>) {
29
+ if (seed) for (const [path, data] of Object.entries(seed)) this.writeSync(path, data);
30
+ }
31
+
32
+ private emit(event: WatchEvent): void {
33
+ for (const cb of this.watchers) cb(event);
34
+ }
35
+
36
+ /** Ensure every ancestor directory of `path` exists. */
37
+ private ensureParents(path: string): void {
38
+ const parts = normalize(path).split('/');
39
+ parts.pop();
40
+ let cur = '';
41
+ for (const part of parts) {
42
+ cur = cur ? `${cur}/${part}` : part;
43
+ if (this.nodes.get(cur)?.type !== 'dir') this.nodes.set(cur, { type: 'dir', mtime: now() });
44
+ }
45
+ }
46
+
47
+ private writeSync(path: string, data: string | Uint8Array): boolean {
48
+ const norm = normalize(path);
49
+ this.ensureParents(norm);
50
+ const existed = this.nodes.get(norm)?.type === 'file';
51
+ const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : new Uint8Array(data);
52
+ this.nodes.set(norm, { type: 'file', data: bytes, mtime: now() });
53
+ return existed;
54
+ }
55
+
56
+ async read(path: string): Promise<string> {
57
+ return new TextDecoder().decode(await this.readBytes(path));
58
+ }
59
+
60
+ async readBytes(path: string): Promise<Uint8Array> {
61
+ const node = this.nodes.get(normalize(path));
62
+ if (node?.type !== 'file')
63
+ throw new Error(`MemStorage.read: no such file '${normalize(path)}'`);
64
+ return node.data;
65
+ }
66
+
67
+ async write(path: string, data: string | Uint8Array): Promise<void> {
68
+ const existed = this.writeSync(path, data);
69
+ this.emit({ type: existed ? 'update' : 'create', path: normalize(path) });
70
+ }
71
+
72
+ async list(dir: string): Promise<DirEntry[]> {
73
+ const dn = normalize(dir);
74
+ if (dn !== '' && this.nodes.get(dn)?.type !== 'dir') {
75
+ throw new Error(`MemStorage.list: no such directory '${dn}'`);
76
+ }
77
+ const entries: DirEntry[] = [];
78
+ for (const [path, node] of this.nodes) {
79
+ const [parent, base] = dirAndBase(path);
80
+ if (parent !== dn || !base) continue;
81
+ const entry: DirEntry = { name: base, path, type: node.type === 'dir' ? 'dir' : 'file' };
82
+ if (node.type === 'file') entry.size = node.data.length;
83
+ entries.push(entry);
84
+ }
85
+ entries.sort((a, b) => {
86
+ if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
87
+ return a.name.localeCompare(b.name);
88
+ });
89
+ return entries;
90
+ }
91
+
92
+ async stat(path: string): Promise<Stat | null> {
93
+ const norm = normalize(path);
94
+ if (norm === '') return { path: '', type: 'dir', size: 0, mtime: 0 };
95
+ const node = this.nodes.get(norm);
96
+ if (!node) return null;
97
+ return node.type === 'file'
98
+ ? { path: norm, type: 'file', size: node.data.length, mtime: node.mtime }
99
+ : { path: norm, type: 'dir', size: 0, mtime: node.mtime };
100
+ }
101
+
102
+ async exists(path: string): Promise<boolean> {
103
+ return (await this.stat(path)) !== null;
104
+ }
105
+
106
+ async mkdir(path: string): Promise<void> {
107
+ const norm = normalize(path);
108
+ if (!norm || this.nodes.get(norm)?.type === 'dir') return;
109
+ this.ensureParents(norm);
110
+ this.nodes.set(norm, { type: 'dir', mtime: now() });
111
+ this.emit({ type: 'create', path: norm });
112
+ }
113
+
114
+ async remove(path: string): Promise<void> {
115
+ const norm = normalize(path);
116
+ if (!norm) return;
117
+ let removed = false;
118
+ for (const key of [...this.nodes.keys()]) {
119
+ if (key === norm || key.startsWith(`${norm}/`)) {
120
+ this.nodes.delete(key);
121
+ removed = true;
122
+ }
123
+ }
124
+ if (removed) this.emit({ type: 'remove', path: norm });
125
+ }
126
+
127
+ watch(callback: WatchCallback): () => void {
128
+ this.watchers.add(callback);
129
+ return () => this.watchers.delete(callback);
130
+ }
131
+ }
132
+
133
+ function now(): number {
134
+ return Date.now();
135
+ }