@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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { OnlineAsset } from '../editor-api';
|
|
2
|
+
|
|
3
|
+
const LIBRARY_STATE_KEY = 'vgai:asset-library:v2';
|
|
4
|
+
|
|
5
|
+
export function assetLibraryStateKey(projectId: string): string {
|
|
6
|
+
return `${LIBRARY_STATE_KEY}:${projectId}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type AssetLibrarySort =
|
|
10
|
+
| 'relevance'
|
|
11
|
+
| 'name'
|
|
12
|
+
| 'source'
|
|
13
|
+
| 'size'
|
|
14
|
+
| 'recently-added'
|
|
15
|
+
| 'recently-used';
|
|
16
|
+
|
|
17
|
+
export interface AssetLibraryFilters {
|
|
18
|
+
readonly licenses: ReadonlySet<string>;
|
|
19
|
+
readonly deliveryKinds: ReadonlySet<string>;
|
|
20
|
+
readonly health: ReadonlySet<string>;
|
|
21
|
+
readonly animatedOnly: boolean;
|
|
22
|
+
readonly runtimeReadyOnly: boolean;
|
|
23
|
+
readonly downloaded: 'all' | 'downloaded' | 'not-downloaded';
|
|
24
|
+
readonly favoritesOnly: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AssetLibraryQueryContext {
|
|
28
|
+
readonly favorites: ReadonlySet<string>;
|
|
29
|
+
readonly recent: readonly string[];
|
|
30
|
+
isDownloaded(asset: OnlineAsset): boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function onlineAssetKey(asset: Pick<OnlineAsset, 'source' | 'id'>): string {
|
|
34
|
+
return `${asset.source}:${asset.id}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function optionalSetMatches(values: ReadonlySet<string>, value: string): boolean {
|
|
38
|
+
return values.size === 0 || values.has(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function deliveryMatches(values: ReadonlySet<string>, asset: OnlineAsset): boolean {
|
|
42
|
+
return values.size === 0 || Boolean(asset.deliveryKinds?.some((value) => values.has(value)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function downloadedMatches(
|
|
46
|
+
filter: AssetLibraryFilters['downloaded'],
|
|
47
|
+
downloaded: boolean,
|
|
48
|
+
): boolean {
|
|
49
|
+
return filter === 'all' || (filter === 'downloaded' ? downloaded : !downloaded);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assetMatches(
|
|
53
|
+
asset: OnlineAsset,
|
|
54
|
+
filters: AssetLibraryFilters,
|
|
55
|
+
context: AssetLibraryQueryContext,
|
|
56
|
+
): boolean {
|
|
57
|
+
const favorite = context.favorites.has(onlineAssetKey(asset));
|
|
58
|
+
return (
|
|
59
|
+
(!filters.favoritesOnly || favorite) &&
|
|
60
|
+
(!filters.runtimeReadyOnly || Boolean(asset.capabilities?.runtimeReady)) &&
|
|
61
|
+
(!filters.animatedOnly || Boolean(asset.capabilities?.animated)) &&
|
|
62
|
+
optionalSetMatches(filters.licenses, asset.license ?? 'Unknown') &&
|
|
63
|
+
optionalSetMatches(filters.health, asset.capabilities?.health ?? 'unsupported') &&
|
|
64
|
+
deliveryMatches(filters.deliveryKinds, asset) &&
|
|
65
|
+
downloadedMatches(filters.downloaded, context.isDownloaded(asset))
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compareAssetSort(
|
|
70
|
+
left: OnlineAsset,
|
|
71
|
+
right: OnlineAsset,
|
|
72
|
+
sort: AssetLibrarySort,
|
|
73
|
+
recentRank: ReadonlyMap<string, number>,
|
|
74
|
+
): number {
|
|
75
|
+
switch (sort) {
|
|
76
|
+
case 'source':
|
|
77
|
+
return left.source.localeCompare(right.source);
|
|
78
|
+
case 'size':
|
|
79
|
+
return (right.sizeBytes ?? -1) - (left.sizeBytes ?? -1);
|
|
80
|
+
case 'recently-added':
|
|
81
|
+
return (right.addedAt ?? '').localeCompare(left.addedAt ?? '');
|
|
82
|
+
case 'recently-used':
|
|
83
|
+
return (
|
|
84
|
+
(recentRank.get(onlineAssetKey(left)) ?? Number.MAX_SAFE_INTEGER) -
|
|
85
|
+
(recentRank.get(onlineAssetKey(right)) ?? Number.MAX_SAFE_INTEGER)
|
|
86
|
+
);
|
|
87
|
+
default:
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function queryAssetLibrary(
|
|
93
|
+
assets: readonly OnlineAsset[],
|
|
94
|
+
filters: AssetLibraryFilters,
|
|
95
|
+
sort: AssetLibrarySort,
|
|
96
|
+
context: AssetLibraryQueryContext,
|
|
97
|
+
): OnlineAsset[] {
|
|
98
|
+
const recentRank = new Map(context.recent.map((key, index) => [key, index]));
|
|
99
|
+
const filtered = assets.filter((asset) => assetMatches(asset, filters, context));
|
|
100
|
+
|
|
101
|
+
if (sort === 'relevance') return filtered;
|
|
102
|
+
return [...filtered].sort((left, right) => {
|
|
103
|
+
const order = compareAssetSort(left, right, sort, recentRank);
|
|
104
|
+
if (order !== 0) return order;
|
|
105
|
+
return left.name.localeCompare(right.name) || left.id.localeCompare(right.id);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export type AssetHealthCode =
|
|
4
|
+
| 'healthy'
|
|
5
|
+
| 'source-only'
|
|
6
|
+
| 'missing-file'
|
|
7
|
+
| 'missing-dependency'
|
|
8
|
+
| 'missing-thumbnail'
|
|
9
|
+
| 'unsupported-format'
|
|
10
|
+
| 'invalid-metadata'
|
|
11
|
+
| 'stale-import'
|
|
12
|
+
| 'conversion-failed'
|
|
13
|
+
| 'missing-attribution'
|
|
14
|
+
| 'broken-scene-reference';
|
|
15
|
+
|
|
16
|
+
export interface AssetHealth {
|
|
17
|
+
readonly status: 'healthy' | 'warning' | 'error';
|
|
18
|
+
readonly codes: readonly AssetHealthCode[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const AssetFileSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
label: z.string().min(1),
|
|
24
|
+
path: z.string().min(1),
|
|
25
|
+
format: z.string().min(1),
|
|
26
|
+
sizeBytes: z.number().int().nonnegative(),
|
|
27
|
+
dependencies: z.array(z.string().min(1)).optional(),
|
|
28
|
+
sha256: z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
31
|
+
.optional(),
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
|
|
35
|
+
export const AssetDeliverySchema = z
|
|
36
|
+
.object({
|
|
37
|
+
id: z.string().min(1),
|
|
38
|
+
kind: z.enum(['local-ssd', 'cloud', 'remote']),
|
|
39
|
+
available: z.boolean(),
|
|
40
|
+
fileIndexes: z.array(z.number().int().nonnegative()).min(1),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
|
|
44
|
+
export const AssetThumbnailSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
path: z.string().min(1),
|
|
47
|
+
state: z.enum(['generated', 'upstream', 'fallback', 'unsupported', 'failed']),
|
|
48
|
+
profileVersion: z.string().min(1),
|
|
49
|
+
sourceHash: z
|
|
50
|
+
.string()
|
|
51
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
52
|
+
.optional(),
|
|
53
|
+
resultHash: z
|
|
54
|
+
.string()
|
|
55
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
56
|
+
.optional(),
|
|
57
|
+
issueCode: z.string().min(1).optional(),
|
|
58
|
+
})
|
|
59
|
+
.strict();
|
|
60
|
+
|
|
61
|
+
export const AssetVariantSchema = z
|
|
62
|
+
.object({
|
|
63
|
+
id: z.string().regex(/^[a-f0-9]{24}$/),
|
|
64
|
+
label: z.string().min(1),
|
|
65
|
+
type: z.enum(['model', 'animation', 'source']),
|
|
66
|
+
files: z.array(AssetFileSchema).min(1),
|
|
67
|
+
deliveries: z.array(AssetDeliverySchema).min(1),
|
|
68
|
+
thumbnail: AssetThumbnailSchema,
|
|
69
|
+
animationCount: z.number().int().nonnegative().optional(),
|
|
70
|
+
clipNames: z.array(z.string()).optional(),
|
|
71
|
+
})
|
|
72
|
+
.strict();
|
|
73
|
+
|
|
74
|
+
export const AssetFamilySchema = z
|
|
75
|
+
.object({
|
|
76
|
+
id: z.string().regex(/^[a-f0-9]{24}$/),
|
|
77
|
+
source: z.string().min(1),
|
|
78
|
+
name: z.string().min(1),
|
|
79
|
+
type: z.enum(['model', 'animation', 'source']),
|
|
80
|
+
categories: z.array(z.string()),
|
|
81
|
+
tags: z.array(z.string()),
|
|
82
|
+
license: z.string().min(1),
|
|
83
|
+
author: z.string().min(1),
|
|
84
|
+
sourceUrl: z.string().url(),
|
|
85
|
+
variants: z.array(AssetVariantSchema).min(1),
|
|
86
|
+
})
|
|
87
|
+
.strict();
|
|
88
|
+
|
|
89
|
+
export const AssetCatalogEnvelopeSchema = z
|
|
90
|
+
.object({
|
|
91
|
+
version: z.literal(2),
|
|
92
|
+
generatedAt: z.string().datetime(),
|
|
93
|
+
families: z.array(z.unknown()),
|
|
94
|
+
})
|
|
95
|
+
.strict();
|
|
96
|
+
|
|
97
|
+
export type AssetFile = z.infer<typeof AssetFileSchema>;
|
|
98
|
+
export type AssetDelivery = z.infer<typeof AssetDeliverySchema>;
|
|
99
|
+
export type AssetVariant = z.infer<typeof AssetVariantSchema>;
|
|
100
|
+
export type AssetFamily = z.infer<typeof AssetFamilySchema>;
|
|
101
|
+
|
|
102
|
+
export interface AssetCatalogIssue {
|
|
103
|
+
readonly familyIndex: number | null;
|
|
104
|
+
readonly familyId?: string;
|
|
105
|
+
readonly path: readonly (string | number)[];
|
|
106
|
+
readonly message: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface AssetCatalog {
|
|
110
|
+
readonly version: 2;
|
|
111
|
+
readonly generatedAt: string;
|
|
112
|
+
readonly families: readonly AssetFamily[];
|
|
113
|
+
readonly issues: readonly AssetCatalogIssue[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseAssetCatalog(input: unknown): AssetCatalog {
|
|
117
|
+
const envelope = AssetCatalogEnvelopeSchema.parse(input);
|
|
118
|
+
const families: AssetFamily[] = [];
|
|
119
|
+
const issues: AssetCatalogIssue[] = [];
|
|
120
|
+
for (const [familyIndex, candidate] of envelope.families.entries()) {
|
|
121
|
+
const result = AssetFamilySchema.safeParse(candidate);
|
|
122
|
+
if (result.success) {
|
|
123
|
+
families.push(result.data);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const familyId =
|
|
127
|
+
candidate &&
|
|
128
|
+
typeof candidate === 'object' &&
|
|
129
|
+
'id' in candidate &&
|
|
130
|
+
typeof candidate.id === 'string'
|
|
131
|
+
? candidate.id
|
|
132
|
+
: undefined;
|
|
133
|
+
for (const issue of result.error.issues) {
|
|
134
|
+
issues.push({
|
|
135
|
+
familyIndex,
|
|
136
|
+
...(familyId ? { familyId } : {}),
|
|
137
|
+
path: issue.path.map((part) => (typeof part === 'symbol' ? String(part) : part)),
|
|
138
|
+
message: issue.message,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { version: 2, generatedAt: envelope.generatedAt, families, issues };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const ImportRecipeSchema = z
|
|
146
|
+
.object({
|
|
147
|
+
version: z.literal(1),
|
|
148
|
+
familyId: z.string().min(1),
|
|
149
|
+
variantId: z.string().min(1),
|
|
150
|
+
deliveryId: z.string().min(1),
|
|
151
|
+
destination: z.string().min(1),
|
|
152
|
+
retainSource: z.boolean(),
|
|
153
|
+
scale: z.number().positive().default(1),
|
|
154
|
+
upAxis: z.enum(['x', 'y', 'z']).default('y'),
|
|
155
|
+
settings: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
|
|
156
|
+
})
|
|
157
|
+
.strict();
|
|
158
|
+
|
|
159
|
+
export const ImportResultSchema = z
|
|
160
|
+
.object({
|
|
161
|
+
recipe: ImportRecipeSchema,
|
|
162
|
+
status: z.enum(['imported', 'source-only', 'failed', 'cancelled']),
|
|
163
|
+
writtenFiles: z.array(z.string()),
|
|
164
|
+
runtimeAssetPath: z.string().optional(),
|
|
165
|
+
sourceAssetPath: z.string().optional(),
|
|
166
|
+
issues: z.array(z.object({ code: z.string(), message: z.string() }).strict()),
|
|
167
|
+
})
|
|
168
|
+
.strict();
|
|
169
|
+
|
|
170
|
+
export type ImportRecipe = z.infer<typeof ImportRecipeSchema>;
|
|
171
|
+
export type ImportResult = z.infer<typeof ImportResultSchema>;
|
|
172
|
+
|
|
173
|
+
export interface ProjectAsset {
|
|
174
|
+
readonly projectPath: string;
|
|
175
|
+
readonly source: SourceAsset | null;
|
|
176
|
+
readonly runtime: RuntimeAsset | null;
|
|
177
|
+
readonly recipe: ImportRecipe | null;
|
|
178
|
+
readonly health: AssetHealth;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface SourceAsset {
|
|
182
|
+
readonly familyId: string;
|
|
183
|
+
readonly variantId: string;
|
|
184
|
+
readonly deliveryId: string;
|
|
185
|
+
readonly originalPath: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface RuntimeAsset {
|
|
189
|
+
readonly projectPath: string;
|
|
190
|
+
readonly format: 'glb' | 'gltf';
|
|
191
|
+
readonly sourceHash: string;
|
|
192
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const ASSET_WORKFLOW_BUDGETS = {
|
|
2
|
+
firstResultsMs: 500,
|
|
3
|
+
filterResponseMs: 50,
|
|
4
|
+
importProgressLatencyMs: 100,
|
|
5
|
+
maxMountedLibraryCards: 120,
|
|
6
|
+
maxPreviewConcurrency: 2,
|
|
7
|
+
maxActiveAssetEditorWebGlContexts: 1,
|
|
8
|
+
maxTelemetryEntries: 200,
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export type AssetMetricStage =
|
|
12
|
+
| 'query'
|
|
13
|
+
| 'thumbnail'
|
|
14
|
+
| 'preview'
|
|
15
|
+
| 'delivery'
|
|
16
|
+
| 'conversion'
|
|
17
|
+
| 'validation'
|
|
18
|
+
| 'commit';
|
|
19
|
+
export interface AssetWorkflowMetric {
|
|
20
|
+
stage: AssetMetricStage;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
outcome: 'ok' | 'failed' | 'cancelled';
|
|
23
|
+
issueCode?: string;
|
|
24
|
+
at: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const metrics: AssetWorkflowMetric[] = [];
|
|
28
|
+
const listeners = new Set<() => void>();
|
|
29
|
+
|
|
30
|
+
/** Local-only, path-free asset telemetry for the Debugger/Console. */
|
|
31
|
+
export function recordAssetWorkflowMetric(metric: Omit<AssetWorkflowMetric, 'at'>): void {
|
|
32
|
+
metrics.push({ ...metric, durationMs: Math.max(0, metric.durationMs), at: Date.now() });
|
|
33
|
+
if (metrics.length > ASSET_WORKFLOW_BUDGETS.maxTelemetryEntries)
|
|
34
|
+
metrics.splice(0, metrics.length - ASSET_WORKFLOW_BUDGETS.maxTelemetryEntries);
|
|
35
|
+
for (const listener of listeners) listener();
|
|
36
|
+
}
|
|
37
|
+
export function assetWorkflowMetrics(): readonly AssetWorkflowMetric[] {
|
|
38
|
+
return metrics;
|
|
39
|
+
}
|
|
40
|
+
export function subscribeAssetWorkflowMetrics(listener: () => void): () => void {
|
|
41
|
+
listeners.add(listener);
|
|
42
|
+
return () => {
|
|
43
|
+
listeners.delete(listener);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function resetAssetWorkflowMetrics(): void {
|
|
47
|
+
metrics.length = 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ASSET_ACTION_RISKS = {
|
|
51
|
+
delete: { destructive: true, confirmation: 'required' },
|
|
52
|
+
overwrite: { destructive: true, confirmation: 'required' },
|
|
53
|
+
'batch-import': { destructive: false, confirmation: 'summary' },
|
|
54
|
+
'external-source': { destructive: false, confirmation: 'external-origin' },
|
|
55
|
+
reveal: { destructive: false, confirmation: 'none' },
|
|
56
|
+
'license-blocked-delivery': { destructive: false, confirmation: 'refuse' },
|
|
57
|
+
} as const;
|
|
58
|
+
export type AssetRiskAction = keyof typeof ASSET_ACTION_RISKS;
|
|
59
|
+
|
|
60
|
+
export function confirmAssetAction(action: AssetRiskAction, message: string): boolean {
|
|
61
|
+
const confirmation = ASSET_ACTION_RISKS[action].confirmation;
|
|
62
|
+
if (confirmation === 'refuse') return false;
|
|
63
|
+
if (confirmation === 'none') return true;
|
|
64
|
+
return window.confirm(message);
|
|
65
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const assetImportSettingsSchema = z.object({
|
|
4
|
+
scale: z.number().positive().default(1),
|
|
5
|
+
sourceUnits: z.enum(['auto', 'mm', 'cm', 'm', 'in', 'ft']).default('auto'),
|
|
6
|
+
upAxis: z.enum(['auto', 'x', 'y', 'z']).default('auto'),
|
|
7
|
+
forwardAxis: z.enum(['auto', 'x', '-x', 'y', '-y', 'z', '-z']).default('auto'),
|
|
8
|
+
normals: z.enum(['preserve', 'generate', 'recalculate']).default('preserve'),
|
|
9
|
+
tangents: z.enum(['preserve', 'generate', 'discard']).default('preserve'),
|
|
10
|
+
materials: z.enum(['import', 'discard']).default('import'),
|
|
11
|
+
textures: z.enum(['copy', 'embed', 'discard']).default('copy'),
|
|
12
|
+
animations: z.array(z.string()).default([]),
|
|
13
|
+
meshOptimization: z.enum(['none', 'safe', 'aggressive']).default('safe'),
|
|
14
|
+
retainSource: z.boolean().default(true),
|
|
15
|
+
});
|
|
16
|
+
export type AssetImportSettings = z.infer<typeof assetImportSettingsSchema>;
|
|
17
|
+
|
|
18
|
+
const safePathSchema = z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.refine(
|
|
22
|
+
(path) => !path.startsWith('/') && !path.split(/[\\/]/).includes('..'),
|
|
23
|
+
'Path must stay within the source/project root.',
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const importFileSchema = z.object({
|
|
27
|
+
path: safePathSchema,
|
|
28
|
+
url: z.string().min(1),
|
|
29
|
+
hash: z.string().min(8),
|
|
30
|
+
size: z.number().int().nonnegative(),
|
|
31
|
+
role: z.enum(['primary', 'dependency', 'material', 'texture']),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const assetImportPlanSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
version: z.literal(1),
|
|
37
|
+
id: z.string().min(1),
|
|
38
|
+
identity: z.object({
|
|
39
|
+
familyId: z.string().min(1),
|
|
40
|
+
variantId: z.string().min(1),
|
|
41
|
+
deliveryId: z.string().min(1),
|
|
42
|
+
}),
|
|
43
|
+
sourceFiles: z.array(importFileSchema).min(1),
|
|
44
|
+
generatedFiles: z.array(safePathSchema),
|
|
45
|
+
destination: safePathSchema,
|
|
46
|
+
contentKind: z
|
|
47
|
+
.enum(['model', 'animation-only', 'image', 'audio', 'material', 'data'])
|
|
48
|
+
.default('model'),
|
|
49
|
+
targetSkeleton: z.string().optional(),
|
|
50
|
+
settings: assetImportSettingsSchema,
|
|
51
|
+
conflicts: z.array(
|
|
52
|
+
z.object({ path: z.string(), resolution: z.enum(['reuse', 'update', 'rename', 'cancel']) }),
|
|
53
|
+
),
|
|
54
|
+
attribution: z.object({
|
|
55
|
+
sourceUrl: z.string(),
|
|
56
|
+
author: z.string(),
|
|
57
|
+
license: z.string(),
|
|
58
|
+
text: z.string(),
|
|
59
|
+
}),
|
|
60
|
+
sourceHash: z.string().min(8),
|
|
61
|
+
runtimeReady: z.boolean(),
|
|
62
|
+
backend: z.enum(['native', 'threejs', 'blender']),
|
|
63
|
+
createdAt: z.string(),
|
|
64
|
+
})
|
|
65
|
+
.superRefine((plan, context) => {
|
|
66
|
+
if (plan.contentKind === 'animation-only' && plan.runtimeReady && !plan.targetSkeleton) {
|
|
67
|
+
context.addIssue({
|
|
68
|
+
code: 'custom',
|
|
69
|
+
path: ['targetSkeleton'],
|
|
70
|
+
message: 'Runtime-ready animation-only imports require a compatible target skeleton.',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
export type AssetImportPlan = z.infer<typeof assetImportPlanSchema>;
|
|
75
|
+
|
|
76
|
+
export const assetImportResultSchema = z.object({
|
|
77
|
+
version: z.literal(1),
|
|
78
|
+
planId: z.string(),
|
|
79
|
+
status: z.enum(['imported', 'skipped', 'failed', 'cancelled']),
|
|
80
|
+
writtenFiles: z.array(
|
|
81
|
+
z.object({ path: z.string(), hash: z.string(), size: z.number().int().nonnegative() }),
|
|
82
|
+
),
|
|
83
|
+
skippedFiles: z.array(z.string()),
|
|
84
|
+
issues: z.array(z.object({ code: z.string(), stage: z.string(), message: z.string() })),
|
|
85
|
+
recipePath: z.string().optional(),
|
|
86
|
+
});
|
|
87
|
+
export type AssetImportResult = z.infer<typeof assetImportResultSchema>;
|
|
88
|
+
|
|
89
|
+
export type AssetImportStage = 'fetch' | 'convert' | 'validate' | 'commit';
|
|
90
|
+
export interface AssetImportProgress {
|
|
91
|
+
stage: AssetImportStage;
|
|
92
|
+
item: string;
|
|
93
|
+
loaded: number;
|
|
94
|
+
total: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type AssetConflictKind =
|
|
98
|
+
| 'same-identity-same-hash'
|
|
99
|
+
| 'same-identity-updated'
|
|
100
|
+
| 'different-identity-same-name'
|
|
101
|
+
| 'user-edited-output';
|
|
102
|
+
export function safeConflictResolution(
|
|
103
|
+
kind: AssetConflictKind,
|
|
104
|
+
): 'reuse' | 'update' | 'rename' | 'cancel' {
|
|
105
|
+
if (kind === 'same-identity-same-hash') return 'reuse';
|
|
106
|
+
if (kind === 'same-identity-updated') return 'update';
|
|
107
|
+
if (kind === 'different-identity-same-name') return 'rename';
|
|
108
|
+
return 'cancel';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function importRecipePath(destination: string): string {
|
|
112
|
+
return `${destination.replace(/^\/+/, '')}.vgai-import.json`;
|
|
113
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
|
|
3
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
4
|
+
import { loadModelThumbnailObject, modelThumbnailFormat } from '../model-thumbnail';
|
|
5
|
+
import type { StagedImportFile } from './asset-import-pipeline';
|
|
6
|
+
import type { AssetImportPlan } from './import-contract';
|
|
7
|
+
|
|
8
|
+
export interface ModelConversionBackend {
|
|
9
|
+
id: 'threejs' | 'blender';
|
|
10
|
+
available(): Promise<boolean>;
|
|
11
|
+
convert(
|
|
12
|
+
plan: AssetImportPlan,
|
|
13
|
+
sources: readonly StagedImportFile[],
|
|
14
|
+
signal: AbortSignal,
|
|
15
|
+
): Promise<readonly StagedImportFile[]>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function exportObjectToGlb(object: THREE.Object3D): Promise<Uint8Array> {
|
|
19
|
+
const animations = object.animations;
|
|
20
|
+
const output = await new Promise<ArrayBuffer>((resolve, reject) => {
|
|
21
|
+
new GLTFExporter().parse(
|
|
22
|
+
object,
|
|
23
|
+
(result) =>
|
|
24
|
+
result instanceof ArrayBuffer
|
|
25
|
+
? resolve(result)
|
|
26
|
+
: reject(new Error('GLTF conversion produced JSON instead of a binary GLB.')),
|
|
27
|
+
reject,
|
|
28
|
+
{ binary: true, animations, onlyVisible: false },
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
return new Uint8Array(output);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function convertModelWithThree(
|
|
35
|
+
plan: AssetImportPlan,
|
|
36
|
+
sources: readonly StagedImportFile[],
|
|
37
|
+
signal: AbortSignal,
|
|
38
|
+
): Promise<readonly StagedImportFile[]> {
|
|
39
|
+
if (signal.aborted) throw signal.reason;
|
|
40
|
+
const primarySpec =
|
|
41
|
+
plan.sourceFiles.find((file) => file.role === 'primary') ?? plan.sourceFiles[0]!;
|
|
42
|
+
const primary = sources.find((file) => file.path === primarySpec.path);
|
|
43
|
+
if (!primary) throw new Error('Import primary source is missing from staging.');
|
|
44
|
+
const format = modelThumbnailFormat(primary.path);
|
|
45
|
+
if (!format) throw new Error(`No model conversion backend supports ${primary.path}.`);
|
|
46
|
+
if (format === 'glb') return [{ path: plan.destination, bytes: primary.bytes }];
|
|
47
|
+
if (format === 'gltf') {
|
|
48
|
+
const base = plan.destination.slice(0, plan.destination.lastIndexOf('/') + 1);
|
|
49
|
+
return sources.map((file) => ({
|
|
50
|
+
path: `${base}${file.path.split('/').pop()}`,
|
|
51
|
+
bytes: file.bytes,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
const urls: string[] = [];
|
|
55
|
+
try {
|
|
56
|
+
const primaryUrl = URL.createObjectURL(new Blob([Uint8Array.from(primary.bytes).buffer]));
|
|
57
|
+
urls.push(primaryUrl);
|
|
58
|
+
const material = sources.find((file) => file.path.toLowerCase().endsWith('.mtl'));
|
|
59
|
+
const materialUrl = material
|
|
60
|
+
? URL.createObjectURL(new Blob([Uint8Array.from(material.bytes).buffer]))
|
|
61
|
+
: undefined;
|
|
62
|
+
if (materialUrl) urls.push(materialUrl);
|
|
63
|
+
const object = await loadModelThumbnailObject({
|
|
64
|
+
url: primaryUrl,
|
|
65
|
+
format,
|
|
66
|
+
...(materialUrl ? { materialUrl } : {}),
|
|
67
|
+
});
|
|
68
|
+
object.scale.multiplyScalar(plan.settings.scale);
|
|
69
|
+
object.updateWorldMatrix(true, true);
|
|
70
|
+
const bytes = await exportObjectToGlb(object);
|
|
71
|
+
disposeConvertedObject(object);
|
|
72
|
+
return [{ path: plan.destination.replace(/\.[^.]+$/, '.glb'), bytes }];
|
|
73
|
+
} finally {
|
|
74
|
+
for (const url of urls) URL.revokeObjectURL(url);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function validateRuntimeModel(files: readonly StagedImportFile[]): Promise<void> {
|
|
79
|
+
const model = files.find((file) => file.path.toLowerCase().endsWith('.glb'));
|
|
80
|
+
if (!model) {
|
|
81
|
+
const gltf = files.find((file) => file.path.toLowerCase().endsWith('.gltf'));
|
|
82
|
+
if (!gltf) throw new Error('Conversion produced no runtime GLB or glTF output.');
|
|
83
|
+
JSON.parse(new TextDecoder().decode(gltf.bytes));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const parsed = await new GLTFLoader().parseAsync(Uint8Array.from(model.bytes).buffer, '');
|
|
87
|
+
let renderable = false;
|
|
88
|
+
parsed.scene.traverse((object) => {
|
|
89
|
+
renderable ||= Boolean(
|
|
90
|
+
(object as THREE.Mesh).isMesh || (object as THREE.SkinnedMesh).isSkinnedMesh,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
if (!renderable && parsed.animations.length === 0)
|
|
94
|
+
throw new Error('Runtime validation found neither renderable geometry nor animation clips.');
|
|
95
|
+
disposeConvertedObject(parsed.scene);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function chooseModelConversionBackend(
|
|
99
|
+
preferred: AssetImportPlan['backend'],
|
|
100
|
+
backends: readonly ModelConversionBackend[],
|
|
101
|
+
): Promise<ModelConversionBackend> {
|
|
102
|
+
const preferredBackend = backends.find((backend) => backend.id === preferred);
|
|
103
|
+
if (preferredBackend && (await preferredBackend.available())) return preferredBackend;
|
|
104
|
+
const fallback = backends.find((backend) => backend.id === 'threejs');
|
|
105
|
+
if (fallback && (await fallback.available())) return fallback;
|
|
106
|
+
throw new Error(
|
|
107
|
+
'No model conversion backend is available. Install Blender for high-fidelity conversion or enable the built-in Three.js backend.',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function disposeConvertedObject(root: THREE.Object3D): void {
|
|
112
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: bounded cleanup must enumerate every geometry, material, and texture owned by the converted graph.
|
|
113
|
+
root.traverse((object) => {
|
|
114
|
+
const mesh = object as THREE.Mesh;
|
|
115
|
+
mesh.geometry?.dispose();
|
|
116
|
+
for (const material of Array.isArray(mesh.material)
|
|
117
|
+
? mesh.material
|
|
118
|
+
: mesh.material
|
|
119
|
+
? [mesh.material]
|
|
120
|
+
: []) {
|
|
121
|
+
for (const value of Object.values(material))
|
|
122
|
+
if (value instanceof THREE.Texture) value.dispose();
|
|
123
|
+
material.dispose();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|