@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,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset SELECTION state (W2, docs/EDITOR-WORKSPACE-SHELL-TODO.md §5.1) — the
|
|
3
|
+
* "single click selects" half of the selection-vs-open contract:
|
|
4
|
+
*
|
|
5
|
+
* - single click in the Project/Assets browser SELECTS the asset and shows
|
|
6
|
+
* compact preview/metadata in the Inspector (the registered
|
|
7
|
+
* `components/asset-selection-section.tsx` section reads this store);
|
|
8
|
+
* - double click / Enter OPENS the full viewer as a center document
|
|
9
|
+
* (`components/asset-documents.tsx`);
|
|
10
|
+
* - drag remains a placement gesture and never touches this store.
|
|
11
|
+
*
|
|
12
|
+
* A tiny module-scope store in the house `useSyncExternalStore` shape
|
|
13
|
+
* (mirrors `inspector-section-registry.ts`): version counter + subscribe +
|
|
14
|
+
* `__resetForTest`. It deliberately does NOT touch the EditorStore's entity
|
|
15
|
+
* selection — clicking an asset must never deselect/reselect scene entities
|
|
16
|
+
* (§6.1 "selection never unexpectedly..."); instead the Inspector section
|
|
17
|
+
* simply appears for the selected asset, and the selection is auto-cleared
|
|
18
|
+
* when the ENTITY selection changes (the user moved on — see
|
|
19
|
+
* `installAssetSelectionAutoClear`).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { AssetCapabilities } from './asset-workflow/asset-capabilities';
|
|
23
|
+
import type { AssetKind } from './editor-store';
|
|
24
|
+
|
|
25
|
+
/** The §5.1 compact-selection payload — enough for the Inspector section to
|
|
26
|
+
* render preview + metadata without re-deriving browser state. */
|
|
27
|
+
export interface SelectedAsset {
|
|
28
|
+
/** Serving path (`/textures/crate.png`) — doubles as the open-document key. */
|
|
29
|
+
readonly path: string;
|
|
30
|
+
/** Source-owned file name as authored (§8 — `crate.png`). */
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly kind: AssetKind | 'folder' | 'unknown';
|
|
33
|
+
/** File size in bytes when the browser knows it. */
|
|
34
|
+
readonly sizeBytes?: number;
|
|
35
|
+
readonly capabilities?: AssetCapabilities;
|
|
36
|
+
readonly health?: 'healthy' | 'source-only' | 'unsupported' | 'stale-import';
|
|
37
|
+
readonly sourcePath?: string;
|
|
38
|
+
readonly selectionCount?: number;
|
|
39
|
+
readonly dependencies?: readonly string[];
|
|
40
|
+
readonly references?: readonly { ownerPath: string; jsonPath: string }[];
|
|
41
|
+
readonly healthCodes?: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let _selected: SelectedAsset | null = null;
|
|
45
|
+
let _version = 0;
|
|
46
|
+
const _listeners = new Set<() => void>();
|
|
47
|
+
|
|
48
|
+
function notifyChanged(): void {
|
|
49
|
+
_version++;
|
|
50
|
+
for (const fn of _listeners) fn();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Subscribe to selection changes. Returns an unsubscribe function. */
|
|
54
|
+
export function subscribeAssetSelection(fn: () => void): () => void {
|
|
55
|
+
_listeners.add(fn);
|
|
56
|
+
return () => {
|
|
57
|
+
_listeners.delete(fn);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Monotonic change counter — the `getSnapshot` for `useSyncExternalStore`. */
|
|
62
|
+
export function assetSelectionVersion(): number {
|
|
63
|
+
return _version;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The currently selected asset, or `null`. */
|
|
67
|
+
export function getSelectedAsset(): SelectedAsset | null {
|
|
68
|
+
return _selected;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Select an asset (single-click). Idempotent on the same path. */
|
|
72
|
+
export function setSelectedAsset(asset: SelectedAsset): void {
|
|
73
|
+
if (_selected && JSON.stringify(_selected) === JSON.stringify(asset)) return;
|
|
74
|
+
_selected = asset;
|
|
75
|
+
notifyChanged();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Clear the asset selection (no-op when already clear). */
|
|
79
|
+
export function clearSelectedAsset(): void {
|
|
80
|
+
if (_selected === null) return;
|
|
81
|
+
_selected = null;
|
|
82
|
+
notifyChanged();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The narrow store surface the auto-clear needs (testable with a fake). */
|
|
86
|
+
export interface AssetSelectionAutoClearStore {
|
|
87
|
+
subscribe(listener: () => void): () => void;
|
|
88
|
+
readonly selectedEntityIds: ReadonlySet<string>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function entitySelectionKey(store: AssetSelectionAutoClearStore): string {
|
|
92
|
+
return [...store.selectedEntityIds].sort().join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Auto-clear wiring (installed once per editor session by the layout): when
|
|
97
|
+
* the ENTITY selection changes while an asset is selected, the user has moved
|
|
98
|
+
* on — drop the asset selection so the Inspector goes back to being purely
|
|
99
|
+
* entity-contextual. The entity-selection snapshot is captured on every asset
|
|
100
|
+
* selection SET (not at install time), so only a change that happens AFTER
|
|
101
|
+
* the asset click clears it.
|
|
102
|
+
*/
|
|
103
|
+
export function installAssetSelectionAutoClear(store: AssetSelectionAutoClearStore): () => void {
|
|
104
|
+
let snapshot = entitySelectionKey(store);
|
|
105
|
+
const unsubscribeSelf = subscribeAssetSelection(() => {
|
|
106
|
+
if (_selected) snapshot = entitySelectionKey(store);
|
|
107
|
+
});
|
|
108
|
+
const unsubscribeStore = store.subscribe(() => {
|
|
109
|
+
if (_selected && entitySelectionKey(store) !== snapshot) clearSelectedAsset();
|
|
110
|
+
});
|
|
111
|
+
return () => {
|
|
112
|
+
unsubscribeSelf();
|
|
113
|
+
unsubscribeStore();
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Test-only reset (mirrors `__resetInspectorSectionRegistryForTest`). */
|
|
118
|
+
export function __resetAssetSelectionForTest(): void {
|
|
119
|
+
_selected = null;
|
|
120
|
+
notifyChanged();
|
|
121
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
export type AssetFormat =
|
|
2
|
+
| 'glb'
|
|
3
|
+
| 'gltf'
|
|
4
|
+
| 'fbx'
|
|
5
|
+
| 'obj'
|
|
6
|
+
| 'dae'
|
|
7
|
+
| 'stl'
|
|
8
|
+
| 'ply'
|
|
9
|
+
| '3ds'
|
|
10
|
+
| 'bvh'
|
|
11
|
+
| 'blend'
|
|
12
|
+
| 'usd'
|
|
13
|
+
| 'usda'
|
|
14
|
+
| 'usdc'
|
|
15
|
+
| 'png'
|
|
16
|
+
| 'jpg'
|
|
17
|
+
| 'jpeg'
|
|
18
|
+
| 'webp'
|
|
19
|
+
| 'gif'
|
|
20
|
+
| 'svg'
|
|
21
|
+
| 'mp3'
|
|
22
|
+
| 'ogg'
|
|
23
|
+
| 'wav'
|
|
24
|
+
| 'flac'
|
|
25
|
+
| 'vscn.json'
|
|
26
|
+
| 'prefab.json'
|
|
27
|
+
| 'mat.json'
|
|
28
|
+
| 'inputmap.json'
|
|
29
|
+
| 'data.json'
|
|
30
|
+
| 'json'
|
|
31
|
+
| 'unknown';
|
|
32
|
+
|
|
33
|
+
export type AssetImportStrategy =
|
|
34
|
+
| 'copy-runtime'
|
|
35
|
+
| 'convert-model'
|
|
36
|
+
| 'animation-source'
|
|
37
|
+
| 'copy-project'
|
|
38
|
+
| 'unsupported';
|
|
39
|
+
|
|
40
|
+
export type AssetCapabilityKind =
|
|
41
|
+
| 'model'
|
|
42
|
+
| 'image'
|
|
43
|
+
| 'audio'
|
|
44
|
+
| 'json'
|
|
45
|
+
| 'scene'
|
|
46
|
+
| 'prefab'
|
|
47
|
+
| 'material'
|
|
48
|
+
| 'source'
|
|
49
|
+
| 'unknown';
|
|
50
|
+
|
|
51
|
+
export interface AssetCapabilities {
|
|
52
|
+
readonly format: AssetFormat;
|
|
53
|
+
readonly kind: AssetCapabilityKind;
|
|
54
|
+
readonly previewable: boolean;
|
|
55
|
+
readonly importable: boolean;
|
|
56
|
+
readonly runtimeReady: boolean;
|
|
57
|
+
readonly placeable: boolean;
|
|
58
|
+
readonly animated: boolean;
|
|
59
|
+
readonly importStrategy: AssetImportStrategy;
|
|
60
|
+
readonly editor: 'model' | 'image' | 'audio' | 'material' | 'data' | 'scene' | 'source' | 'none';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const MODEL_PREVIEW_FORMATS = ['glb', 'gltf', 'fbx', 'obj', 'dae', 'stl', 'ply', '3ds'] as const;
|
|
64
|
+
const SOURCE_MODEL_FORMATS = ['blend', 'usd', 'usda', 'usdc'] as const;
|
|
65
|
+
const IMAGE_FORMATS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'svg'] as const;
|
|
66
|
+
const AUDIO_FORMATS = ['mp3', 'ogg', 'wav', 'flac'] as const;
|
|
67
|
+
|
|
68
|
+
function row(
|
|
69
|
+
format: AssetFormat,
|
|
70
|
+
values: Omit<AssetCapabilities, 'format'>,
|
|
71
|
+
): readonly [AssetFormat, AssetCapabilities] {
|
|
72
|
+
return [format, { format, ...values }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const MODEL_SOURCE_BASE = {
|
|
76
|
+
kind: 'source',
|
|
77
|
+
previewable: true,
|
|
78
|
+
importable: true,
|
|
79
|
+
runtimeReady: false,
|
|
80
|
+
placeable: false,
|
|
81
|
+
animated: false,
|
|
82
|
+
importStrategy: 'convert-model',
|
|
83
|
+
editor: 'model',
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
export const ASSET_CAPABILITY_REGISTRY: ReadonlyMap<AssetFormat, AssetCapabilities> = new Map([
|
|
87
|
+
...MODEL_PREVIEW_FORMATS.map((format) =>
|
|
88
|
+
row(format, {
|
|
89
|
+
...MODEL_SOURCE_BASE,
|
|
90
|
+
kind: format === 'glb' || format === 'gltf' ? 'model' : 'source',
|
|
91
|
+
runtimeReady: format === 'glb' || format === 'gltf',
|
|
92
|
+
placeable: format === 'glb' || format === 'gltf',
|
|
93
|
+
importStrategy: format === 'glb' || format === 'gltf' ? 'copy-runtime' : 'convert-model',
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
96
|
+
row('bvh', {
|
|
97
|
+
kind: 'source',
|
|
98
|
+
previewable: true,
|
|
99
|
+
importable: true,
|
|
100
|
+
runtimeReady: false,
|
|
101
|
+
placeable: false,
|
|
102
|
+
animated: true,
|
|
103
|
+
importStrategy: 'animation-source',
|
|
104
|
+
editor: 'model',
|
|
105
|
+
}),
|
|
106
|
+
...SOURCE_MODEL_FORMATS.map((format) =>
|
|
107
|
+
row(format, { ...MODEL_SOURCE_BASE, previewable: false, editor: 'source' }),
|
|
108
|
+
),
|
|
109
|
+
...IMAGE_FORMATS.map((format) =>
|
|
110
|
+
row(format, {
|
|
111
|
+
kind: 'image',
|
|
112
|
+
previewable: true,
|
|
113
|
+
importable: true,
|
|
114
|
+
runtimeReady: true,
|
|
115
|
+
placeable: false,
|
|
116
|
+
animated: format === 'gif',
|
|
117
|
+
importStrategy: 'copy-project',
|
|
118
|
+
editor: 'image',
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
...AUDIO_FORMATS.map((format) =>
|
|
122
|
+
row(format, {
|
|
123
|
+
kind: 'audio',
|
|
124
|
+
previewable: true,
|
|
125
|
+
importable: true,
|
|
126
|
+
runtimeReady: true,
|
|
127
|
+
placeable: false,
|
|
128
|
+
animated: false,
|
|
129
|
+
importStrategy: 'copy-project',
|
|
130
|
+
editor: 'audio',
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
133
|
+
row('vscn.json', {
|
|
134
|
+
kind: 'scene',
|
|
135
|
+
previewable: false,
|
|
136
|
+
importable: true,
|
|
137
|
+
runtimeReady: true,
|
|
138
|
+
placeable: false,
|
|
139
|
+
animated: false,
|
|
140
|
+
importStrategy: 'copy-project',
|
|
141
|
+
editor: 'scene',
|
|
142
|
+
}),
|
|
143
|
+
row('prefab.json', {
|
|
144
|
+
kind: 'prefab',
|
|
145
|
+
previewable: true,
|
|
146
|
+
importable: true,
|
|
147
|
+
runtimeReady: true,
|
|
148
|
+
placeable: true,
|
|
149
|
+
animated: false,
|
|
150
|
+
importStrategy: 'copy-project',
|
|
151
|
+
editor: 'scene',
|
|
152
|
+
}),
|
|
153
|
+
row('mat.json', {
|
|
154
|
+
kind: 'material',
|
|
155
|
+
previewable: true,
|
|
156
|
+
importable: true,
|
|
157
|
+
runtimeReady: true,
|
|
158
|
+
placeable: false,
|
|
159
|
+
animated: false,
|
|
160
|
+
importStrategy: 'copy-project',
|
|
161
|
+
editor: 'material',
|
|
162
|
+
}),
|
|
163
|
+
row('inputmap.json', {
|
|
164
|
+
kind: 'json',
|
|
165
|
+
previewable: true,
|
|
166
|
+
importable: true,
|
|
167
|
+
runtimeReady: true,
|
|
168
|
+
placeable: false,
|
|
169
|
+
animated: false,
|
|
170
|
+
importStrategy: 'copy-project',
|
|
171
|
+
editor: 'data',
|
|
172
|
+
}),
|
|
173
|
+
row('data.json', {
|
|
174
|
+
kind: 'json',
|
|
175
|
+
previewable: true,
|
|
176
|
+
importable: true,
|
|
177
|
+
runtimeReady: true,
|
|
178
|
+
placeable: false,
|
|
179
|
+
animated: false,
|
|
180
|
+
importStrategy: 'copy-project',
|
|
181
|
+
editor: 'data',
|
|
182
|
+
}),
|
|
183
|
+
row('json', {
|
|
184
|
+
kind: 'json',
|
|
185
|
+
previewable: true,
|
|
186
|
+
importable: true,
|
|
187
|
+
runtimeReady: false,
|
|
188
|
+
placeable: false,
|
|
189
|
+
animated: false,
|
|
190
|
+
importStrategy: 'copy-project',
|
|
191
|
+
editor: 'data',
|
|
192
|
+
}),
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
const UNKNOWN_CAPABILITIES: AssetCapabilities = {
|
|
196
|
+
format: 'unknown',
|
|
197
|
+
kind: 'unknown',
|
|
198
|
+
previewable: false,
|
|
199
|
+
importable: false,
|
|
200
|
+
runtimeReady: false,
|
|
201
|
+
placeable: false,
|
|
202
|
+
animated: false,
|
|
203
|
+
importStrategy: 'unsupported',
|
|
204
|
+
editor: 'none',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export function assetFormatFromPath(path: string): AssetFormat {
|
|
208
|
+
const clean = path.split(/[?#]/, 1)[0]!.toLowerCase();
|
|
209
|
+
for (const compound of [
|
|
210
|
+
'vscn.json',
|
|
211
|
+
'prefab.json',
|
|
212
|
+
'mat.json',
|
|
213
|
+
'inputmap.json',
|
|
214
|
+
'data.json',
|
|
215
|
+
] as const) {
|
|
216
|
+
if (clean.endsWith(`.${compound}`)) return compound;
|
|
217
|
+
}
|
|
218
|
+
const dot = clean.lastIndexOf('.');
|
|
219
|
+
const extension = dot >= 0 ? clean.slice(dot + 1) : '';
|
|
220
|
+
return ASSET_CAPABILITY_REGISTRY.has(extension as AssetFormat)
|
|
221
|
+
? (extension as AssetFormat)
|
|
222
|
+
: extension === 'json'
|
|
223
|
+
? 'json'
|
|
224
|
+
: 'unknown';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function assetCapabilities(pathOrFormat: string): AssetCapabilities {
|
|
228
|
+
const direct = ASSET_CAPABILITY_REGISTRY.get(pathOrFormat.toLowerCase() as AssetFormat);
|
|
229
|
+
if (direct) return direct;
|
|
230
|
+
return ASSET_CAPABILITY_REGISTRY.get(assetFormatFromPath(pathOrFormat)) ?? UNKNOWN_CAPABILITIES;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function supportedAssetFormats(): readonly AssetFormat[] {
|
|
234
|
+
return [...ASSET_CAPABILITY_REGISTRY.keys()];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Editor document kind for a capability row; null means no honest editor exists. */
|
|
238
|
+
export function assetDocumentKind(
|
|
239
|
+
capability: AssetCapabilities,
|
|
240
|
+
): 'model' | 'image' | 'audio' | 'json' | 'scene' | 'prefab' | 'material' | 'source' | null {
|
|
241
|
+
switch (capability.editor) {
|
|
242
|
+
case 'model':
|
|
243
|
+
return 'model';
|
|
244
|
+
case 'image':
|
|
245
|
+
return 'image';
|
|
246
|
+
case 'audio':
|
|
247
|
+
return 'audio';
|
|
248
|
+
case 'material':
|
|
249
|
+
return 'material';
|
|
250
|
+
case 'data':
|
|
251
|
+
return 'json';
|
|
252
|
+
case 'source':
|
|
253
|
+
return 'source';
|
|
254
|
+
case 'scene':
|
|
255
|
+
return capability.kind === 'prefab' ? 'prefab' : 'scene';
|
|
256
|
+
default:
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type AssetLoadStage =
|
|
2
|
+
| 'fetching'
|
|
3
|
+
| 'parsing'
|
|
4
|
+
| 'dependency-resolution'
|
|
5
|
+
| 'conversion'
|
|
6
|
+
| 'gpu-upload'
|
|
7
|
+
| 'rendering'
|
|
8
|
+
| 'ready';
|
|
9
|
+
|
|
10
|
+
export interface AssetEditorIssue {
|
|
11
|
+
code: string;
|
|
12
|
+
stage: AssetLoadStage;
|
|
13
|
+
message: string;
|
|
14
|
+
recovery: Array<'retry' | 'reveal-source' | 'choose-variant' | 'reimport'>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Convert loader-specific failures into actionable, stable editor diagnostics. */
|
|
18
|
+
export function classifyAssetEditorError(error: unknown, stage: AssetLoadStage): AssetEditorIssue {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
const lower = message.toLowerCase();
|
|
21
|
+
if (lower.includes('404') || lower.includes('failed to fetch')) {
|
|
22
|
+
return {
|
|
23
|
+
code: 'asset.fetch.failed',
|
|
24
|
+
stage: 'fetching',
|
|
25
|
+
message,
|
|
26
|
+
recovery: ['retry', 'reveal-source'],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (lower.includes('dependency') || lower.includes('buffer') || lower.includes('texture')) {
|
|
30
|
+
return {
|
|
31
|
+
code: 'asset.dependency.missing',
|
|
32
|
+
stage: 'dependency-resolution',
|
|
33
|
+
message,
|
|
34
|
+
recovery: ['reveal-source', 'reimport'],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (lower.includes('bounds') || lower.includes('geometry')) {
|
|
38
|
+
return {
|
|
39
|
+
code: 'asset.render.no-geometry',
|
|
40
|
+
stage: 'rendering',
|
|
41
|
+
message: `${message} The source may be animation-only or contain hidden/zero-sized geometry.`,
|
|
42
|
+
recovery: ['choose-variant', 'reimport'],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { code: `asset.${stage}.failed`, stage, message, recovery: ['retry', 'reimport'] };
|
|
46
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { HistoryService } from '../history/history-service';
|
|
2
|
+
import { getProjectFileHistory } from '../history/project-file-history';
|
|
3
|
+
import { sha256Hex } from '../history/snapshot-store';
|
|
4
|
+
import type { StorageBackend } from '../storage/types';
|
|
5
|
+
import { recordAssetWorkflowMetric } from './asset-workflow-quality';
|
|
6
|
+
import {
|
|
7
|
+
type AssetImportPlan,
|
|
8
|
+
type AssetImportProgress,
|
|
9
|
+
type AssetImportResult,
|
|
10
|
+
assetImportPlanSchema,
|
|
11
|
+
importRecipePath,
|
|
12
|
+
} from './import-contract';
|
|
13
|
+
|
|
14
|
+
export interface StagedImportFile {
|
|
15
|
+
path: string;
|
|
16
|
+
bytes: Uint8Array;
|
|
17
|
+
}
|
|
18
|
+
export interface AssetImportPipelineDependencies {
|
|
19
|
+
fetch(
|
|
20
|
+
file: AssetImportPlan['sourceFiles'][number],
|
|
21
|
+
signal: AbortSignal,
|
|
22
|
+
progress: (loaded: number, total: number) => void,
|
|
23
|
+
): Promise<Uint8Array>;
|
|
24
|
+
convert(
|
|
25
|
+
plan: AssetImportPlan,
|
|
26
|
+
sources: readonly StagedImportFile[],
|
|
27
|
+
signal: AbortSignal,
|
|
28
|
+
): Promise<readonly StagedImportFile[]>;
|
|
29
|
+
validate(
|
|
30
|
+
plan: AssetImportPlan,
|
|
31
|
+
files: readonly StagedImportFile[],
|
|
32
|
+
signal: AbortSignal,
|
|
33
|
+
): Promise<void>;
|
|
34
|
+
commit(plan: AssetImportPlan, files: readonly StagedImportFile[]): Promise<boolean>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function abortIfNeeded(signal: AbortSignal): void {
|
|
38
|
+
if (signal.aborted) throw signal.reason ?? new DOMException('Import cancelled', 'AbortError');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Four-stage import: no project mutation occurs until all staged bytes validate. */
|
|
42
|
+
export async function runAssetImportPipeline(
|
|
43
|
+
rawPlan: AssetImportPlan,
|
|
44
|
+
dependencies: AssetImportPipelineDependencies,
|
|
45
|
+
options: { signal: AbortSignal; onProgress?: (progress: AssetImportProgress) => void },
|
|
46
|
+
): Promise<AssetImportResult> {
|
|
47
|
+
const plan = assetImportPlanSchema.parse(rawPlan);
|
|
48
|
+
const stagedSources: StagedImportFile[] = [];
|
|
49
|
+
let stageStarted = performance.now();
|
|
50
|
+
const metric = (
|
|
51
|
+
stage: 'delivery' | 'conversion' | 'validation' | 'commit',
|
|
52
|
+
outcome: 'ok' | 'failed' | 'cancelled' = 'ok',
|
|
53
|
+
) => {
|
|
54
|
+
recordAssetWorkflowMetric({ stage, durationMs: performance.now() - stageStarted, outcome });
|
|
55
|
+
stageStarted = performance.now();
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
for (const file of plan.sourceFiles) {
|
|
59
|
+
abortIfNeeded(options.signal);
|
|
60
|
+
const bytes = await dependencies.fetch(file, options.signal, (loaded, total) =>
|
|
61
|
+
options.onProgress?.({ stage: 'fetch', item: file.path, loaded, total }),
|
|
62
|
+
);
|
|
63
|
+
stagedSources.push({ path: file.path, bytes });
|
|
64
|
+
}
|
|
65
|
+
metric('delivery');
|
|
66
|
+
abortIfNeeded(options.signal);
|
|
67
|
+
options.onProgress?.({ stage: 'convert', item: plan.destination, loaded: 0, total: 1 });
|
|
68
|
+
const outputs = await dependencies.convert(plan, stagedSources, options.signal);
|
|
69
|
+
metric('conversion');
|
|
70
|
+
abortIfNeeded(options.signal);
|
|
71
|
+
options.onProgress?.({
|
|
72
|
+
stage: 'validate',
|
|
73
|
+
item: plan.destination,
|
|
74
|
+
loaded: 0,
|
|
75
|
+
total: outputs.length,
|
|
76
|
+
});
|
|
77
|
+
await dependencies.validate(plan, outputs, options.signal);
|
|
78
|
+
metric('validation');
|
|
79
|
+
abortIfNeeded(options.signal);
|
|
80
|
+
options.onProgress?.({
|
|
81
|
+
stage: 'commit',
|
|
82
|
+
item: plan.destination,
|
|
83
|
+
loaded: 0,
|
|
84
|
+
total: outputs.length,
|
|
85
|
+
});
|
|
86
|
+
if (!(await dependencies.commit(plan, outputs)))
|
|
87
|
+
throw new Error('Atomic project commit was rejected.');
|
|
88
|
+
metric('commit');
|
|
89
|
+
return {
|
|
90
|
+
version: 1,
|
|
91
|
+
planId: plan.id,
|
|
92
|
+
status: 'imported',
|
|
93
|
+
writtenFiles: await Promise.all(
|
|
94
|
+
outputs.map(async (file) => ({
|
|
95
|
+
path: file.path,
|
|
96
|
+
hash: await sha256Hex(file.bytes),
|
|
97
|
+
size: file.bytes.byteLength,
|
|
98
|
+
})),
|
|
99
|
+
),
|
|
100
|
+
skippedFiles: [],
|
|
101
|
+
issues: [],
|
|
102
|
+
recipePath: importRecipePath(plan.destination),
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const cancelled =
|
|
106
|
+
options.signal.aborted || (error instanceof DOMException && error.name === 'AbortError');
|
|
107
|
+
metric('commit', cancelled ? 'cancelled' : 'failed');
|
|
108
|
+
return {
|
|
109
|
+
version: 1,
|
|
110
|
+
planId: plan.id,
|
|
111
|
+
status: cancelled ? 'cancelled' : 'failed',
|
|
112
|
+
writtenFiles: [],
|
|
113
|
+
skippedFiles: [],
|
|
114
|
+
issues: [
|
|
115
|
+
{
|
|
116
|
+
code: cancelled ? 'import.cancelled' : 'import.failed',
|
|
117
|
+
stage: 'staging',
|
|
118
|
+
message: error instanceof Error ? error.message : String(error),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function commitAssetImport(
|
|
126
|
+
plan: AssetImportPlan,
|
|
127
|
+
outputs: readonly StagedImportFile[],
|
|
128
|
+
dependencies: { backend: StorageBackend; history: HistoryService },
|
|
129
|
+
): Promise<boolean> {
|
|
130
|
+
const recipe = JSON.stringify(plan, null, 2);
|
|
131
|
+
const changes = [
|
|
132
|
+
...outputs.map((file) => ({ path: file.path, data: file.bytes })),
|
|
133
|
+
{ path: importRecipePath(plan.destination), data: recipe, contentType: 'application/json' },
|
|
134
|
+
];
|
|
135
|
+
return getProjectFileHistory(dependencies.history, dependencies.backend).mutate(changes, {
|
|
136
|
+
label: `Import ${plan.identity.familyId}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function fetchImportFile(
|
|
141
|
+
file: AssetImportPlan['sourceFiles'][number],
|
|
142
|
+
signal: AbortSignal,
|
|
143
|
+
progress: (loaded: number, total: number) => void,
|
|
144
|
+
): Promise<Uint8Array> {
|
|
145
|
+
const response = await fetch(file.url, { signal, cache: 'no-store' });
|
|
146
|
+
if (!response.ok) throw new Error(`Fetch failed with HTTP ${response.status}: ${file.url}`);
|
|
147
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
148
|
+
progress(bytes.byteLength, Number(response.headers.get('content-length')) || bytes.byteLength);
|
|
149
|
+
if ((await sha256Hex(bytes)) !== file.hash)
|
|
150
|
+
throw new Error(`Source hash mismatch for ${file.path}.`);
|
|
151
|
+
return bytes;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface AttributionEntry {
|
|
155
|
+
familyId: string;
|
|
156
|
+
name: string;
|
|
157
|
+
author: string;
|
|
158
|
+
license: string;
|
|
159
|
+
sourceUrl: string;
|
|
160
|
+
text: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function generateAttributionReports(entries: readonly AttributionEntry[]): {
|
|
164
|
+
json: string;
|
|
165
|
+
markdown: string;
|
|
166
|
+
} {
|
|
167
|
+
const unique = [
|
|
168
|
+
...new Map(
|
|
169
|
+
entries.map((entry) => [`${entry.familyId}:${entry.license}:${entry.sourceUrl}`, entry]),
|
|
170
|
+
).values(),
|
|
171
|
+
].sort((a, b) => a.name.localeCompare(b.name) || a.familyId.localeCompare(b.familyId));
|
|
172
|
+
return {
|
|
173
|
+
json: `${JSON.stringify({ version: 1, assets: unique }, null, 2)}\n`,
|
|
174
|
+
markdown: `# Asset attribution\n\n${unique.map((entry) => `## ${entry.name}\n\n${entry.text}\n\nAuthor: ${entry.author} \nLicense: ${entry.license} \nSource: ${entry.sourceUrl}`).join('\n\n')}\n`,
|
|
175
|
+
};
|
|
176
|
+
}
|