@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,185 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
export interface ModelInspection {
|
|
4
|
+
nodes: number;
|
|
5
|
+
meshes: number;
|
|
6
|
+
primitives: number;
|
|
7
|
+
vertices: number;
|
|
8
|
+
triangles: number;
|
|
9
|
+
materials: number;
|
|
10
|
+
textures: number;
|
|
11
|
+
bones: number;
|
|
12
|
+
clips: number;
|
|
13
|
+
morphTargets: number;
|
|
14
|
+
memoryBytes: number;
|
|
15
|
+
bounds: { x: number; y: number; z: number } | null;
|
|
16
|
+
hierarchy: Array<{ id: string; name: string; type: string; depth: number }>;
|
|
17
|
+
materialNames: string[];
|
|
18
|
+
textureNames: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const textureKeys = [
|
|
22
|
+
'map',
|
|
23
|
+
'alphaMap',
|
|
24
|
+
'aoMap',
|
|
25
|
+
'bumpMap',
|
|
26
|
+
'displacementMap',
|
|
27
|
+
'emissiveMap',
|
|
28
|
+
'envMap',
|
|
29
|
+
'lightMap',
|
|
30
|
+
'metalnessMap',
|
|
31
|
+
'normalMap',
|
|
32
|
+
'roughnessMap',
|
|
33
|
+
'specularMap',
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
/** Exact, side-effect-free statistics used by the Asset Editor and import validation. */
|
|
37
|
+
export function inspectModel(root: THREE.Object3D): ModelInspection {
|
|
38
|
+
const materials = new Set<THREE.Material>();
|
|
39
|
+
const textures = new Set<THREE.Texture>();
|
|
40
|
+
const hierarchy: ModelInspection['hierarchy'] = [];
|
|
41
|
+
let nodes = 0;
|
|
42
|
+
let meshes = 0;
|
|
43
|
+
let primitives = 0;
|
|
44
|
+
let vertices = 0;
|
|
45
|
+
let triangles = 0;
|
|
46
|
+
let bones = 0;
|
|
47
|
+
let morphTargets = 0;
|
|
48
|
+
let memoryBytes = 0;
|
|
49
|
+
|
|
50
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one traversal intentionally reconciles geometry, material, texture, animation, and hierarchy statistics.
|
|
51
|
+
root.traverse((object) => {
|
|
52
|
+
nodes++;
|
|
53
|
+
let depth = 0;
|
|
54
|
+
for (let parent = object.parent; parent && parent !== root.parent; parent = parent.parent)
|
|
55
|
+
depth++;
|
|
56
|
+
hierarchy.push({ id: object.uuid, name: object.name || object.type, type: object.type, depth });
|
|
57
|
+
if ((object as THREE.Bone).isBone) bones++;
|
|
58
|
+
const renderable = object as THREE.Mesh;
|
|
59
|
+
const drawable = object as THREE.Object3D & {
|
|
60
|
+
isMesh?: boolean;
|
|
61
|
+
isLine?: boolean;
|
|
62
|
+
isPoints?: boolean;
|
|
63
|
+
};
|
|
64
|
+
if (!drawable.isMesh && !drawable.isLine && !drawable.isPoints) return;
|
|
65
|
+
meshes++;
|
|
66
|
+
const geometry = renderable.geometry;
|
|
67
|
+
if (geometry) {
|
|
68
|
+
const position = geometry.getAttribute('position');
|
|
69
|
+
vertices += position?.count ?? 0;
|
|
70
|
+
triangles += geometry.index
|
|
71
|
+
? Math.floor(geometry.index.count / 3)
|
|
72
|
+
: Math.floor((position?.count ?? 0) / 3);
|
|
73
|
+
primitives += Math.max(geometry.groups.length, 1);
|
|
74
|
+
morphTargets += geometry.morphAttributes['position']?.length ?? 0;
|
|
75
|
+
if (geometry.index) memoryBytes += geometry.index.array.byteLength;
|
|
76
|
+
for (const attribute of Object.values(geometry.attributes))
|
|
77
|
+
memoryBytes += attribute.array.byteLength;
|
|
78
|
+
}
|
|
79
|
+
const objectMaterials = Array.isArray(renderable.material)
|
|
80
|
+
? renderable.material
|
|
81
|
+
: renderable.material
|
|
82
|
+
? [renderable.material]
|
|
83
|
+
: [];
|
|
84
|
+
for (const material of objectMaterials) {
|
|
85
|
+
materials.add(material);
|
|
86
|
+
for (const key of textureKeys) {
|
|
87
|
+
const texture = (material as THREE.Material & Record<string, unknown>)[key];
|
|
88
|
+
if (texture instanceof THREE.Texture) textures.add(texture);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
for (const texture of textures) {
|
|
93
|
+
const image = texture.image as { width?: number; height?: number } | undefined;
|
|
94
|
+
memoryBytes += (image?.width ?? 0) * (image?.height ?? 0) * 4;
|
|
95
|
+
}
|
|
96
|
+
const box = new THREE.Box3().setFromObject(root);
|
|
97
|
+
const size = box.isEmpty() ? null : box.getSize(new THREE.Vector3());
|
|
98
|
+
return {
|
|
99
|
+
nodes,
|
|
100
|
+
meshes,
|
|
101
|
+
primitives,
|
|
102
|
+
vertices,
|
|
103
|
+
triangles,
|
|
104
|
+
materials: materials.size,
|
|
105
|
+
textures: textures.size,
|
|
106
|
+
bones,
|
|
107
|
+
clips: root.animations.length,
|
|
108
|
+
morphTargets,
|
|
109
|
+
memoryBytes,
|
|
110
|
+
bounds: size ? { x: size.x, y: size.y, z: size.z } : null,
|
|
111
|
+
hierarchy,
|
|
112
|
+
materialNames: [...materials].map((material) => material.name || material.type),
|
|
113
|
+
textureNames: [...textures].map(
|
|
114
|
+
(texture) => texture.name || texture.source.data?.src || 'Texture',
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type ModelDiagnosticMode =
|
|
120
|
+
| 'lit'
|
|
121
|
+
| 'unlit'
|
|
122
|
+
| 'wireframe'
|
|
123
|
+
| 'normals'
|
|
124
|
+
| 'uv'
|
|
125
|
+
| 'vertex-colors'
|
|
126
|
+
| 'bounds'
|
|
127
|
+
| 'skeleton'
|
|
128
|
+
| 'collision'
|
|
129
|
+
| 'lod';
|
|
130
|
+
|
|
131
|
+
export type ModelCameraPreset =
|
|
132
|
+
| 'front'
|
|
133
|
+
| 'back'
|
|
134
|
+
| 'left'
|
|
135
|
+
| 'right'
|
|
136
|
+
| 'top'
|
|
137
|
+
| 'bottom'
|
|
138
|
+
| 'isometric';
|
|
139
|
+
|
|
140
|
+
export function cameraPresetDirection(preset: ModelCameraPreset): THREE.Vector3 {
|
|
141
|
+
const directions: Record<ModelCameraPreset, [number, number, number]> = {
|
|
142
|
+
front: [0, 0, 1],
|
|
143
|
+
back: [0, 0, -1],
|
|
144
|
+
left: [-1, 0, 0],
|
|
145
|
+
right: [1, 0, 0],
|
|
146
|
+
top: [0, 1, 0],
|
|
147
|
+
bottom: [0, -1, 0],
|
|
148
|
+
isometric: [1, 0.72, 1],
|
|
149
|
+
};
|
|
150
|
+
return new THREE.Vector3(...directions[preset]).normalize();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function supportedModelDiagnosticModes(root: THREE.Object3D): Array<{
|
|
154
|
+
mode: ModelDiagnosticMode;
|
|
155
|
+
available: boolean;
|
|
156
|
+
reason?: string;
|
|
157
|
+
}> {
|
|
158
|
+
let uv = false;
|
|
159
|
+
let colors = false;
|
|
160
|
+
let bones = false;
|
|
161
|
+
let lod = false;
|
|
162
|
+
root.traverse((object) => {
|
|
163
|
+
const mesh = object as THREE.Mesh;
|
|
164
|
+
uv ||= Boolean(mesh.geometry?.getAttribute('uv'));
|
|
165
|
+
colors ||= Boolean(mesh.geometry?.getAttribute('color'));
|
|
166
|
+
bones ||= Boolean((object as THREE.Bone).isBone || (mesh as THREE.SkinnedMesh).isSkinnedMesh);
|
|
167
|
+
lod ||= (object as THREE.LOD).isLOD === true;
|
|
168
|
+
});
|
|
169
|
+
return [
|
|
170
|
+
{ mode: 'lit', available: true },
|
|
171
|
+
{ mode: 'unlit', available: true },
|
|
172
|
+
{ mode: 'wireframe', available: true },
|
|
173
|
+
{ mode: 'normals', available: true },
|
|
174
|
+
{ mode: 'uv', available: uv, ...(!uv ? { reason: 'No UV channel' } : {}) },
|
|
175
|
+
{
|
|
176
|
+
mode: 'vertex-colors',
|
|
177
|
+
available: colors,
|
|
178
|
+
...(!colors ? { reason: 'No vertex colors' } : {}),
|
|
179
|
+
},
|
|
180
|
+
{ mode: 'bounds', available: true },
|
|
181
|
+
{ mode: 'skeleton', available: bones, ...(!bones ? { reason: 'No skeleton' } : {}) },
|
|
182
|
+
{ mode: 'collision', available: false, reason: 'No collision metadata' },
|
|
183
|
+
{ mode: 'lod', available: lod, ...(!lod ? { reason: 'No LOD groups' } : {}) },
|
|
184
|
+
];
|
|
185
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const PROJECT_ASSET_COMMANDS = {
|
|
2
|
+
open: { id: 'project-asset.open', shortcut: 'Enter' },
|
|
3
|
+
rename: { id: 'project-asset.rename', shortcut: 'F2' },
|
|
4
|
+
duplicate: { id: 'project-asset.duplicate', shortcut: 'Mod+D' },
|
|
5
|
+
delete: { id: 'project-asset.delete', shortcut: 'Delete' },
|
|
6
|
+
copyPath: { id: 'project-asset.copy-path', shortcut: 'Mod+Shift+C' },
|
|
7
|
+
reveal: { id: 'project-asset.reveal', shortcut: null },
|
|
8
|
+
refresh: { id: 'project-asset.refresh', shortcut: 'Mod+R' },
|
|
9
|
+
import: { id: 'project-asset.import', shortcut: null },
|
|
10
|
+
createFolder: { id: 'project-asset.create-folder', shortcut: 'Mod+Shift+N' },
|
|
11
|
+
reimport: { id: 'project-asset.reimport', shortcut: null },
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export type ProjectAssetCommandId =
|
|
15
|
+
(typeof PROJECT_ASSET_COMMANDS)[keyof typeof PROJECT_ASSET_COMMANDS]['id'];
|
|
16
|
+
|
|
17
|
+
export function projectAssetCommandIds(): readonly ProjectAssetCommandId[] {
|
|
18
|
+
return Object.values(PROJECT_ASSET_COMMANDS).map((command) => command.id);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function projectAssetBrowserStateKey(projectId: string): string {
|
|
22
|
+
return `vgai:project-assets:v1:${projectId}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { StorageBackend } from '../storage/types';
|
|
2
|
+
import { assetCapabilities } from './asset-capabilities';
|
|
3
|
+
import type { AssetHealth, AssetHealthCode } from './asset-types';
|
|
4
|
+
|
|
5
|
+
export interface ProjectAssetFileSnapshot {
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly text?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProjectAssetRecipeSnapshot {
|
|
11
|
+
readonly sourcePath: string;
|
|
12
|
+
readonly recordedSourceHash: string;
|
|
13
|
+
readonly currentSourceHash: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProjectAssetReference {
|
|
17
|
+
readonly ownerPath: string;
|
|
18
|
+
readonly jsonPath: string;
|
|
19
|
+
readonly assetPath: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProjectAssetHealthResult {
|
|
23
|
+
readonly health: AssetHealth;
|
|
24
|
+
readonly dependencies: readonly string[];
|
|
25
|
+
readonly references: readonly ProjectAssetReference[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function collectStringPaths(value: unknown, path = '$'): { path: string; value: string }[] {
|
|
29
|
+
if (typeof value === 'string') return [{ path, value }];
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
return value.flatMap((entry, index) => collectStringPaths(entry, `${path}[${index}]`));
|
|
32
|
+
}
|
|
33
|
+
if (!value || typeof value !== 'object') return [];
|
|
34
|
+
return Object.entries(value).flatMap(([key, entry]) =>
|
|
35
|
+
collectStringPaths(entry, `${path}.${key}`),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function externalDependencies(assetPath: string, text: string | undefined): string[] {
|
|
40
|
+
if (!text || !assetPath.toLowerCase().endsWith('.gltf')) return [];
|
|
41
|
+
try {
|
|
42
|
+
const json = JSON.parse(text) as { buffers?: { uri?: string }[]; images?: { uri?: string }[] };
|
|
43
|
+
const base = assetPath.includes('/') ? assetPath.slice(0, assetPath.lastIndexOf('/') + 1) : '';
|
|
44
|
+
return [...(json.buffers ?? []), ...(json.images ?? [])].flatMap((entry) =>
|
|
45
|
+
entry.uri && !entry.uri.startsWith('data:') ? [`${base}${entry.uri}`] : [],
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function inspectProjectAssetHealth(input: {
|
|
53
|
+
readonly assetPath: string;
|
|
54
|
+
readonly files: readonly ProjectAssetFileSnapshot[];
|
|
55
|
+
readonly recipe?: ProjectAssetRecipeSnapshot;
|
|
56
|
+
}): ProjectAssetHealthResult {
|
|
57
|
+
const byPath = new Map(input.files.map((file) => [file.path.replace(/^\//, ''), file]));
|
|
58
|
+
const assetPath = input.assetPath.replace(/^\//, '');
|
|
59
|
+
const asset = byPath.get(assetPath);
|
|
60
|
+
const codes = new Set<AssetHealthCode>();
|
|
61
|
+
const capability = assetCapabilities(assetPath);
|
|
62
|
+
if (!asset) codes.add('missing-file');
|
|
63
|
+
if (!capability.importable) codes.add('unsupported-format');
|
|
64
|
+
else if (!capability.runtimeReady) codes.add('source-only');
|
|
65
|
+
|
|
66
|
+
if (asset?.text && capability.kind === 'json') {
|
|
67
|
+
try {
|
|
68
|
+
JSON.parse(asset.text);
|
|
69
|
+
} catch {
|
|
70
|
+
codes.add('invalid-metadata');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const dependencies = externalDependencies(assetPath, asset?.text);
|
|
75
|
+
if (dependencies.some((path) => !byPath.has(path))) codes.add('missing-dependency');
|
|
76
|
+
if (
|
|
77
|
+
assetPath.startsWith('asset-library/local/') &&
|
|
78
|
+
!byPath.has(`${assetPath.slice(0, assetPath.lastIndexOf('/') + 1)}_VGAI_ASSET.json`)
|
|
79
|
+
) {
|
|
80
|
+
codes.add('missing-attribution');
|
|
81
|
+
}
|
|
82
|
+
if (input.recipe && input.recipe.recordedSourceHash !== input.recipe.currentSourceHash) {
|
|
83
|
+
codes.add('stale-import');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const references: ProjectAssetReference[] = [];
|
|
87
|
+
for (const file of input.files) {
|
|
88
|
+
if (!file.text || !/\.(?:vscn|prefab|mat)\.json$/i.test(file.path)) continue;
|
|
89
|
+
try {
|
|
90
|
+
for (const candidate of collectStringPaths(JSON.parse(file.text))) {
|
|
91
|
+
const normalized = candidate.value.replace(/^\//, '');
|
|
92
|
+
if (normalized === assetPath) {
|
|
93
|
+
references.push({ ownerPath: file.path, jsonPath: candidate.path, assetPath });
|
|
94
|
+
} else if (normalized.startsWith('assets/') && !byPath.has(normalized)) {
|
|
95
|
+
codes.add('broken-scene-reference');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
codes.add('invalid-metadata');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (codes.size === 0) codes.add('healthy');
|
|
104
|
+
const errorCodes = new Set<AssetHealthCode>([
|
|
105
|
+
'missing-file',
|
|
106
|
+
'missing-dependency',
|
|
107
|
+
'invalid-metadata',
|
|
108
|
+
'broken-scene-reference',
|
|
109
|
+
]);
|
|
110
|
+
return {
|
|
111
|
+
health: {
|
|
112
|
+
status: [...codes].some((code) => errorCodes.has(code))
|
|
113
|
+
? 'error'
|
|
114
|
+
: codes.has('healthy')
|
|
115
|
+
? 'healthy'
|
|
116
|
+
: 'warning',
|
|
117
|
+
codes: [...codes],
|
|
118
|
+
},
|
|
119
|
+
dependencies,
|
|
120
|
+
references,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function snapshotProjectFiles(
|
|
125
|
+
backend: StorageBackend,
|
|
126
|
+
dir = '',
|
|
127
|
+
): Promise<ProjectAssetFileSnapshot[]> {
|
|
128
|
+
const entries = await backend.list(dir);
|
|
129
|
+
const snapshots: ProjectAssetFileSnapshot[] = [];
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const path = dir ? `${dir}/${entry.name}` : entry.name;
|
|
132
|
+
if (entry.type === 'dir') {
|
|
133
|
+
snapshots.push(...(await snapshotProjectFiles(backend, path)));
|
|
134
|
+
} else if (/\.json$|\.gltf$/i.test(path)) {
|
|
135
|
+
snapshots.push({ path, text: await backend.read(path) });
|
|
136
|
+
} else {
|
|
137
|
+
snapshots.push({ path });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return snapshots;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function inspectProjectAssetHealthFromStorage(
|
|
144
|
+
backend: StorageBackend,
|
|
145
|
+
assetPath: string,
|
|
146
|
+
recipe?: ProjectAssetRecipeSnapshot,
|
|
147
|
+
): Promise<ProjectAssetHealthResult> {
|
|
148
|
+
return inspectProjectAssetHealth({
|
|
149
|
+
assetPath,
|
|
150
|
+
files: await snapshotProjectFiles(backend),
|
|
151
|
+
...(recipe ? { recipe } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { HistoryService } from '../history/history-service';
|
|
2
|
+
import { getProjectFileHistory } from '../history/project-file-history';
|
|
3
|
+
import type { StorageBackend } from '../storage/types';
|
|
4
|
+
|
|
5
|
+
export type ProjectAssetOperation =
|
|
6
|
+
| { readonly type: 'create-folder'; readonly path: string }
|
|
7
|
+
| { readonly type: 'duplicate'; readonly source: string; readonly destination: string }
|
|
8
|
+
| { readonly type: 'move'; readonly source: string; readonly destination: string }
|
|
9
|
+
| { readonly type: 'delete'; readonly paths: readonly string[] }
|
|
10
|
+
| { readonly type: 'import'; readonly files: readonly { path: string; bytes: Uint8Array }[] }
|
|
11
|
+
| { readonly type: 'reimport'; readonly files: readonly { path: string; bytes: Uint8Array }[] };
|
|
12
|
+
|
|
13
|
+
function emitAssetMutation(detail: {
|
|
14
|
+
type: 'move' | 'delete' | 'change';
|
|
15
|
+
oldPath?: string;
|
|
16
|
+
newPath?: string;
|
|
17
|
+
paths?: readonly string[];
|
|
18
|
+
}): void {
|
|
19
|
+
if (typeof window === 'undefined') return;
|
|
20
|
+
window.dispatchEvent(new CustomEvent('editor:asset-mutation', { detail }));
|
|
21
|
+
window.dispatchEvent(new CustomEvent('editor:assets-changed'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validPath(path: string): string {
|
|
25
|
+
const normalized = path
|
|
26
|
+
.replace(/^\/+/, '')
|
|
27
|
+
.replace(/\\/g, '/')
|
|
28
|
+
.replace(/\/{2,}/g, '/');
|
|
29
|
+
if (
|
|
30
|
+
!normalized ||
|
|
31
|
+
normalized === '.' ||
|
|
32
|
+
normalized.split('/').some((part) => !part || part === '..')
|
|
33
|
+
) {
|
|
34
|
+
throw new Error(`Invalid project asset path: ${path}`);
|
|
35
|
+
}
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function assertDestinationsFree(backend: StorageBackend, paths: readonly string[]) {
|
|
40
|
+
for (const path of paths) {
|
|
41
|
+
if (await backend.exists(path)) throw new Error(`An asset already exists at ${path}.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Executes all writes through exact-byte project history; no UI owns filesystem mutation. */
|
|
46
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the tagged operation union is intentionally centralized so every project mutation shares one atomic history boundary.
|
|
47
|
+
export async function executeProjectAssetOperation(
|
|
48
|
+
operation: ProjectAssetOperation,
|
|
49
|
+
dependencies: { readonly backend: StorageBackend; readonly history: HistoryService },
|
|
50
|
+
): Promise<boolean> {
|
|
51
|
+
const files = getProjectFileHistory(dependencies.history, dependencies.backend);
|
|
52
|
+
switch (operation.type) {
|
|
53
|
+
case 'create-folder': {
|
|
54
|
+
const folder = validPath(operation.path);
|
|
55
|
+
const marker = `${folder}/.vgai-folder`;
|
|
56
|
+
if (await dependencies.backend.exists(folder))
|
|
57
|
+
throw new Error(`Folder already exists: ${folder}.`);
|
|
58
|
+
return files.write(marker, '', { label: `Create folder ${folder}` });
|
|
59
|
+
}
|
|
60
|
+
case 'duplicate': {
|
|
61
|
+
const source = validPath(operation.source);
|
|
62
|
+
const destination = validPath(operation.destination);
|
|
63
|
+
await assertDestinationsFree(dependencies.backend, [destination]);
|
|
64
|
+
const bytes = await dependencies.backend.readBytes(source);
|
|
65
|
+
return files.write(destination, bytes, { label: `Duplicate ${source}` });
|
|
66
|
+
}
|
|
67
|
+
case 'move': {
|
|
68
|
+
const source = validPath(operation.source);
|
|
69
|
+
const destination = validPath(operation.destination);
|
|
70
|
+
await assertDestinationsFree(dependencies.backend, [destination]);
|
|
71
|
+
const bytes = await dependencies.backend.readBytes(source);
|
|
72
|
+
const changed = await files.mutate(
|
|
73
|
+
[
|
|
74
|
+
{ path: destination, data: bytes },
|
|
75
|
+
{ path: source, data: null },
|
|
76
|
+
],
|
|
77
|
+
{ label: `Move ${source}` },
|
|
78
|
+
);
|
|
79
|
+
if (changed) emitAssetMutation({ type: 'move', oldPath: source, newPath: destination });
|
|
80
|
+
return changed;
|
|
81
|
+
}
|
|
82
|
+
case 'delete': {
|
|
83
|
+
const paths = [...new Set(operation.paths.map(validPath))];
|
|
84
|
+
const changed = await files.mutate(
|
|
85
|
+
paths.map((path) => ({ path, data: null })),
|
|
86
|
+
{ label: `Delete ${paths.length} asset${paths.length === 1 ? '' : 's'}` },
|
|
87
|
+
);
|
|
88
|
+
if (changed) emitAssetMutation({ type: 'delete', paths });
|
|
89
|
+
return changed;
|
|
90
|
+
}
|
|
91
|
+
case 'import': {
|
|
92
|
+
const imported = operation.files.map((file) => ({
|
|
93
|
+
path: validPath(file.path),
|
|
94
|
+
data: file.bytes,
|
|
95
|
+
}));
|
|
96
|
+
await assertDestinationsFree(
|
|
97
|
+
dependencies.backend,
|
|
98
|
+
imported.map((file) => file.path),
|
|
99
|
+
);
|
|
100
|
+
const changed = await files.mutate(imported, {
|
|
101
|
+
label: `Import ${imported.length} asset${imported.length === 1 ? '' : 's'}`,
|
|
102
|
+
});
|
|
103
|
+
if (changed) emitAssetMutation({ type: 'change', paths: imported.map((file) => file.path) });
|
|
104
|
+
return changed;
|
|
105
|
+
}
|
|
106
|
+
case 'reimport': {
|
|
107
|
+
const reimported = operation.files.map((file) => ({
|
|
108
|
+
path: validPath(file.path),
|
|
109
|
+
data: file.bytes,
|
|
110
|
+
}));
|
|
111
|
+
const changed = await files.mutate(reimported, {
|
|
112
|
+
label: `Reimport ${reimported.length} asset${reimported.length === 1 ? '' : 's'}`,
|
|
113
|
+
});
|
|
114
|
+
if (changed)
|
|
115
|
+
emitAssetMutation({ type: 'change', paths: reimported.map((file) => file.path) });
|
|
116
|
+
return changed;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getStorageBackend } from '../storage';
|
|
3
|
+
import type { AssetCatalog } from './asset-types';
|
|
4
|
+
|
|
5
|
+
export const THUMBNAIL_PROFILE = {
|
|
6
|
+
version: 'vgai-thumbnail-v2',
|
|
7
|
+
width: 256,
|
|
8
|
+
height: 256,
|
|
9
|
+
background: 'neutral',
|
|
10
|
+
framing: 'bounds-isometric-1.3',
|
|
11
|
+
lighting: 'studio-key-fill-v1',
|
|
12
|
+
colorManagement: 'srgb-aces-filmic',
|
|
13
|
+
animationPose: 'representative-35-percent',
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export const thumbnailManifestSchema = z.object({
|
|
17
|
+
version: z.literal(1),
|
|
18
|
+
profile: z.literal(THUMBNAIL_PROFILE.version),
|
|
19
|
+
generatedAt: z.string(),
|
|
20
|
+
entries: z.record(
|
|
21
|
+
z.string(),
|
|
22
|
+
z.object({
|
|
23
|
+
state: z.enum(['generated', 'upstream', 'fallback', 'unsupported', 'failed']),
|
|
24
|
+
path: z.string().optional(),
|
|
25
|
+
profileVersion: z.string(),
|
|
26
|
+
sourceHash: z.string().optional(),
|
|
27
|
+
resultHash: z.string().optional(),
|
|
28
|
+
issueCode: z.string().optional(),
|
|
29
|
+
animation: z.object({ clip: z.string(), time: z.number().nonnegative() }).optional(),
|
|
30
|
+
framing: z
|
|
31
|
+
.object({
|
|
32
|
+
position: z.tuple([z.number(), z.number(), z.number()]),
|
|
33
|
+
target: z.tuple([z.number(), z.number(), z.number()]),
|
|
34
|
+
})
|
|
35
|
+
.optional(),
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
export type ThumbnailManifest = z.infer<typeof thumbnailManifestSchema>;
|
|
40
|
+
|
|
41
|
+
let manifestPromise: Promise<ThumbnailManifest | null> | null = null;
|
|
42
|
+
export function loadProjectThumbnailManifest(): Promise<ThumbnailManifest | null> {
|
|
43
|
+
if (!manifestPromise) {
|
|
44
|
+
const backend = getStorageBackend() as ReturnType<typeof getStorageBackend> | undefined;
|
|
45
|
+
manifestPromise = backend
|
|
46
|
+
? backend.read('.vgai/thumbnails.json').then(
|
|
47
|
+
(content) => thumbnailManifestSchema.parse(JSON.parse(content)),
|
|
48
|
+
() => null,
|
|
49
|
+
)
|
|
50
|
+
: Promise.resolve(null);
|
|
51
|
+
}
|
|
52
|
+
return manifestPromise;
|
|
53
|
+
}
|
|
54
|
+
export function resetProjectThumbnailManifestCache(): void {
|
|
55
|
+
manifestPromise = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface QueueJob<T> {
|
|
59
|
+
key: string;
|
|
60
|
+
priority: number;
|
|
61
|
+
consumers: number;
|
|
62
|
+
started: boolean;
|
|
63
|
+
resolve(value: T): void;
|
|
64
|
+
reject(error: unknown): void;
|
|
65
|
+
promise: Promise<T>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Bounded, priority-aware, deduplicating fallback queue shared by all visible cards. */
|
|
69
|
+
export class ThumbnailJobQueue<T> {
|
|
70
|
+
private active = 0;
|
|
71
|
+
private readonly jobs = new Map<string, QueueJob<T>>();
|
|
72
|
+
private pending: QueueJob<T>[] = [];
|
|
73
|
+
constructor(
|
|
74
|
+
private readonly execute: (key: string) => Promise<T>,
|
|
75
|
+
readonly concurrency = 2,
|
|
76
|
+
) {}
|
|
77
|
+
request(key: string, priority = 0): { promise: Promise<T>; cancel(): void } {
|
|
78
|
+
let job = this.jobs.get(key);
|
|
79
|
+
if (!job) {
|
|
80
|
+
let resolve!: (value: T) => void;
|
|
81
|
+
let reject!: (error: unknown) => void;
|
|
82
|
+
const promise = new Promise<T>((ok, fail) => {
|
|
83
|
+
resolve = ok;
|
|
84
|
+
reject = fail;
|
|
85
|
+
});
|
|
86
|
+
job = { key, priority, consumers: 0, started: false, resolve, reject, promise };
|
|
87
|
+
this.jobs.set(key, job);
|
|
88
|
+
this.pending.push(job);
|
|
89
|
+
}
|
|
90
|
+
job.consumers++;
|
|
91
|
+
job.priority = Math.max(job.priority, priority);
|
|
92
|
+
this.pending.sort((a, b) => b.priority - a.priority);
|
|
93
|
+
this.pump();
|
|
94
|
+
let cancelled = false;
|
|
95
|
+
return {
|
|
96
|
+
promise: job.promise,
|
|
97
|
+
cancel: () => {
|
|
98
|
+
if (cancelled) return;
|
|
99
|
+
cancelled = true;
|
|
100
|
+
job!.consumers--;
|
|
101
|
+
if (!job!.started && job!.consumers === 0) {
|
|
102
|
+
this.pending = this.pending.filter((item) => item !== job);
|
|
103
|
+
this.jobs.delete(job!.key);
|
|
104
|
+
job!.reject(new DOMException('Thumbnail request cancelled', 'AbortError'));
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
snapshot(): { active: number; pending: number; keys: number } {
|
|
110
|
+
return { active: this.active, pending: this.pending.length, keys: this.jobs.size };
|
|
111
|
+
}
|
|
112
|
+
private pump(): void {
|
|
113
|
+
while (this.active < this.concurrency) {
|
|
114
|
+
const job = this.pending.shift();
|
|
115
|
+
if (!job) return;
|
|
116
|
+
if (job.consumers === 0) continue;
|
|
117
|
+
job.started = true;
|
|
118
|
+
this.active++;
|
|
119
|
+
void this.execute(job.key)
|
|
120
|
+
.then(job.resolve, job.reject)
|
|
121
|
+
.finally(() => {
|
|
122
|
+
this.active--;
|
|
123
|
+
this.jobs.delete(job.key);
|
|
124
|
+
this.pump();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ThumbnailQualitySample {
|
|
131
|
+
alphaCoverage: number;
|
|
132
|
+
luminanceVariance: number;
|
|
133
|
+
edgeCoverage: number;
|
|
134
|
+
}
|
|
135
|
+
export function auditThumbnailQuality(sample: ThumbnailQualitySample): string[] {
|
|
136
|
+
const issues: string[] = [];
|
|
137
|
+
if (sample.alphaCoverage < 0.01) issues.push('thumbnail.blank-transparent');
|
|
138
|
+
if (sample.luminanceVariance < 0.0005 && sample.edgeCoverage < 0.01)
|
|
139
|
+
issues.push('thumbnail.near-uniform');
|
|
140
|
+
if (sample.edgeCoverage > 0.45) issues.push('thumbnail.possibly-clipped');
|
|
141
|
+
return issues;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function catalogThumbnailCoverage(catalog: AssetCatalog): {
|
|
145
|
+
total: number;
|
|
146
|
+
generated: number;
|
|
147
|
+
upstream: number;
|
|
148
|
+
fallback: number;
|
|
149
|
+
unsupported: number;
|
|
150
|
+
failed: number;
|
|
151
|
+
} {
|
|
152
|
+
const result = { total: 0, generated: 0, upstream: 0, fallback: 0, unsupported: 0, failed: 0 };
|
|
153
|
+
for (const family of catalog.families) {
|
|
154
|
+
for (const variant of family.variants) {
|
|
155
|
+
result.total++;
|
|
156
|
+
result[variant.thumbnail.state]++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function representativeAnimationPose(
|
|
163
|
+
clips: readonly { name: string; duration: number }[],
|
|
164
|
+
): { clip: string; time: number } | null {
|
|
165
|
+
if (!clips.length) return null;
|
|
166
|
+
const ranked = [...clips].sort((a, b) => {
|
|
167
|
+
const score = (name: string) => (/walk|run|idle|action/i.test(name) ? 1 : 0);
|
|
168
|
+
return score(b.name) - score(a.name) || b.duration - a.duration || a.name.localeCompare(b.name);
|
|
169
|
+
});
|
|
170
|
+
const clip = ranked[0]!;
|
|
171
|
+
return { clip: clip.name, time: Number((clip.duration * 0.35).toFixed(6)) };
|
|
172
|
+
}
|
|
@@ -48,3 +48,18 @@ export function setActiveNetworking(n: SystemAdapters['networking'] | null): voi
|
|
|
48
48
|
export function getActiveNetworking(): SystemAdapters['networking'] | null {
|
|
49
49
|
return _systems.networking ?? null;
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
export function setActiveDebug(d: SystemAdapters['debug'] | null): void {
|
|
53
|
+
const next = { ..._systems };
|
|
54
|
+
if (d) next.debug = d;
|
|
55
|
+
else delete next.debug;
|
|
56
|
+
_systems = next;
|
|
57
|
+
}
|
|
58
|
+
/** The active game's debug seam (`DebugAdapter`, SYNTHETIC-PLAYER-SPEC §3.1) —
|
|
59
|
+
* installed with the whole bag by play-mode's `setActiveSystems`, read by
|
|
60
|
+
* the Debug Console / State Watch panels and the CLI command relay. Null
|
|
61
|
+
* outside play (or for a mount exposing no debug adapter) — the panels'
|
|
62
|
+
* hide signal. */
|
|
63
|
+
export function getActiveDebug(): SystemAdapters['debug'] | null {
|
|
64
|
+
return _systems.debug ?? null;
|
|
65
|
+
}
|