@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,266 @@
1
+ /**
2
+ * Pure, immutable tree operations on the UI scene-graph (M3/M4 backbone).
3
+ *
4
+ * All ops return NEW trees (structural sharing where cheap) so the editing store
5
+ * can snapshot for undo and React can diff. Nodes are addressed by stable id;
6
+ * `ensureIds` guarantees every node has one on load. These functions are pure and
7
+ * unit-tested in isolation (no React, no DOM).
8
+ */
9
+ import type { UINode, UIRoot } from '@engine/scene/schema';
10
+
11
+ let _idCounter = 0;
12
+ /** Deterministic-ish unique id for a new/unidentified node. */
13
+ export function newId(prefix = 'ui'): string {
14
+ _idCounter += 1;
15
+ return `${prefix}_${_idCounter.toString(36)}_${(globalThis.performance?.now?.() ?? 0).toString(36).replace('.', '')}`;
16
+ }
17
+
18
+ /** Assign ids to every node missing one (immutably). Run on load. */
19
+ export function ensureIds(roots: UIRoot[]): UIRoot[] {
20
+ const visit = <T extends UINode>(node: T): T => {
21
+ const id = node.id ?? newId(node.kind);
22
+ const children = node.children?.map(visit);
23
+ return { ...node, id, ...(children ? { children } : {}) } as T;
24
+ };
25
+ return roots.map(visit);
26
+ }
27
+
28
+ export interface NodeLocation {
29
+ node: UINode;
30
+ /** Parent node, or null if `node` is a root. */
31
+ parent: UINode | null;
32
+ /** Index within the parent's children (or within roots if a root). */
33
+ index: number;
34
+ /** True if `node` is a top-level UI root. */
35
+ isRoot: boolean;
36
+ }
37
+
38
+ /** Find a node and its location by id. */
39
+ export function findNode(roots: UIRoot[], id: string): NodeLocation | null {
40
+ for (let i = 0; i < roots.length; i++) {
41
+ const r = roots[i] as UIRoot;
42
+ if (r.id === id) return { node: r, parent: null, index: i, isRoot: true };
43
+ const found = findInChildren(r, id);
44
+ if (found) return found;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function findInChildren(parent: UINode, id: string): NodeLocation | null {
50
+ const kids = parent.children ?? [];
51
+ for (let i = 0; i < kids.length; i++) {
52
+ const c = kids[i] as UINode;
53
+ if (c.id === id) return { node: c, parent, index: i, isRoot: false };
54
+ const deeper = findInChildren(c, id);
55
+ if (deeper) return deeper;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /** Map a single node (by id) through `updater`, returning a new tree. */
61
+ export function updateNode(
62
+ roots: UIRoot[],
63
+ id: string,
64
+ updater: (node: UINode) => UINode,
65
+ ): UIRoot[] {
66
+ const visit = (node: UINode): UINode => {
67
+ if (node.id === id) return updater(node);
68
+ if (!node.children) return node;
69
+ let changed = false;
70
+ const children = node.children.map((c) => {
71
+ const n = visit(c);
72
+ if (n !== c) changed = true;
73
+ return n;
74
+ });
75
+ return changed ? { ...node, children } : node;
76
+ };
77
+ return roots.map((r) => visit(r) as UIRoot);
78
+ }
79
+
80
+ /** Immutably set a dotted path (e.g. "layout.gap", "props.text", "style.color") on a node. */
81
+ export function setNodePath(roots: UIRoot[], id: string, path: string, value: unknown): UIRoot[] {
82
+ return updateNode(roots, id, (node) => deepSet(node, path.split('.'), value) as UINode);
83
+ }
84
+
85
+ /** Immutably delete a dotted path on a node (used to clear overrides / unbind). */
86
+ export function deleteNodePath(roots: UIRoot[], id: string, path: string): UIRoot[] {
87
+ return updateNode(roots, id, (node) => deepDelete(node, path.split('.')) as UINode);
88
+ }
89
+
90
+ function deepSet(obj: unknown, keys: string[], value: unknown): unknown {
91
+ const [head, ...rest] = keys;
92
+ const base: Record<string, unknown> =
93
+ obj && typeof obj === 'object' ? { ...(obj as Record<string, unknown>) } : {};
94
+ if (rest.length === 0) {
95
+ base[head as string] = value;
96
+ } else {
97
+ base[head as string] = deepSet(base[head as string], rest, value);
98
+ }
99
+ return base;
100
+ }
101
+
102
+ function deepDelete(obj: unknown, keys: string[]): unknown {
103
+ if (!obj || typeof obj !== 'object') return obj;
104
+ const [head, ...rest] = keys;
105
+ const base = { ...(obj as Record<string, unknown>) };
106
+ if (rest.length === 0) {
107
+ delete base[head as string];
108
+ } else {
109
+ base[head as string] = deepDelete(base[head as string], rest);
110
+ }
111
+ return base;
112
+ }
113
+
114
+ /** Read a dotted path from a node. */
115
+ export function getNodePath(node: UINode, path: string): unknown {
116
+ let cur: unknown = node;
117
+ for (const k of path.split('.')) {
118
+ if (cur == null || typeof cur !== 'object') return undefined;
119
+ cur = (cur as Record<string, unknown>)[k];
120
+ }
121
+ return cur;
122
+ }
123
+
124
+ /** Insert `child` under `parentId` at `index` (default: append). */
125
+ export function insertNode(
126
+ roots: UIRoot[],
127
+ parentId: string,
128
+ child: UINode,
129
+ index?: number,
130
+ ): UIRoot[] {
131
+ return updateNode(roots, parentId, (parent) => {
132
+ const kids = [...(parent.children ?? [])];
133
+ const at = index == null ? kids.length : Math.max(0, Math.min(index, kids.length));
134
+ kids.splice(at, 0, child);
135
+ return { ...parent, children: kids };
136
+ });
137
+ }
138
+
139
+ /** Remove a node (and its subtree) by id. Roots can be removed too. */
140
+ export function removeNode(roots: UIRoot[], id: string): UIRoot[] {
141
+ const next = roots.filter((r) => r.id !== id);
142
+ if (next.length !== roots.length) return next;
143
+ const strip = (node: UINode): UINode => {
144
+ if (!node.children) return node;
145
+ const children = node.children.filter((c) => c.id !== id).map(strip);
146
+ return { ...node, children };
147
+ };
148
+ return roots.map((r) => strip(r) as UIRoot);
149
+ }
150
+
151
+ /** Deep-clone a node, assigning fresh ids throughout (for duplicate/paste). */
152
+ export function cloneWithNewIds(node: UINode): UINode {
153
+ const children = node.children?.map(cloneWithNewIds);
154
+ return { ...node, id: newId(node.kind), ...(children ? { children } : {}) };
155
+ }
156
+
157
+ /** Duplicate a node as a sibling immediately after itself. Returns [tree, newId]. */
158
+ export function duplicateNode(
159
+ roots: UIRoot[],
160
+ id: string,
161
+ ): { roots: UIRoot[]; newId: string | null } {
162
+ const loc = findNode(roots, id);
163
+ if (!loc) return { roots, newId: null };
164
+ const copy = cloneWithNewIds(loc.node);
165
+ if (loc.isRoot) {
166
+ const next = [...roots];
167
+ next.splice(loc.index + 1, 0, copy as UIRoot);
168
+ return { roots: next, newId: copy.id ?? null };
169
+ }
170
+ if (!loc.parent?.id) return { roots, newId: null };
171
+ return { roots: insertNode(roots, loc.parent.id, copy, loc.index + 1), newId: copy.id ?? null };
172
+ }
173
+
174
+ /** Move a node under a new parent at an index (reparent). Guards against cycles. */
175
+ export function moveNode(
176
+ roots: UIRoot[],
177
+ id: string,
178
+ newParentId: string,
179
+ index?: number,
180
+ ): UIRoot[] {
181
+ if (id === newParentId) return roots;
182
+ const loc = findNode(roots, id);
183
+ if (!loc || loc.isRoot) return roots; // roots aren't reparentable
184
+ // target parent must exist, else detach+insert would silently drop the node
185
+ if (!findNode(roots, newParentId)) return roots;
186
+ // cycle guard: can't move a node into its own descendant
187
+ if (isDescendant(loc.node, newParentId)) return roots;
188
+ const detached = removeNode(roots, id);
189
+ return insertNode(detached, newParentId, loc.node, index);
190
+ }
191
+
192
+ function isDescendant(node: UINode, id: string): boolean {
193
+ for (const c of node.children ?? []) {
194
+ if (c.id === id || isDescendant(c, id)) return true;
195
+ }
196
+ return false;
197
+ }
198
+
199
+ /** Reorder a node within its current parent to a new index. */
200
+ export function reorderNode(roots: UIRoot[], id: string, newIndex: number): UIRoot[] {
201
+ const loc = findNode(roots, id);
202
+ if (!loc || loc.isRoot || !loc.parent?.id) return roots;
203
+ return updateNode(roots, loc.parent.id, (parent) => {
204
+ const kids = [...(parent.children ?? [])];
205
+ const from = kids.findIndex((c) => c.id === id);
206
+ if (from < 0) return parent;
207
+ const [moved] = kids.splice(from, 1);
208
+ kids.splice(Math.max(0, Math.min(newIndex, kids.length)), 0, moved as UINode);
209
+ return { ...parent, children: kids };
210
+ });
211
+ }
212
+
213
+ /** Wrap a node in a new container of `kind` (the node becomes the container's child). */
214
+ export function wrapNode(
215
+ roots: UIRoot[],
216
+ id: string,
217
+ kind: UINode['kind'] = 'box',
218
+ ): { roots: UIRoot[]; wrapperId: string | null } {
219
+ const loc = findNode(roots, id);
220
+ if (!loc || loc.isRoot) return { roots, wrapperId: null };
221
+ const wrapper: UINode = {
222
+ id: newId(kind),
223
+ name: `${capitalize(kind)}Wrap`,
224
+ kind,
225
+ children: [loc.node],
226
+ };
227
+ // replace the node in place with the wrapper containing it
228
+ const next = updateNode(roots, id, () => wrapper);
229
+ return { roots: next, wrapperId: wrapper.id ?? null };
230
+ }
231
+
232
+ /** Unwrap a container: replace it with its children in its parent (inverse of wrap). */
233
+ export function unwrapNode(roots: UIRoot[], id: string): UIRoot[] {
234
+ const loc = findNode(roots, id);
235
+ if (!loc || loc.isRoot || !loc.parent?.id) return roots;
236
+ const children = loc.node.children ?? [];
237
+ return updateNode(roots, loc.parent.id, (parent) => {
238
+ const kids = [...(parent.children ?? [])];
239
+ const at = kids.findIndex((c) => c.id === id);
240
+ if (at < 0) return parent;
241
+ kids.splice(at, 1, ...children);
242
+ return { ...parent, children: kids };
243
+ });
244
+ }
245
+
246
+ function capitalize(s: string): string {
247
+ return s.charAt(0).toUpperCase() + s.slice(1);
248
+ }
249
+
250
+ /** Walk every node depth-first (roots first), calling `fn(node, parent)`. */
251
+ export function walk(roots: UIRoot[], fn: (node: UINode, parent: UINode | null) => void): void {
252
+ const visit = (node: UINode, parent: UINode | null): void => {
253
+ fn(node, parent);
254
+ for (const c of node.children ?? []) visit(c, node);
255
+ };
256
+ for (const r of roots) visit(r, null);
257
+ }
258
+
259
+ /** Total node count (for perf budget / stats). */
260
+ export function countNodes(roots: UIRoot[]): number {
261
+ let n = 0;
262
+ walk(roots, () => {
263
+ n += 1;
264
+ });
265
+ return n;
266
+ }
@@ -0,0 +1,380 @@
1
+ /**
2
+ * SelectionOverlay — the visual-edit selection overlay applied in-realm (C5/C10).
3
+ *
4
+ * A transparent layer over the rendered UI. Hover highlights, click selects
5
+ * (shift = multi), drag on empty space marquee-selects, drag on the body moves an
6
+ * absolute node, the SE handle resizes, and measurement guides show spacing
7
+ * between the hovered and selected nodes. Hit-testing uses document.elementFromPoint
8
+ * against the live DOM `data-ui-id` tags (no iframe, no postMessage — in-realm).
9
+ */
10
+ import { type RefObject, useEffect, useRef, useState } from 'react';
11
+ import { getElementsInRect, type MarqueeCandidate } from '../ui-source/inspect';
12
+ import { findNode, getNodePath } from './node-ops';
13
+ import { hitTest, hitTestAttr, nodeRect, useUIStore } from './react-store';
14
+ import type { UIEditStore } from './ui-edit-store';
15
+
16
+ interface Props {
17
+ store: UIEditStore;
18
+ stageRef: RefObject<HTMLDivElement | null>;
19
+ }
20
+
21
+ interface Rect {
22
+ x: number;
23
+ y: number;
24
+ w: number;
25
+ h: number;
26
+ }
27
+
28
+ const ACCENT = '#0d99ff';
29
+ const GRID = 8; // snap grid (K3)
30
+
31
+ export function SelectionOverlay({ store, stageRef }: Props): React.ReactNode {
32
+ useUIStore(store);
33
+ const overlayRef = useRef<HTMLDivElement>(null);
34
+ const [hoverId, setHoverId] = useState<string | null>(null);
35
+ const [marquee, setMarquee] = useState<Rect | null>(null);
36
+ const drag = useRef<null | {
37
+ mode: 'move' | 'resize' | 'marquee';
38
+ id?: string;
39
+ startX: number;
40
+ startY: number;
41
+ orig: Rect;
42
+ startOffset?: { left: number; top: number; right: number; bottom: number } | null;
43
+ }>(null);
44
+
45
+ // Force one re-render after mount so refs populate for rect computation.
46
+ const [, setMounted] = useState(0);
47
+ useEffect(() => setMounted(1), []);
48
+
49
+ // Read refs LIVE (not a render-time snapshot) so handlers see the mounted nodes.
50
+ const rectOf = (id: string): Rect | null => {
51
+ const stage = stageRef.current;
52
+ if (!stage) return null;
53
+ const r = nodeRect(stage, id);
54
+ return r ? { x: r.x, y: r.y, w: r.width, h: r.height } : null;
55
+ };
56
+
57
+ useEffect(() => {
58
+ function onUp(): void {
59
+ if (drag.current && drag.current.mode !== 'marquee') store.endEdit();
60
+ if (drag.current?.mode === 'marquee' && marquee) commitMarquee(marquee);
61
+ drag.current = null;
62
+ setMarquee(null);
63
+ }
64
+ window.addEventListener('mouseup', onUp);
65
+ return () => window.removeEventListener('mouseup', onUp);
66
+ // eslint-disable-next-line react-hooks/exhaustive-deps
67
+ }, [marquee]);
68
+
69
+ function commitMarquee(m: Rect): void {
70
+ const stage = stageRef.current;
71
+ if (!stage) return;
72
+ const ids: string[] = [];
73
+ const els = stage.querySelectorAll('[data-ui-id]');
74
+ const s = stage.getBoundingClientRect();
75
+ els.forEach((el) => {
76
+ const r = el.getBoundingClientRect();
77
+ const x = r.left - s.left;
78
+ const y = r.top - s.top;
79
+ if (x >= m.x && y >= m.y && x + r.width <= m.x + m.w && y + r.height <= m.y + m.h) {
80
+ const id = (el as HTMLElement).dataset['uiId'];
81
+ if (id && !id.includes('/')) ids.push(id); // prefer top-level-ish nodes
82
+ }
83
+ });
84
+ if (ids.length) store.select(ids);
85
+ // Cap 8 (React visual-edit parity): a marquee over component-internal (data-oid)
86
+ // elements also surfaces the DEEPEST source element (getElementsInRect) for the
87
+ // source-edit path — so a rubber-band selects into a component's JSX, not just UI nodes.
88
+ const oidEls = Array.from(stage.querySelectorAll<HTMLElement>('[data-oid]'));
89
+ const candidates: MarqueeCandidate[] = oidEls.map((el) => {
90
+ const rr = el.getBoundingClientRect();
91
+ return {
92
+ oid: el.getAttribute('data-oid') ?? '',
93
+ rect: { x: rr.left - s.left, y: rr.top - s.top, width: rr.width, height: rr.height },
94
+ descendantOids: Array.from(el.querySelectorAll<HTMLElement>('[data-oid]')).map(
95
+ (d) => d.getAttribute('data-oid') ?? '',
96
+ ),
97
+ };
98
+ });
99
+ const inRect = getElementsInRect({ x: m.x, y: m.y, width: m.w, height: m.h }, candidates);
100
+ if (inRect[0]) store.setSourceSelection(inRect[0].oid);
101
+ }
102
+
103
+ function onMouseMove(e: React.MouseEvent): void {
104
+ const stage = stageRef.current;
105
+ if (!stage) return;
106
+ if (drag.current) {
107
+ const dx = e.clientX - drag.current.startX;
108
+ const dy = e.clientY - drag.current.startY;
109
+ if (drag.current.mode === 'marquee') {
110
+ const x0 = drag.current.orig.x;
111
+ const y0 = drag.current.orig.y;
112
+ setMarquee({
113
+ x: Math.min(x0, x0 + dx),
114
+ y: Math.min(y0, y0 + dy),
115
+ w: Math.abs(dx),
116
+ h: Math.abs(dy),
117
+ });
118
+ return;
119
+ }
120
+ const id = drag.current.id;
121
+ if (!id) return;
122
+ const loc = findNode(store.roots, id);
123
+ if (!loc) return;
124
+ // K3: snap to an 8px grid unless Alt is held (free move).
125
+ const snap = (v: number): number => (e.altKey ? Math.round(v) : Math.round(v / GRID) * GRID);
126
+ if (drag.current.mode === 'move') {
127
+ // Move an absolute-positioned node by adjusting its offsets.
128
+ const o = drag.current.startOffset;
129
+ if (o) {
130
+ store.previewSetProp(id, 'layout.offset', {
131
+ left: snap(o.left + dx),
132
+ top: snap(o.top + dy),
133
+ right: snap(o.right + dx),
134
+ bottom: snap(o.bottom + dy),
135
+ });
136
+ }
137
+ } else if (drag.current.mode === 'resize') {
138
+ const w = Math.max(8, snap(drag.current.orig.w + dx));
139
+ const h = Math.max(8, snap(drag.current.orig.h + dy));
140
+ store.previewSetProp(id, 'layout.width', w);
141
+ store.previewSetProp(id, 'layout.height', h);
142
+ }
143
+ return;
144
+ }
145
+ const id = hitTest(stage, e.clientX, e.clientY);
146
+ setHoverId(id);
147
+ }
148
+
149
+ function onMouseDown(e: React.MouseEvent): void {
150
+ const stage = stageRef.current;
151
+ if (!stage) return;
152
+ const id = hitTest(stage, e.clientX, e.clientY);
153
+ if (!id) {
154
+ // start marquee
155
+ const s = stage.getBoundingClientRect();
156
+ drag.current = {
157
+ mode: 'marquee',
158
+ startX: e.clientX,
159
+ startY: e.clientY,
160
+ orig: { x: e.clientX - s.left, y: e.clientY - s.top, w: 0, h: 0 },
161
+ };
162
+ return;
163
+ }
164
+ store.selectOne(id, e.shiftKey);
165
+ // If a component-internal element (data-oid, stamped by the OID Vite plugin) is
166
+ // under the point, also select it for the LIVE source-edit path (C7).
167
+ const oid = hitTestAttr(stage, e.clientX, e.clientY, 'data-oid');
168
+ store.setSourceSelection(oid ?? null);
169
+ const loc = findNode(store.roots, id);
170
+ const r = rectOf(id);
171
+ if (loc && r && loc.node.layout?.position === 'absolute') {
172
+ const o =
173
+ (getNodePath(loc.node, 'layout.offset') as
174
+ | { left: number; top: number; right: number; bottom: number }
175
+ | undefined) ?? null;
176
+ store.beginEdit();
177
+ drag.current = {
178
+ mode: 'move',
179
+ id,
180
+ startX: e.clientX,
181
+ startY: e.clientY,
182
+ orig: r,
183
+ startOffset: o,
184
+ };
185
+ }
186
+ }
187
+
188
+ // Cap 3 (React visual-edit parity): double-click a component-internal (data-oid) element
189
+ // to edit its text — surfaces the source element so the SourceEditPanel's text field
190
+ // (which commits to the JSX source via /__ui-source/text) targets it.
191
+ function onDoubleClick(e: React.MouseEvent): void {
192
+ const stage = stageRef.current;
193
+ if (!stage) return;
194
+ const oid = hitTestAttr(stage, e.clientX, e.clientY, 'data-oid');
195
+ if (oid) store.setSourceSelection(oid);
196
+ }
197
+
198
+ function startResize(e: React.MouseEvent, id: string): void {
199
+ e.stopPropagation();
200
+ const r = rectOf(id);
201
+ if (!r) return;
202
+ store.beginEdit();
203
+ drag.current = { mode: 'resize', id, startX: e.clientX, startY: e.clientY, orig: r };
204
+ }
205
+
206
+ const selection = store.selection;
207
+ const hoverRect = hoverId && !selection.includes(hoverId) ? rectOf(hoverId) : null;
208
+
209
+ return (
210
+ <div
211
+ ref={overlayRef}
212
+ data-ui-overlay
213
+ onMouseMove={onMouseMove}
214
+ onMouseDown={onMouseDown}
215
+ onDoubleClick={onDoubleClick}
216
+ style={{
217
+ position: 'absolute',
218
+ inset: 0,
219
+ zIndex: 10,
220
+ cursor: drag.current?.mode === 'move' ? 'grabbing' : 'default',
221
+ }}
222
+ >
223
+ {hoverRect && <Box rect={hoverRect} color={ACCENT} />}
224
+ {(() => {
225
+ const oid = store.sourceOid;
226
+ const stage = stageRef.current;
227
+ if (!oid || !stage) return null;
228
+ const el = stage.querySelector(`[data-oid="${oid}"]`);
229
+ if (!el) return null;
230
+ const r = el.getBoundingClientRect();
231
+ const s = stage.getBoundingClientRect();
232
+ return (
233
+ <Box
234
+ rect={{ x: r.left - s.left, y: r.top - s.top, w: r.width, h: r.height }}
235
+ color="#f80"
236
+ solid
237
+ label={`source · ${oid}`}
238
+ />
239
+ );
240
+ })()}
241
+ {selection.map((id) => {
242
+ const r = rectOf(id);
243
+ if (!r) return null;
244
+ return (
245
+ <div key={id}>
246
+ <Box rect={r} color={ACCENT} solid label={labelFor(store, id)} />
247
+ <Handle rect={r} onMouseDown={(e) => startResize(e, id)} />
248
+ </div>
249
+ );
250
+ })}
251
+ {/* measurement guide between primary selection and hover (C10) */}
252
+ {hoverRect && selection.length === 1 && rectOf(selection[0] as string) && (
253
+ <Measure a={rectOf(selection[0] as string) as Rect} b={hoverRect} />
254
+ )}
255
+ {marquee && (
256
+ <div
257
+ style={{
258
+ position: 'absolute',
259
+ left: marquee.x,
260
+ top: marquee.y,
261
+ width: marquee.w,
262
+ height: marquee.h,
263
+ border: `1px solid ${ACCENT}`,
264
+ background: 'rgba(13,153,255,0.1)',
265
+ pointerEvents: 'none',
266
+ }}
267
+ />
268
+ )}
269
+ </div>
270
+ );
271
+ }
272
+
273
+ function labelFor(store: UIEditStore, id: string): string {
274
+ const loc = findNode(store.roots, id);
275
+ return loc ? `${loc.node.name} · ${loc.node.kind}` : id;
276
+ }
277
+
278
+ function Box({
279
+ rect,
280
+ color,
281
+ solid,
282
+ label,
283
+ }: {
284
+ rect: Rect;
285
+ color: string;
286
+ solid?: boolean;
287
+ label?: string;
288
+ }): React.ReactNode {
289
+ return (
290
+ <div
291
+ style={{
292
+ position: 'absolute',
293
+ left: rect.x,
294
+ top: rect.y,
295
+ width: rect.w,
296
+ height: rect.h,
297
+ border: `${solid ? 2 : 1}px solid ${color}`,
298
+ boxSizing: 'border-box',
299
+ pointerEvents: 'none',
300
+ }}
301
+ >
302
+ {label && (
303
+ <div
304
+ style={{
305
+ position: 'absolute',
306
+ top: -18,
307
+ left: 0,
308
+ background: color,
309
+ color: '#fff',
310
+ fontSize: 10,
311
+ padding: '1px 4px',
312
+ whiteSpace: 'nowrap',
313
+ fontFamily: 'monospace',
314
+ }}
315
+ >
316
+ {label}
317
+ </div>
318
+ )}
319
+ </div>
320
+ );
321
+ }
322
+
323
+ function Handle({
324
+ rect,
325
+ onMouseDown,
326
+ }: {
327
+ rect: Rect;
328
+ onMouseDown: (e: React.MouseEvent) => void;
329
+ }): React.ReactNode {
330
+ return (
331
+ <div
332
+ data-ui-handle="se"
333
+ onMouseDown={onMouseDown}
334
+ style={{
335
+ position: 'absolute',
336
+ left: rect.x + rect.w - 5,
337
+ top: rect.y + rect.h - 5,
338
+ width: 10,
339
+ height: 10,
340
+ background: '#fff',
341
+ border: `1px solid ${ACCENT}`,
342
+ cursor: 'nwse-resize',
343
+ pointerEvents: 'auto',
344
+ }}
345
+ />
346
+ );
347
+ }
348
+
349
+ function Measure({ a, b }: { a: Rect; b: Rect }): React.ReactNode {
350
+ // horizontal gap between the two rects (simple spacing indicator, C10)
351
+ const gap = Math.round(Math.max(0, b.x - (a.x + a.w)));
352
+ if (gap <= 0) return null;
353
+ const y = a.y + a.h / 2;
354
+ return (
355
+ <div
356
+ style={{
357
+ position: 'absolute',
358
+ left: a.x + a.w,
359
+ top: y,
360
+ width: gap,
361
+ height: 0,
362
+ borderTop: '1px dashed #f0f',
363
+ pointerEvents: 'none',
364
+ }}
365
+ >
366
+ <span
367
+ style={{
368
+ position: 'absolute',
369
+ left: gap / 2 - 8,
370
+ top: -14,
371
+ color: '#f0f',
372
+ fontSize: 10,
373
+ fontFamily: 'monospace',
374
+ }}
375
+ >
376
+ {gap}px
377
+ </span>
378
+ </div>
379
+ );
380
+ }