@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
@@ -1,38 +1,137 @@
1
- import { gltfLoader } from '@engine/loader';
2
1
  import { useEffect, useRef, useState } from 'react';
3
2
  import * as THREE from 'three';
4
3
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
5
- import { SectionHeader } from '../primitives/SectionHeader';
4
+ import {
5
+ type AssetPreviewBackground,
6
+ captureModelAssetPreview,
7
+ captureObjectAssetPreview,
8
+ createAssetPreviewSnapshot,
9
+ disposeAssetPreviewSnapshot,
10
+ loadProjectAssetModel,
11
+ measureAssetPreview,
12
+ } from '../../asset-preview';
13
+ import {
14
+ type AssetEditorIssue,
15
+ type AssetLoadStage,
16
+ classifyAssetEditorError,
17
+ } from '../../asset-workflow/asset-editor-errors';
18
+ import {
19
+ cameraPresetDirection,
20
+ inspectModel,
21
+ type ModelCameraPreset,
22
+ type ModelDiagnosticMode,
23
+ type ModelInspection,
24
+ supportedModelDiagnosticModes,
25
+ } from '../../asset-workflow/model-inspection';
26
+ import {
27
+ loadProjectThumbnailManifest,
28
+ resetProjectThumbnailManifestCache,
29
+ THUMBNAIL_PROFILE,
30
+ } from '../../asset-workflow/thumbnail-system';
31
+ import {
32
+ Button,
33
+ bg,
34
+ Checkbox,
35
+ danger,
36
+ radius,
37
+ SectionHeader,
38
+ Select,
39
+ TextInput,
40
+ text,
41
+ } from '../../design-system';
42
+ import { useEditorStore } from '../../EditorContext';
43
+ import {
44
+ createModelPreviewSource,
45
+ loadModelThumbnailObject,
46
+ modelThumbnailFormat,
47
+ } from '../../model-thumbnail';
48
+ import { AssetEditorShell } from '../AssetEditorShell';
49
+ import { AssetImportDetails } from '../AssetImportDetails';
50
+ import { writeProjectAsset } from '../asset-editor-persistence';
6
51
 
7
- const labelStyle: React.CSSProperties = { fontSize: 11, color: '#8c8c8c', marginBottom: 2 };
52
+ interface ModelViewerProps {
53
+ assetPath: string | undefined;
54
+ entityId: string | undefined;
55
+ materialPath?: string | undefined;
56
+ displayName?: string | undefined;
57
+ embedded?: boolean | undefined;
58
+ active?: boolean | undefined;
59
+ }
60
+
61
+ interface ModelViewerRuntime {
62
+ camera: THREE.PerspectiveCamera | THREE.OrthographicCamera;
63
+ controls: OrbitControls;
64
+ renderer: THREE.WebGLRenderer;
65
+ scene: THREE.Scene;
66
+ model: THREE.Object3D;
67
+ helpers: THREE.Object3D[];
68
+ mixer: THREE.AnimationMixer | null;
69
+ clock: THREE.Clock;
70
+ }
8
71
 
9
- export function ModelViewer({ assetPath }: { assetPath: string }) {
72
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one viewer coordinates embedded/full chrome and both live-entity and file-backed Asset Editor sources.
73
+ export function ModelViewer({
74
+ assetPath,
75
+ entityId,
76
+ materialPath,
77
+ displayName: suppliedDisplayName,
78
+ embedded = false,
79
+ active = true,
80
+ }: ModelViewerProps) {
81
+ const store = useEditorStore();
10
82
  const containerRef = useRef<HTMLDivElement>(null);
11
- const [error, setError] = useState(false);
83
+ const runtimeRef = useRef<ModelViewerRuntime | null>(null);
84
+ const [error, setError] = useState<string | null>(null);
85
+ const [issue, setIssue] = useState<AssetEditorIssue | null>(null);
86
+ const [loadStage, setLoadStage] = useState<AssetLoadStage>('fetching');
87
+ const loadStageRef = useRef<AssetLoadStage>('fetching');
88
+ const [retryKey, setRetryKey] = useState(0);
12
89
  const [loading, setLoading] = useState(true);
13
- const fileName = assetPath.split('/').pop() ?? assetPath;
90
+ const [capturing, setCapturing] = useState(false);
91
+ const [background, setBackground] = useState<AssetPreviewBackground>('neutral');
92
+ const [exposure, setExposure] = useState(1);
93
+ const [grid, setGrid] = useState(true);
94
+ const [projection, setProjection] = useState<'perspective' | 'orthographic'>('perspective');
95
+ const [lighting, setLighting] = useState<'studio' | 'outdoor' | 'flat'>('studio');
96
+ const [inspection, setInspection] = useState<ModelInspection | null>(null);
97
+ const [diagnosticMode, setDiagnosticMode] = useState<ModelDiagnosticMode>('lit');
98
+ const [playing, setPlaying] = useState(false);
99
+ const [loop, setLoop] = useState(true);
100
+ const [speed, setSpeed] = useState(1);
101
+ const [time, setTime] = useState(0);
102
+ const [selectedClip, setSelectedClip] = useState(0);
103
+ const playbackRef = useRef({ playing: false, speed: 1, selectedClip: 0 });
104
+ const [contactSheet, setContactSheet] = useState<string | null>(null);
105
+ const sourceObject = entityId ? store.objectMap.get(entityId) : undefined;
106
+ const displayName =
107
+ suppliedDisplayName ??
108
+ (assetPath
109
+ ? (assetPath.split('/').pop() ?? assetPath)
110
+ : sourceObject?.name || entityId || 'Asset Editor');
14
111
 
112
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: WebGL setup, async model ownership, resize, and deterministic cleanup form one resource lifetime.
15
113
  useEffect(() => {
16
114
  const container = containerRef.current;
17
- if (!container) return;
115
+ if (!container || !active) return;
18
116
 
19
- setError(false);
117
+ setError(null);
118
+ setIssue(null);
119
+ setLoadStage('fetching');
120
+ loadStageRef.current = 'fetching';
20
121
  setLoading(true);
122
+ setContactSheet(null);
21
123
 
22
124
  const width = container.clientWidth;
23
- const height = width; // square aspect
24
-
25
- // Renderer
125
+ const height = width;
26
126
  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
27
127
  renderer.setSize(width, height);
28
128
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
29
129
  renderer.outputColorSpace = THREE.SRGBColorSpace;
30
130
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
31
- renderer.toneMappingExposure = 1.0;
131
+ renderer.toneMappingExposure = 1;
32
132
  renderer.setClearColor(0x1e1e1e, 1);
33
133
  container.appendChild(renderer.domElement);
34
134
 
35
- // Scene
36
135
  const scene = new THREE.Scene();
37
136
  scene.add(new THREE.AmbientLight(0xffffff, 0.8));
38
137
  const key = new THREE.DirectionalLight(0xffffff, 1.5);
@@ -42,139 +141,724 @@ export function ModelViewer({ assetPath }: { assetPath: string }) {
42
141
  fill.position.set(-3, -1, 2);
43
142
  scene.add(fill);
44
143
 
45
- // Camera
46
144
  const camera = new THREE.PerspectiveCamera(35, 1, 0.01, 1000);
47
-
48
- // Controls
49
145
  const controls = new OrbitControls(camera, renderer.domElement);
50
146
  controls.enableDamping = true;
51
147
  controls.dampingFactor = 0.1;
52
148
 
53
- // Animation loop
54
149
  let rafId = 0;
150
+ let disposed = false;
151
+ const modelLoadController = new AbortController();
152
+ let ownedModel: THREE.Object3D | null = null;
153
+ let ownedSnapshot: THREE.Object3D | null = null;
154
+ const clock = new THREE.Clock();
55
155
  const animate = () => {
56
156
  rafId = requestAnimationFrame(animate);
157
+ const runtime = runtimeRef.current;
158
+ if (runtime?.mixer && playbackRef.current.playing) {
159
+ runtime.mixer.update(clock.getDelta() * playbackRef.current.speed);
160
+ const clip = runtime.model.animations[playbackRef.current.selectedClip];
161
+ if (clip) setTime(runtime.mixer.time % Math.max(clip.duration, 0.001));
162
+ } else clock.getDelta();
57
163
  controls.update();
58
- renderer.render(scene, camera);
164
+ renderer.render(scene, runtimeRef.current?.camera ?? camera);
59
165
  };
60
166
 
61
- // Load model via the shared, DRACO-wired GLTFLoader (A2 / CB2). This is a
62
- // one-off preview: `.load()` parses fresh (no caching/cloning) and the result
63
- // is disposed on unmount, independent of the asset-loaders GLTF cache.
64
- const loader = gltfLoader;
65
- loader.load(
66
- assetPath,
67
- (gltf) => {
68
- const model = gltf.scene;
69
- scene.add(model);
70
-
71
- // Frame camera around model
72
- const box = new THREE.Box3().setFromObject(model);
73
- const center = box.getCenter(new THREE.Vector3());
74
- const size = box.getSize(new THREE.Vector3());
75
- const maxDim = Math.max(size.x, size.y, size.z) || 1;
76
- const fovRad = (camera.fov * Math.PI) / 180;
77
- const dist = (maxDim / (2 * Math.tan(fovRad / 2))) * 1.3;
78
-
79
- camera.position.set(center.x + dist * 0.65, center.y + dist * 0.45, center.z + dist * 0.65);
80
- camera.near = dist * 0.01;
81
- camera.far = dist * 20;
82
- camera.updateProjectionMatrix();
83
-
84
- controls.target.copy(center);
85
- controls.update();
86
-
87
- setLoading(false);
88
- animate();
89
- },
90
- undefined,
91
- () => {
92
- setError(true);
93
- setLoading(false);
94
- },
95
- );
167
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: installation owns framing, inspection, animation, and bounded resource registration as one lifetime transition.
168
+ const install = (model: THREE.Object3D, ownsResources: boolean) => {
169
+ if (disposed) {
170
+ if (ownsResources) disposeModel(model);
171
+ return;
172
+ }
173
+ if (ownsResources) ownedModel = model;
174
+ scene.add(model);
175
+ loadStageRef.current = 'gpu-upload';
176
+ setLoadStage('gpu-upload');
177
+ frameInteractiveModel(model, camera, controls);
178
+ const nextInspection = inspectModel(model);
179
+ setInspection(nextInspection);
180
+ let mixer: THREE.AnimationMixer | null = null;
181
+ if (model.animations.length > 0) {
182
+ mixer = new THREE.AnimationMixer(model);
183
+ const action = mixer.clipAction(
184
+ model.animations[Math.min(selectedClip, model.animations.length - 1)]!,
185
+ );
186
+ action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, loop ? Infinity : 1);
187
+ action.play();
188
+ }
189
+ runtimeRef.current = { camera, controls, renderer, scene, model, helpers: [], mixer, clock };
190
+ applyModelDiagnosticMode(runtimeRef.current, diagnosticMode);
191
+ setLoading(false);
192
+ loadStageRef.current = 'ready';
193
+ setLoadStage('ready');
194
+ animate();
195
+ };
96
196
 
97
- // Handle resize if container width changes
98
- const ro = new ResizeObserver((entries) => {
197
+ const fail = (err: unknown) => {
198
+ const nextIssue = classifyAssetEditorError(err, loadStageRef.current);
199
+ setIssue(nextIssue);
200
+ setError(nextIssue.message);
201
+ setLoading(false);
202
+ };
203
+
204
+ if (sourceObject) {
205
+ try {
206
+ ownedSnapshot = createAssetPreviewSnapshot(sourceObject);
207
+ install(ownedSnapshot, false);
208
+ } catch (err) {
209
+ if (ownedSnapshot) disposeAssetPreviewSnapshot(ownedSnapshot);
210
+ ownedSnapshot = null;
211
+ fail(err);
212
+ }
213
+ } else if (assetPath) {
214
+ const format = modelThumbnailFormat(assetPath);
215
+ loadStageRef.current =
216
+ format === 'gltf' || format === 'obj' ? 'dependency-resolution' : 'parsing';
217
+ setLoadStage(loadStageRef.current);
218
+ const modelPromise =
219
+ format && format !== 'glb' && format !== 'gltf'
220
+ ? loadModelThumbnailObject({
221
+ url: assetPath,
222
+ format,
223
+ ...(materialPath ? { materialUrl: materialPath } : {}),
224
+ })
225
+ : loadProjectAssetModel(assetPath, modelLoadController.signal);
226
+ void modelPromise.then(
227
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the completion path must dispose correctly across unmount, snapshot failure, and successful installation.
228
+ (loadedModel) => {
229
+ const model = createModelPreviewSource(loadedModel);
230
+ try {
231
+ if (disposed) {
232
+ disposeModel(model);
233
+ return;
234
+ }
235
+ ownedModel = model;
236
+ ownedSnapshot = createAssetPreviewSnapshot(model);
237
+ install(ownedSnapshot, false);
238
+ } catch (err) {
239
+ if (ownedSnapshot) disposeAssetPreviewSnapshot(ownedSnapshot);
240
+ ownedSnapshot = null;
241
+ if (ownedModel) disposeModel(ownedModel);
242
+ ownedModel = null;
243
+ fail(err);
244
+ }
245
+ },
246
+ (err) => {
247
+ if (!disposed) fail(err);
248
+ },
249
+ );
250
+ } else {
251
+ setError('Asset Editor source is no longer available.');
252
+ setLoading(false);
253
+ }
254
+
255
+ const resizeObserver = new ResizeObserver((entries) => {
99
256
  for (const entry of entries) {
100
- const w = entry.contentRect.width;
101
- const h = w;
102
- renderer.setSize(w, h);
103
- camera.aspect = 1;
104
- camera.updateProjectionMatrix();
257
+ const nextWidth = entry.contentRect.width;
258
+ const nextHeight = Math.max(entry.contentRect.height, 1);
259
+ renderer.setSize(nextWidth, nextHeight);
260
+ const activeCamera = runtimeRef.current?.camera ?? camera;
261
+ const aspect = nextWidth / nextHeight;
262
+ if (activeCamera instanceof THREE.PerspectiveCamera) activeCamera.aspect = aspect;
263
+ else {
264
+ const halfHeight = (activeCamera.top - activeCamera.bottom) / 2;
265
+ activeCamera.left = -halfHeight * aspect;
266
+ activeCamera.right = halfHeight * aspect;
267
+ }
268
+ activeCamera.updateProjectionMatrix();
105
269
  }
106
270
  });
107
- ro.observe(container);
271
+ resizeObserver.observe(container);
108
272
 
109
273
  return () => {
274
+ disposed = true;
275
+ modelLoadController.abort(new Error('Asset Editor model viewer closed.'));
110
276
  cancelAnimationFrame(rafId);
111
- ro.disconnect();
277
+ resizeObserver.disconnect();
112
278
  controls.dispose();
279
+ runtimeRef.current?.mixer?.stopAllAction();
280
+ for (const helper of runtimeRef.current?.helpers ?? []) disposeAssetPreviewSnapshot(helper);
281
+ runtimeRef.current = null;
113
282
  renderer.dispose();
114
- // Clean up model geometry/materials
115
- scene.traverse((obj) => {
116
- const mesh = obj as THREE.Mesh;
117
- if (mesh.geometry) mesh.geometry.dispose();
118
- if (mesh.material) {
119
- const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
120
- for (const mat of mats) {
121
- for (const val of Object.values(mat)) {
122
- if (val instanceof THREE.Texture) val.dispose();
123
- }
124
- mat.dispose();
125
- }
126
- }
127
- });
128
- if (container.contains(renderer.domElement)) {
129
- container.removeChild(renderer.domElement);
130
- }
283
+ renderer.forceContextLoss();
284
+ if (ownedSnapshot) disposeAssetPreviewSnapshot(ownedSnapshot);
285
+ if (ownedModel) disposeModel(ownedModel);
286
+ if (container.contains(renderer.domElement)) container.removeChild(renderer.domElement);
131
287
  };
132
- }, [assetPath]);
288
+ }, [active, assetPath, materialPath, retryKey, sourceObject]);
289
+
290
+ useEffect(() => {
291
+ playbackRef.current = { playing, speed, selectedClip };
292
+ }, [playing, selectedClip, speed]);
293
+
294
+ useEffect(() => {
295
+ const runtime = runtimeRef.current;
296
+ if (!runtime) return;
297
+ runtime.renderer.toneMappingExposure = exposure;
298
+ runtime.renderer.setClearColor(
299
+ background === 'transparent' ? 0x000000 : 0x20242a,
300
+ background === 'transparent' ? 0 : 1,
301
+ );
302
+ const existing = runtime.scene.getObjectByName('__asset_editor_grid');
303
+ if (grid && !existing) {
304
+ const helper = new THREE.GridHelper(10, 20, 0x51606f, 0x343b44);
305
+ helper.name = '__asset_editor_grid';
306
+ runtime.scene.add(helper);
307
+ runtime.helpers.push(helper);
308
+ } else if (!grid && existing) {
309
+ runtime.scene.remove(existing);
310
+ runtime.helpers = runtime.helpers.filter((item) => item !== existing);
311
+ disposeAssetPreviewSnapshot(existing);
312
+ }
313
+ }, [background, exposure, grid]);
314
+
315
+ useEffect(() => {
316
+ const runtime = runtimeRef.current;
317
+ if (!runtime) return;
318
+ const isPerspective = runtime.camera instanceof THREE.PerspectiveCamera;
319
+ if ((projection === 'perspective') === isPerspective) return;
320
+ const aspect =
321
+ Math.max(containerRef.current?.clientWidth ?? 1, 1) /
322
+ Math.max(containerRef.current?.clientHeight ?? 1, 1);
323
+ const next =
324
+ projection === 'perspective'
325
+ ? new THREE.PerspectiveCamera(35, aspect, runtime.camera.near, runtime.camera.far)
326
+ : new THREE.OrthographicCamera(
327
+ -aspect,
328
+ aspect,
329
+ 1,
330
+ -1,
331
+ runtime.camera.near,
332
+ runtime.camera.far,
333
+ );
334
+ next.position.copy(runtime.camera.position);
335
+ next.quaternion.copy(runtime.camera.quaternion);
336
+ next.up.copy(runtime.camera.up);
337
+ runtime.camera = next;
338
+ (runtime.controls as unknown as { object: THREE.Camera }).object = next;
339
+ frameRuntime(runtime);
340
+ }, [projection]);
341
+
342
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: each named lighting preset maps all owned light types in one visible state transition.
343
+ useEffect(() => {
344
+ const runtime = runtimeRef.current;
345
+ if (!runtime) return;
346
+ const lights = runtime.scene.children.filter(
347
+ (child): child is THREE.Light => child instanceof THREE.Light,
348
+ );
349
+ for (const light of lights) {
350
+ if (light instanceof THREE.AmbientLight)
351
+ light.intensity = lighting === 'flat' ? 2 : lighting === 'outdoor' ? 1.15 : 0.8;
352
+ else
353
+ light.intensity =
354
+ lighting === 'flat'
355
+ ? 0
356
+ : lighting === 'outdoor'
357
+ ? 2.2
358
+ : light.color.getHex() === 0xffffff
359
+ ? 1.5
360
+ : 0.4;
361
+ }
362
+ }, [lighting]);
363
+
364
+ useEffect(() => {
365
+ if (runtimeRef.current) applyModelDiagnosticMode(runtimeRef.current, diagnosticMode);
366
+ }, [diagnosticMode]);
367
+
368
+ useEffect(() => {
369
+ const runtime = runtimeRef.current;
370
+ if (!runtime?.mixer || runtime.model.animations.length === 0) return;
371
+ runtime.mixer.stopAllAction();
372
+ runtime.mixer.time = 0;
373
+ const clip =
374
+ runtime.model.animations[Math.min(selectedClip, runtime.model.animations.length - 1)]!;
375
+ const action = runtime.mixer.clipAction(clip);
376
+ action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, loop ? Infinity : 1);
377
+ action.play();
378
+ setTime(0);
379
+ }, [selectedClip, loop]);
380
+
381
+ const capture = async () => {
382
+ setCapturing(true);
383
+ setError(null);
384
+ try {
385
+ const result = assetPath
386
+ ? await captureModelAssetPreview(assetPath, { background })
387
+ : sourceObject
388
+ ? captureObjectAssetPreview(sourceObject, { background })
389
+ : null;
390
+ if (!result) throw new Error('Asset Editor source is no longer available.');
391
+ setContactSheet(`data:image/png;base64,${result.contactSheet.base64}`);
392
+ } catch (err) {
393
+ setError(err instanceof Error ? err.message : String(err));
394
+ } finally {
395
+ setCapturing(false);
396
+ }
397
+ };
398
+
399
+ const saveThumbnailFraming = async (reset = false) => {
400
+ if (!assetPath) return;
401
+ const manifest = (await loadProjectThumbnailManifest()) ?? {
402
+ version: 1 as const,
403
+ profile: THUMBNAIL_PROFILE.version,
404
+ generatedAt: new Date().toISOString(),
405
+ entries: {},
406
+ };
407
+ const key = assetPath.replace(/^\/+/, '');
408
+ const runtime = runtimeRef.current;
409
+ const prior = manifest.entries[key] ?? {
410
+ state: 'fallback' as const,
411
+ profileVersion: THUMBNAIL_PROFILE.version,
412
+ issueCode: 'thumbnail.pending-regeneration',
413
+ };
414
+ const entry =
415
+ reset || !runtime
416
+ ? { ...prior, framing: undefined }
417
+ : {
418
+ ...prior,
419
+ framing: {
420
+ position: runtime.camera.position.toArray() as [number, number, number],
421
+ target: runtime.controls.target.toArray() as [number, number, number],
422
+ },
423
+ };
424
+ const updated = {
425
+ ...manifest,
426
+ generatedAt: new Date().toISOString(),
427
+ entries: { ...manifest.entries, [key]: entry },
428
+ };
429
+ if (
430
+ await writeProjectAsset('.vgai/thumbnails.json', `${JSON.stringify(updated, null, 2)}\n`, {
431
+ label: `${reset ? 'Reset' : 'Save'} thumbnail framing ${assetPath}`,
432
+ kind: 'project-file',
433
+ })
434
+ )
435
+ resetProjectThumbnailManifestCache();
436
+ };
437
+
438
+ const viewport = (
439
+ <div
440
+ ref={containerRef}
441
+ data-testid="asset-editor-viewport"
442
+ role="region"
443
+ aria-label={`Interactive 3D preview: ${displayName}`}
444
+ aria-busy={loading}
445
+ style={{
446
+ width: '100%',
447
+ height: '100%',
448
+ minHeight: embedded ? 220 : 360,
449
+ background: bg.inset,
450
+ borderRadius: radius.sm,
451
+ overflow: 'hidden',
452
+ display: 'flex',
453
+ alignItems: 'center',
454
+ justifyContent: 'center',
455
+ position: 'relative',
456
+ }}
457
+ >
458
+ {loading && !error && (
459
+ <span style={{ color: text[2], fontSize: 11, position: 'absolute' }}>Loading...</span>
460
+ )}
461
+ {!active && (
462
+ <span style={{ color: text[2], fontSize: 11, position: 'absolute' }}>
463
+ Preview suspended while this editor is inactive.
464
+ </span>
465
+ )}
466
+ {error && (
467
+ <div
468
+ style={{ color: danger, fontSize: 11, position: 'absolute', padding: 12, maxWidth: 520 }}
469
+ >
470
+ <strong>{issue?.code ?? 'asset.rendering.failed'}</strong> · {issue?.stage ?? loadStage}
471
+ <br />
472
+ {error}
473
+ <br />
474
+ {issue?.recovery.includes('retry') && (
475
+ <Button type="button" onClick={() => setRetryKey((value) => value + 1)}>
476
+ Retry
477
+ </Button>
478
+ )}
479
+ {issue?.recovery.includes('reimport') && <Button type="button">Reimport</Button>}
480
+ </div>
481
+ )}
482
+ </div>
483
+ );
484
+
485
+ if (embedded) return viewport;
486
+
487
+ const pathFormat = assetPath ? modelThumbnailFormat(assetPath) : undefined;
488
+ const deterministicCaptureSupported = !assetPath || pathFormat === 'glb' || pathFormat === 'gltf';
489
+
490
+ const modes = runtimeRef.current ? supportedModelDiagnosticModes(runtimeRef.current.model) : [];
491
+ const selectedDuration = runtimeRef.current?.model.animations[selectedClip]?.duration ?? 0;
492
+ const hierarchy = inspection?.hierarchy ?? [];
133
493
 
134
494
  return (
135
- <div style={{ padding: 8, display: 'flex', flexDirection: 'column', gap: 8 }}>
136
- <div style={{ fontWeight: 600, fontSize: 12, color: '#c5c8ce', wordBreak: 'break-all' }}>
137
- {fileName}
138
- </div>
139
- <div
140
- ref={containerRef}
141
- style={{
142
- width: '100%',
143
- aspectRatio: '1',
144
- background: '#1e1e1e',
145
- borderRadius: 4,
146
- overflow: 'hidden',
147
- display: 'flex',
148
- alignItems: 'center',
149
- justifyContent: 'center',
150
- position: 'relative',
151
- }}
152
- >
153
- {loading && !error && (
154
- <span style={{ color: '#8c8c8c', fontSize: 11, position: 'absolute' }}>Loading...</span>
155
- )}
156
- {error && (
157
- <span style={{ color: '#8c8c8c', fontSize: 11, position: 'absolute' }}>
158
- Failed to load model
159
- </span>
160
- )}
161
- </div>
162
- <SectionHeader label="Info">
163
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
164
- <div>
165
- <div style={labelStyle}>Path</div>
166
- <div style={{ fontSize: 11, color: '#c5c8ce', wordBreak: 'break-all' }}>
167
- {assetPath}
168
- </div>
169
- </div>
170
- <div>
171
- <div style={labelStyle}>Format</div>
172
- <div style={{ fontSize: 11, color: '#c5c8ce' }}>
173
- {assetPath.endsWith('.glb') ? 'GLB (Binary)' : 'glTF'}
495
+ <AssetEditorShell
496
+ type="model"
497
+ title={displayName}
498
+ toolbar={
499
+ <>
500
+ <Button type="button" onClick={() => frameRuntime(runtimeRef.current)}>
501
+ Frame
502
+ </Button>
503
+ {(['front', 'back', 'left', 'right', 'top', 'bottom', 'isometric'] as const).map(
504
+ (preset) => (
505
+ <Button
506
+ key={preset}
507
+ type="button"
508
+ onClick={() => setCameraPreset(runtimeRef.current, preset)}
509
+ >
510
+ {preset}
511
+ </Button>
512
+ ),
513
+ )}
514
+ <Select
515
+ aria-label="Model diagnostic view"
516
+ value={diagnosticMode}
517
+ onChange={(event) => setDiagnosticMode(event.target.value as ModelDiagnosticMode)}
518
+ >
519
+ {modes
520
+ .filter((mode) => mode.available)
521
+ .map((mode) => (
522
+ <option key={mode.mode} value={mode.mode}>
523
+ {mode.mode}
524
+ </option>
525
+ ))}
526
+ </Select>
527
+ <Select
528
+ aria-label="Camera projection"
529
+ value={projection}
530
+ onChange={(event) =>
531
+ setProjection(event.target.value as 'perspective' | 'orthographic')
532
+ }
533
+ >
534
+ <option value="perspective">Perspective</option>
535
+ <option value="orthographic">Orthographic</option>
536
+ </Select>
537
+ <Select
538
+ aria-label="Lighting preset"
539
+ value={lighting}
540
+ onChange={(event) => setLighting(event.target.value as 'studio' | 'outdoor' | 'flat')}
541
+ >
542
+ <option value="studio">Studio light</option>
543
+ <option value="outdoor">Outdoor light</option>
544
+ <option value="flat">Flat light</option>
545
+ </Select>
546
+ <Select
547
+ aria-label="Asset preview background"
548
+ value={background}
549
+ onChange={(event) => setBackground(event.target.value as AssetPreviewBackground)}
550
+ >
551
+ <option value="neutral">Neutral</option>
552
+ <option value="transparent">Transparent</option>
553
+ </Select>
554
+ <label>
555
+ Exposure{' '}
556
+ <TextInput
557
+ aria-label="Preview exposure"
558
+ type="range"
559
+ min="0.2"
560
+ max="2"
561
+ step="0.1"
562
+ value={exposure}
563
+ onChange={(event) => setExposure(Number(event.target.value))}
564
+ />
565
+ </label>
566
+ <Button type="button" aria-pressed={grid} onClick={() => setGrid((value) => !value)}>
567
+ Grid
568
+ </Button>
569
+ <Button
570
+ type="button"
571
+ onClick={() => {
572
+ setExposure(1);
573
+ setBackground('neutral');
574
+ setGrid(true);
575
+ setProjection('perspective');
576
+ setLighting('studio');
577
+ setDiagnosticMode('lit');
578
+ frameRuntime(runtimeRef.current);
579
+ }}
580
+ >
581
+ Reset
582
+ </Button>
583
+ {assetPath && (
584
+ <Button type="button" onClick={() => void saveThumbnailFraming(false)}>
585
+ Save thumbnail view
586
+ </Button>
587
+ )}
588
+ {assetPath && (
589
+ <Button type="button" onClick={() => void saveThumbnailFraming(true)}>
590
+ Reset thumbnail view
591
+ </Button>
592
+ )}
593
+ </>
594
+ }
595
+ structure={
596
+ <div>
597
+ <SectionHeader label="Hierarchy">
598
+ <div role="tree" aria-label="Model hierarchy">
599
+ {hierarchy.map((node) => (
600
+ <Button
601
+ type="button"
602
+ role="treeitem"
603
+ key={node.id}
604
+ variant="ghost"
605
+ size="compact"
606
+ className="vgai-model-tree-item"
607
+ style={{
608
+ display: 'block',
609
+ width: '100%',
610
+ paddingLeft: node.depth * 10 + 4,
611
+ }}
612
+ >
613
+ {node.name} <small>{node.type}</small>
614
+ </Button>
615
+ ))}
174
616
  </div>
617
+ </SectionHeader>
618
+ <SectionHeader label="Materials">
619
+ {inspection?.materialNames.map((name, index) => (
620
+ <div key={`${name}:${index}`}>{name}</div>
621
+ ))}
622
+ </SectionHeader>
623
+ </div>
624
+ }
625
+ details={
626
+ <div>
627
+ <SectionHeader label="Statistics">
628
+ <ModelStatistics value={inspection} />
629
+ </SectionHeader>
630
+ <SectionHeader label="Diagnostics">
631
+ {modes.map((mode) => (
632
+ <div key={mode.mode}>
633
+ {mode.mode}: {mode.available ? 'available' : mode.reason}
634
+ </div>
635
+ ))}
636
+ </SectionHeader>
637
+ <SectionHeader label="Source">
638
+ <div style={{ wordBreak: 'break-all' }}>{assetPath ?? `Entity ${entityId}`}</div>
639
+ </SectionHeader>
640
+ {assetPath && <AssetImportDetails assetPath={assetPath} />}
641
+ {deterministicCaptureSupported && (
642
+ <SectionHeader label="Deterministic capture">
643
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
644
+ <Button
645
+ type="button"
646
+ variant="secondary"
647
+ data-testid="capture-asset-preview"
648
+ disabled={capturing || loading}
649
+ onClick={() => void capture()}
650
+ >
651
+ {capturing ? 'Capturing…' : 'Capture front · right · top · ¾'}
652
+ </Button>
653
+ {contactSheet && (
654
+ <img
655
+ src={contactSheet}
656
+ data-testid="asset-preview-contact-sheet"
657
+ alt="Front, right, top, and three-quarter asset preview"
658
+ style={{ width: '100%', display: 'block', background: bg.inset }}
659
+ />
660
+ )}
661
+ </div>
662
+ </SectionHeader>
663
+ )}
664
+ </div>
665
+ }
666
+ status={
667
+ inspection?.clips ? (
668
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
669
+ <Select
670
+ aria-label="Animation clip"
671
+ value={selectedClip}
672
+ onChange={(event) => setSelectedClip(Number(event.target.value))}
673
+ >
674
+ {runtimeRef.current?.model.animations.map((clip, index) => (
675
+ <option key={`${clip.name}:${index}`} value={index}>
676
+ {clip.name || `Clip ${index + 1}`}
677
+ </option>
678
+ ))}
679
+ </Select>
680
+ <Button type="button" onClick={() => setPlaying((value) => !value)}>
681
+ {playing ? 'Pause' : 'Play'}
682
+ </Button>
683
+ <label>
684
+ Time{' '}
685
+ <TextInput
686
+ aria-label="Animation time"
687
+ type="range"
688
+ min={0}
689
+ max={selectedDuration || 1}
690
+ step={1 / 30}
691
+ value={Math.min(time, selectedDuration || 1)}
692
+ onChange={(event) => {
693
+ const next = Number(event.target.value);
694
+ runtimeRef.current?.mixer?.setTime(next);
695
+ setTime(next);
696
+ }}
697
+ />
698
+ </label>
699
+ <span>
700
+ {time.toFixed(2)} / {selectedDuration.toFixed(2)}s
701
+ </span>
702
+ <label>
703
+ <Checkbox checked={loop} onChange={(event) => setLoop(event.target.checked)} /> Loop
704
+ </label>
705
+ <label>
706
+ Speed{' '}
707
+ <Select value={speed} onChange={(event) => setSpeed(Number(event.target.value))}>
708
+ <option value={0.25}>¼×</option>
709
+ <option value={0.5}>½×</option>
710
+ <option value={1}>1×</option>
711
+ <option value={2}>2×</option>
712
+ </Select>
713
+ </label>
175
714
  </div>
715
+ ) : (
716
+ `${loading ? `${loadStage}…` : error ? `${issue?.stage ?? 'rendering'} failed · ${issue?.code}` : 'Ready'} · ${inspection?.triangles.toLocaleString() ?? 0} triangles`
717
+ )
718
+ }
719
+ >
720
+ {viewport}
721
+ </AssetEditorShell>
722
+ );
723
+ }
724
+
725
+ function ModelStatistics({ value }: { value: ModelInspection | null }) {
726
+ if (!value) return <div>Loading inspection…</div>;
727
+ const rows: Array<[string, string | number]> = [
728
+ ['Nodes', value.nodes],
729
+ ['Meshes', value.meshes],
730
+ ['Primitives', value.primitives],
731
+ ['Vertices', value.vertices.toLocaleString()],
732
+ ['Triangles', value.triangles.toLocaleString()],
733
+ ['Materials', value.materials],
734
+ ['Textures', value.textures],
735
+ ['Bones', value.bones],
736
+ ['Clips', value.clips],
737
+ ['Morph targets', value.morphTargets],
738
+ ['GPU estimate', `${(value.memoryBytes / 1_048_576).toFixed(2)} MiB`],
739
+ [
740
+ 'Bounds',
741
+ value.bounds
742
+ ? `${value.bounds.x.toFixed(2)} × ${value.bounds.y.toFixed(2)} × ${value.bounds.z.toFixed(2)} units`
743
+ : 'No renderable bounds',
744
+ ],
745
+ ];
746
+ return (
747
+ <dl>
748
+ {rows.map(([label, result]) => (
749
+ <div key={label} style={{ display: 'flex', justifyContent: 'space-between' }}>
750
+ <dt>{label}</dt>
751
+ <dd>{result}</dd>
176
752
  </div>
177
- </SectionHeader>
178
- </div>
753
+ ))}
754
+ </dl>
179
755
  );
180
756
  }
757
+
758
+ export function frameInteractiveModel(
759
+ model: THREE.Object3D,
760
+ camera: THREE.PerspectiveCamera | THREE.OrthographicCamera,
761
+ controls: OrbitControls,
762
+ ): void {
763
+ const { center, size } = measureAssetPreview(model, 'perspective');
764
+ const maxDim = Math.max(size.x, size.y, size.z) || 1;
765
+ const distance =
766
+ camera instanceof THREE.PerspectiveCamera
767
+ ? (maxDim / (2 * Math.tan((camera.fov * Math.PI) / 360))) * 1.3
768
+ : maxDim * 2;
769
+ const direction = new THREE.Vector3(1, 0.72, 1).normalize();
770
+ camera.position.copy(center).addScaledVector(direction, distance);
771
+ camera.near = distance * 0.01;
772
+ camera.far = distance * 20;
773
+ if (camera instanceof THREE.OrthographicCamera) {
774
+ const element = controls.domElement;
775
+ const aspect = Math.max(element?.clientWidth ?? 1, 1) / Math.max(element?.clientHeight ?? 1, 1);
776
+ const halfHeight = maxDim * 0.65;
777
+ camera.left = -halfHeight * aspect;
778
+ camera.right = halfHeight * aspect;
779
+ camera.top = halfHeight;
780
+ camera.bottom = -halfHeight;
781
+ camera.zoom = 1;
782
+ }
783
+ camera.updateProjectionMatrix();
784
+ controls.target.copy(center);
785
+ controls.update();
786
+ }
787
+
788
+ function frameRuntime(runtime: ModelViewerRuntime | null): void {
789
+ if (runtime) frameInteractiveModel(runtime.model, runtime.camera, runtime.controls);
790
+ }
791
+
792
+ function setCameraPreset(runtime: ModelViewerRuntime | null, preset: ModelCameraPreset): void {
793
+ if (!runtime) return;
794
+ const { center, size } = measureAssetPreview(runtime.model, 'perspective');
795
+ const maxDim = Math.max(size.x, size.y, size.z) || 1;
796
+ const distance =
797
+ runtime.camera instanceof THREE.PerspectiveCamera
798
+ ? (maxDim / (2 * Math.tan((runtime.camera.fov * Math.PI) / 360))) * 1.3
799
+ : maxDim * 2;
800
+ runtime.camera.position.copy(center).addScaledVector(cameraPresetDirection(preset), distance);
801
+ runtime.camera.up.set(0, preset === 'top' ? 0 : 1, preset === 'top' ? -1 : 0);
802
+ runtime.camera.lookAt(center);
803
+ runtime.camera.updateProjectionMatrix();
804
+ runtime.controls.target.copy(center);
805
+ runtime.controls.update();
806
+ }
807
+
808
+ function applyModelDiagnosticMode(runtime: ModelViewerRuntime, mode: ModelDiagnosticMode): void {
809
+ for (const helper of [...runtime.helpers]) {
810
+ if (!helper.name.startsWith('__asset_editor_diagnostic')) continue;
811
+ runtime.scene.remove(helper);
812
+ runtime.helpers.splice(runtime.helpers.indexOf(helper), 1);
813
+ disposeAssetPreviewSnapshot(helper);
814
+ }
815
+ runtime.scene.overrideMaterial?.dispose();
816
+ runtime.scene.overrideMaterial = null;
817
+ if (mode === 'unlit')
818
+ runtime.scene.overrideMaterial = new THREE.MeshBasicMaterial({ color: 0xbec7d1 });
819
+ if (mode === 'wireframe')
820
+ runtime.scene.overrideMaterial = new THREE.MeshBasicMaterial({
821
+ color: 0x8ed0ff,
822
+ wireframe: true,
823
+ });
824
+ if (mode === 'normals') runtime.scene.overrideMaterial = new THREE.MeshNormalMaterial();
825
+ if (mode === 'vertex-colors')
826
+ runtime.scene.overrideMaterial = new THREE.MeshBasicMaterial({ vertexColors: true });
827
+ if (mode === 'uv') {
828
+ runtime.scene.overrideMaterial = new THREE.ShaderMaterial({
829
+ vertexShader:
830
+ 'varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }',
831
+ fragmentShader: 'varying vec2 vUv; void main(){ gl_FragColor=vec4(vUv,0.0,1.0); }',
832
+ });
833
+ }
834
+ if (mode === 'bounds') {
835
+ const helper = new THREE.BoxHelper(runtime.model, 0xffb454);
836
+ helper.name = '__asset_editor_diagnostic_bounds';
837
+ runtime.scene.add(helper);
838
+ runtime.helpers.push(helper);
839
+ }
840
+ if (mode === 'skeleton') {
841
+ const helper = new THREE.SkeletonHelper(runtime.model);
842
+ helper.name = '__asset_editor_diagnostic_skeleton';
843
+ runtime.scene.add(helper);
844
+ runtime.helpers.push(helper);
845
+ }
846
+ }
847
+
848
+ function disposeModel(root: THREE.Object3D): void {
849
+ disposeAssetPreviewSnapshot(root);
850
+ root.traverse((object) => {
851
+ const instanced = object as THREE.InstancedMesh;
852
+ if (instanced.isInstancedMesh) instanced.morphTexture?.dispose();
853
+ const mesh = object as THREE.Mesh;
854
+ mesh.geometry?.dispose();
855
+ if (!mesh.material) return;
856
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
857
+ for (const material of materials) {
858
+ for (const value of Object.values(material)) {
859
+ if (value instanceof THREE.Texture) value.dispose();
860
+ }
861
+ material.dispose();
862
+ }
863
+ });
864
+ }