@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,552 @@
1
+ /**
2
+ * UIEditStore — the editing state + mutation ops for the UI authoring surface.
3
+ *
4
+ * Holds the UI roots (the .vscn `ui` tree, the source of truth — C9), the
5
+ * selection, a 50-deep undo stack, a clipboard, the live data source for bindings,
6
+ * and named board states/scenarios (C12). Plain observable store (subscribe/
7
+ * version) so React binds via useSyncExternalStore. Edits route through here;
8
+ * each user gesture pushes exactly one undo entry (preview-vs-commit, C6/K4).
9
+ */
10
+ import type { UIAnimation, UINode, UIRoot } from '@engine/scene/schema';
11
+ import { createDataSource, type UIDataSource } from '@vgai/scene-ui/data-source';
12
+ import { notifyUndoableOp, registerUndoAdapter } from '../authoring/undo-timeline';
13
+ import {
14
+ cloneWithNewIds,
15
+ countNodes,
16
+ deleteNodePath,
17
+ duplicateNode,
18
+ ensureIds,
19
+ findNode,
20
+ insertNode,
21
+ moveNode,
22
+ newId,
23
+ removeNode,
24
+ reorderNode,
25
+ setNodePath,
26
+ unwrapNode,
27
+ updateNode,
28
+ wrapNode,
29
+ } from './node-ops';
30
+
31
+ const MAX_UNDO = 50;
32
+
33
+ /** A default node skeleton for a newly-created node of a given kind (palette). */
34
+ export function defaultNodeForKind(kind: UINode['kind']): UINode {
35
+ const id = newId(kind);
36
+ const base: UINode = { id, name: capitalize(kind), kind };
37
+ switch (kind) {
38
+ case 'text':
39
+ return { ...base, props: { text: 'Text' } };
40
+ case 'button':
41
+ return {
42
+ ...base,
43
+ props: { text: 'Button' },
44
+ style: { backgroundColor: '#4af', color: '#000', borderRadius: 6 },
45
+ };
46
+ case 'box':
47
+ return { ...base, layout: { direction: 'row', gap: 8 }, style: {} };
48
+ case 'panel':
49
+ return {
50
+ ...base,
51
+ layout: { padding: [8, 8, 8, 8] },
52
+ style: { backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 6 },
53
+ };
54
+ case 'grid':
55
+ return { ...base, layout: { columns: 3, gap: 8 } };
56
+ case 'image':
57
+ return { ...base, layout: { width: 48, height: 48 }, props: {} };
58
+ case 'progress':
59
+ return {
60
+ ...base,
61
+ layout: { width: 120, height: 10 },
62
+ props: { value: 50, max: 100 },
63
+ style: { backgroundColor: '#4af' },
64
+ };
65
+ case 'input':
66
+ return { ...base, props: { placeholder: 'Input' } };
67
+ case 'toggle':
68
+ return { ...base, props: { text: 'Toggle', checked: false } };
69
+ case 'slider':
70
+ return { ...base, props: { value: 50, min: 0, max: 100 } };
71
+ default:
72
+ return base;
73
+ }
74
+ }
75
+
76
+ function capitalize(s: string): string {
77
+ return s.charAt(0).toUpperCase() + s.slice(1);
78
+ }
79
+
80
+ /**
81
+ * The minimal schema-valid empty canvas root (design/24-scene-ui.md D10):
82
+ * `UIRootSchema` requires only `kind:'canvas'` plus the shared node fields
83
+ * (schema/ui.ts) — `space`/`scaling`/`theme` are all optional. Seeds a
84
+ * ui-less scene in PROJECT mode (a registry is present) instead of the demo
85
+ * fixture `COMPLEX_UI_ROOTS` — an author's own project should never land the
86
+ * demo tree, only an empty stage to build on.
87
+ */
88
+ export function defaultCanvasRoot(): UIRoot {
89
+ return { id: newId('canvas'), kind: 'canvas', name: 'Canvas', space: 'screen', children: [] };
90
+ }
91
+
92
+ export interface UIEditStoreOptions {
93
+ /** Initial live data source state for bindings (game state). */
94
+ data?: Record<string, unknown>;
95
+ /** Called after a committed edit so the host can persist to .vscn (C9). */
96
+ onCommit?: (roots: UIRoot[]) => void;
97
+ }
98
+
99
+ export class UIEditStore {
100
+ private _roots: UIRoot[];
101
+ private _selection: string[] = [];
102
+ private _undo: string[] = [];
103
+ private _redo: string[] = [];
104
+ private _clipboard: UINode | null = null;
105
+ private _version = 0;
106
+ private _listeners = new Set<() => void>();
107
+ private _interactive: string | null = null; // snapshot taken at gesture start
108
+ private _states = new Map<string, Record<string, unknown>>();
109
+ private _sourceOid: string | null = null;
110
+ private _locale = 'en';
111
+ private _previewResolution: { width: number; height: number } | null = null;
112
+ private _onCommit: ((roots: UIRoot[]) => void) | undefined;
113
+
114
+ readonly data: UIDataSource;
115
+
116
+ constructor(roots: UIRoot[], opts: UIEditStoreOptions = {}) {
117
+ this._roots = ensureIds(roots);
118
+ this.data = createDataSource(opts.data ?? {});
119
+ this._onCommit = opts.onCommit;
120
+ // T3.2 slice 2: this store IS the undo/redo target the editor-global timeline
121
+ // dispatches to for entries it records below (`pushUndo`/`endEdit`). C3's
122
+ // `edit-mode-authoring.ts` (`exitEditModeAuthoring`) unregisters it on
123
+ // teardown for the unified editor's scene-UI child; the standalone
124
+ // harness (harness.tsx) never tears down, so it stays registered for the
125
+ // page's lifetime — harmless, mirrors `EditorStore`.
126
+ registerUndoAdapter(this);
127
+ }
128
+
129
+ // --- subscription ---
130
+ subscribe = (l: () => void): (() => void) => {
131
+ this._listeners.add(l);
132
+ return () => this._listeners.delete(l);
133
+ };
134
+ getVersion = (): number => this._version;
135
+ private notify(): void {
136
+ this._version += 1;
137
+ for (const l of this._listeners) l();
138
+ }
139
+
140
+ // --- reads ---
141
+ get roots(): UIRoot[] {
142
+ return this._roots;
143
+ }
144
+ get selection(): string[] {
145
+ return this._selection;
146
+ }
147
+ get primary(): string | null {
148
+ return this._selection[this._selection.length - 1] ?? null;
149
+ }
150
+ get locale(): string {
151
+ return this._locale;
152
+ }
153
+ get previewResolution(): { width: number; height: number } | null {
154
+ return this._previewResolution;
155
+ }
156
+ get clipboardHasContent(): boolean {
157
+ return this._clipboard !== null;
158
+ }
159
+ get canUndo(): boolean {
160
+ return this._undo.length > 0;
161
+ }
162
+ get canRedo(): boolean {
163
+ return this._redo.length > 0;
164
+ }
165
+ /**
166
+ * True while an interactive gesture (drag/resize) is live — between
167
+ * `beginEdit()` and `endEdit()`, `_roots` holds uncommitted preview state.
168
+ * The viewport layer's content-sync (`scene-ui-layer.ts`) reads this to
169
+ * avoid reloading the committed document over an in-flight gesture, which
170
+ * would revert the drag and wipe selection/local-undo mid-motion.
171
+ */
172
+ get inGesture(): boolean {
173
+ return this._interactive != null;
174
+ }
175
+ stateNames(): string[] {
176
+ return [...this._states.keys()];
177
+ }
178
+ nodeCount(): number {
179
+ return countNodes(this._roots);
180
+ }
181
+ find(id: string) {
182
+ return findNode(this._roots, id);
183
+ }
184
+
185
+ // --- selection ---
186
+ select(ids: string[]): void {
187
+ this._selection = ids;
188
+ this.notify();
189
+ }
190
+ selectOne(id: string | null, additive = false): void {
191
+ if (id == null) this._selection = [];
192
+ else if (additive)
193
+ this._selection = this._selection.includes(id)
194
+ ? this._selection.filter((s) => s !== id)
195
+ : [...this._selection, id];
196
+ else this._selection = [id];
197
+ if (!additive) this._sourceOid = null; // a node selection clears any source selection
198
+ this.notify();
199
+ }
200
+
201
+ /** The selected component-internal source element (an OID), for live source edits (C7). */
202
+ get sourceOid(): string | null {
203
+ return this._sourceOid;
204
+ }
205
+ setSourceSelection(oid: string | null): void {
206
+ this._sourceOid = oid;
207
+ this.notify();
208
+ }
209
+
210
+ // --- undo plumbing ---
211
+ private snapshot(): string {
212
+ return JSON.stringify(this._roots);
213
+ }
214
+ private pushUndo(): void {
215
+ this._undo.push(this.snapshot());
216
+ if (this._undo.length > MAX_UNDO) this._undo.shift();
217
+ this._redo = [];
218
+ notifyUndoableOp(this); // T3.2 slice 2 — tag this op on the global timeline
219
+ }
220
+ private commit(next: UIRoot[]): void {
221
+ this.pushUndo();
222
+ this._roots = next;
223
+ this._onCommit?.(this._roots);
224
+ this.notify();
225
+ }
226
+
227
+ undo(): boolean {
228
+ // `false` tells the undo timeline "couldn't act — put the entry back"
229
+ // (see UndoAdapter in ../authoring/undo-timeline.ts).
230
+ const prev = this._undo.pop();
231
+ if (prev == null) return false;
232
+ this._redo.push(this.snapshot());
233
+ this._roots = JSON.parse(prev);
234
+ this._onCommit?.(this._roots);
235
+ this.notify();
236
+ return true;
237
+ }
238
+ redo(): boolean {
239
+ const next = this._redo.pop();
240
+ if (next == null) return false;
241
+ this._undo.push(this.snapshot());
242
+ this._roots = JSON.parse(next);
243
+ this._onCommit?.(this._roots);
244
+ this.notify();
245
+ return true;
246
+ }
247
+
248
+ // --- editing (each is one undo step) ---
249
+ setProp(id: string, path: string, value: unknown): void {
250
+ this.commit(setNodePath(this._roots, id, path, value));
251
+ }
252
+ /** Multi-select shared-property edit (K9). One undo step for all targets. */
253
+ setPropMulti(ids: string[], path: string, value: unknown): void {
254
+ let next = this._roots;
255
+ for (const id of ids) next = setNodePath(next, id, path, value);
256
+ this.commit(next);
257
+ }
258
+ deleteProp(id: string, path: string): void {
259
+ this.commit(deleteNodePath(this._roots, id, path));
260
+ }
261
+
262
+ /** Apply multiple path->value patches to each id in ONE undo step (e.g. anchor presets). */
263
+ setMany(ids: string[], patches: Record<string, unknown>): void {
264
+ let next = this._roots;
265
+ for (const id of ids) {
266
+ for (const [path, value] of Object.entries(patches))
267
+ next = setNodePath(next, id, path, value);
268
+ }
269
+ this.commit(next);
270
+ }
271
+
272
+ addNode(parentId: string, kind: UINode['kind'], index?: number): string {
273
+ const node = defaultNodeForKind(kind);
274
+ this.commit(insertNode(this._roots, parentId, node, index));
275
+ const id = node.id as string;
276
+ this.selectOne(id);
277
+ return id;
278
+ }
279
+ addNodeValue(parentId: string, node: UINode, index?: number): string {
280
+ const withIds = cloneWithNewIds(node);
281
+ this.commit(insertNode(this._roots, parentId, withIds, index));
282
+ return withIds.id as string;
283
+ }
284
+ remove(id: string): void {
285
+ this.commit(removeNode(this._roots, id));
286
+ this._selection = this._selection.filter((s) => s !== id);
287
+ }
288
+ /** D4.R2 (spec27 §6 D4 reopen) — remove every id in ONE undo step (one
289
+ * `commit`), the `StructureProvider.removeMany` backing for
290
+ * `UIAuthoringAdapter`. `removeSelected` (below) is now just this called
291
+ * with the current selection. */
292
+ removeMany(ids: readonly string[]): void {
293
+ if (ids.length === 0) return;
294
+ let next = this._roots;
295
+ for (const id of ids) next = removeNode(next, id);
296
+ this.commit(next);
297
+ const removed = new Set(ids);
298
+ this._selection = this._selection.filter((s) => !removed.has(s));
299
+ }
300
+ removeSelected(): void {
301
+ this.removeMany(this._selection);
302
+ }
303
+ duplicate(id: string): string | null {
304
+ const res = duplicateNode(this._roots, id);
305
+ if (res.newId) {
306
+ this.commit(res.roots);
307
+ this.selectOne(res.newId);
308
+ }
309
+ return res.newId;
310
+ }
311
+ reparent(id: string, newParentId: string, index?: number): void {
312
+ this.commit(moveNode(this._roots, id, newParentId, index));
313
+ }
314
+ reorder(id: string, newIndex: number): void {
315
+ this.commit(reorderNode(this._roots, id, newIndex));
316
+ }
317
+ /** Wrap a node in a new container (C8). Returns the wrapper id. */
318
+ wrap(id: string, kind: UINode['kind'] = 'box'): string | null {
319
+ const res = wrapNode(this._roots, id, kind);
320
+ if (res.wrapperId) {
321
+ this.commit(res.roots);
322
+ this.selectOne(res.wrapperId);
323
+ }
324
+ return res.wrapperId;
325
+ }
326
+ /** Unwrap a container, splicing its children into the parent (C8). */
327
+ unwrap(id: string): void {
328
+ this.commit(unwrapNode(this._roots, id));
329
+ }
330
+ /** Inline text edit (C8) — set a node's text prop. */
331
+ editText(id: string, text: string): void {
332
+ this.commit(setNodePath(this._roots, id, 'props.text', text));
333
+ }
334
+
335
+ // --- keyframe / timeline animation (S1) ---
336
+
337
+ /** Map a node's animation clip immutably and commit (one undo step). */
338
+ private updateClip(id: string, animIndex: number, fn: (clip: UIAnimation) => UIAnimation): void {
339
+ this.commit(
340
+ updateNode(this._roots, id, (node) => {
341
+ const anims = [...(node.animations ?? [])];
342
+ const clip = anims[animIndex];
343
+ if (!clip) return node;
344
+ anims[animIndex] = fn(clip);
345
+ return { ...node, animations: anims };
346
+ }),
347
+ );
348
+ }
349
+
350
+ /** Add a (default) animation clip to a node. Returns its index. */
351
+ addAnimation(id: string, clip?: UIAnimation): number {
352
+ const loc = findNode(this._roots, id);
353
+ if (!loc) return -1;
354
+ const count = loc.node.animations?.length ?? 0;
355
+ const next: UIAnimation = clip ?? {
356
+ name: `clip${count + 1}`,
357
+ duration: 1,
358
+ loop: false,
359
+ tracks: [],
360
+ };
361
+ this.commit(
362
+ updateNode(this._roots, id, (node) => ({
363
+ ...node,
364
+ animations: [...(node.animations ?? []), next],
365
+ })),
366
+ );
367
+ return count;
368
+ }
369
+ removeAnimation(id: string, animIndex: number): void {
370
+ this.commit(
371
+ updateNode(this._roots, id, (node) => {
372
+ const anims = (node.animations ?? []).filter((_, i) => i !== animIndex);
373
+ return { ...node, animations: anims };
374
+ }),
375
+ );
376
+ }
377
+ setAnimationField(
378
+ id: string,
379
+ animIndex: number,
380
+ field: 'name' | 'duration' | 'loop' | 'autoplay',
381
+ value: unknown,
382
+ ): void {
383
+ this.updateClip(id, animIndex, (c) => ({ ...c, [field]: value }));
384
+ }
385
+ addTrack(
386
+ id: string,
387
+ animIndex: number,
388
+ property: UIAnimation['tracks'][number]['property'],
389
+ ): void {
390
+ this.updateClip(id, animIndex, (c) =>
391
+ c.tracks.some((t) => t.property === property)
392
+ ? c
393
+ : { ...c, tracks: [...c.tracks, { property, keyframes: [] }] },
394
+ );
395
+ }
396
+ removeTrack(id: string, animIndex: number, trackIndex: number): void {
397
+ this.updateClip(id, animIndex, (c) => ({
398
+ ...c,
399
+ tracks: c.tracks.filter((_, i) => i !== trackIndex),
400
+ }));
401
+ }
402
+ /** Upsert a keyframe on a track at `time` (record mode + manual add), keeping order. */
403
+ setKeyframe(
404
+ id: string,
405
+ animIndex: number,
406
+ trackIndex: number,
407
+ time: number,
408
+ value: number | string,
409
+ easing?: UIAnimation['tracks'][number]['keyframes'][number]['easing'],
410
+ ): void {
411
+ this.updateClip(id, animIndex, (c) => {
412
+ const tracks = [...c.tracks];
413
+ const track = tracks[trackIndex];
414
+ if (!track) return c;
415
+ const kfs = track.keyframes.filter((k) => Math.abs(k.time - time) > 1e-4);
416
+ kfs.push({ time, value, ...(easing ? { easing } : {}) });
417
+ kfs.sort((a, b) => a.time - b.time);
418
+ tracks[trackIndex] = { ...track, keyframes: kfs };
419
+ return { ...c, tracks };
420
+ });
421
+ }
422
+ removeKeyframe(id: string, animIndex: number, trackIndex: number, kfIndex: number): void {
423
+ this.updateClip(id, animIndex, (c) => {
424
+ const tracks = [...c.tracks];
425
+ const track = tracks[trackIndex];
426
+ if (!track) return c;
427
+ tracks[trackIndex] = { ...track, keyframes: track.keyframes.filter((_, i) => i !== kfIndex) };
428
+ return { ...c, tracks };
429
+ });
430
+ }
431
+ setKeyframeEasing(
432
+ id: string,
433
+ animIndex: number,
434
+ trackIndex: number,
435
+ kfIndex: number,
436
+ easing: UIAnimation['tracks'][number]['keyframes'][number]['easing'],
437
+ ): void {
438
+ this.updateClip(id, animIndex, (c) => {
439
+ const tracks = [...c.tracks];
440
+ const track = tracks[trackIndex];
441
+ if (!track) return c;
442
+ const kfs = track.keyframes.map((k, i) => (i === kfIndex ? { ...k, easing } : k));
443
+ tracks[trackIndex] = { ...track, keyframes: kfs };
444
+ return { ...c, tracks };
445
+ });
446
+ }
447
+
448
+ // --- clipboard (K8) ---
449
+ copy(id?: string): void {
450
+ const target = id ?? this.primary;
451
+ if (!target) return;
452
+ const loc = findNode(this._roots, target);
453
+ if (loc) this._clipboard = JSON.parse(JSON.stringify(loc.node));
454
+ }
455
+ cut(id?: string): void {
456
+ const target = id ?? this.primary;
457
+ if (!target) return;
458
+ this.copy(target);
459
+ this.remove(target);
460
+ }
461
+ paste(targetParentId?: string): string | null {
462
+ if (!this._clipboard) return null;
463
+ const parentId = targetParentId ?? this.primary ?? this._roots[0]?.id;
464
+ if (!parentId) return null;
465
+ // paste INTO the selected container, or as sibling if selection is a leaf
466
+ const loc = findNode(this._roots, parentId);
467
+ const intoId =
468
+ loc && (loc.node.children || isContainer(loc.node))
469
+ ? parentId
470
+ : (loc?.parent?.id ?? this._roots[0]?.id);
471
+ if (!intoId) return null;
472
+ return this.addNodeValue(intoId, this._clipboard);
473
+ }
474
+
475
+ // --- live preview vs commit (C6/K4) ---
476
+ /** Begin an interactive gesture (drag): snapshot once for a single undo step. */
477
+ beginEdit(): void {
478
+ this._interactive = this.snapshot();
479
+ }
480
+ /** Live property set during a gesture — NO undo push, NO persist callback. */
481
+ previewSetProp(id: string, path: string, value: unknown): void {
482
+ this._roots = setNodePath(this._roots, id, path, value);
483
+ this.notify();
484
+ }
485
+ previewSetPropMulti(ids: string[], path: string, value: unknown): void {
486
+ let next = this._roots;
487
+ for (const id of ids) next = setNodePath(next, id, path, value);
488
+ this._roots = next;
489
+ this.notify();
490
+ }
491
+ /** End the gesture: push the pre-gesture snapshot as the single undo step. */
492
+ endEdit(): void {
493
+ if (this._interactive == null) return;
494
+ this._undo.push(this._interactive);
495
+ if (this._undo.length > MAX_UNDO) this._undo.shift();
496
+ this._redo = [];
497
+ notifyUndoableOp(this); // T3.2 slice 2 — tag this op on the global timeline
498
+ this._interactive = null;
499
+ this._onCommit?.(this._roots);
500
+ this.notify();
501
+ }
502
+
503
+ // --- board states / in-realm scenarios (C12) ---
504
+ defineState(name: string, data: Record<string, unknown>): void {
505
+ this._states.set(name, data);
506
+ }
507
+ loadState(name: string): void {
508
+ const s = this._states.get(name);
509
+ if (!s) return;
510
+ for (const k of Object.keys(s)) this.data.set(k, s[k]);
511
+ this.notify();
512
+ }
513
+
514
+ // --- responsive preview (I3) + locale (G5) ---
515
+ setPreviewResolution(res: { width: number; height: number } | null): void {
516
+ this._previewResolution = res;
517
+ this.notify();
518
+ }
519
+ setLocale(locale: string): void {
520
+ this._locale = locale;
521
+ this.notify();
522
+ }
523
+
524
+ /** Serialize the UI tree for persistence into a SceneFile.ui (C9). */
525
+ serialize(): UIRoot[] {
526
+ return JSON.parse(JSON.stringify(this._roots));
527
+ }
528
+ /** Replace the whole tree (e.g. external load). Clears undo. */
529
+ load(roots: UIRoot[]): void {
530
+ this._roots = ensureIds(roots);
531
+ this._undo = [];
532
+ this._redo = [];
533
+ this._selection = [];
534
+ this.notify();
535
+ }
536
+ }
537
+
538
+ function isContainer(node: UINode): boolean {
539
+ return [
540
+ 'canvas',
541
+ 'box',
542
+ 'grid',
543
+ 'stack',
544
+ 'scroll',
545
+ 'panel',
546
+ 'center',
547
+ 'wrap',
548
+ 'aspect',
549
+ 'sizebox',
550
+ 'tab',
551
+ ].includes(node.kind);
552
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * UI prefab reuse (H1-H4): instantiate a saved UI subtree with fresh ids, apply
3
+ * per-instance overrides (H2), expose named props/slots (H3), and resolve NESTED
4
+ * prefab references with cycle protection (H4). Pure functions — unit tested.
5
+ */
6
+ import type { UINode, UIPrefabFile } from '@engine/scene/schema';
7
+ import { cloneWithNewIds } from './node-ops';
8
+
9
+ /** Per-instance overrides for a prefab instance (H2). */
10
+ export interface UIPrefabOverrides {
11
+ props?: Record<string, unknown>;
12
+ style?: Record<string, unknown>;
13
+ /** Content injected into named slots (H3): slotName → nodes. */
14
+ slots?: Record<string, UINode[]>;
15
+ }
16
+
17
+ /** A registry resolving a prefab name/path → its file (for nested reuse, H4). */
18
+ export type UIPrefabRegistry = Record<string, UIPrefabFile>;
19
+
20
+ const MAX_DEPTH = 32;
21
+
22
+ /**
23
+ * Instantiate a prefab into a UINode tree: fresh ids, overrides merged onto the
24
+ * root (H2), nested `component` references to other prefabs resolved recursively
25
+ * (H4), and slot content injected (H3). Throws on a cyclic prefab reference.
26
+ */
27
+ export function instantiateUIPrefab(
28
+ prefab: UIPrefabFile,
29
+ overrides: UIPrefabOverrides = {},
30
+ registry: UIPrefabRegistry = {},
31
+ _stack: string[] = [],
32
+ ): UINode {
33
+ if (_stack.includes(prefab.name)) {
34
+ throw new Error(`cyclic UI prefab reference: ${[..._stack, prefab.name].join(' -> ')}`);
35
+ }
36
+ if (_stack.length > MAX_DEPTH) throw new Error('UI prefab nesting too deep');
37
+
38
+ const root = cloneWithNewIds(prefab.root);
39
+ const resolved = resolveNested(root, registry, [..._stack, prefab.name]);
40
+ return mergeUIPrefabInstance(resolved, overrides, prefab);
41
+ }
42
+
43
+ /** Apply per-instance overrides to a prefab root, gated by exposedProps/slots (H2/H3). */
44
+ export function mergeUIPrefabInstance(
45
+ node: UINode,
46
+ overrides: UIPrefabOverrides,
47
+ prefab?: UIPrefabFile,
48
+ ): UINode {
49
+ const next: UINode = { ...node };
50
+ if (overrides.props) {
51
+ const allowed = prefab?.exposedProps;
52
+ const incoming = allowed ? pick(overrides.props, allowed) : overrides.props;
53
+ next.props = { ...next.props, ...incoming } as UINode['props'];
54
+ }
55
+ if (overrides.style) {
56
+ next.style = { ...next.style, ...overrides.style } as UINode['style'];
57
+ }
58
+ if (overrides.slots) {
59
+ next.children = injectSlots(next.children ?? [], overrides.slots);
60
+ }
61
+ return next;
62
+ }
63
+
64
+ /** Replace `component` nodes that reference a prefab in the registry with its instance (H4). */
65
+ function resolveNested(node: UINode, registry: UIPrefabRegistry, stack: string[]): UINode {
66
+ if (node.kind === 'component' && node.component && registry[node.component]) {
67
+ const ov: UIPrefabOverrides = node.props
68
+ ? { props: node.props as Record<string, unknown> }
69
+ : {};
70
+ return instantiateUIPrefab(registry[node.component] as UIPrefabFile, ov, registry, stack);
71
+ }
72
+ if (!node.children) return node;
73
+ return { ...node, children: node.children.map((c) => resolveNested(c, registry, stack)) };
74
+ }
75
+
76
+ /** Inject slot content where a child is a slot marker (kind 'component' named '$slot:name'). */
77
+ function injectSlots(children: UINode[], slots: Record<string, UINode[]>): UINode[] {
78
+ const out: UINode[] = [];
79
+ for (const c of children) {
80
+ const slotName =
81
+ c.kind === 'component' && c.component?.startsWith('$slot:') ? c.component.slice(6) : null;
82
+ if (slotName && slots[slotName]) {
83
+ out.push(...slots[slotName].map(cloneWithNewIds));
84
+ } else {
85
+ out.push(c.children ? { ...c, children: injectSlots(c.children, slots) } : c);
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+
91
+ function pick(obj: Record<string, unknown>, keys: string[]): Record<string, unknown> {
92
+ const out: Record<string, unknown> = {};
93
+ for (const k of keys) if (k in obj) out[k] = obj[k];
94
+ return out;
95
+ }