@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,795 @@
1
+ /**
2
+ * ReactDomAuthoringAdapter — the editor's {@link AuthoringAdapter} for an
3
+ * INGESTED (unmodified) native-React world, wave 10 Slice A
4
+ * (`docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md` §1, D-K1..D-K5).
5
+ *
6
+ * Unlike {@link ReactWorldAuthoringAdapter} (the first-party react-WORLD
7
+ * adapter, T6.2 slice 2), an ingested react game's JSX is NEVER `data-oid`-
8
+ * stamped (three independent guards — see the design doc's §0) and is never
9
+ * written back to (the never-modify-game-source rule, `CLAUDE.md`). So this
10
+ * adapter cannot reuse `walkOidTree` (it would see an EMPTY tree — nothing
11
+ * is stamped) and cannot reuse the JSX write-back seam at all. It is
12
+ * structurally independent:
13
+ *
14
+ * - Hierarchy: an on-demand walk of every ELEMENT node under a live DOM
15
+ * root (snapshot per call — the DOM is live and may re-render, exactly
16
+ * like `walkOidTree`'s own "no cached/mirrored state" contract). Identity
17
+ * is a STRUCTURAL path, not an authored id: `'rdom:' + '/'-joined
18
+ * segments`, each segment `<tag>[k]` where `k` is the element's index
19
+ * among preceding same-tag element SIBLINGS (the DOM analog of the
20
+ * Object3D structural-path scheme other ingest adapters already use).
21
+ * Deterministic and stable while structure is stable; disambiguates
22
+ * same-tag siblings (`div[0]` vs `div[1]`). The root itself is not a
23
+ * node — its element children are the hierarchy roots.
24
+ * - Labels derive from real DOM only — tag, plus `#id` or the first CSS
25
+ * class when present — never a fabricated/authored name (anti-shim).
26
+ * - Persistence is overlay-only: edits write the live element's inline
27
+ * `style` DIRECTLY (a runtime override, gone on reload unless saved) and
28
+ * record the same write into a {@link ReactDomOverlay} document, which the
29
+ * caller persists via `saveWorldOverlay`/`loadWorldOverlay`
30
+ * (`./world-overlay.ts`) — the SAME `.vgai/overlays/<world-id>.json`
31
+ * convention every other manifest-world overlay uses. There is NO
32
+ * `SourceWriteBackend` import anywhere in this file and NO `/__ui-source`
33
+ * traffic of any kind, ever — this is the one hard invariant the design
34
+ * doc's §4 NOTs pin (AC-K7).
35
+ * - `object3D()`/`idForObject3D()` return `null` — these entities are DOM,
36
+ * not `Object3D` (no 3D gizmo binding, mirroring `ReactWorldAuthoringAdapter`).
37
+ *
38
+ * Inspector reads/writes reuse `ReactWorldAuthoringAdapter`'s pure style
39
+ * helpers (`styleProp`/`numericStyleValue`/`cssColorToHex` — exported for
40
+ * this purpose, D-K5) but declare their OWN `STYLE_PROPERTIES` list: this
41
+ * adapter's writes are not routed through the JSX writer's supported-property
42
+ * ceiling, so its list is free to diverge (kept identical today for
43
+ * predictability — nothing stops a later slice from growing it).
44
+ *
45
+ * Apply-on-mount (D-K3): a pre-loaded overlay's entries are applied to the
46
+ * live DOM right after construction. An id that no longer resolves (the
47
+ * mounted tree moved on since the overlay was authored) is NOT applied, is
48
+ * loudly `console.warn`-reported (the `overlay-report.ts` orphan spirit,
49
+ * without importing that Object3D-shaped apparatus), and is RETAINED in the
50
+ * document — user data is never silently dropped (AC-K4).
51
+ *
52
+ * Undo/redo (D-K3, AC-K6) is adapter-local (`PendingUndo`, mirroring
53
+ * `ReactWorldAuthoringAdapter`'s pattern) and self-registers with the
54
+ * editor-global undo timeline (`./undo-timeline.ts`) exactly like every
55
+ * other ingest/world adapter — so Ctrl+Z reaches it while it's the active
56
+ * session, and its entries are dropped (not misapplied) once the session
57
+ * ends (the route's teardown calls `unregisterUndoAdapter`).
58
+ */
59
+
60
+ import type {
61
+ AuthoringAdapter,
62
+ AuthoringCapabilities,
63
+ BoxEditProvider,
64
+ ColorSampleProvider,
65
+ DOMRectLike,
66
+ EditorNode,
67
+ HierarchyProvider,
68
+ InspectorProvider,
69
+ PersistenceProvider,
70
+ PickProvider,
71
+ PropertyDescriptor,
72
+ RectProvider,
73
+ SelectionProvider,
74
+ } from '@engine/adapter';
75
+ import type { EditorStore } from '../editor-store';
76
+ import { browserOrInlineResolver, getComputedStyleValue } from '../ui-source/inspect';
77
+ import {
78
+ cssColorToHex,
79
+ mapBoxEditPatchKey,
80
+ numericStyleValue,
81
+ styleProp,
82
+ } from './react-world-authoring-adapter';
83
+ import { notifyUndoableOp, registerUndoAdapter } from './undo-timeline';
84
+
85
+ /**
86
+ * The minimal structural shape this adapter needs from a live DOM element —
87
+ * deliberately NOT `HTMLElement` (same testability reasoning as
88
+ * `OidElementLike` in `react-world-authoring-adapter.ts`: this repo's vitest
89
+ * environment for THIS adapter's own unit test is `node`, no real DOM). A
90
+ * real `Element`/`HTMLElement` satisfies this structurally (`style` is
91
+ * optional here for the same reason it is on `OidElementLike` — a plain
92
+ * `Element` has no `style` getter, only `HTMLElement`/`SVGElement` do, but
93
+ * TS treats an optional field as satisfied by its absence).
94
+ */
95
+ export interface DomElementLike {
96
+ readonly tagName: string;
97
+ readonly children: ArrayLike<DomElementLike>;
98
+ readonly id: string;
99
+ readonly className: string;
100
+ readonly style?: unknown;
101
+ /**
102
+ * T0 (spec 27 §2) — the element's live viewport rect, for `rects.rect`'s
103
+ * host-relative geometry below (see `OidElementLike.getBoundingClientRect`'s
104
+ * identical doc comment in `react-world-authoring-adapter.ts`). Optional so
105
+ * existing plain-object test fixtures keep type-checking unchanged — a node
106
+ * with no `getBoundingClientRect` simply has no rect (never a fabricated 0×0).
107
+ */
108
+ getBoundingClientRect?(): {
109
+ left: number;
110
+ top: number;
111
+ right: number;
112
+ bottom: number;
113
+ width: number;
114
+ height: number;
115
+ };
116
+ }
117
+
118
+ /** One structural-path node resolved from a live-DOM walk. */
119
+ interface DomNode {
120
+ /** `'rdom:' + '/'-joined <tag>[k] segments` — see the class doc comment. */
121
+ id: string;
122
+ tag: string;
123
+ el: DomElementLike;
124
+ parentId: string | null;
125
+ childIds: string[];
126
+ }
127
+
128
+ interface DomTree {
129
+ nodes: Map<string, DomNode>;
130
+ rootIds: string[];
131
+ }
132
+
133
+ const ID_PREFIX = 'rdom:';
134
+
135
+ /** First whitespace-separated CSS class, or `undefined` if `className` is empty/absent. */
136
+ function firstClassName(className: string): string | undefined {
137
+ const trimmed = className.trim();
138
+ if (!trimmed) return undefined;
139
+ return trimmed.split(/\s+/)[0];
140
+ }
141
+
142
+ /** Real-DOM-only label (anti-shim): tag, `#id` when present, else `.firstClass` when present. */
143
+ function labelFor(el: DomElementLike, tag: string): string {
144
+ if (el.id) return `${tag}#${el.id}`;
145
+ const cls = firstClassName(el.className ?? '');
146
+ if (cls) return `${tag}.${cls}`;
147
+ return tag;
148
+ }
149
+
150
+ /**
151
+ * Walk a live DOM root's ELEMENT descendants into a {@link DomTree}, snapshot
152
+ * per call (D-K1: "the DOM is live and may re-render"). Structural-path id
153
+ * scheme: each segment is `<tag>[k]`, `k` = the element's index among
154
+ * preceding SAME-TAG element siblings under the SAME parent — computed with
155
+ * one `Map<tag, count>` per sibling group, reset at each recursion level.
156
+ */
157
+ function walkDomTree(root: DomElementLike): DomTree {
158
+ const nodes = new Map<string, DomNode>();
159
+ const rootIds: string[] = [];
160
+
161
+ function visitChildren(
162
+ children: ArrayLike<DomElementLike>,
163
+ parentId: string | null,
164
+ parentPath: string,
165
+ ): void {
166
+ const tagCounts = new Map<string, number>();
167
+ for (const el of Array.from(children)) {
168
+ const tag = el.tagName.toLowerCase();
169
+ const k = tagCounts.get(tag) ?? 0;
170
+ tagCounts.set(tag, k + 1);
171
+ const path = parentPath ? `${parentPath}/${tag}[${k}]` : `${tag}[${k}]`;
172
+ const id = `${ID_PREFIX}${path}`;
173
+ const node: DomNode = { id, tag, el, parentId, childIds: [] };
174
+ nodes.set(id, node);
175
+ if (parentId) nodes.get(parentId)?.childIds.push(id);
176
+ else rootIds.push(id);
177
+ visitChildren(el.children, id, path);
178
+ }
179
+ }
180
+
181
+ visitChildren(root.children, null, '');
182
+ return { nodes, rootIds };
183
+ }
184
+
185
+ /** A reasonably useful, curated inline-style property list — this adapter's
186
+ * OWN list (D-K2: not shared with `ReactWorldAuthoringAdapter`'s, though
187
+ * kept identical in shape today — this one is not bounded by a JSX writer's
188
+ * supported-property ceiling). */
189
+ const STYLE_PROPERTIES: ReadonlyArray<{
190
+ prop: string;
191
+ label: string;
192
+ type: PropertyDescriptor['type'];
193
+ options?: string[];
194
+ }> = [
195
+ { prop: 'color', label: 'Color', type: 'color' },
196
+ { prop: 'backgroundColor', label: 'Background', type: 'color' },
197
+ { prop: 'borderColor', label: 'Border Color', type: 'color' },
198
+ { prop: 'fontSize', label: 'Font Size', type: 'number' },
199
+ { prop: 'width', label: 'Width', type: 'number' },
200
+ { prop: 'height', label: 'Height', type: 'number' },
201
+ { prop: 'padding', label: 'Padding', type: 'number' },
202
+ { prop: 'margin', label: 'Margin', type: 'number' },
203
+ { prop: 'borderRadius', label: 'Border Radius', type: 'number' },
204
+ { prop: 'gap', label: 'Gap', type: 'number' },
205
+ { prop: 'display', label: 'Display', type: 'enum', options: ['flex', 'block', 'none', 'grid'] },
206
+ { prop: 'textAlign', label: 'Text Align', type: 'enum', options: ['left', 'center', 'right'] },
207
+ { prop: 'flexDirection', label: 'Flex Direction', type: 'enum', options: ['row', 'column'] },
208
+ ];
209
+ const STYLE_PATH_PREFIX = 'style.';
210
+
211
+ /** `prop -> declared type` — see `ReactWorldAuthoringAdapter`'s identical map
212
+ * for why `inspector.get` needs this (a real CSSOM value is always a
213
+ * unit-suffixed string / an `rgb(...)` string, never a bare number/hex). */
214
+ const STYLE_PROPERTY_TYPE: ReadonlyMap<string, PropertyDescriptor['type']> = new Map(
215
+ STYLE_PROPERTIES.map(({ prop, type }) => [prop, type]),
216
+ );
217
+
218
+ /** structural id -> cssProp -> value (D-K3). Values are always strings — the
219
+ * same literal an inline style write / a saved overlay round-trips. */
220
+ export type ReactDomOverlay = Record<string, Record<string, string>>;
221
+
222
+ export interface ReactDomAuthoringOptions {
223
+ /** Pre-loaded overlay (from `loadWorldOverlay<ReactDomOverlay>`) — applied
224
+ * to the live DOM on construction (D-K3 apply-on-mount) and mutated
225
+ * in-place by every subsequent `inspector.set` (so `serialize()`/a later
226
+ * `save()` always reflects every write, including unresolved-at-mount-time
227
+ * ones from BEFORE this session, and new ones from THIS session). */
228
+ overlay: ReactDomOverlay;
229
+ /** `save()` flushes the current document through this — routes pass
230
+ * `(o) => saveWorldOverlay(worldId, o)`. */
231
+ persist: (overlay: ReactDomOverlay) => Promise<void>;
232
+ /** Human/agent-readable destination (`PersistenceProvider.destination`) —
233
+ * routes pass `worldOverlayPath(worldId)`. */
234
+ destination: string;
235
+ }
236
+
237
+ interface PendingUndo {
238
+ undo: () => void;
239
+ redo: () => void;
240
+ }
241
+
242
+ export class ReactDomAuthoringAdapter implements AuthoringAdapter {
243
+ readonly capabilities: AuthoringCapabilities = {
244
+ transform: false, // no 3D gizmo — DOM has no Object3D pose
245
+ material: false,
246
+ inspectorFields: true,
247
+ create: false, // element insertion out of v1 scope
248
+ delete: false, // structural delete out of v1 scope (overlay-only, no structure provider)
249
+ reparent: false,
250
+ persist: true,
251
+ };
252
+
253
+ private readonly doc: ReactDomOverlay;
254
+ private readonly persistFn: (overlay: ReactDomOverlay) => Promise<void>;
255
+ private readonly destinationPath: string;
256
+ private dirty = false;
257
+ private readonly undoStack: PendingUndo[] = [];
258
+ private readonly redoStack: PendingUndo[] = [];
259
+ /** Synchronous-burst cache for `snapshot()` — see that method's doc. */
260
+ private snapCache: DomTree | null = null;
261
+ private snapInvalidationScheduled = false;
262
+ /**
263
+ * T0 (spec 27 §4, B1) — the currently-open `boxEdit` begin/apply×N/end gesture, or
264
+ * `null` between gestures. Mirrors `ReactWorldAuthoringAdapter.boxEditSession` (see
265
+ * its doc comment for why `priorInline` exists) — `touched` here maps prop → the
266
+ * FINAL CSS-STRING value to write (this adapter's overlay route always stores CSS
267
+ * strings, never bare numbers, unlike the react-world source-write route).
268
+ */
269
+ private boxEditSession: {
270
+ id: string;
271
+ touched: Map<string, string>;
272
+ priorInline: Map<string, string>;
273
+ } | null = null;
274
+ /** D4 (spec27 §6 D4, layer-tree "lock" toggle) — SESSION-LOCAL, never
275
+ * written to the overlay JSON: an ingested game's DOM has no persisted
276
+ * "locked" concept (its overlay only ever records style overrides). See
277
+ * `ReactWorldAuthoringAdapter.lockedIds`'s doc comment for the same
278
+ * reasoning, INCLUDING D4.R1 (now read by `pickable.pick` below and by
279
+ * the overlay's marquee candidate pool; NOT by `hierarchy`/`selection` —
280
+ * a locked node stays selectable/unlockable from the layer tree). */
281
+ private readonly lockedIds = new Set<string>();
282
+
283
+ constructor(
284
+ private readonly root: DomElementLike,
285
+ private readonly store: EditorStore,
286
+ opts: ReactDomAuthoringOptions,
287
+ ) {
288
+ this.doc = opts.overlay;
289
+ this.persistFn = opts.persist;
290
+ this.destinationPath = opts.destination;
291
+ // This adapter IS an undo/redo target the editor-global timeline
292
+ // dispatches to (mirrors `ReactWorldAuthoringAdapter`'s identical call) —
293
+ // the caller (the ingest route's teardown) unregisters it on session end.
294
+ registerUndoAdapter(this);
295
+ this.applyOverlayOnMount();
296
+ }
297
+
298
+ /**
299
+ * A live-DOM walk, memoized for the current SYNCHRONOUS burst only.
300
+ *
301
+ * `hierarchy.node(id)` is called once per node by the shell's row flattener
302
+ * (`flattenIngestRows`), and a large live react game (react-rpg's dungeon
303
+ * is ~1900 walked rows over a several-thousand-element DOM) turned that into
304
+ * an O(N²) re-walk-per-node — a ~9s synchronous block that hung the editor
305
+ * on selection (the hierarchy re-renders synchronously inside the click's
306
+ * discrete-event flush). JS is single-threaded, so the DOM cannot change
307
+ * mid-render: caching the walk for the current synchronous stack and
308
+ * invalidating it on the next microtask keeps the "the DOM is live, never
309
+ * stale across renders" contract (a subsequent render, after the game may
310
+ * have re-rendered, re-walks) while collapsing one render's N node() calls
311
+ * to ONE walk. Style READS stay live regardless — `inspector.get` reads
312
+ * `n.el.style` off the cached element ref, not a cached value. Structural
313
+ * mutations (none in v1 — this adapter has no `structure` provider) would
314
+ * land between ticks (the game's own re-render), which the microtask
315
+ * invalidation already covers.
316
+ */
317
+ private snapshot(): DomTree {
318
+ if (this.snapCache) return this.snapCache;
319
+ const tree = walkDomTree(this.root);
320
+ this.snapCache = tree;
321
+ if (!this.snapInvalidationScheduled) {
322
+ this.snapInvalidationScheduled = true;
323
+ queueMicrotask(() => {
324
+ this.snapCache = null;
325
+ this.snapInvalidationScheduled = false;
326
+ });
327
+ }
328
+ return tree;
329
+ }
330
+
331
+ /**
332
+ * D-L1 (wave 10 Slice B, docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md
333
+ * §2) — total ELEMENT nodes currently walked under the root: a fresh
334
+ * snapshot every call (same "the DOM is live and may re-render, never
335
+ * cached" discipline as `hierarchy.roots()`/`hierarchy.node()` above), not
336
+ * a count taken once at construction. Backs
337
+ * `__vgaiIngestReact.domEvidence().hierarchyNodeCount` — one of the four
338
+ * legs `vgai doctor`'s react-ingest MOUNTED bar requires
339
+ * (`clearsReactIngestMountedBar`, `packages/editor/src/doctor/report.ts`).
340
+ */
341
+ nodeCount(): number {
342
+ return this.snapshot().nodes.size;
343
+ }
344
+
345
+ private toEditorNode(n: DomNode): EditorNode {
346
+ return {
347
+ id: n.id,
348
+ label: labelFor(n.el, n.tag),
349
+ kind: n.tag,
350
+ parentId: n.parentId,
351
+ childIds: n.childIds,
352
+ flags: { runtimeOnly: true },
353
+ };
354
+ }
355
+
356
+ /**
357
+ * D-K3 apply-on-mount: apply every resolvable overlay entry to the live
358
+ * DOM; an id that doesn't resolve is loudly reported and left untouched
359
+ * (and stays in `this.doc` — it is never deleted here).
360
+ */
361
+ private applyOverlayOnMount(): void {
362
+ const { nodes } = this.snapshot();
363
+ for (const [id, props] of Object.entries(this.doc)) {
364
+ const n = nodes.get(id);
365
+ if (!n) {
366
+ // biome-ignore lint/suspicious/noConsole: deliberate, greppable orphan report — mirrors overlay-report.ts's own console.warn
367
+ console.warn(
368
+ `[ReactDomAuthoringAdapter] overlay entry "${id}" does not resolve to any live ` +
369
+ 'element on this mount — not applied, retained in the overlay document.',
370
+ );
371
+ continue;
372
+ }
373
+ for (const [prop, value] of Object.entries(props)) {
374
+ this.applyStyleToElement(n.el, prop, value);
375
+ }
376
+ }
377
+ }
378
+
379
+ private applyStyleToElement(el: DomElementLike, prop: string, value: string): void {
380
+ if (!el.style) return;
381
+ (el.style as Record<string, unknown>)[prop] = value;
382
+ }
383
+
384
+ private recordOverlayEntry(id: string, prop: string, value: string): void {
385
+ const entry = this.doc[id] ?? {};
386
+ entry[prop] = value;
387
+ this.doc[id] = entry;
388
+ }
389
+
390
+ private removeOverlayEntry(id: string, prop: string): void {
391
+ const entry = this.doc[id];
392
+ if (!entry) return;
393
+ delete entry[prop];
394
+ if (Object.keys(entry).length === 0) delete this.doc[id];
395
+ }
396
+
397
+ readonly hierarchy: HierarchyProvider = {
398
+ roots: () => {
399
+ const { nodes, rootIds } = this.snapshot();
400
+ return rootIds.map((id) => this.toEditorNode(nodes.get(id)!));
401
+ },
402
+ node: (id) => {
403
+ const n = this.snapshot().nodes.get(id);
404
+ return n ? this.toEditorNode(n) : null;
405
+ },
406
+ object3D: () => null, // DOM, not Object3D — no 3D gizmo binding
407
+ idForObject3D: () => null,
408
+ };
409
+
410
+ readonly selection: SelectionProvider = {
411
+ get: () => [...this.store.selectedEntityIds],
412
+ set: (ids) => this.store.selectMultiple(ids),
413
+ };
414
+
415
+ /**
416
+ * A2 gap close-out (spec 27 §2, spec:237-254/spec:251-254) — GEOMETRIC rect
417
+ * hit-test over the live DOM tree, symmetric with
418
+ * `ReactWorldAuthoringAdapter.pickable` (see that provider's doc comment
419
+ * for the full rationale, identical here): NOT `document.elementFromPoint`
420
+ * (which skips a `pointer-events:none` design-time wrapper), smallest-area
421
+ * element containing the point wins (the deepest/most-specific node), ties
422
+ * broken by later tree-walk order (the topmost). Without this, an ingested
423
+ * react world's `rdom:`-id nodes are never `pickable`, so the shell's
424
+ * `pickTopmost` (`layered-pick.ts`) always falls through to `null` for this
425
+ * adapter — click-to-select through the viewport never resolves any node,
426
+ * even though `hierarchy`/`selection` already work from the panel.
427
+ *
428
+ * The winning id is the SAME structural `rdom:<tag>[k]` id
429
+ * `hierarchy`/`selection` already use — a hit routes straight into
430
+ * `composite.selection.set([id])` with no translation.
431
+ */
432
+ readonly pickable: PickProvider = {
433
+ pick: (clientX, clientY) => {
434
+ const { nodes } = this.snapshot();
435
+ let bestId: string | null = null;
436
+ let bestArea = Number.POSITIVE_INFINITY;
437
+ let bestOrder = -1;
438
+ let order = -1;
439
+ for (const node of nodes.values()) {
440
+ order++;
441
+ // D4.R1 — a locked node is SKIPPED, not returned (falls through to
442
+ // whatever unlocked node is behind/around it) — mirrors
443
+ // `ReactWorldAuthoringAdapter.pickable`'s identical fix and the
444
+ // first-party `viewport-raycast.ts` locked-skip.
445
+ if (this.lockedIds.has(node.id)) continue;
446
+ const rect = node.el.getBoundingClientRect?.();
447
+ if (!rect) continue; // no live rect (test fixture, or unmounted) — never a candidate
448
+ if (
449
+ clientX < rect.left ||
450
+ clientX > rect.right ||
451
+ clientY < rect.top ||
452
+ clientY > rect.bottom
453
+ ) {
454
+ continue;
455
+ }
456
+ const area = rect.width * rect.height;
457
+ // smallest-area (deepest) wins; ties broken by later tree-order (topmost)
458
+ if (area < bestArea || (area === bestArea && order > bestOrder)) {
459
+ bestId = node.id;
460
+ bestArea = area;
461
+ bestOrder = order;
462
+ }
463
+ }
464
+ return bestId;
465
+ },
466
+ };
467
+
468
+ /** Host rect to subtract for {@link rects}' HOST-RELATIVE geometry — same
469
+ * reasoning as `ReactWorldAuthoringAdapter.hostRect` (this adapter's own
470
+ * `this.root` is symmetrically the ingested world's mounted DOM layer). */
471
+ private hostRect(): { left: number; top: number } {
472
+ const r = this.root.getBoundingClientRect?.();
473
+ return { left: r?.left ?? 0, top: r?.top ?? 0 };
474
+ }
475
+
476
+ private toHostRelative(r: {
477
+ left: number;
478
+ top: number;
479
+ width: number;
480
+ height: number;
481
+ }): DOMRectLike {
482
+ const host = this.hostRect();
483
+ return { x: r.left - host.left, y: r.top - host.top, width: r.width, height: r.height };
484
+ }
485
+
486
+ /**
487
+ * T0 (spec 27 §2) — per-node screen geometry, HOST-RELATIVE (see
488
+ * {@link hostRect}'s doc comment), symmetric with
489
+ * `ReactWorldAuthoringAdapter.rects` so the overlay this feeds (Phase A3)
490
+ * treats both react adapters identically.
491
+ */
492
+ readonly rects: RectProvider = {
493
+ rect: (id) => {
494
+ const n = this.snapshot().nodes.get(id);
495
+ const r = n?.el.getBoundingClientRect?.();
496
+ return r ? this.toHostRelative(r) : null;
497
+ },
498
+ contextRects: (id) => {
499
+ const { nodes } = this.snapshot();
500
+ const n = nodes.get(id);
501
+ if (!n) return {};
502
+ const parentNode = n.parentId ? nodes.get(n.parentId) : undefined;
503
+ const parentRect = parentNode?.el.getBoundingClientRect?.();
504
+ const siblingIds = (parentNode ? parentNode.childIds : this.snapshot().rootIds).filter(
505
+ (sid) => sid !== id,
506
+ );
507
+ const siblings = siblingIds
508
+ .map((sid) => nodes.get(sid)?.el.getBoundingClientRect?.())
509
+ .filter((r): r is NonNullable<typeof r> => r != null)
510
+ .map((r) => this.toHostRelative(r));
511
+ const el = n.el;
512
+ const rect = el.getBoundingClientRect?.();
513
+ let paddingBox: DOMRectLike | undefined;
514
+ if (rect) {
515
+ const bt =
516
+ numericStyleValue(getComputedStyleValue(el, 'borderTopWidth', browserOrInlineResolver)) ??
517
+ 0;
518
+ const br =
519
+ numericStyleValue(
520
+ getComputedStyleValue(el, 'borderRightWidth', browserOrInlineResolver),
521
+ ) ?? 0;
522
+ const bb =
523
+ numericStyleValue(
524
+ getComputedStyleValue(el, 'borderBottomWidth', browserOrInlineResolver),
525
+ ) ?? 0;
526
+ const bl =
527
+ numericStyleValue(
528
+ getComputedStyleValue(el, 'borderLeftWidth', browserOrInlineResolver),
529
+ ) ?? 0;
530
+ paddingBox = this.toHostRelative({
531
+ left: rect.left + bl,
532
+ top: rect.top + bt,
533
+ width: Math.max(0, rect.width - bl - br),
534
+ height: Math.max(0, rect.height - bt - bb),
535
+ });
536
+ }
537
+ return {
538
+ ...(parentRect ? { parent: this.toHostRelative(parentRect) } : {}),
539
+ ...(siblings.length ? { siblings } : {}),
540
+ ...(paddingBox ? { paddingBox } : {}),
541
+ };
542
+ },
543
+ };
544
+
545
+ /**
546
+ * D3.d (spec 27 §6) — eyedropper FALLBACK color-sample path, symmetric with
547
+ * `ReactWorldAuthoringAdapter.colorSample` (see that provider's doc comment
548
+ * for the full rationale — raw/un-normalized values, hit-element first).
549
+ */
550
+ readonly colorSample: ColorSampleProvider = {
551
+ backgroundChainAt: (clientX, clientY) => {
552
+ const hitId = this.pickable.pick(clientX, clientY);
553
+ if (!hitId) return null;
554
+ const { nodes } = this.snapshot();
555
+ const chain: string[] = [];
556
+ let cur: DomNode | undefined = nodes.get(hitId);
557
+ while (cur) {
558
+ chain.push(getComputedStyleValue(cur.el, 'backgroundColor', browserOrInlineResolver));
559
+ cur = cur.parentId ? nodes.get(cur.parentId) : undefined;
560
+ }
561
+ return chain;
562
+ },
563
+ };
564
+
565
+ /**
566
+ * T0 (spec 27 §4, B1) — same interface as `ReactWorldAuthoringAdapter.boxEdit`,
567
+ * routed to the OVERLAY-JSON instead of source (spec:305 — "Ingested adapter routes
568
+ * to its overlay-JSON instead of source, same interface"). `apply` is LIVE PREVIEW
569
+ * ONLY — it mutates the live element's inline style via `applyStyleToElement`
570
+ * WITHOUT touching `this.doc`/`dirty` (no overlay traffic, no spam). `end` commits
571
+ * every touched prop ONCE through {@link writeStyleEntry} (preserving the
572
+ * restore-BOTH-halves undo invariant, D-K2), composed into exactly ONE undo entry
573
+ * for the whole gesture (acceptance:310) even across multiple touched props.
574
+ */
575
+ readonly boxEdit: BoxEditProvider = {
576
+ begin: (id) => {
577
+ this.boxEditSession = { id, touched: new Map(), priorInline: new Map() };
578
+ },
579
+ apply: (id, patch) => {
580
+ const session = this.boxEditSession;
581
+ if (!session || session.id !== id) return; // no open gesture for this id
582
+ const n = this.snapshot().nodes.get(id);
583
+ if (!n) return; // unresolved id — no-op (per contract)
584
+ const pos = getComputedStyleValue(n.el, 'position', browserOrInlineResolver);
585
+ const isPositioned = pos === 'absolute' || pos === 'fixed';
586
+ for (const [key, v] of Object.entries(patch)) {
587
+ const mapped = mapBoxEditPatchKey(key, isPositioned);
588
+ if (!mapped) {
589
+ console.warn(
590
+ `[ReactDomAuthoringAdapter] boxEdit: dropping patch key "${key}" for "${id}" — ` +
591
+ (key === 'x' || key === 'y'
592
+ ? `node is not absolutely/fixed positioned (computed position: "${pos}"), ` +
593
+ 'no left/top to move'
594
+ : 'unrecognized box-edit patch key'),
595
+ );
596
+ continue;
597
+ }
598
+ // Lazily snapshot the TRUE pre-gesture inline value the first time THIS
599
+ // prop is touched in this gesture — before mutating it (see
600
+ // `boxEditSession`'s doc comment, mirroring the react-world adapter).
601
+ if (!session.priorInline.has(mapped.prop)) {
602
+ const prevRaw = styleProp(n.el.style, mapped.prop);
603
+ session.priorInline.set(mapped.prop, prevRaw == null ? '' : String(prevRaw));
604
+ }
605
+ const cssValue = mapped.cssValue(v);
606
+ this.applyStyleToElement(n.el, mapped.prop, cssValue);
607
+ session.touched.set(mapped.prop, cssValue); // this route always commits CSS strings
608
+ }
609
+ },
610
+ end: (id) => {
611
+ const session = this.boxEditSession;
612
+ this.boxEditSession = null;
613
+ if (!session || session.id !== id || session.touched.size === 0) return;
614
+ this.commitBoxEdit(id, session.touched, session.priorInline);
615
+ },
616
+ };
617
+
618
+ /**
619
+ * T0 (spec 27 §4, B1) — commit every prop touched by one `boxEdit` gesture, each
620
+ * through {@link writeStyleEntry} (preserving the restore-BOTH-halves undo
621
+ * invariant per prop), composed into exactly ONE undo entry (acceptance:310) whose
622
+ * inverse runs in REVERSE order and whose redo runs in gesture order.
623
+ */
624
+ private commitBoxEdit(
625
+ id: string,
626
+ touched: Map<string, string>,
627
+ priorInline: Map<string, string>,
628
+ ): void {
629
+ const entries: PendingUndo[] = [];
630
+ for (const [prop, value] of touched) {
631
+ const entry = this.writeStyleEntry(id, prop, value, priorInline.get(prop));
632
+ if (entry) entries.push(entry);
633
+ }
634
+ if (entries.length === 0) return;
635
+ this.pushUndo({
636
+ undo: () => {
637
+ for (let i = entries.length - 1; i >= 0; i--) entries[i]!.undo();
638
+ },
639
+ redo: () => {
640
+ for (const entry of entries) entry.redo();
641
+ },
642
+ });
643
+ }
644
+
645
+ readonly inspector: InspectorProvider = {
646
+ properties: (): PropertyDescriptor[] =>
647
+ STYLE_PROPERTIES.map(({ prop, label, type, options }) => ({
648
+ path: `${STYLE_PATH_PREFIX}${prop}`,
649
+ label,
650
+ type,
651
+ ...(options ? { options } : {}),
652
+ })),
653
+ get: (id, path) => {
654
+ // D4 (spec27 §6 D4, layer-tree visibility/lock) — see
655
+ // `ReactWorldAuthoringAdapter`'s matching branch for the reasoning
656
+ // (`locked` session-local; `visible` sugar for the `visibility` inline
657
+ // style — this adapter writes synchronously, so no echo is needed).
658
+ if (path === 'locked') return this.lockedIds.has(id);
659
+ if (path === 'visible') {
660
+ const n = this.snapshot().nodes.get(id);
661
+ if (!n) return undefined;
662
+ return styleProp(n.el.style, 'visibility') !== 'hidden';
663
+ }
664
+ if (!path.startsWith(STYLE_PATH_PREFIX)) return undefined;
665
+ const prop = path.slice(STYLE_PATH_PREFIX.length);
666
+ const n = this.snapshot().nodes.get(id);
667
+ if (!n) return undefined;
668
+ const raw = styleProp(n.el.style, prop);
669
+ const declaredType = STYLE_PROPERTY_TYPE.get(prop);
670
+ if (declaredType === 'number') return numericStyleValue(raw);
671
+ if (declaredType === 'color') return cssColorToHex(raw);
672
+ return raw;
673
+ },
674
+ set: (id, path, value) => {
675
+ if (path === 'locked') {
676
+ if (value) this.lockedIds.add(id);
677
+ else this.lockedIds.delete(id);
678
+ this.store.notifyIngestEdit();
679
+ return;
680
+ }
681
+ if (path === 'visible') {
682
+ this.writeStyle(id, 'visibility', value ? 'visible' : 'hidden');
683
+ return;
684
+ }
685
+ if (!path.startsWith(STYLE_PATH_PREFIX)) return;
686
+ const prop = path.slice(STYLE_PATH_PREFIX.length);
687
+ this.writeStyle(id, prop, String(value));
688
+ },
689
+ };
690
+
691
+ /**
692
+ * T0 (spec 27 §4, B1) — the write body extracted from {@link writeStyle}, RETURNING
693
+ * the resulting {@link PendingUndo} entry instead of pushing it, so a caller that
694
+ * writes MULTIPLE props for one logical gesture ({@link commitBoxEdit}) can compose
695
+ * them into a single undo entry itself. `writeStyle` (below) is now a one-line
696
+ * wrapper — identical behavior for its existing single-prop caller (`inspector.set`).
697
+ * `priorInlineOverride`, when given, is used as the captured PRE-gesture inline value
698
+ * instead of re-reading `n.el.style` — needed because `boxEdit.apply` already mutated
699
+ * the live inline style for preview before `end` gets here (see `boxEditSession`'s
700
+ * doc comment). D-K2's restore-BOTH-halves invariant (inline AND overlay) is
701
+ * unchanged — only WHERE `prevInline` comes from differs when an override is given.
702
+ */
703
+ private writeStyleEntry(
704
+ id: string,
705
+ prop: string,
706
+ value: string,
707
+ priorInlineOverride?: string,
708
+ ): PendingUndo | null {
709
+ const n = this.snapshot().nodes.get(id);
710
+ if (!n) return null;
711
+ const prevRaw =
712
+ priorInlineOverride !== undefined ? priorInlineOverride : styleProp(n.el.style, prop);
713
+ const prevInline = prevRaw == null ? '' : String(prevRaw);
714
+ const existingEntry = this.doc[id];
715
+ const hadOverlayEntry = existingEntry ? Object.hasOwn(existingEntry, prop) : false;
716
+ const prevOverlayValue = existingEntry?.[prop];
717
+
718
+ this.applyStyleToElement(n.el, prop, value);
719
+ this.recordOverlayEntry(id, prop, value);
720
+ this.dirty = true;
721
+ this.store.notifyIngestEdit();
722
+
723
+ return {
724
+ undo: () => {
725
+ // Re-resolve rather than cache the element ref — the DOM is live and
726
+ // may have re-rendered since this entry was pushed (same discipline
727
+ // as `ReactWorldAuthoringAdapter.applyStyleWrite`).
728
+ const cur = this.snapshot().nodes.get(id);
729
+ if (cur) this.applyStyleToElement(cur.el, prop, prevInline);
730
+ if (hadOverlayEntry) this.recordOverlayEntry(id, prop, prevOverlayValue!);
731
+ else this.removeOverlayEntry(id, prop);
732
+ this.store.notifyIngestEdit();
733
+ },
734
+ redo: () => {
735
+ const cur = this.snapshot().nodes.get(id);
736
+ if (cur) this.applyStyleToElement(cur.el, prop, value);
737
+ this.recordOverlayEntry(id, prop, value);
738
+ this.store.notifyIngestEdit();
739
+ },
740
+ };
741
+ }
742
+
743
+ /**
744
+ * D-K2: mutate the live element's inline style AND the overlay document,
745
+ * mark dirty, push an adapter-local undo entry that restores BOTH the
746
+ * prior inline value and the prior overlay-entry state (present, absent,
747
+ * or a different value) — never just one half (AC-K6).
748
+ */
749
+ private writeStyle(id: string, prop: string, value: string): void {
750
+ const entry = this.writeStyleEntry(id, prop, value);
751
+ if (entry) this.pushUndo(entry);
752
+ }
753
+
754
+ private pushUndo(entry: PendingUndo): void {
755
+ this.undoStack.push(entry);
756
+ this.redoStack.length = 0;
757
+ notifyUndoableOp(this);
758
+ }
759
+
760
+ undo(): boolean {
761
+ const e = this.undoStack.pop();
762
+ if (!e) return false;
763
+ e.undo();
764
+ this.redoStack.push(e);
765
+ return true;
766
+ }
767
+
768
+ redo(): boolean {
769
+ const e = this.redoStack.pop();
770
+ if (!e) return false;
771
+ e.redo();
772
+ this.undoStack.push(e);
773
+ return true;
774
+ }
775
+
776
+ subscribe(listener: () => void): () => void {
777
+ return this.store.subscribe(listener);
778
+ }
779
+
780
+ // A class GETTER, not a field initializer — same reason as every other
781
+ // adapter here (`ui-authoring-adapter.ts`/`vgai-scene-authoring-adapter.ts`):
782
+ // a field initializer runs before the constructor body assigns
783
+ // `this.destinationPath`/`this.persistFn`.
784
+ get persistence(): PersistenceProvider {
785
+ return {
786
+ isDirty: () => this.dirty,
787
+ save: async () => {
788
+ await this.persistFn(this.doc);
789
+ this.dirty = false;
790
+ },
791
+ serialize: () => this.doc,
792
+ destination: this.destinationPath,
793
+ };
794
+ }
795
+ }