@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.
- package/README.md +24 -0
- package/dist/assets/axe-C7YBtOuM.js +30 -0
- package/dist/assets/browserAll-DDtGGE48.js +1 -0
- package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
- package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
- package/dist/assets/game-BdTHR2_X.js +1 -0
- package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
- package/dist/assets/game-CcHfYxjP.js +1 -0
- package/dist/assets/game-Cqzj_lpD.js +1 -0
- package/dist/assets/game-DSZdeCHu.js +1 -0
- package/dist/assets/game-Dch9sAPc.js +1 -0
- package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
- package/dist/assets/game-loop-DMIgSMV-.js +1 -0
- package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
- package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
- package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
- package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
- package/dist/assets/index-CQEfi217.js +15412 -0
- package/dist/assets/index-Dvi-V0NE.css +1 -0
- package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
- package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
- package/dist/assets/react-18-C9eHn8x1.js +1 -0
- package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
- package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
- package/dist/index.html +15 -13
- package/dist/ingest/racing-game/vgai.game.json +5 -4
- package/dist/ingest/simcity/vgai.game.json +5 -4
- package/dist/ingest/tanks/vgai.game.json +5 -4
- package/dist/project-scripts.bundle.js +3917 -0
- package/dist/scenes/untitled.vscn.json +2 -0
- package/dist-server/packaged.mjs +7697 -3435
- package/package.json +31 -4
- package/scripts/probe-reporter.ts +349 -0
- package/scripts/run-probe.ts +2260 -0
- package/server/asset-catalog-audit.ts +268 -0
- package/server/asset-catalog-v2.ts +146 -0
- package/server/asset-library-routes.ts +276 -58
- package/server/canonical-path.ts +1 -1
- package/server/dev.ts +148 -44
- package/server/editor-server.ts +888 -61
- package/server/editor-sse.ts +115 -4
- package/server/harness-chat-service.ts +796 -0
- package/server/local-asset-catalog.ts +157 -22
- package/server/packaged.ts +51 -21
- package/server/process-shutdown.ts +136 -0
- package/server/prod.ts +9 -6
- package/server/project-hmr-files.ts +41 -0
- package/server/project-operations.ts +343 -0
- package/server/project-optimize-deps-entries.ts +4 -4
- package/server/project-output-writer.ts +156 -0
- package/server/project-validation.ts +140 -0
- package/server/project-view.ts +8 -50
- package/server/server-utils.ts +75 -10
- package/server/session-registry.ts +12 -0
- package/src/EditorContext.tsx +107 -3
- package/src/action-registry.ts +115 -17
- package/src/adapter-resolver.ts +213 -478
- package/src/asset-events.ts +64 -1
- package/src/asset-preview.ts +906 -0
- package/src/asset-selection.ts +121 -0
- package/src/asset-workflow/asset-capabilities.ts +259 -0
- package/src/asset-workflow/asset-editor-errors.ts +46 -0
- package/src/asset-workflow/asset-import-pipeline.ts +176 -0
- package/src/asset-workflow/asset-library-query.ts +107 -0
- package/src/asset-workflow/asset-types.ts +192 -0
- package/src/asset-workflow/asset-workflow-quality.ts +65 -0
- package/src/asset-workflow/import-contract.ts +113 -0
- package/src/asset-workflow/model-conversion.ts +126 -0
- package/src/asset-workflow/model-inspection.ts +185 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +153 -0
- package/src/asset-workflow/project-asset-operations.ts +119 -0
- package/src/asset-workflow/thumbnail-system.ts +172 -0
- package/src/authoring/active-systems.ts +15 -0
- package/src/authoring/boundary-authoring-adapter.ts +16 -5
- package/src/authoring/composite-authoring-adapter.ts +305 -124
- package/src/authoring/design-time-layers.ts +300 -117
- package/src/authoring/edit-mode-authoring.ts +278 -293
- package/src/authoring/embed-only-adapter.ts +4 -2
- package/src/authoring/first-party-hierarchy-menu.ts +7 -2
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
- package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
- package/src/authoring/first-party-inspector/register.tsx +13 -2
- package/src/authoring/ingest-authoring-adapter.ts +142 -45
- package/src/authoring/mount-failure-report.ts +5 -5
- package/src/authoring/provenance.ts +41 -0
- package/src/authoring/react-dom-authoring-adapter.ts +92 -92
- package/src/authoring/react-inspector-section.tsx +489 -51
- package/src/authoring/react-world-authoring-adapter.ts +541 -306
- package/src/authoring/story-overlay.ts +1 -1
- package/src/authoring/story-types.ts +11 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
- package/src/authoring/viewport-tool-context.ts +78 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +186 -0
- package/src/authoring/world-pan-state.ts +33 -17
- package/src/authoring/world-session-state.ts +1 -1
- package/src/authoring/world2d-authoring-adapter.ts +109 -52
- package/src/availability-tick.ts +56 -0
- package/src/browser-play-entry.ts +5 -5
- package/src/browser-project-scripts.ts +1 -0
- package/src/build-identity.ts +15 -0
- package/src/command-listener.ts +893 -19
- package/src/components/AlignToolbar.tsx +74 -63
- package/src/components/AppRoot.tsx +193 -26
- package/src/components/ApplicationMenus.tsx +272 -0
- package/src/components/AssetBrowser.tsx +1450 -293
- package/src/components/AssetEditorShell.tsx +141 -0
- package/src/components/AssetImportDetails.tsx +218 -0
- package/src/components/AssetLibraryPanel.tsx +14 -0
- package/src/components/CameraInfo.tsx +11 -20
- package/src/components/CenterDocuments.tsx +585 -0
- package/src/components/CommandPalette.tsx +75 -30
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +57 -112
- package/src/components/DebugConsolePanel.tsx +461 -0
- package/src/components/DebuggerPanel.tsx +165 -0
- package/src/components/DefaultEditorLayout.tsx +167 -231
- package/src/components/GameHierarchy.tsx +873 -290
- package/src/components/GamePerformanceOverlay.tsx +46 -0
- package/src/components/HarnessChatPanel.tsx +2352 -0
- package/src/components/HistoryPanel.tsx +226 -0
- package/src/components/Inspector.tsx +153 -99
- package/src/components/InspectorToolSection.tsx +6 -4
- package/src/components/MountFailureBanner.tsx +31 -48
- package/src/components/NetworkSection.tsx +25 -14
- package/src/components/OnlineAssetBrowser.tsx +794 -343
- package/src/components/OverlayReportBanner.tsx +38 -64
- package/src/components/PerformancePanel.tsx +206 -0
- package/src/components/PlayBar.tsx +93 -263
- package/src/components/PrefabSaveDialog.tsx +55 -127
- package/src/components/ProjectHeader.css +60 -0
- package/src/components/ProjectHeader.tsx +170 -105
- package/src/components/ProjectScreen.tsx +246 -184
- package/src/components/ReactCanvasControls.tsx +185 -0
- package/src/components/ResolutionPicker.tsx +85 -0
- package/src/components/RootActivateDialog.tsx +39 -68
- package/src/components/SaveDialog.tsx +55 -127
- package/src/components/SaveStatus.tsx +11 -29
- package/src/components/StartupLoadingScreen.tsx +96 -0
- package/src/components/StateWatchPanel.tsx +302 -0
- package/src/components/StatsOverlay.tsx +12 -5
- package/src/components/StatusBar.tsx +64 -0
- package/src/components/ThemeManagerDialog.css +89 -0
- package/src/components/ThemeManagerDialog.tsx +371 -0
- package/src/components/ToolHost.tsx +26 -15
- package/src/components/Toolbar.tsx +117 -214
- package/src/components/VgaiVoxelLogo.tsx +89 -0
- package/src/components/ViewportOverlay.tsx +97 -172
- package/src/components/ViewportPanel.tsx +130 -31
- package/src/components/WorkspaceDock.tsx +891 -0
- package/src/components/WorldSelectionOverlay.tsx +51 -63
- package/src/components/WorldTextEditor.tsx +9 -5
- package/src/components/asset-documents.tsx +390 -0
- package/src/components/asset-editor-persistence.ts +44 -0
- package/src/components/asset-selection-section.tsx +338 -0
- package/src/components/asset-viewers/AudioViewer.tsx +73 -21
- package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
- package/src/components/asset-viewers/ImageViewer.tsx +43 -11
- package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
- package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
- package/src/components/asset-viewers/ModelViewer.tsx +805 -121
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
- package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
- package/src/components/asset-workflow.css +506 -0
- package/src/components/build-documents.tsx +696 -0
- package/src/components/core-utilities.tsx +75 -0
- package/src/components/data-documents.tsx +364 -0
- package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
- package/src/components/debugger-panel-state.ts +57 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
- package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
- package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
- package/src/components/inspector-widgets/FontPicker.tsx +13 -21
- package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
- package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
- package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
- package/src/components/inspector-widgets/shared.tsx +90 -49
- package/src/components/inspectors/AnimationSection.tsx +39 -40
- package/src/components/inspectors/AudioSection.tsx +28 -39
- package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
- package/src/components/inspectors/CameraSection.tsx +14 -23
- package/src/components/inspectors/ComponentsSection.tsx +39 -43
- package/src/components/inspectors/JointsSection.tsx +34 -43
- package/src/components/inspectors/LightSection.tsx +90 -35
- package/src/components/inspectors/MaterialSection.tsx +137 -174
- package/src/components/inspectors/MeshSection.tsx +9 -17
- package/src/components/inspectors/NavigationSection.tsx +8 -15
- package/src/components/inspectors/ParticleSection.tsx +91 -76
- package/src/components/inspectors/PhysicsSection.tsx +42 -46
- package/src/components/inspectors/PostProcessingSection.tsx +28 -36
- package/src/components/inspectors/RenderingSection.tsx +12 -20
- package/src/components/inspectors/ShadowSection.tsx +7 -8
- package/src/components/inspectors/SplineSection.tsx +31 -42
- package/src/components/inspectors/TransformSection.tsx +14 -22
- package/src/components/patterns/Dialog.tsx +95 -0
- package/src/components/patterns/Fields.tsx +44 -0
- package/src/components/patterns/List.tsx +25 -0
- package/src/components/patterns/StateSurface.tsx +40 -0
- package/src/components/patterns/Surfaces.tsx +115 -0
- package/src/components/patterns/Tabs.tsx +80 -0
- package/src/components/patterns/Toolbar.tsx +51 -0
- package/src/components/patterns/Tree.tsx +72 -0
- package/src/components/primitives/Button.tsx +46 -0
- package/src/components/primitives/ColorInput.tsx +11 -47
- package/src/components/primitives/EditorIcon.tsx +99 -0
- package/src/components/primitives/FormControls.tsx +71 -0
- package/src/components/primitives/JsonInput.tsx +15 -2
- package/src/components/primitives/Layout.tsx +100 -0
- package/src/components/primitives/Menu.tsx +105 -0
- package/src/components/primitives/NumberInput.tsx +8 -35
- package/src/components/primitives/Panel.tsx +49 -44
- package/src/components/primitives/SectionHeader.tsx +10 -28
- package/src/components/primitives/Text.tsx +46 -0
- package/src/components/primitives/Tooltip.tsx +20 -12
- package/src/components/primitives/Vec3Input.tsx +3 -0
- package/src/components/primitives/banner-tones.ts +33 -0
- package/src/components/primitives/editor-icons.ts +102 -0
- package/src/components/primitives/panel-header-styles.ts +30 -0
- package/src/components/project-operation-documents.tsx +226 -0
- package/src/components/status-contributions.tsx +175 -0
- package/src/components/story-documents-register.ts +15 -0
- package/src/components/story-documents.tsx +1185 -0
- package/src/components/tool-documents.tsx +110 -0
- package/src/components/workspace-dock.css +293 -0
- package/src/components/workspace-history.ts +33 -0
- package/src/components/world-documents.tsx +145 -0
- package/src/components/xstate-documents.tsx +209 -0
- package/src/composite-screenshot.ts +248 -0
- package/src/design-system/index.ts +70 -0
- package/src/design-system-stories/Foundation.stories.tsx +183 -0
- package/src/design-system-stories/Patterns.stories.tsx +251 -0
- package/src/design-system-stories/Workspace.stories.tsx +538 -0
- package/src/doctor/report.ts +57 -13
- package/src/editor-api.ts +295 -31
- package/src/editor-compatibility.ts +200 -0
- package/src/editor-console.ts +35 -17
- package/src/editor-hotkeys.ts +85 -33
- package/src/editor-store.ts +538 -233
- package/src/editor-viewport.ts +160 -34
- package/src/entity-factory.ts +14 -3
- package/src/gizmo-registry.ts +21 -5
- package/src/harness-chat-editor-context.ts +37 -0
- package/src/harness-chat-indicator.ts +68 -0
- package/src/harness-chat-layout.ts +53 -0
- package/src/harness-chat-lifecycle.ts +170 -0
- package/src/harness-chat-reconnect.ts +85 -0
- package/src/harness-chat-types.ts +409 -0
- package/src/hierarchy-drop.ts +50 -0
- package/src/hierarchy-expansion-state.ts +100 -0
- package/src/hierarchy-menu-registry.ts +1 -1
- package/src/hierarchy-projection.ts +76 -0
- package/src/history/editor-session.ts +21 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-service.ts +904 -0
- package/src/history/json-history-resource.ts +165 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +266 -0
- package/src/history/project-root-history-backends.ts +53 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +108 -0
- package/src/history/source-history-backend.ts +374 -0
- package/src/history/types.ts +100 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hosted-example.ts +12 -2
- package/src/hotkeys.ts +89 -18
- package/src/ingest/discovery-react.ts +9 -10
- package/src/ingest/discovery.ts +4 -4
- package/src/ingest/discovery2d.ts +5 -5
- package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
- package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
- package/src/ingest/games/games-fps/vgai.game.json +3 -3
- package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
- package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
- package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
- package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
- package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
- package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
- package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
- package/src/ingest/types.ts +1 -1
- package/src/ingest-mode.ts +47 -52
- package/src/ingest-siblings.ts +23 -33
- package/src/initial-scene.ts +1 -2
- package/src/main.tsx +16 -8
- package/src/manifest-project.ts +10 -12
- package/src/model-thumbnail.ts +261 -26
- package/src/module-mode.ts +44 -50
- package/src/play-log-events.ts +26 -0
- package/src/play-mode.ts +495 -401
- package/src/play-scene-adoption.ts +23 -0
- package/src/playground/main.ts +68 -24
- package/src/playwright-shim.ts +473 -0
- package/src/project-manager.ts +90 -10
- package/src/project-operations.ts +112 -0
- package/src/project-story-discovery.ts +25 -0
- package/src/project-tool-discovery.ts +43 -0
- package/src/scene-serializer.ts +0 -6
- package/src/scene-sync.ts +7 -2
- package/src/script-loader.ts +3 -3
- package/src/skeleton-helper.ts +57 -0
- package/src/storage/seed.ts +68 -8
- package/src/stories/StoryPreviewMount.tsx +265 -0
- package/src/stories/action-stories.fixture.tsx +44 -0
- package/src/stories/button-stories.fixture.tsx +50 -0
- package/src/stories/compose-project-stories.ts +182 -0
- package/src/stories/loader-stories.fixture.tsx +53 -0
- package/src/stories/story-discovery.ts +130 -0
- package/src/stories/story-registry.ts +146 -0
- package/src/theme-css.ts +1049 -0
- package/src/theme-library.ts +238 -0
- package/src/theme-preference.ts +130 -0
- package/src/theme.ts +527 -0
- package/src/tool-loader.ts +80 -24
- package/src/ui-source/inspect.ts +27 -4
- package/src/ui-source/source-write-backend.ts +83 -22
- package/src/widgets/index.ts +9 -18
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-dock-controller.ts +64 -0
- package/src/workspace-document-ids.ts +5 -0
- package/src/workspace-document-registry.ts +367 -0
- package/src/workspace-state-persistence.ts +455 -0
- package/src/workspace-status-registry.ts +110 -0
- package/src/workspace-utility-commands.ts +49 -0
- package/src/workspace-utility-registry.ts +161 -0
- package/src/world-document-routing.ts +16 -0
- package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
- package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
- package/src/xstate-inspect/use-live-actor-state.ts +39 -0
- package/src/xstate-inspect/xstate-graph.ts +265 -0
- package/src/xstate-inspect/xstate-layout.ts +76 -0
- package/src/z-index.ts +26 -0
- package/template/.claude/skills/editor/SKILL.md +129 -90
- package/template/AGENTS.md +1110 -0
- package/template/CLAUDE.md +1 -512
- package/template/IDIOMS.md +50 -0
- package/template/check-idioms.ts +706 -0
- package/template/manifest-entry-modules-plugin.ts +53 -0
- package/template/package.json +13 -4
- package/template/public/scenes/default.scn2d.json +6 -0
- package/template/public/scenes/default.vscn.json +7 -1
- package/template/public/scenes/untitled.vscn.json +10 -0
- package/template/scripts/playtest.ts +40 -0
- package/template/server/loopback.ts +75 -0
- package/template/server/rooms/arena-room.ts +69 -1
- package/template/server/rooms/game-room.ts +68 -1
- package/template/src/data/README.md +9 -4
- package/template/src/data/data-ref.ts +35 -0
- package/template/src/data/tuning.schema.ts +1 -1
- package/template/src/main.ts +41 -62
- package/template/src/manifest-entry-modules.d.ts +3 -0
- package/template/src/operations/README.md +88 -0
- package/template/src/runtime/pixi-adapter.ts +24 -0
- package/template/src/scripts/components/data-spinner.ts +43 -2
- package/template/src/scripts/components/spin-lap-math.ts +27 -0
- package/template/src/tools/README.md +5 -3
- package/template/src/tools/example.tool.tsx +13 -7
- package/template/src/ui/Button.stories.tsx +66 -0
- package/template/src/ui/RebindPanel.tsx +2 -6
- package/template/src/ui/game.tsx +17 -0
- package/template/tests/acceptance/example.spec.ts +64 -0
- package/template/tests/logic/example.test.ts +31 -0
- package/template/tests/logic/room.test.ts +100 -0
- package/template/tests/tsconfig.json +30 -0
- package/template/validate-manifest.ts +5 -5
- package/template/validate-scenes.ts +50 -18
- package/template/vgai.game.json +20 -13
- package/template/vite.config.ts +77 -16
- package/vite-plugin-ui-oid.ts +261 -17
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
- package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
- package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
- package/dist/assets/browserAll-CEkWYlFk.js +0 -1
- package/dist/assets/create-runtime-DYIWr3va.js +0 -1
- package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
- package/dist/assets/game-8SDZPPgH.js +0 -1
- package/dist/assets/game-CevorxyV.js +0 -1
- package/dist/assets/game-Di7XlywD.js +0 -1
- package/dist/assets/game-DjnCaw8R.js +0 -1
- package/dist/assets/game-Q9A0aXSV.js +0 -1
- package/dist/assets/index-CPVV6pjw.js +0 -1
- package/dist/assets/index-CpOnYyeD.js +0 -213
- package/dist/assets/index-Dr8H_6Rl.js +0 -1
- package/dist/assets/index-_bCaipUk.js +0 -13650
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
- package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
- package/dist/assets/scene-query-BivAHhJu.js +0 -1
- package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
- package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
- package/src/authoring/scene-ui-layer.ts +0 -485
- package/src/authoring/ui-authoring-adapter.ts +0 -325
- package/src/authoring/ui-hierarchy-menu.ts +0 -40
- package/src/authoring/ui-inspector-sections.tsx +0 -156
- package/src/authoring/undo-timeline.ts +0 -145
- package/src/components/BottomPanel.tsx +0 -326
- package/src/components/BuildPanel.tsx +0 -455
- package/src/components/FolderTree.tsx +0 -179
- package/src/components/RightPanel.tsx +0 -166
- package/src/components/WorldContextMenu.tsx +0 -257
- package/src/components/asset-viewers/AssetViewer.tsx +0 -38
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
- package/src/scene-ui-mode/active-scene-ui.ts +0 -58
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
- package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
- package/src/ui-editor/inspector.tsx +0 -563
- package/src/ui-editor/node-ops.ts +0 -266
- package/src/ui-editor/overlay.tsx +0 -380
- package/src/ui-editor/palette.tsx +0 -114
- package/src/ui-editor/react-store.ts +0 -78
- package/src/ui-editor/timeline-panel.tsx +0 -443
- package/src/ui-editor/ui-edit-store.ts +0 -552
- package/src/ui-editor/ui-prefab.ts +0 -95
- package/template/project.json +0 -9
- package/template/src/scripts/main.ts +0 -94
- package/template/src/ui/DefaultHud.tsx +0 -40
- package/template/src/ui/game-state.tsx +0 -37
- package/template/src/ui/mount-default-hud.tsx +0 -19
- package/template/src/ui/mount-ui.ts +0 -15
- package/vite-plugin-react-world-provider.ts +0 -81
package/src/ingest-siblings.ts
CHANGED
|
@@ -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
|
|
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 `
|
|
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 {
|
|
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 {
|
|
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
|
|
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:
|
|
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
|
-
* (`
|
|
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:
|
|
178
|
+
world: ResolvedAdapterRoot,
|
|
180
179
|
gameContainer: HTMLElement,
|
|
181
180
|
projectRoot: string,
|
|
182
181
|
store: EditorStore | undefined,
|
|
183
182
|
): Promise<SiblingMount> {
|
|
184
|
-
const Entry = await
|
|
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
|
-
* `
|
|
287
|
-
* scope guard only fires for `
|
|
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:
|
|
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.
|
|
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.
|
|
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:
|
|
392
|
+
function rejectUnmountableSibling(world: ResolvedAdapterRoot): never {
|
|
403
393
|
throw new Error(
|
|
404
|
-
`sibling world "${world.id}" (kind: ${world.
|
|
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:
|
|
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 === '
|
|
422
|
-
world.adapter.identity === '
|
|
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 === '
|
|
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.
|
|
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.
|
|
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.
|
|
449
|
+
kind: world.surface,
|
|
460
450
|
identity: world.adapter.identity,
|
|
461
451
|
message: formatMountFailureMessage(err),
|
|
462
452
|
});
|
package/src/initial-scene.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
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
|
-
|
|
28
|
+
const editorRoot = document.getElementById('editor-root')!;
|
|
29
|
+
const editorChromeRoot = document.getElementById('editor-chrome-root')!;
|
|
30
|
+
installEditorTheme(editorChromeRoot);
|
|
31
|
+
createRoot(editorRoot).render(<AppRoot />);
|
package/src/manifest-project.ts
CHANGED
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
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
|
|
17
|
-
*
|
|
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
|
|
23
|
-
const
|
|
24
|
-
|
|
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
|
|
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 `
|
|
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> {
|
package/src/model-thumbnail.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Offscreen Three.js renderer that generates thumbnail images for
|
|
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
|
|
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
|
-
|
|
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(
|
|
70
|
-
const
|
|
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(
|
|
119
|
+
const inflight = this.pending.get(key);
|
|
75
120
|
if (inflight) return inflight;
|
|
76
121
|
|
|
77
|
-
const promise = this._render(
|
|
78
|
-
this.pending.set(
|
|
122
|
+
const promise = this._render(resolved);
|
|
123
|
+
this.pending.set(key, promise);
|
|
79
124
|
try {
|
|
80
|
-
|
|
125
|
+
const dataUrl = await promise;
|
|
126
|
+
this.cache.set(key, dataUrl);
|
|
127
|
+
return dataUrl;
|
|
81
128
|
} finally {
|
|
82
|
-
this.pending.delete(
|
|
129
|
+
this.pending.delete(key);
|
|
83
130
|
}
|
|
84
131
|
}
|
|
85
132
|
|
|
86
|
-
private async _render(
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
122
|
-
if (
|
|
123
|
-
if (
|
|
124
|
-
const materials = Array.isArray(
|
|
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
|
+
}
|