@vgai/editor 0.2.0 → 0.4.0-canary.20260715.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 (430) hide show
  1. package/README.md +24 -0
  2. package/dist/assets/axe-C7YBtOuM.js +30 -0
  3. package/dist/assets/browserAll-DDtGGE48.js +1 -0
  4. package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
  5. package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
  6. package/dist/assets/game-BdTHR2_X.js +1 -0
  7. package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
  8. package/dist/assets/game-CcHfYxjP.js +1 -0
  9. package/dist/assets/game-Cqzj_lpD.js +1 -0
  10. package/dist/assets/game-DSZdeCHu.js +1 -0
  11. package/dist/assets/game-Dch9sAPc.js +1 -0
  12. package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
  13. package/dist/assets/game-loop-DMIgSMV-.js +1 -0
  14. package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
  15. package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
  16. package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
  17. package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
  18. package/dist/assets/index-CQEfi217.js +15412 -0
  19. package/dist/assets/index-Dvi-V0NE.css +1 -0
  20. package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
  21. package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
  22. package/dist/assets/react-18-C9eHn8x1.js +1 -0
  23. package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
  24. package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
  25. package/dist/index.html +15 -13
  26. package/dist/ingest/racing-game/vgai.game.json +5 -4
  27. package/dist/ingest/simcity/vgai.game.json +5 -4
  28. package/dist/ingest/tanks/vgai.game.json +5 -4
  29. package/dist/project-scripts.bundle.js +3917 -0
  30. package/dist/scenes/untitled.vscn.json +2 -0
  31. package/dist-server/packaged.mjs +7697 -3435
  32. package/package.json +31 -4
  33. package/scripts/probe-reporter.ts +349 -0
  34. package/scripts/run-probe.ts +2260 -0
  35. package/server/asset-catalog-audit.ts +268 -0
  36. package/server/asset-catalog-v2.ts +146 -0
  37. package/server/asset-library-routes.ts +276 -58
  38. package/server/canonical-path.ts +1 -1
  39. package/server/dev.ts +148 -44
  40. package/server/editor-server.ts +888 -61
  41. package/server/editor-sse.ts +115 -4
  42. package/server/harness-chat-service.ts +796 -0
  43. package/server/local-asset-catalog.ts +157 -22
  44. package/server/packaged.ts +51 -21
  45. package/server/process-shutdown.ts +136 -0
  46. package/server/prod.ts +9 -6
  47. package/server/project-hmr-files.ts +41 -0
  48. package/server/project-operations.ts +343 -0
  49. package/server/project-optimize-deps-entries.ts +4 -4
  50. package/server/project-output-writer.ts +156 -0
  51. package/server/project-validation.ts +140 -0
  52. package/server/project-view.ts +8 -50
  53. package/server/server-utils.ts +75 -10
  54. package/server/session-registry.ts +12 -0
  55. package/src/EditorContext.tsx +107 -3
  56. package/src/action-registry.ts +115 -17
  57. package/src/adapter-resolver.ts +213 -478
  58. package/src/asset-events.ts +64 -1
  59. package/src/asset-preview.ts +906 -0
  60. package/src/asset-selection.ts +121 -0
  61. package/src/asset-workflow/asset-capabilities.ts +259 -0
  62. package/src/asset-workflow/asset-editor-errors.ts +46 -0
  63. package/src/asset-workflow/asset-import-pipeline.ts +176 -0
  64. package/src/asset-workflow/asset-library-query.ts +107 -0
  65. package/src/asset-workflow/asset-types.ts +192 -0
  66. package/src/asset-workflow/asset-workflow-quality.ts +65 -0
  67. package/src/asset-workflow/import-contract.ts +113 -0
  68. package/src/asset-workflow/model-conversion.ts +126 -0
  69. package/src/asset-workflow/model-inspection.ts +185 -0
  70. package/src/asset-workflow/project-asset-commands.ts +23 -0
  71. package/src/asset-workflow/project-asset-health.ts +153 -0
  72. package/src/asset-workflow/project-asset-operations.ts +119 -0
  73. package/src/asset-workflow/thumbnail-system.ts +172 -0
  74. package/src/authoring/active-systems.ts +15 -0
  75. package/src/authoring/boundary-authoring-adapter.ts +16 -5
  76. package/src/authoring/composite-authoring-adapter.ts +305 -124
  77. package/src/authoring/design-time-layers.ts +300 -117
  78. package/src/authoring/edit-mode-authoring.ts +278 -293
  79. package/src/authoring/embed-only-adapter.ts +4 -2
  80. package/src/authoring/first-party-hierarchy-menu.ts +7 -2
  81. package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
  82. package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
  83. package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
  84. package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
  85. package/src/authoring/first-party-inspector/register.tsx +13 -2
  86. package/src/authoring/ingest-authoring-adapter.ts +142 -45
  87. package/src/authoring/mount-failure-report.ts +5 -5
  88. package/src/authoring/provenance.ts +41 -0
  89. package/src/authoring/react-dom-authoring-adapter.ts +92 -92
  90. package/src/authoring/react-inspector-section.tsx +489 -51
  91. package/src/authoring/react-world-authoring-adapter.ts +541 -306
  92. package/src/authoring/story-overlay.ts +1 -1
  93. package/src/authoring/story-types.ts +11 -0
  94. package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
  95. package/src/authoring/viewport-tool-context.ts +78 -0
  96. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  97. package/src/authoring/world-hidden-viewport.ts +186 -0
  98. package/src/authoring/world-pan-state.ts +33 -17
  99. package/src/authoring/world-session-state.ts +1 -1
  100. package/src/authoring/world2d-authoring-adapter.ts +109 -52
  101. package/src/availability-tick.ts +56 -0
  102. package/src/browser-play-entry.ts +5 -5
  103. package/src/browser-project-scripts.ts +1 -0
  104. package/src/build-identity.ts +15 -0
  105. package/src/command-listener.ts +893 -19
  106. package/src/components/AlignToolbar.tsx +74 -63
  107. package/src/components/AppRoot.tsx +193 -26
  108. package/src/components/ApplicationMenus.tsx +272 -0
  109. package/src/components/AssetBrowser.tsx +1450 -293
  110. package/src/components/AssetEditorShell.tsx +141 -0
  111. package/src/components/AssetImportDetails.tsx +218 -0
  112. package/src/components/AssetLibraryPanel.tsx +14 -0
  113. package/src/components/CameraInfo.tsx +11 -20
  114. package/src/components/CenterDocuments.tsx +585 -0
  115. package/src/components/CommandPalette.tsx +75 -30
  116. package/src/components/ConsolePanel.css +55 -0
  117. package/src/components/ConsolePanel.tsx +57 -112
  118. package/src/components/DebugConsolePanel.tsx +461 -0
  119. package/src/components/DebuggerPanel.tsx +165 -0
  120. package/src/components/DefaultEditorLayout.tsx +167 -231
  121. package/src/components/GameHierarchy.tsx +873 -290
  122. package/src/components/GamePerformanceOverlay.tsx +46 -0
  123. package/src/components/HarnessChatPanel.tsx +2352 -0
  124. package/src/components/HistoryPanel.tsx +226 -0
  125. package/src/components/Inspector.tsx +153 -99
  126. package/src/components/InspectorToolSection.tsx +6 -4
  127. package/src/components/MountFailureBanner.tsx +31 -48
  128. package/src/components/NetworkSection.tsx +25 -14
  129. package/src/components/OnlineAssetBrowser.tsx +794 -343
  130. package/src/components/OverlayReportBanner.tsx +38 -64
  131. package/src/components/PerformancePanel.tsx +206 -0
  132. package/src/components/PlayBar.tsx +93 -263
  133. package/src/components/PrefabSaveDialog.tsx +55 -127
  134. package/src/components/ProjectHeader.css +60 -0
  135. package/src/components/ProjectHeader.tsx +170 -105
  136. package/src/components/ProjectScreen.tsx +246 -184
  137. package/src/components/ReactCanvasControls.tsx +185 -0
  138. package/src/components/ResolutionPicker.tsx +85 -0
  139. package/src/components/RootActivateDialog.tsx +39 -68
  140. package/src/components/SaveDialog.tsx +55 -127
  141. package/src/components/SaveStatus.tsx +11 -29
  142. package/src/components/StartupLoadingScreen.tsx +96 -0
  143. package/src/components/StateWatchPanel.tsx +302 -0
  144. package/src/components/StatsOverlay.tsx +12 -5
  145. package/src/components/StatusBar.tsx +64 -0
  146. package/src/components/ThemeManagerDialog.css +89 -0
  147. package/src/components/ThemeManagerDialog.tsx +371 -0
  148. package/src/components/ToolHost.tsx +26 -15
  149. package/src/components/Toolbar.tsx +117 -214
  150. package/src/components/VgaiVoxelLogo.tsx +89 -0
  151. package/src/components/ViewportOverlay.tsx +97 -172
  152. package/src/components/ViewportPanel.tsx +130 -31
  153. package/src/components/WorkspaceDock.tsx +891 -0
  154. package/src/components/WorldSelectionOverlay.tsx +51 -63
  155. package/src/components/WorldTextEditor.tsx +9 -5
  156. package/src/components/asset-documents.tsx +390 -0
  157. package/src/components/asset-editor-persistence.ts +44 -0
  158. package/src/components/asset-selection-section.tsx +338 -0
  159. package/src/components/asset-viewers/AudioViewer.tsx +73 -21
  160. package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
  161. package/src/components/asset-viewers/ImageViewer.tsx +43 -11
  162. package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
  163. package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
  164. package/src/components/asset-viewers/ModelViewer.tsx +805 -121
  165. package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
  166. package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
  167. package/src/components/asset-workflow.css +506 -0
  168. package/src/components/build-documents.tsx +696 -0
  169. package/src/components/core-utilities.tsx +75 -0
  170. package/src/components/data-documents.tsx +364 -0
  171. package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
  172. package/src/components/debugger-panel-state.ts +57 -0
  173. package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
  174. package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
  175. package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
  176. package/src/components/inspector-widgets/FontPicker.tsx +13 -21
  177. package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
  178. package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
  179. package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
  180. package/src/components/inspector-widgets/shared.tsx +90 -49
  181. package/src/components/inspectors/AnimationSection.tsx +39 -40
  182. package/src/components/inspectors/AudioSection.tsx +28 -39
  183. package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
  184. package/src/components/inspectors/CameraSection.tsx +14 -23
  185. package/src/components/inspectors/ComponentsSection.tsx +39 -43
  186. package/src/components/inspectors/JointsSection.tsx +34 -43
  187. package/src/components/inspectors/LightSection.tsx +90 -35
  188. package/src/components/inspectors/MaterialSection.tsx +137 -174
  189. package/src/components/inspectors/MeshSection.tsx +9 -17
  190. package/src/components/inspectors/NavigationSection.tsx +8 -15
  191. package/src/components/inspectors/ParticleSection.tsx +91 -76
  192. package/src/components/inspectors/PhysicsSection.tsx +42 -46
  193. package/src/components/inspectors/PostProcessingSection.tsx +28 -36
  194. package/src/components/inspectors/RenderingSection.tsx +12 -20
  195. package/src/components/inspectors/ShadowSection.tsx +7 -8
  196. package/src/components/inspectors/SplineSection.tsx +31 -42
  197. package/src/components/inspectors/TransformSection.tsx +14 -22
  198. package/src/components/patterns/Dialog.tsx +95 -0
  199. package/src/components/patterns/Fields.tsx +44 -0
  200. package/src/components/patterns/List.tsx +25 -0
  201. package/src/components/patterns/StateSurface.tsx +40 -0
  202. package/src/components/patterns/Surfaces.tsx +115 -0
  203. package/src/components/patterns/Tabs.tsx +80 -0
  204. package/src/components/patterns/Toolbar.tsx +51 -0
  205. package/src/components/patterns/Tree.tsx +72 -0
  206. package/src/components/primitives/Button.tsx +46 -0
  207. package/src/components/primitives/ColorInput.tsx +11 -47
  208. package/src/components/primitives/EditorIcon.tsx +99 -0
  209. package/src/components/primitives/FormControls.tsx +71 -0
  210. package/src/components/primitives/JsonInput.tsx +15 -2
  211. package/src/components/primitives/Layout.tsx +100 -0
  212. package/src/components/primitives/Menu.tsx +105 -0
  213. package/src/components/primitives/NumberInput.tsx +8 -35
  214. package/src/components/primitives/Panel.tsx +49 -44
  215. package/src/components/primitives/SectionHeader.tsx +10 -28
  216. package/src/components/primitives/Text.tsx +46 -0
  217. package/src/components/primitives/Tooltip.tsx +20 -12
  218. package/src/components/primitives/Vec3Input.tsx +3 -0
  219. package/src/components/primitives/banner-tones.ts +33 -0
  220. package/src/components/primitives/editor-icons.ts +102 -0
  221. package/src/components/primitives/panel-header-styles.ts +30 -0
  222. package/src/components/project-operation-documents.tsx +226 -0
  223. package/src/components/status-contributions.tsx +175 -0
  224. package/src/components/story-documents-register.ts +15 -0
  225. package/src/components/story-documents.tsx +1185 -0
  226. package/src/components/tool-documents.tsx +110 -0
  227. package/src/components/workspace-dock.css +293 -0
  228. package/src/components/workspace-history.ts +33 -0
  229. package/src/components/world-documents.tsx +145 -0
  230. package/src/components/xstate-documents.tsx +209 -0
  231. package/src/composite-screenshot.ts +248 -0
  232. package/src/design-system/index.ts +70 -0
  233. package/src/design-system-stories/Foundation.stories.tsx +183 -0
  234. package/src/design-system-stories/Patterns.stories.tsx +251 -0
  235. package/src/design-system-stories/Workspace.stories.tsx +538 -0
  236. package/src/doctor/report.ts +57 -13
  237. package/src/editor-api.ts +295 -31
  238. package/src/editor-compatibility.ts +200 -0
  239. package/src/editor-console.ts +35 -17
  240. package/src/editor-hotkeys.ts +85 -33
  241. package/src/editor-store.ts +538 -233
  242. package/src/editor-viewport.ts +160 -34
  243. package/src/entity-factory.ts +14 -3
  244. package/src/gizmo-registry.ts +21 -5
  245. package/src/harness-chat-editor-context.ts +37 -0
  246. package/src/harness-chat-indicator.ts +68 -0
  247. package/src/harness-chat-layout.ts +53 -0
  248. package/src/harness-chat-lifecycle.ts +170 -0
  249. package/src/harness-chat-reconnect.ts +85 -0
  250. package/src/harness-chat-types.ts +409 -0
  251. package/src/hierarchy-drop.ts +50 -0
  252. package/src/hierarchy-expansion-state.ts +100 -0
  253. package/src/hierarchy-menu-registry.ts +1 -1
  254. package/src/hierarchy-projection.ts +76 -0
  255. package/src/history/editor-session.ts +21 -0
  256. package/src/history/history-commands.ts +147 -0
  257. package/src/history/history-service.ts +904 -0
  258. package/src/history/json-history-resource.ts +165 -0
  259. package/src/history/persistence-coordinator.ts +35 -0
  260. package/src/history/project-file-history.ts +266 -0
  261. package/src/history/project-root-history-backends.ts +53 -0
  262. package/src/history/resource-registry.ts +209 -0
  263. package/src/history/snapshot-store.ts +108 -0
  264. package/src/history/source-history-backend.ts +374 -0
  265. package/src/history/types.ts +100 -0
  266. package/src/hmr-registration-group.ts +67 -0
  267. package/src/hmr-stable-react-context.ts +23 -0
  268. package/src/hosted-example.ts +12 -2
  269. package/src/hotkeys.ts +89 -18
  270. package/src/ingest/discovery-react.ts +9 -10
  271. package/src/ingest/discovery.ts +4 -4
  272. package/src/ingest/discovery2d.ts +5 -5
  273. package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
  274. package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
  275. package/src/ingest/games/games-fps/vgai.game.json +3 -3
  276. package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
  277. package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
  278. package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
  279. package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
  280. package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
  281. package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
  282. package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
  283. package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
  284. package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
  285. package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
  286. package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
  287. package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
  288. package/src/ingest/types.ts +1 -1
  289. package/src/ingest-mode.ts +47 -52
  290. package/src/ingest-siblings.ts +23 -33
  291. package/src/initial-scene.ts +1 -2
  292. package/src/main.tsx +16 -8
  293. package/src/manifest-project.ts +10 -12
  294. package/src/model-thumbnail.ts +261 -26
  295. package/src/module-mode.ts +44 -50
  296. package/src/play-log-events.ts +26 -0
  297. package/src/play-mode.ts +495 -401
  298. package/src/play-scene-adoption.ts +23 -0
  299. package/src/playground/main.ts +68 -24
  300. package/src/playwright-shim.ts +473 -0
  301. package/src/project-manager.ts +90 -10
  302. package/src/project-operations.ts +112 -0
  303. package/src/project-story-discovery.ts +25 -0
  304. package/src/project-tool-discovery.ts +43 -0
  305. package/src/scene-serializer.ts +0 -6
  306. package/src/scene-sync.ts +7 -2
  307. package/src/script-loader.ts +3 -3
  308. package/src/skeleton-helper.ts +57 -0
  309. package/src/storage/seed.ts +68 -8
  310. package/src/stories/StoryPreviewMount.tsx +265 -0
  311. package/src/stories/action-stories.fixture.tsx +44 -0
  312. package/src/stories/button-stories.fixture.tsx +50 -0
  313. package/src/stories/compose-project-stories.ts +182 -0
  314. package/src/stories/loader-stories.fixture.tsx +53 -0
  315. package/src/stories/story-discovery.ts +130 -0
  316. package/src/stories/story-registry.ts +146 -0
  317. package/src/theme-css.ts +1049 -0
  318. package/src/theme-library.ts +238 -0
  319. package/src/theme-preference.ts +130 -0
  320. package/src/theme.ts +527 -0
  321. package/src/tool-loader.ts +80 -24
  322. package/src/ui-source/inspect.ts +27 -4
  323. package/src/ui-source/source-write-backend.ts +83 -22
  324. package/src/widgets/index.ts +9 -18
  325. package/src/workspace-aux-commands.ts +11 -0
  326. package/src/workspace-dock-controller.ts +64 -0
  327. package/src/workspace-document-ids.ts +5 -0
  328. package/src/workspace-document-registry.ts +367 -0
  329. package/src/workspace-state-persistence.ts +455 -0
  330. package/src/workspace-status-registry.ts +110 -0
  331. package/src/workspace-utility-commands.ts +49 -0
  332. package/src/workspace-utility-registry.ts +161 -0
  333. package/src/world-document-routing.ts +16 -0
  334. package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
  335. package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
  336. package/src/xstate-inspect/use-live-actor-state.ts +39 -0
  337. package/src/xstate-inspect/xstate-graph.ts +265 -0
  338. package/src/xstate-inspect/xstate-layout.ts +76 -0
  339. package/src/z-index.ts +26 -0
  340. package/template/.claude/skills/editor/SKILL.md +129 -90
  341. package/template/AGENTS.md +1110 -0
  342. package/template/CLAUDE.md +1 -512
  343. package/template/IDIOMS.md +50 -0
  344. package/template/check-idioms.ts +706 -0
  345. package/template/manifest-entry-modules-plugin.ts +53 -0
  346. package/template/package.json +13 -4
  347. package/template/public/scenes/default.scn2d.json +6 -0
  348. package/template/public/scenes/default.vscn.json +7 -1
  349. package/template/public/scenes/untitled.vscn.json +10 -0
  350. package/template/scripts/playtest.ts +40 -0
  351. package/template/server/loopback.ts +75 -0
  352. package/template/server/rooms/arena-room.ts +69 -1
  353. package/template/server/rooms/game-room.ts +68 -1
  354. package/template/src/data/README.md +9 -4
  355. package/template/src/data/data-ref.ts +35 -0
  356. package/template/src/data/tuning.schema.ts +1 -1
  357. package/template/src/main.ts +41 -62
  358. package/template/src/manifest-entry-modules.d.ts +3 -0
  359. package/template/src/operations/README.md +88 -0
  360. package/template/src/runtime/pixi-adapter.ts +24 -0
  361. package/template/src/scripts/components/data-spinner.ts +43 -2
  362. package/template/src/scripts/components/spin-lap-math.ts +27 -0
  363. package/template/src/tools/README.md +5 -3
  364. package/template/src/tools/example.tool.tsx +13 -7
  365. package/template/src/ui/Button.stories.tsx +66 -0
  366. package/template/src/ui/RebindPanel.tsx +2 -6
  367. package/template/src/ui/game.tsx +17 -0
  368. package/template/tests/acceptance/example.spec.ts +64 -0
  369. package/template/tests/logic/example.test.ts +31 -0
  370. package/template/tests/logic/room.test.ts +100 -0
  371. package/template/tests/tsconfig.json +30 -0
  372. package/template/validate-manifest.ts +5 -5
  373. package/template/validate-scenes.ts +50 -18
  374. package/template/vgai.game.json +20 -13
  375. package/template/vite.config.ts +77 -16
  376. package/vite-plugin-ui-oid.ts +261 -17
  377. package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
  378. package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
  379. package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
  380. package/dist/assets/browserAll-CEkWYlFk.js +0 -1
  381. package/dist/assets/create-runtime-DYIWr3va.js +0 -1
  382. package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
  383. package/dist/assets/game-8SDZPPgH.js +0 -1
  384. package/dist/assets/game-CevorxyV.js +0 -1
  385. package/dist/assets/game-Di7XlywD.js +0 -1
  386. package/dist/assets/game-DjnCaw8R.js +0 -1
  387. package/dist/assets/game-Q9A0aXSV.js +0 -1
  388. package/dist/assets/index-CPVV6pjw.js +0 -1
  389. package/dist/assets/index-CpOnYyeD.js +0 -213
  390. package/dist/assets/index-Dr8H_6Rl.js +0 -1
  391. package/dist/assets/index-_bCaipUk.js +0 -13650
  392. package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
  393. package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
  394. package/dist/assets/scene-query-BivAHhJu.js +0 -1
  395. package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
  396. package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
  397. package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
  398. package/src/authoring/scene-ui-layer.ts +0 -485
  399. package/src/authoring/ui-authoring-adapter.ts +0 -325
  400. package/src/authoring/ui-hierarchy-menu.ts +0 -40
  401. package/src/authoring/ui-inspector-sections.tsx +0 -156
  402. package/src/authoring/undo-timeline.ts +0 -145
  403. package/src/components/BottomPanel.tsx +0 -326
  404. package/src/components/BuildPanel.tsx +0 -455
  405. package/src/components/FolderTree.tsx +0 -179
  406. package/src/components/RightPanel.tsx +0 -166
  407. package/src/components/WorldContextMenu.tsx +0 -257
  408. package/src/components/asset-viewers/AssetViewer.tsx +0 -38
  409. package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
  410. package/src/scene-ui-mode/active-scene-ui.ts +0 -58
  411. package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
  412. package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
  413. package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
  414. package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
  415. package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
  416. package/src/ui-editor/inspector.tsx +0 -563
  417. package/src/ui-editor/node-ops.ts +0 -266
  418. package/src/ui-editor/overlay.tsx +0 -380
  419. package/src/ui-editor/palette.tsx +0 -114
  420. package/src/ui-editor/react-store.ts +0 -78
  421. package/src/ui-editor/timeline-panel.tsx +0 -443
  422. package/src/ui-editor/ui-edit-store.ts +0 -552
  423. package/src/ui-editor/ui-prefab.ts +0 -95
  424. package/template/project.json +0 -9
  425. package/template/src/scripts/main.ts +0 -94
  426. package/template/src/ui/DefaultHud.tsx +0 -40
  427. package/template/src/ui/game-state.tsx +0 -37
  428. package/template/src/ui/mount-default-hud.tsx +0 -19
  429. package/template/src/ui/mount-ui.ts +0 -15
  430. package/vite-plugin-react-world-provider.ts +0 -81
@@ -2,7 +2,7 @@
2
2
  * Composite sibling mounts — D-V2/D-V3/D-V4/D-V7
3
3
  * (docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md, F24 residual), extended by D-B1/
4
4
  * D-B3 (docs/WAVE6-DOCTOR-DESIGN.md Slice B, wave-5 residual closure): when a
5
- * manifest declares exactly one `{ ingest }` world alongside OTHER worlds
5
+ * manifest declares exactly one `{ ingest }` world alongside OTHER roots
6
6
  * ("siblings"), `ingest-mode.ts`'s composite routes mount the ingest world
7
7
  * through their existing enter path and then call {@link mountIngestSiblings}
8
8
  * here to mount every OTHER world in manifest order (already z-sorted by the
@@ -20,7 +20,7 @@
20
20
  * structurally unreachable here, D-V2 already fails the whole route
21
21
  * before any sibling loop runs, so `resolveAllWorlds`'s one-element call
22
22
  * below never hits its `MULTI_WORLD_ALLOWED_IDENTITIES` guard, which only
23
- * applies for `worlds.length > 1`) — a SCOPED single-world runtime, D-V4:
23
+ * applies for `roots.length > 1`) — a SCOPED single-world runtime, D-V4:
24
24
  * the exact resolve+mount `module-mode.ts`'s `enterModuleModeWorldsPath`
25
25
  * proves out, reused via `mountModuleWorldRuntime`, but with NONE of that
26
26
  * route's global side effects (no `setActiveAuthoring`/`setActiveSystems`,
@@ -45,14 +45,14 @@
45
45
  */
46
46
 
47
47
  import type { AuthoringAdapter } from '@engine/adapter';
48
- import type { ResolvedGameManifest, ResolvedWorldEntry } from '@engine/manifest/load';
48
+ import type { ResolvedAdapterRoot, ResolvedGameManifest } from '@engine/manifest/load';
49
49
  import type { Overlay2D } from '@engine/world2d/authoring-2d';
50
50
  import { createPhysics2DRegistry } from '@engine/world2d/physics2d-registry';
51
51
  import { createPhysicsAdapter2D } from '@engine/world2d/system-adapters-2d';
52
52
  import { Component, createElement, type ReactNode } from 'react';
53
53
  import { flushSync } from 'react-dom';
54
54
  import { createRoot } from 'react-dom/client';
55
- import { resolveReactWorldEntryComponent } from './adapter-resolver';
55
+ import { resolveReactAdapterRootComponent } from './adapter-resolver';
56
56
  import { makeEmbedOnlyAdapter } from './authoring/embed-only-adapter';
57
57
  import { IngestAuthoringAdapter } from './authoring/ingest-authoring-adapter';
58
58
  import type { IngestOverlay } from './authoring/ingest-overlay';
@@ -62,7 +62,6 @@ import {
62
62
  type ReactDomOverlay,
63
63
  } from './authoring/react-dom-authoring-adapter';
64
64
  import { ReactWorldAuthoringAdapter, walkOidTree } from './authoring/react-world-authoring-adapter';
65
- import { unregisterUndoAdapter } from './authoring/undo-timeline';
66
65
  import { loadWorldOverlay, saveWorldOverlay, worldOverlayPath } from './authoring/world-overlay';
67
66
  import { World2DAuthoringAdapter } from './authoring/world2d-authoring-adapter';
68
67
  import { editorConsole } from './editor-console';
@@ -141,13 +140,13 @@ class SiblingErrorBoundary extends Component<
141
140
  /** Position/size/z-order every sibling layer shares — same absolute-fill
142
141
  * convention `create-runtime.ts`'s own react-world DOM layer uses
143
142
  * (`mountOneReactWorld`), so a sibling stacks exactly like a native world
144
- * would have. `pointerEvents` differs per sibling kind below (module worlds
143
+ * would have. `pointerEvents` differs per sibling kind below (module roots
145
144
  * own real canvases that need real hit-testing; the react sibling's layer
146
145
  * stays `none` by the same HUD-overlay convention `mountOneReactWorld` uses,
147
146
  * its rendered content opts back in per-element). */
148
147
  function styleSiblingLayer(
149
148
  layer: HTMLElement,
150
- world: ResolvedWorldEntry,
149
+ world: ResolvedAdapterRoot,
151
150
  pointerEvents: string,
152
151
  ): void {
153
152
  layer.style.position = 'absolute';
@@ -163,7 +162,7 @@ function styleSiblingLayer(
163
162
  * D-V3: mount a `default-react` sibling as a bare DOM layer — no
164
163
  * `<GameProvider>` wrap (anti-shim: there is no native `Game` to hand it).
165
164
  * Reuses `adapter-resolver.ts`'s exact entry-loading step
166
- * (`resolveReactWorldEntryComponent` — the same D4 `scene`-forbidden/
165
+ * (`resolveReactAdapterRootComponent` — the same D4 `scene`-forbidden/
167
166
  * `entry`-required checks and default-export validation
168
167
  * `resolveDefaultReactAdapter` itself uses) rather than duplicating it.
169
168
  *
@@ -176,12 +175,12 @@ function styleSiblingLayer(
176
175
  * other sibling failure — the ingest world stays mounted regardless.
177
176
  */
178
177
  async function mountDefaultReactSibling(
179
- world: ResolvedWorldEntry,
178
+ world: ResolvedAdapterRoot,
180
179
  gameContainer: HTMLElement,
181
180
  projectRoot: string,
182
181
  store: EditorStore | undefined,
183
182
  ): Promise<SiblingMount> {
184
- const Entry = await resolveReactWorldEntryComponent(world, projectRoot);
183
+ const Entry = await resolveReactAdapterRootComponent(world, projectRoot);
185
184
 
186
185
  const layer = document.createElement('div');
187
186
  styleSiblingLayer(layer, world, 'none');
@@ -254,15 +253,6 @@ async function mountDefaultReactSibling(
254
253
  worldId: world.id,
255
254
  ...(authoring ? { authoring } : {}),
256
255
  dispose(): void {
257
- // D-M3: both branches above self-register with the global undo
258
- // timeline at construction (`registerUndoAdapter`, in each adapter's
259
- // own constructor) — drop it here so any of its entries still on the
260
- // global stack are dropped (with a warn) rather than misapplied to a
261
- // torn-down layer, mirroring every other undo-registering adapter's
262
- // teardown in this codebase (`disposeReactSession`/`disposeThreejsSession`
263
- // in `ingest-mode.ts`, the scene-UI child's `UIEditStore` unregister
264
- // in `edit-mode-authoring.ts`'s `exitEditModeAuthoring`).
265
- if (authoring) unregisterUndoAdapter(authoring);
266
256
  root.unmount();
267
257
  layer.remove();
268
258
  },
@@ -283,8 +273,8 @@ async function mountDefaultReactSibling(
283
273
  *
284
274
  * `mountModuleWorldRuntime` is kind-agnostic BY CONSTRUCTION here, not just
285
275
  * for `{ module }` adapters: it calls `resolveAllWorlds` with a ONE-element
286
- * `worlds` array, and `resolveAllWorlds`'s `MULTI_WORLD_ALLOWED_IDENTITIES`
287
- * scope guard only fires for `worlds.length > 1` — so `resolveWorldAdapter`'s
276
+ * `roots` array, and `resolveAllWorlds`'s `MULTI_WORLD_ALLOWED_IDENTITIES`
277
+ * scope guard only fires for `roots.length > 1` — so `resolveRootAdapter`'s
288
278
  * full identity switch (including `default-three`/`default-pixi`) is reached
289
279
  * directly, unmodified, with the guard never consulted (D-B3's ground-truth
290
280
  * finding; `MULTI_WORLD_ALLOWED_IDENTITIES` itself stays byte-identical).
@@ -326,7 +316,7 @@ async function mountDefaultReactSibling(
326
316
  */
327
317
  async function mountScopedRuntimeSibling(
328
318
  manifest: ResolvedGameManifest,
329
- world: ResolvedWorldEntry,
319
+ world: ResolvedAdapterRoot,
330
320
  gameContainer: HTMLElement,
331
321
  projectRoot: string,
332
322
  store: EditorStore | undefined,
@@ -339,10 +329,10 @@ async function mountScopedRuntimeSibling(
339
329
  const w = gameContainer.clientWidth || 800;
340
330
  const h = gameContainer.clientHeight || 600;
341
331
  const { session } = await mountModuleWorldRuntime(manifest, world, projectRoot, layer, w, h);
342
- // D-G1: `session.game.worlds[0]` is the SAME `WorldInstance` shape
332
+ // D-G1: `session.game.roots[0]` is the SAME `WorldInstance` shape
343
333
  // play-mode iterates (`kind`, `threeRoot()`, `pixiRoot()`, `physics2d`) —
344
334
  // a single-element `resolveAllWorlds` call always registers exactly one.
345
- const worldInstance = session.game.worlds[0]!;
335
+ const worldInstance = session.game.roots[0]!;
346
336
  const mountedAuthoring = worldInstance.mounted.authoring;
347
337
  let authoring: AuthoringAdapter | undefined;
348
338
  if (store) {
@@ -399,9 +389,9 @@ async function mountScopedRuntimeSibling(
399
389
  * unreachable here — D-V2's route-level guard already fails the whole
400
390
  * composite before any sibling loop runs.)
401
391
  */
402
- function rejectUnmountableSibling(world: ResolvedWorldEntry): never {
392
+ function rejectUnmountableSibling(world: ResolvedAdapterRoot): never {
403
393
  throw new Error(
404
- `sibling world "${world.id}" (kind: ${world.kind}, adapter identity: ` +
394
+ `sibling world "${world.id}" (kind: ${world.surface}, adapter identity: ` +
405
395
  `"${world.adapter.identity}") has no known composite-sibling mount path — every ` +
406
396
  'currently-resolvable identity (default-three/default-pixi/default-react/module) is ' +
407
397
  'routed above; this is reached only for a genuinely unroutable shape ' +
@@ -411,26 +401,26 @@ function rejectUnmountableSibling(world: ResolvedWorldEntry): never {
411
401
 
412
402
  async function mountOneIngestSibling(
413
403
  manifest: ResolvedGameManifest,
414
- world: ResolvedWorldEntry,
404
+ world: ResolvedAdapterRoot,
415
405
  gameContainer: HTMLElement,
416
406
  projectRoot: string,
417
407
  store: EditorStore | undefined,
418
408
  ): Promise<SiblingMount> {
419
409
  if (
420
410
  world.adapter.type === 'module' ||
421
- world.adapter.identity === 'default-three' ||
422
- world.adapter.identity === 'default-pixi'
411
+ world.adapter.identity === 'threejs' ||
412
+ world.adapter.identity === 'pixijs'
423
413
  ) {
424
414
  return mountScopedRuntimeSibling(manifest, world, gameContainer, projectRoot, store);
425
415
  }
426
- if (world.adapter.identity === 'default-react') {
416
+ if (world.adapter.identity === 'react') {
427
417
  return mountDefaultReactSibling(world, gameContainer, projectRoot, store);
428
418
  }
429
419
  rejectUnmountableSibling(world);
430
420
  }
431
421
 
432
422
  /**
433
- * D-V2: mount every world in `manifest.worlds` OTHER than the primary ingest
423
+ * D-V2: mount every world in `manifest.roots` OTHER than the primary ingest
434
424
  * world (`ingestWorldId`), in manifest order (already z-sorted by the
435
425
  * loader). A failing sibling never unmounts the ingest world or stops the
436
426
  * loop — it is caught, reported (D-V5's multi-entry mount-failure report,
@@ -448,7 +438,7 @@ export async function mountIngestSiblings(
448
438
  store?: EditorStore,
449
439
  ): Promise<SiblingMount[]> {
450
440
  const mounted: SiblingMount[] = [];
451
- for (const world of manifest.worlds) {
441
+ for (const world of manifest.roots) {
452
442
  if (world.id === ingestWorldId) continue;
453
443
  try {
454
444
  mounted.push(await mountOneIngestSibling(manifest, world, gameContainer, projectRoot, store));
@@ -456,7 +446,7 @@ export async function mountIngestSiblings(
456
446
  editorConsole.error(`Composite sibling "${world.id}" failed to mount: ${err}`, 'ingest');
457
447
  addMountFailureReport({
458
448
  worldId: world.id,
459
- kind: world.kind,
449
+ kind: world.surface,
460
450
  identity: world.adapter.identity,
461
451
  message: formatMountFailureMessage(err),
462
452
  });
@@ -124,8 +124,7 @@ export async function loadInitialScene(store: EditorStore): Promise<void> {
124
124
  }
125
125
 
126
126
  // A KNOWN project (readProjectView resolved it — see project-view.ts) that
127
- // declares NO scene at all (its `defaultScene` is explicit `null`, or —
128
- // legacy `project.json` verbatim pass-through — the key is simply absent)
127
+ // declares NO scene at all (its `defaultScene` is explicit `null`)
129
128
  // is a legal shape (an entry-only first world). NEVER guess a path or probe
130
129
  // the network for one here: that is exactly what silently WROTE a
131
130
  // placeholder 'scenes/untitled.vscn.json' for a project the user never
package/src/main.tsx CHANGED
@@ -1,5 +1,10 @@
1
- import { registerSceneUIRenderer } from '@vgai/scene-ui/register-scene-ui';
2
1
  import { createRoot } from 'react-dom/client';
2
+ // W2 (§5.1/§4.2) — the asset-selection Inspector section and the
3
+ // story-document contributions (Story Args section + Actions/Interactions
4
+ // utilities) register at import time; imported FIRST so they render ABOVE
5
+ // the adapter contributions below when matched.
6
+ import './components/asset-selection-section';
7
+ import './components/story-documents-register';
3
8
  // Adapter-module contribution-seam registrations (side-effect imports only):
4
9
  // the permanent escape hatch for first-party UI that isn't yet contract-
5
10
  // expressible (A2's hierarchy-menu-registry.ts: prefab Unpack/Open/Save-as
@@ -11,13 +16,16 @@ import { createRoot } from 'react-dom/client';
11
16
  import './authoring/first-party-hierarchy-menu';
12
17
  import './authoring/first-party-inspector/register';
13
18
  import './authoring/react-inspector-section';
14
- import './authoring/ui-hierarchy-menu';
15
- import './authoring/ui-inspector-sections';
16
- import './scene-ui-mode/ui-prefab-hierarchy-menu';
17
19
  import { AppRoot } from './components/AppRoot';
20
+ import { ensureEditorTheme } from './theme-css';
21
+ import { installEditorTheme } from './theme-preference';
18
22
 
19
- // Wire the React UI runtime into the engine's scene-UI bridge so a scene's `ui`
20
- // tree renders at game-time (B1) — the engine itself stays React-free.
21
- registerSceneUIRenderer();
23
+ // Install the shared design-token stylesheet (editor-style-polish U0) before
24
+ // the editor chrome mounts, so any `.vgai-*` classes it renders are styled
25
+ // from the first paint.
26
+ ensureEditorTheme();
22
27
 
23
- createRoot(document.getElementById('editor-root')!).render(<AppRoot />);
28
+ const editorRoot = document.getElementById('editor-root')!;
29
+ const editorChromeRoot = document.getElementById('editor-chrome-root')!;
30
+ installEditorTheme(editorChromeRoot);
31
+ createRoot(editorRoot).render(<AppRoot />);
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * The dev server serves the project's RAW `vgai.game.json` at `/vgai.game.json`
6
6
  * (editor-server.ts's project-file-serving middleware, mirroring how it
7
- * special-cases `/project.json`) with no view synthesis — the browser parses
8
- * it itself through the PURE half of the manifest loader (`loadGameManifest`,
7
+ * with no view synthesis — the browser parses it itself through the PURE half
8
+ * of the manifest loader (`loadGameManifest`,
9
9
  * `@engine/manifest/load`), never `load-file.ts` (that needs `node:fs`, which
10
10
  * has no place in a browser bundle).
11
11
  */
@@ -13,15 +13,13 @@
13
13
  import { loadGameManifest, type ResolvedGameManifest } from '@engine/manifest/load';
14
14
 
15
15
  /**
16
- * Fetch + parse the current project's manifest, or `null` if it has none
17
- * (legacy `project.json`-only project — the raw-manifest route 404s, same as
18
- * any other missing project-root file per editor-server.ts). A manifest that
19
- * EXISTS but fails validation still throws (same fail-loud contract
20
- * `loadGameManifest` always has) — never silently falls back.
16
+ * Fetch + parse the current project's sole v2 config. Bundled examples use
17
+ * their staged manifest URL. Missing and invalid manifests both fail loudly.
21
18
  */
22
- export async function fetchGameManifest(): Promise<ResolvedGameManifest | null> {
23
- const res = await fetch('/vgai.game.json');
24
- if (!res.ok) return null;
19
+ export async function fetchGameManifest(exampleId?: string): Promise<ResolvedGameManifest> {
20
+ const url = exampleId ? `/examples/${exampleId}/vgai.game.json` : '/vgai.game.json';
21
+ const res = await fetch(url);
22
+ if (!res.ok) throw new Error(`Failed to load ${url}: ${res.status} ${res.statusText}`);
25
23
  return loadGameManifest(await res.json());
26
24
  }
27
25
 
@@ -30,10 +28,10 @@ export async function fetchGameManifest(): Promise<ResolvedGameManifest | null>
30
28
  * — or `null` when the project genuinely has none (the route 404s). Unlike
31
29
  * {@link fetchGameManifest}, this NEVER throws on a schema violation: edit mode
32
30
  * must tolerate a manifest with ONE bad world (e.g. an unknown `kind`) by
33
- * surfacing that world as an error node (#18) while the VALID worlds declared
31
+ * surfacing that world as an error node (#18) while the VALID roots declared
34
32
  * alongside it still open — strict `loadGameManifest` would reject the whole
35
33
  * file and make every declared world vanish. The caller (`edit-mode-authoring
36
- * .ts`) reads `worlds[]` leniently; a genuinely absent manifest (this returns
34
+ * .ts`) reads `roots[]` leniently; a genuinely absent manifest (this returns
37
35
  * `null`) is the only case that synthesizes a single-world game.
38
36
  */
39
37
  export async function fetchRawGameManifest(): Promise<unknown | null> {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Offscreen Three.js renderer that generates thumbnail images for .glb/.gltf models.
2
+ * Offscreen Three.js renderer that generates thumbnail images for supported 3D source formats.
3
3
  *
4
4
  * Usage:
5
5
  * const renderer = getModelThumbnailRenderer();
@@ -9,27 +9,67 @@
9
9
  * - Singleton: one renderer is shared across all callers.
10
10
  * - Deduplicating: concurrent requests for the same URL share one render pass.
11
11
  * - Cached: once rendered, the data URL is returned immediately.
12
- * - Self-cleaning: each loaded GLTF is disposed after the thumbnail is captured.
12
+ * - Self-cleaning: each loaded object is disposed after the thumbnail is captured.
13
13
  */
14
14
 
15
15
  import { gltfLoader } from '@engine/loader';
16
16
  import * as THREE from 'three';
17
+ import { BVHLoader } from 'three/addons/loaders/BVHLoader.js';
18
+ import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js';
19
+ import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
20
+ import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
21
+ import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
22
+ import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
23
+ import { STLLoader } from 'three/addons/loaders/STLLoader.js';
24
+ import { TDSLoader } from 'three/addons/loaders/TDSLoader.js';
17
25
 
18
26
  const SIZE = 128;
19
27
 
20
28
  let instance: ModelThumbnailRenderer | null = null;
21
29
 
22
- class ModelThumbnailRenderer {
30
+ export type ModelThumbnailFormat =
31
+ | 'glb'
32
+ | 'gltf'
33
+ | 'fbx'
34
+ | 'obj'
35
+ | 'dae'
36
+ | 'stl'
37
+ | 'ply'
38
+ | 'bvh'
39
+ | '3ds';
40
+
41
+ export interface ModelThumbnailSpec {
42
+ url: string;
43
+ format?: ModelThumbnailFormat;
44
+ materialUrl?: string;
45
+ background?: 'transparent' | 'neutral';
46
+ output?: 'png' | 'webp';
47
+ }
48
+
49
+ const MODEL_THUMBNAIL_FORMATS = new Set<ModelThumbnailFormat>([
50
+ 'glb',
51
+ 'gltf',
52
+ 'fbx',
53
+ 'obj',
54
+ 'dae',
55
+ 'stl',
56
+ 'ply',
57
+ 'bvh',
58
+ '3ds',
59
+ ]);
60
+
61
+ export function modelThumbnailFormat(url: string): ModelThumbnailFormat | undefined {
62
+ const pathname = url.split(/[?#]/, 1)[0]!.toLowerCase();
63
+ const extension = pathname.slice(pathname.lastIndexOf('.') + 1) as ModelThumbnailFormat;
64
+ return MODEL_THUMBNAIL_FORMATS.has(extension) ? extension : undefined;
65
+ }
66
+
67
+ export class ModelThumbnailRenderer {
23
68
  private renderer: THREE.WebGLRenderer;
24
69
  private scene: THREE.Scene;
25
70
  private camera: THREE.PerspectiveCamera;
26
71
  private cache = new Map<string, string>();
27
72
  private pending = new Map<string, Promise<string>>();
28
- // Shared, DRACO-wired GLTFLoader (A2 / CB2). One-off previews call `.load()`
29
- // directly (no caching/cloning) and dispose the result after capture, which is
30
- // safe — each parse is independent of the shared asset-loaders GLTF cache.
31
- private loader = gltfLoader;
32
-
33
73
  constructor() {
34
74
  const canvas = document.createElement('canvas');
35
75
  canvas.width = SIZE;
@@ -66,32 +106,43 @@ class ModelThumbnailRenderer {
66
106
  }
67
107
 
68
108
  /** Render a thumbnail for the given model URL. Returns a PNG data-URL. */
69
- async render(url: string): Promise<string> {
70
- const cached = this.cache.get(url);
109
+ async render(input: string | ModelThumbnailSpec): Promise<string> {
110
+ const spec: ModelThumbnailSpec = typeof input === 'string' ? { url: input } : input;
111
+ const format = spec.format ?? modelThumbnailFormat(spec.url);
112
+ if (!format) throw new Error(`No thumbnail loader for ${spec.url}`);
113
+ const resolved: ModelThumbnailSpec & { format: ModelThumbnailFormat } = { ...spec, format };
114
+ const key = JSON.stringify(resolved);
115
+ const cached = this.cache.get(key);
71
116
  if (cached) return cached;
72
117
 
73
118
  // Deduplicate concurrent requests for the same URL
74
- const inflight = this.pending.get(url);
119
+ const inflight = this.pending.get(key);
75
120
  if (inflight) return inflight;
76
121
 
77
- const promise = this._render(url);
78
- this.pending.set(url, promise);
122
+ const promise = this._render(resolved);
123
+ this.pending.set(key, promise);
79
124
  try {
80
- return await promise;
125
+ const dataUrl = await promise;
126
+ this.cache.set(key, dataUrl);
127
+ return dataUrl;
81
128
  } finally {
82
- this.pending.delete(url);
129
+ this.pending.delete(key);
83
130
  }
84
131
  }
85
132
 
86
- private async _render(url: string): Promise<string> {
87
- const group = await new Promise<THREE.Group>((resolve, reject) => {
88
- this.loader.load(url, (gltf) => resolve(gltf.scene), undefined, reject);
89
- });
133
+ private async _render(
134
+ spec: ModelThumbnailSpec & { format: ModelThumbnailFormat },
135
+ ): Promise<string> {
136
+ const group = createModelPreviewSource(await loadModelThumbnailObject(spec));
90
137
 
91
138
  this.scene.add(group);
92
139
 
93
140
  // Auto-frame the camera around the model
94
141
  const box = new THREE.Box3().setFromObject(group);
142
+ if (box.isEmpty()) {
143
+ this.scene.remove(group);
144
+ throw new Error('Model contains no previewable geometry.');
145
+ }
95
146
  const center = box.getCenter(new THREE.Vector3());
96
147
  const size = box.getSize(new THREE.Vector3());
97
148
  const maxDim = Math.max(size.x, size.y, size.z) || 1;
@@ -110,18 +161,25 @@ class ModelThumbnailRenderer {
110
161
  this.camera.updateProjectionMatrix();
111
162
 
112
163
  // Render
113
- this.renderer.setClearColor(0x000000, 0);
164
+ if (spec.background === 'neutral') this.renderer.setClearColor(0x20242a, 1);
165
+ else this.renderer.setClearColor(0x000000, 0);
114
166
  this.renderer.clear();
115
167
  this.renderer.render(this.scene, this.camera);
116
- const dataUrl = this.renderer.domElement.toDataURL('image/png');
168
+ const dataUrl =
169
+ spec.output === 'webp'
170
+ ? this.renderer.domElement.toDataURL('image/webp', 0.86)
171
+ : this.renderer.domElement.toDataURL('image/png');
117
172
 
118
173
  // Clean up the loaded GLTF to free GPU memory
119
174
  this.scene.remove(group);
175
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Disposal enumerates the texture slots used by the supported Three.js materials.
120
176
  group.traverse((obj) => {
121
- const mesh = obj as THREE.Mesh;
122
- if (mesh.geometry) mesh.geometry.dispose();
123
- if (mesh.material) {
124
- const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
177
+ const renderable = obj as THREE.Mesh | THREE.Line | THREE.Points;
178
+ if (renderable.geometry) renderable.geometry.dispose();
179
+ if (renderable.material) {
180
+ const materials = Array.isArray(renderable.material)
181
+ ? renderable.material
182
+ : [renderable.material];
125
183
  for (const mat of materials) {
126
184
  // Dispose any textures attached to the material
127
185
  if ('map' in mat && mat.map instanceof THREE.Texture) mat.map.dispose();
@@ -138,19 +196,196 @@ class ModelThumbnailRenderer {
138
196
  }
139
197
  });
140
198
 
141
- this.cache.set(url, dataUrl);
142
199
  return dataUrl;
143
200
  }
144
201
 
145
202
  dispose() {
146
203
  this.renderer.dispose();
204
+ this.renderer.forceContextLoss();
147
205
  this.cache.clear();
148
206
  instance = null;
149
207
  }
150
208
  }
151
209
 
210
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: This format dispatcher keeps each Three.js loader path visible in one place.
211
+ export async function loadModelThumbnailObject(
212
+ spec: ModelThumbnailSpec & { format: ModelThumbnailFormat },
213
+ ): Promise<THREE.Object3D> {
214
+ switch (spec.format) {
215
+ case 'glb':
216
+ case 'gltf':
217
+ return (await gltfLoader.loadAsync(spec.url)).scene;
218
+ case 'fbx':
219
+ return new FBXLoader().loadAsync(spec.url);
220
+ case 'obj': {
221
+ const materialUrl = spec.materialUrl ?? (await discoverObjMaterial(spec.url));
222
+ const loader = new OBJLoader();
223
+ if (materialUrl) {
224
+ const materials = await new MTLLoader().loadAsync(materialUrl);
225
+ materials.preload();
226
+ loader.setMaterials(materials);
227
+ }
228
+ return loader.loadAsync(spec.url);
229
+ }
230
+ case 'dae':
231
+ return (await new ColladaLoader().loadAsync(spec.url)).scene;
232
+ case 'stl': {
233
+ const geometry = await new STLLoader().loadAsync(spec.url);
234
+ return new THREE.Mesh(
235
+ geometry,
236
+ new THREE.MeshStandardMaterial({ color: 0x75a7d8, roughness: 0.75 }),
237
+ );
238
+ }
239
+ case 'ply': {
240
+ const geometry = await new PLYLoader().loadAsync(spec.url);
241
+ if (geometry.index || geometry.hasAttribute('normal')) {
242
+ if (!geometry.hasAttribute('normal')) geometry.computeVertexNormals();
243
+ return new THREE.Mesh(
244
+ geometry,
245
+ new THREE.MeshStandardMaterial({
246
+ color: geometry.hasAttribute('color') ? 0xffffff : 0xb8d09b,
247
+ vertexColors: geometry.hasAttribute('color'),
248
+ roughness: 0.8,
249
+ side: THREE.DoubleSide,
250
+ }),
251
+ );
252
+ }
253
+ geometry.computeBoundingBox();
254
+ const diagonal = geometry.boundingBox?.getSize(new THREE.Vector3()).length() ?? 1;
255
+ return new THREE.Points(
256
+ geometry,
257
+ new THREE.PointsMaterial({
258
+ color: geometry.hasAttribute('color') ? 0xffffff : 0xb8d09b,
259
+ vertexColors: geometry.hasAttribute('color'),
260
+ size: Math.max(diagonal / 220, 0.0001),
261
+ sizeAttenuation: true,
262
+ }),
263
+ );
264
+ }
265
+ case 'bvh': {
266
+ const result = await new BVHLoader().loadAsync(spec.url);
267
+ const root = result.skeleton.bones[0];
268
+ if (!root) throw new Error('BVH contains no skeleton root.');
269
+ const group = new THREE.Group();
270
+ group.add(root);
271
+ const helper = new THREE.SkeletonHelper(root);
272
+ group.add(helper);
273
+ const mixer = new THREE.AnimationMixer(root);
274
+ mixer.clipAction(result.clip).play();
275
+ mixer.setTime(result.clip.duration * 0.35);
276
+ group.updateWorldMatrix(true, true);
277
+ helper.updateMatrixWorld(true);
278
+ return group;
279
+ }
280
+ case '3ds':
281
+ return new TDSLoader().loadAsync(spec.url);
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Give animation-only model files an honest renderable representation.
287
+ * FBX/glTF animation exports commonly contain a complete bone hierarchy and
288
+ * clips but no mesh. They are valid assets, so Asset Editor visualizes their
289
+ * current skeleton instead of misreporting them as an empty source.
290
+ */
291
+ export function createModelPreviewSource(model: THREE.Object3D): THREE.Object3D {
292
+ if (hasRenderableModelContent(model)) return model;
293
+
294
+ model.updateWorldMatrix(true, true);
295
+ const helper = new THREE.SkeletonHelper(model);
296
+ const positions = helper.geometry.getAttribute('position');
297
+ if (!positions || positions.count === 0) {
298
+ helper.dispose();
299
+ return model;
300
+ }
301
+
302
+ helper.name = 'Asset Editor skeleton preview';
303
+ helper.frustumCulled = false;
304
+ const previewRoot = new THREE.Group();
305
+ previewRoot.name = model.name;
306
+ previewRoot.animations = [...model.animations];
307
+ previewRoot.add(model, helper);
308
+ previewRoot.updateWorldMatrix(true, true);
309
+ // SkeletonHelper updates its dynamic line vertices from updateMatrixWorld,
310
+ // while the read-only preview measurer intentionally calls updateWorldMatrix.
311
+ // Materialize the current pose once so framing sees the real skeleton span.
312
+ helper.updateMatrixWorld(true);
313
+ return previewRoot;
314
+ }
315
+
316
+ function hasRenderableModelContent(model: THREE.Object3D): boolean {
317
+ let renderable = false;
318
+ model.traverseVisible((object) => {
319
+ if (!renderable && objectHasRenderableModelContent(object)) renderable = true;
320
+ });
321
+ return renderable;
322
+ }
323
+
324
+ function objectHasRenderableModelContent(object: THREE.Object3D): boolean {
325
+ const sprite = object as THREE.Sprite;
326
+ if (sprite.isSprite) return sprite.material.visible;
327
+ const mesh = object as THREE.Mesh;
328
+ const line = object as THREE.Line;
329
+ const points = object as THREE.Points;
330
+ if (!mesh.isMesh && !line.isLine && !points.isPoints) return false;
331
+ const drawable = object as THREE.Mesh;
332
+ const instanced = object as THREE.InstancedMesh;
333
+ if (instanced.isInstancedMesh && instanced.count === 0) return false;
334
+ const materials = Array.isArray(drawable.material) ? drawable.material : [drawable.material];
335
+ if (materials.length === 0 || materials.every((material) => !material?.visible)) return false;
336
+ const positions = drawable.geometry?.getAttribute('position');
337
+ return Boolean(positions && positions.count > 0 && drawable.geometry.drawRange.count !== 0);
338
+ }
339
+
340
+ async function discoverObjMaterial(url: string): Promise<string | undefined> {
341
+ const pathname = url.split(/[?#]/, 1)[0]!;
342
+ const materialUrl = `${pathname.slice(0, pathname.lastIndexOf('.'))}.mtl`;
343
+ try {
344
+ const response = await fetch(materialUrl, { method: 'HEAD' });
345
+ return response.ok ? materialUrl : undefined;
346
+ } catch {
347
+ return undefined;
348
+ }
349
+ }
350
+
351
+ /** Normalize an upstream/source preview image through the same thumbnail canvas. */
352
+ export async function renderImageThumbnail(
353
+ url: string,
354
+ options: { output?: 'png' | 'webp'; background?: 'transparent' | 'neutral' } = {},
355
+ ): Promise<string> {
356
+ const image = await new Promise<HTMLImageElement>((resolve, reject) => {
357
+ const element = new Image();
358
+ element.onload = () => resolve(element);
359
+ element.onerror = () => reject(new Error(`Failed to load preview image: ${url}`));
360
+ element.src = url;
361
+ });
362
+ const canvas = document.createElement('canvas');
363
+ canvas.width = SIZE;
364
+ canvas.height = SIZE;
365
+ const context = canvas.getContext('2d');
366
+ if (!context) throw new Error('2D thumbnail canvas is unavailable.');
367
+ if (options.background === 'neutral') {
368
+ context.fillStyle = '#20242a';
369
+ context.fillRect(0, 0, SIZE, SIZE);
370
+ }
371
+ const scale = Math.min(SIZE / image.naturalWidth, SIZE / image.naturalHeight);
372
+ const width = image.naturalWidth * scale;
373
+ const height = image.naturalHeight * scale;
374
+ context.drawImage(image, (SIZE - width) / 2, (SIZE - height) / 2, width, height);
375
+ return options.output === 'webp'
376
+ ? canvas.toDataURL('image/webp', 0.86)
377
+ : canvas.toDataURL('image/png');
378
+ }
379
+
152
380
  /** Get (or create) the shared thumbnail renderer. */
153
381
  export function getModelThumbnailRenderer(): ModelThumbnailRenderer {
154
382
  if (!instance) instance = new ModelThumbnailRenderer();
155
383
  return instance;
156
384
  }
385
+
386
+ // A Fast Refresh invalidation creates a fresh module singleton. Release the
387
+ // previous module's offscreen WebGL context first instead of silently leaking
388
+ // one context per AssetBrowser edit.
389
+ if (import.meta.hot) {
390
+ import.meta.hot.dispose(() => instance?.dispose());
391
+ }