@vgai/editor 0.2.0 → 0.4.0-canary.20260715.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/assets/axe-C7YBtOuM.js +30 -0
- package/dist/assets/browserAll-DDtGGE48.js +1 -0
- package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
- package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
- package/dist/assets/game-BdTHR2_X.js +1 -0
- package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
- package/dist/assets/game-CcHfYxjP.js +1 -0
- package/dist/assets/game-Cqzj_lpD.js +1 -0
- package/dist/assets/game-DSZdeCHu.js +1 -0
- package/dist/assets/game-Dch9sAPc.js +1 -0
- package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
- package/dist/assets/game-loop-DMIgSMV-.js +1 -0
- package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
- package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
- package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
- package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
- package/dist/assets/index-CQEfi217.js +15412 -0
- package/dist/assets/index-Dvi-V0NE.css +1 -0
- package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
- package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
- package/dist/assets/react-18-C9eHn8x1.js +1 -0
- package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
- package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
- package/dist/index.html +15 -13
- package/dist/ingest/racing-game/vgai.game.json +5 -4
- package/dist/ingest/simcity/vgai.game.json +5 -4
- package/dist/ingest/tanks/vgai.game.json +5 -4
- package/dist/project-scripts.bundle.js +3917 -0
- package/dist/scenes/untitled.vscn.json +2 -0
- package/dist-server/packaged.mjs +7697 -3435
- package/package.json +31 -4
- package/scripts/probe-reporter.ts +349 -0
- package/scripts/run-probe.ts +2260 -0
- package/server/asset-catalog-audit.ts +268 -0
- package/server/asset-catalog-v2.ts +146 -0
- package/server/asset-library-routes.ts +276 -58
- package/server/canonical-path.ts +1 -1
- package/server/dev.ts +148 -44
- package/server/editor-server.ts +888 -61
- package/server/editor-sse.ts +115 -4
- package/server/harness-chat-service.ts +796 -0
- package/server/local-asset-catalog.ts +157 -22
- package/server/packaged.ts +51 -21
- package/server/process-shutdown.ts +136 -0
- package/server/prod.ts +9 -6
- package/server/project-hmr-files.ts +41 -0
- package/server/project-operations.ts +343 -0
- package/server/project-optimize-deps-entries.ts +4 -4
- package/server/project-output-writer.ts +156 -0
- package/server/project-validation.ts +140 -0
- package/server/project-view.ts +8 -50
- package/server/server-utils.ts +75 -10
- package/server/session-registry.ts +12 -0
- package/src/EditorContext.tsx +107 -3
- package/src/action-registry.ts +115 -17
- package/src/adapter-resolver.ts +213 -478
- package/src/asset-events.ts +64 -1
- package/src/asset-preview.ts +906 -0
- package/src/asset-selection.ts +121 -0
- package/src/asset-workflow/asset-capabilities.ts +259 -0
- package/src/asset-workflow/asset-editor-errors.ts +46 -0
- package/src/asset-workflow/asset-import-pipeline.ts +176 -0
- package/src/asset-workflow/asset-library-query.ts +107 -0
- package/src/asset-workflow/asset-types.ts +192 -0
- package/src/asset-workflow/asset-workflow-quality.ts +65 -0
- package/src/asset-workflow/import-contract.ts +113 -0
- package/src/asset-workflow/model-conversion.ts +126 -0
- package/src/asset-workflow/model-inspection.ts +185 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +153 -0
- package/src/asset-workflow/project-asset-operations.ts +119 -0
- package/src/asset-workflow/thumbnail-system.ts +172 -0
- package/src/authoring/active-systems.ts +15 -0
- package/src/authoring/boundary-authoring-adapter.ts +16 -5
- package/src/authoring/composite-authoring-adapter.ts +305 -124
- package/src/authoring/design-time-layers.ts +300 -117
- package/src/authoring/edit-mode-authoring.ts +278 -293
- package/src/authoring/embed-only-adapter.ts +4 -2
- package/src/authoring/first-party-hierarchy-menu.ts +7 -2
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
- package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
- package/src/authoring/first-party-inspector/register.tsx +13 -2
- package/src/authoring/ingest-authoring-adapter.ts +142 -45
- package/src/authoring/mount-failure-report.ts +5 -5
- package/src/authoring/provenance.ts +41 -0
- package/src/authoring/react-dom-authoring-adapter.ts +92 -92
- package/src/authoring/react-inspector-section.tsx +489 -51
- package/src/authoring/react-world-authoring-adapter.ts +541 -306
- package/src/authoring/story-overlay.ts +1 -1
- package/src/authoring/story-types.ts +11 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
- package/src/authoring/viewport-tool-context.ts +78 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +186 -0
- package/src/authoring/world-pan-state.ts +33 -17
- package/src/authoring/world-session-state.ts +1 -1
- package/src/authoring/world2d-authoring-adapter.ts +109 -52
- package/src/availability-tick.ts +56 -0
- package/src/browser-play-entry.ts +5 -5
- package/src/browser-project-scripts.ts +1 -0
- package/src/build-identity.ts +15 -0
- package/src/command-listener.ts +893 -19
- package/src/components/AlignToolbar.tsx +74 -63
- package/src/components/AppRoot.tsx +193 -26
- package/src/components/ApplicationMenus.tsx +272 -0
- package/src/components/AssetBrowser.tsx +1450 -293
- package/src/components/AssetEditorShell.tsx +141 -0
- package/src/components/AssetImportDetails.tsx +218 -0
- package/src/components/AssetLibraryPanel.tsx +14 -0
- package/src/components/CameraInfo.tsx +11 -20
- package/src/components/CenterDocuments.tsx +585 -0
- package/src/components/CommandPalette.tsx +75 -30
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +57 -112
- package/src/components/DebugConsolePanel.tsx +461 -0
- package/src/components/DebuggerPanel.tsx +165 -0
- package/src/components/DefaultEditorLayout.tsx +167 -231
- package/src/components/GameHierarchy.tsx +873 -290
- package/src/components/GamePerformanceOverlay.tsx +46 -0
- package/src/components/HarnessChatPanel.tsx +2352 -0
- package/src/components/HistoryPanel.tsx +226 -0
- package/src/components/Inspector.tsx +153 -99
- package/src/components/InspectorToolSection.tsx +6 -4
- package/src/components/MountFailureBanner.tsx +31 -48
- package/src/components/NetworkSection.tsx +25 -14
- package/src/components/OnlineAssetBrowser.tsx +794 -343
- package/src/components/OverlayReportBanner.tsx +38 -64
- package/src/components/PerformancePanel.tsx +206 -0
- package/src/components/PlayBar.tsx +93 -263
- package/src/components/PrefabSaveDialog.tsx +55 -127
- package/src/components/ProjectHeader.css +60 -0
- package/src/components/ProjectHeader.tsx +170 -105
- package/src/components/ProjectScreen.tsx +246 -184
- package/src/components/ReactCanvasControls.tsx +185 -0
- package/src/components/ResolutionPicker.tsx +85 -0
- package/src/components/RootActivateDialog.tsx +39 -68
- package/src/components/SaveDialog.tsx +55 -127
- package/src/components/SaveStatus.tsx +11 -29
- package/src/components/StartupLoadingScreen.tsx +96 -0
- package/src/components/StateWatchPanel.tsx +302 -0
- package/src/components/StatsOverlay.tsx +12 -5
- package/src/components/StatusBar.tsx +64 -0
- package/src/components/ThemeManagerDialog.css +89 -0
- package/src/components/ThemeManagerDialog.tsx +371 -0
- package/src/components/ToolHost.tsx +26 -15
- package/src/components/Toolbar.tsx +117 -214
- package/src/components/VgaiVoxelLogo.tsx +89 -0
- package/src/components/ViewportOverlay.tsx +97 -172
- package/src/components/ViewportPanel.tsx +130 -31
- package/src/components/WorkspaceDock.tsx +891 -0
- package/src/components/WorldSelectionOverlay.tsx +51 -63
- package/src/components/WorldTextEditor.tsx +9 -5
- package/src/components/asset-documents.tsx +390 -0
- package/src/components/asset-editor-persistence.ts +44 -0
- package/src/components/asset-selection-section.tsx +338 -0
- package/src/components/asset-viewers/AudioViewer.tsx +73 -21
- package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
- package/src/components/asset-viewers/ImageViewer.tsx +43 -11
- package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
- package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
- package/src/components/asset-viewers/ModelViewer.tsx +805 -121
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
- package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
- package/src/components/asset-workflow.css +506 -0
- package/src/components/build-documents.tsx +696 -0
- package/src/components/core-utilities.tsx +75 -0
- package/src/components/data-documents.tsx +364 -0
- package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
- package/src/components/debugger-panel-state.ts +57 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
- package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
- package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
- package/src/components/inspector-widgets/FontPicker.tsx +13 -21
- package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
- package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
- package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
- package/src/components/inspector-widgets/shared.tsx +90 -49
- package/src/components/inspectors/AnimationSection.tsx +39 -40
- package/src/components/inspectors/AudioSection.tsx +28 -39
- package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
- package/src/components/inspectors/CameraSection.tsx +14 -23
- package/src/components/inspectors/ComponentsSection.tsx +39 -43
- package/src/components/inspectors/JointsSection.tsx +34 -43
- package/src/components/inspectors/LightSection.tsx +90 -35
- package/src/components/inspectors/MaterialSection.tsx +137 -174
- package/src/components/inspectors/MeshSection.tsx +9 -17
- package/src/components/inspectors/NavigationSection.tsx +8 -15
- package/src/components/inspectors/ParticleSection.tsx +91 -76
- package/src/components/inspectors/PhysicsSection.tsx +42 -46
- package/src/components/inspectors/PostProcessingSection.tsx +28 -36
- package/src/components/inspectors/RenderingSection.tsx +12 -20
- package/src/components/inspectors/ShadowSection.tsx +7 -8
- package/src/components/inspectors/SplineSection.tsx +31 -42
- package/src/components/inspectors/TransformSection.tsx +14 -22
- package/src/components/patterns/Dialog.tsx +95 -0
- package/src/components/patterns/Fields.tsx +44 -0
- package/src/components/patterns/List.tsx +25 -0
- package/src/components/patterns/StateSurface.tsx +40 -0
- package/src/components/patterns/Surfaces.tsx +115 -0
- package/src/components/patterns/Tabs.tsx +80 -0
- package/src/components/patterns/Toolbar.tsx +51 -0
- package/src/components/patterns/Tree.tsx +72 -0
- package/src/components/primitives/Button.tsx +46 -0
- package/src/components/primitives/ColorInput.tsx +11 -47
- package/src/components/primitives/EditorIcon.tsx +99 -0
- package/src/components/primitives/FormControls.tsx +71 -0
- package/src/components/primitives/JsonInput.tsx +15 -2
- package/src/components/primitives/Layout.tsx +100 -0
- package/src/components/primitives/Menu.tsx +105 -0
- package/src/components/primitives/NumberInput.tsx +8 -35
- package/src/components/primitives/Panel.tsx +49 -44
- package/src/components/primitives/SectionHeader.tsx +10 -28
- package/src/components/primitives/Text.tsx +46 -0
- package/src/components/primitives/Tooltip.tsx +20 -12
- package/src/components/primitives/Vec3Input.tsx +3 -0
- package/src/components/primitives/banner-tones.ts +33 -0
- package/src/components/primitives/editor-icons.ts +102 -0
- package/src/components/primitives/panel-header-styles.ts +30 -0
- package/src/components/project-operation-documents.tsx +226 -0
- package/src/components/status-contributions.tsx +175 -0
- package/src/components/story-documents-register.ts +15 -0
- package/src/components/story-documents.tsx +1185 -0
- package/src/components/tool-documents.tsx +110 -0
- package/src/components/workspace-dock.css +293 -0
- package/src/components/workspace-history.ts +33 -0
- package/src/components/world-documents.tsx +145 -0
- package/src/components/xstate-documents.tsx +209 -0
- package/src/composite-screenshot.ts +248 -0
- package/src/design-system/index.ts +70 -0
- package/src/design-system-stories/Foundation.stories.tsx +183 -0
- package/src/design-system-stories/Patterns.stories.tsx +251 -0
- package/src/design-system-stories/Workspace.stories.tsx +538 -0
- package/src/doctor/report.ts +57 -13
- package/src/editor-api.ts +295 -31
- package/src/editor-compatibility.ts +200 -0
- package/src/editor-console.ts +35 -17
- package/src/editor-hotkeys.ts +85 -33
- package/src/editor-store.ts +538 -233
- package/src/editor-viewport.ts +160 -34
- package/src/entity-factory.ts +14 -3
- package/src/gizmo-registry.ts +21 -5
- package/src/harness-chat-editor-context.ts +37 -0
- package/src/harness-chat-indicator.ts +68 -0
- package/src/harness-chat-layout.ts +53 -0
- package/src/harness-chat-lifecycle.ts +170 -0
- package/src/harness-chat-reconnect.ts +85 -0
- package/src/harness-chat-types.ts +409 -0
- package/src/hierarchy-drop.ts +50 -0
- package/src/hierarchy-expansion-state.ts +100 -0
- package/src/hierarchy-menu-registry.ts +1 -1
- package/src/hierarchy-projection.ts +76 -0
- package/src/history/editor-session.ts +21 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-service.ts +904 -0
- package/src/history/json-history-resource.ts +165 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +266 -0
- package/src/history/project-root-history-backends.ts +53 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +108 -0
- package/src/history/source-history-backend.ts +374 -0
- package/src/history/types.ts +100 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hosted-example.ts +12 -2
- package/src/hotkeys.ts +89 -18
- package/src/ingest/discovery-react.ts +9 -10
- package/src/ingest/discovery.ts +4 -4
- package/src/ingest/discovery2d.ts +5 -5
- package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
- package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
- package/src/ingest/games/games-fps/vgai.game.json +3 -3
- package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
- package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
- package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
- package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
- package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
- package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
- package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
- package/src/ingest/types.ts +1 -1
- package/src/ingest-mode.ts +47 -52
- package/src/ingest-siblings.ts +23 -33
- package/src/initial-scene.ts +1 -2
- package/src/main.tsx +16 -8
- package/src/manifest-project.ts +10 -12
- package/src/model-thumbnail.ts +261 -26
- package/src/module-mode.ts +44 -50
- package/src/play-log-events.ts +26 -0
- package/src/play-mode.ts +495 -401
- package/src/play-scene-adoption.ts +23 -0
- package/src/playground/main.ts +68 -24
- package/src/playwright-shim.ts +473 -0
- package/src/project-manager.ts +90 -10
- package/src/project-operations.ts +112 -0
- package/src/project-story-discovery.ts +25 -0
- package/src/project-tool-discovery.ts +43 -0
- package/src/scene-serializer.ts +0 -6
- package/src/scene-sync.ts +7 -2
- package/src/script-loader.ts +3 -3
- package/src/skeleton-helper.ts +57 -0
- package/src/storage/seed.ts +68 -8
- package/src/stories/StoryPreviewMount.tsx +265 -0
- package/src/stories/action-stories.fixture.tsx +44 -0
- package/src/stories/button-stories.fixture.tsx +50 -0
- package/src/stories/compose-project-stories.ts +182 -0
- package/src/stories/loader-stories.fixture.tsx +53 -0
- package/src/stories/story-discovery.ts +130 -0
- package/src/stories/story-registry.ts +146 -0
- package/src/theme-css.ts +1049 -0
- package/src/theme-library.ts +238 -0
- package/src/theme-preference.ts +130 -0
- package/src/theme.ts +527 -0
- package/src/tool-loader.ts +80 -24
- package/src/ui-source/inspect.ts +27 -4
- package/src/ui-source/source-write-backend.ts +83 -22
- package/src/widgets/index.ts +9 -18
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-dock-controller.ts +64 -0
- package/src/workspace-document-ids.ts +5 -0
- package/src/workspace-document-registry.ts +367 -0
- package/src/workspace-state-persistence.ts +455 -0
- package/src/workspace-status-registry.ts +110 -0
- package/src/workspace-utility-commands.ts +49 -0
- package/src/workspace-utility-registry.ts +161 -0
- package/src/world-document-routing.ts +16 -0
- package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
- package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
- package/src/xstate-inspect/use-live-actor-state.ts +39 -0
- package/src/xstate-inspect/xstate-graph.ts +265 -0
- package/src/xstate-inspect/xstate-layout.ts +76 -0
- package/src/z-index.ts +26 -0
- package/template/.claude/skills/editor/SKILL.md +129 -90
- package/template/AGENTS.md +1110 -0
- package/template/CLAUDE.md +1 -512
- package/template/IDIOMS.md +50 -0
- package/template/check-idioms.ts +706 -0
- package/template/manifest-entry-modules-plugin.ts +53 -0
- package/template/package.json +13 -4
- package/template/public/scenes/default.scn2d.json +6 -0
- package/template/public/scenes/default.vscn.json +7 -1
- package/template/public/scenes/untitled.vscn.json +10 -0
- package/template/scripts/playtest.ts +40 -0
- package/template/server/loopback.ts +75 -0
- package/template/server/rooms/arena-room.ts +69 -1
- package/template/server/rooms/game-room.ts +68 -1
- package/template/src/data/README.md +9 -4
- package/template/src/data/data-ref.ts +35 -0
- package/template/src/data/tuning.schema.ts +1 -1
- package/template/src/main.ts +41 -62
- package/template/src/manifest-entry-modules.d.ts +3 -0
- package/template/src/operations/README.md +88 -0
- package/template/src/runtime/pixi-adapter.ts +24 -0
- package/template/src/scripts/components/data-spinner.ts +43 -2
- package/template/src/scripts/components/spin-lap-math.ts +27 -0
- package/template/src/tools/README.md +5 -3
- package/template/src/tools/example.tool.tsx +13 -7
- package/template/src/ui/Button.stories.tsx +66 -0
- package/template/src/ui/RebindPanel.tsx +2 -6
- package/template/src/ui/game.tsx +17 -0
- package/template/tests/acceptance/example.spec.ts +64 -0
- package/template/tests/logic/example.test.ts +31 -0
- package/template/tests/logic/room.test.ts +100 -0
- package/template/tests/tsconfig.json +30 -0
- package/template/validate-manifest.ts +5 -5
- package/template/validate-scenes.ts +50 -18
- package/template/vgai.game.json +20 -13
- package/template/vite.config.ts +77 -16
- package/vite-plugin-ui-oid.ts +261 -17
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
- package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
- package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
- package/dist/assets/browserAll-CEkWYlFk.js +0 -1
- package/dist/assets/create-runtime-DYIWr3va.js +0 -1
- package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
- package/dist/assets/game-8SDZPPgH.js +0 -1
- package/dist/assets/game-CevorxyV.js +0 -1
- package/dist/assets/game-Di7XlywD.js +0 -1
- package/dist/assets/game-DjnCaw8R.js +0 -1
- package/dist/assets/game-Q9A0aXSV.js +0 -1
- package/dist/assets/index-CPVV6pjw.js +0 -1
- package/dist/assets/index-CpOnYyeD.js +0 -213
- package/dist/assets/index-Dr8H_6Rl.js +0 -1
- package/dist/assets/index-_bCaipUk.js +0 -13650
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
- package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
- package/dist/assets/scene-query-BivAHhJu.js +0 -1
- package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
- package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
- package/src/authoring/scene-ui-layer.ts +0 -485
- package/src/authoring/ui-authoring-adapter.ts +0 -325
- package/src/authoring/ui-hierarchy-menu.ts +0 -40
- package/src/authoring/ui-inspector-sections.tsx +0 -156
- package/src/authoring/undo-timeline.ts +0 -145
- package/src/components/BottomPanel.tsx +0 -326
- package/src/components/BuildPanel.tsx +0 -455
- package/src/components/FolderTree.tsx +0 -179
- package/src/components/RightPanel.tsx +0 -166
- package/src/components/WorldContextMenu.tsx +0 -257
- package/src/components/asset-viewers/AssetViewer.tsx +0 -38
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
- package/src/scene-ui-mode/active-scene-ui.ts +0 -58
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
- package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
- package/src/ui-editor/inspector.tsx +0 -563
- package/src/ui-editor/node-ops.ts +0 -266
- package/src/ui-editor/overlay.tsx +0 -380
- package/src/ui-editor/palette.tsx +0 -114
- package/src/ui-editor/react-store.ts +0 -78
- package/src/ui-editor/timeline-panel.tsx +0 -443
- package/src/ui-editor/ui-edit-store.ts +0 -552
- package/src/ui-editor/ui-prefab.ts +0 -95
- package/template/project.json +0 -9
- package/template/src/scripts/main.ts +0 -94
- package/template/src/ui/DefaultHud.tsx +0 -40
- package/template/src/ui/game-state.tsx +0 -37
- package/template/src/ui/mount-default-hud.tsx +0 -19
- package/template/src/ui/mount-ui.ts +0 -15
- package/vite-plugin-react-world-provider.ts +0 -81
package/src/editor-store.ts
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
* - Undo / redo
|
|
12
12
|
* - Prefab resolution
|
|
13
13
|
* - Asset path rename
|
|
14
|
+
* - Referenced material asset changes
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import type { PersistenceProvider } from '@engine/adapter';
|
|
17
18
|
import { applyAssetRename } from '@engine/scene/asset-paths';
|
|
19
|
+
import { parseSceneFile } from '@engine/scene/parse';
|
|
18
20
|
import {
|
|
19
21
|
mergePrefabInstance,
|
|
20
22
|
type PrefabFile,
|
|
@@ -30,9 +32,19 @@ import {
|
|
|
30
32
|
getActiveAuthoring,
|
|
31
33
|
hasAuthoringOverride,
|
|
32
34
|
} from './authoring/active-adapter';
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
+
import {
|
|
36
|
+
loadEditorState,
|
|
37
|
+
readProjectTextFile,
|
|
38
|
+
saveEditorState,
|
|
39
|
+
saveFile,
|
|
40
|
+
saveThumbnail,
|
|
41
|
+
} from './editor-api';
|
|
42
|
+
import { editorConsole } from './editor-console';
|
|
35
43
|
import { createEntityObject } from './entity-factory';
|
|
44
|
+
import type { HistoryService } from './history/history-service';
|
|
45
|
+
import { getProjectFileHistory } from './history/project-file-history';
|
|
46
|
+
import { sha256Hex } from './history/snapshot-store';
|
|
47
|
+
import type { ResourceDescriptor, ResourceDriver } from './history/types';
|
|
36
48
|
import { diffScene } from './scene-diff';
|
|
37
49
|
import type { SceneMetadata } from './scene-serializer';
|
|
38
50
|
import { cleanDocument, serializeScene } from './scene-serializer';
|
|
@@ -40,6 +52,7 @@ import {
|
|
|
40
52
|
applyEnvironment,
|
|
41
53
|
applyShadingOverride,
|
|
42
54
|
buildPreview,
|
|
55
|
+
clearMaterialRefCache,
|
|
43
56
|
registerParticles,
|
|
44
57
|
unregisterParticles,
|
|
45
58
|
updateEntityPreview,
|
|
@@ -55,16 +68,10 @@ export type PivotMode = 'active-element' | 'median-point' | 'individual-origins'
|
|
|
55
68
|
export type ShadingMode = 'solid' | 'wireframe' | 'unlit';
|
|
56
69
|
|
|
57
70
|
/**
|
|
58
|
-
* Which persistence regime the
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* `createEphemeralPersistence`), `'overlay'` for a multi-world (≥2) session
|
|
63
|
-
* (composite of per-world overlay-persisted adapters, edits survive the
|
|
64
|
-
* session). `null` while not playing. Never derive this independently of the
|
|
65
|
-
* real provider decision — see `play-mode.ts`'s `enterPlayMode`, which sets it
|
|
66
|
-
* from the SAME `_isMultiWorldSession` boolean that selects
|
|
67
|
-
* `installMultiWorldAuthoring` vs `installPlayAuthoring`.
|
|
71
|
+
* Which persistence regime the current play session exposes. D19 makes play
|
|
72
|
+
* edits ephemeral for every world count; `overlay` remains in the type while
|
|
73
|
+
* older/external session surfaces migrate, but native play no longer selects
|
|
74
|
+
* it by cardinality. `null` while not playing.
|
|
68
75
|
*/
|
|
69
76
|
export type PlayEditRegime = 'ephemeral' | 'overlay' | null;
|
|
70
77
|
|
|
@@ -90,15 +97,6 @@ export interface OnlineAssetInfo {
|
|
|
90
97
|
sourceUrl?: string;
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
export interface AssetTab {
|
|
94
|
-
key: string;
|
|
95
|
-
label: string;
|
|
96
|
-
assetPath: string;
|
|
97
|
-
kind: AssetKind;
|
|
98
|
-
/** Present when this tab represents an online (not-yet-downloaded) asset. */
|
|
99
|
-
online?: OnlineAssetInfo;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
100
|
export interface HelperVisibility {
|
|
103
101
|
lights: boolean;
|
|
104
102
|
cameras: boolean;
|
|
@@ -106,6 +104,7 @@ export interface HelperVisibility {
|
|
|
106
104
|
audio: boolean;
|
|
107
105
|
splines: boolean;
|
|
108
106
|
navmesh: boolean;
|
|
107
|
+
skeletons: boolean;
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
export type ViewportAction =
|
|
@@ -119,24 +118,38 @@ export type ViewportAction =
|
|
|
119
118
|
fov?: number;
|
|
120
119
|
};
|
|
121
120
|
|
|
122
|
-
const MAX_UNDO = 50;
|
|
123
121
|
/** Cooldown (ms) to prevent tight feedback loops between auto-save and external updates. */
|
|
124
122
|
const SYNC_COOLDOWN_MS = 500;
|
|
123
|
+
const sceneEncoder = new TextEncoder();
|
|
124
|
+
const sceneDecoder = new TextDecoder();
|
|
125
125
|
|
|
126
126
|
function deepClone<T>(obj: T): T {
|
|
127
127
|
return JSON.parse(JSON.stringify(obj)) as T;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Ensure every entity has a UUID and a children array. Mutates in place.
|
|
132
|
+
* Returns `true` iff at least one entity was missing an `id` and got one
|
|
133
|
+
* assigned here — callers that persist to disk (`loadDocument`) use this to
|
|
134
|
+
* know the freshly-assigned ids need writing back, not just holding in
|
|
135
|
+
* memory (see that call site's comment for why: an id assigned here but
|
|
136
|
+
* never persisted is exactly the `apply-diff`/`vgai status` id-mismatch
|
|
137
|
+
* trap).
|
|
138
|
+
*/
|
|
139
|
+
export function assignMissingIds(entities: SceneEntity[]): boolean {
|
|
140
|
+
let assignedAny = false;
|
|
132
141
|
const walk = (list: SceneEntity[]) => {
|
|
133
142
|
for (const e of list) {
|
|
134
|
-
if (!e.id)
|
|
143
|
+
if (!e.id) {
|
|
144
|
+
e.id = crypto.randomUUID();
|
|
145
|
+
assignedAny = true;
|
|
146
|
+
}
|
|
135
147
|
if (!e.children) e.children = [];
|
|
136
148
|
walk(e.children);
|
|
137
149
|
}
|
|
138
150
|
};
|
|
139
151
|
walk(entities);
|
|
152
|
+
return assignedAny;
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
/** Walk entity Object3Ds in the scene graph and call fn on each. */
|
|
@@ -163,17 +176,6 @@ function walkEntities(entities: SceneEntity[], fn: (e: SceneEntity) => void): vo
|
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
export class EditorStore {
|
|
166
|
-
/**
|
|
167
|
-
* Register with the editor-global undo TIMELINE (T3.2 slice 2) once, for the
|
|
168
|
-
* lifetime of this store — first-party never "disposes" mid-session, so unlike
|
|
169
|
-
* ingest/UI sessions there is no matching `unregisterUndoAdapter` call. Safe for
|
|
170
|
-
* headless test instances too: the timeline only ever calls INTO
|
|
171
|
-
* `this.undo()`/`this.redo()` when notified, never the reverse.
|
|
172
|
-
*/
|
|
173
|
-
constructor() {
|
|
174
|
-
registerUndoAdapter(this);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
179
|
// --- Scene graph references (set via bindScene) ---
|
|
178
180
|
private _scene: THREE.Scene | null = null;
|
|
179
181
|
private _editorScene: THREE.Scene | null = null; // stored while game scene is active
|
|
@@ -209,6 +211,7 @@ export class EditorStore {
|
|
|
209
211
|
audio: true,
|
|
210
212
|
splines: true,
|
|
211
213
|
navmesh: true,
|
|
214
|
+
skeletons: false,
|
|
212
215
|
};
|
|
213
216
|
private _showStats = false;
|
|
214
217
|
private _shadingMode: ShadingMode = 'solid';
|
|
@@ -229,16 +232,21 @@ export class EditorStore {
|
|
|
229
232
|
* (undo/redo), and applying the echo would clobber the newer state.
|
|
230
233
|
*/
|
|
231
234
|
private _lastSavedContent: string | null = null;
|
|
235
|
+
/** #131 — consecutive saves skipped because the on-disk file changed under
|
|
236
|
+
* the editor and could not be adopted (see `_resolveSaveConflict`). */
|
|
237
|
+
private _saveConflictSkips = 0;
|
|
232
238
|
/** True when the most recent auto-save / manual save attempt failed. */
|
|
233
239
|
private _lastSaveFailed = false;
|
|
234
240
|
/** True while a coalesced interactive edit gesture is in progress. */
|
|
235
241
|
private _interactiveActive = false;
|
|
236
242
|
private _navMeshBaked = false;
|
|
237
243
|
private _savePath: string | null = null;
|
|
238
|
-
private
|
|
239
|
-
private
|
|
240
|
-
private
|
|
241
|
-
private
|
|
244
|
+
private _history: HistoryService | null = null;
|
|
245
|
+
private _historyDescriptor: ResourceDescriptor | null = null;
|
|
246
|
+
private _unregisterHistoryDriver: (() => void) | null = null;
|
|
247
|
+
private _pendingHistoryEdit: { label: string; beforeBytes: Uint8Array } | null = null;
|
|
248
|
+
private _historyGestureActive = false;
|
|
249
|
+
private _historyRevision = 0;
|
|
242
250
|
private _documentType: 'scene' | 'prefab' = 'scene';
|
|
243
251
|
private _playState: 'stopped' | 'playing' | 'paused' = 'stopped';
|
|
244
252
|
/** See {@link PlayEditRegime} — set by play-mode.ts on play-enter, cleared on play-exit. */
|
|
@@ -270,6 +278,8 @@ export class EditorStore {
|
|
|
270
278
|
* `applyAssetMove`.
|
|
271
279
|
*/
|
|
272
280
|
private _pendingExternalUpdate: SceneFile | null = null;
|
|
281
|
+
/** A referenced material changed while play owned the live scene. */
|
|
282
|
+
private _pendingMaterialRefresh = false;
|
|
273
283
|
private _pendingAssetMove: { oldPath: string; newPath: string } | null = null;
|
|
274
284
|
|
|
275
285
|
// --- Bind to scene graph (call once at editor init) ---
|
|
@@ -299,11 +309,174 @@ export class EditorStore {
|
|
|
299
309
|
|
|
300
310
|
getSnapshot = (): number => this._version;
|
|
301
311
|
|
|
312
|
+
get projectHistory(): HistoryService | null {
|
|
313
|
+
return this._history;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async saveMaterialAsReference(id: string, path: string, content: string): Promise<boolean> {
|
|
317
|
+
return this._writeFileAndMutateDocument(
|
|
318
|
+
path,
|
|
319
|
+
content,
|
|
320
|
+
{ label: `Save ${path.split('/').pop() ?? 'Material'}`, kind: 'material' },
|
|
321
|
+
(entity) => {
|
|
322
|
+
if (entity.id !== id) return false;
|
|
323
|
+
delete entity.material;
|
|
324
|
+
entity.materialRef = path;
|
|
325
|
+
return true;
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async savePrefabAndConvertInstance(id: string, path: string, content: string): Promise<boolean> {
|
|
331
|
+
return this._writeFileAndMutateDocument(
|
|
332
|
+
path,
|
|
333
|
+
content,
|
|
334
|
+
{ label: `Save ${path.split('/').pop() ?? 'Prefab'}`, kind: 'prefab' },
|
|
335
|
+
(entity) => {
|
|
336
|
+
if (entity.id !== id) return false;
|
|
337
|
+
const replacement: SceneEntity = {
|
|
338
|
+
id: entity.id,
|
|
339
|
+
name: entity.name,
|
|
340
|
+
prefab: `/${path}`,
|
|
341
|
+
...(entity.transform ? { transform: entity.transform } : {}),
|
|
342
|
+
};
|
|
343
|
+
for (const key of Object.keys(entity) as (keyof SceneEntity)[]) delete entity[key];
|
|
344
|
+
Object.assign(entity, replacement);
|
|
345
|
+
return true;
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private async _writeFileAndMutateDocument(
|
|
351
|
+
path: string,
|
|
352
|
+
content: string,
|
|
353
|
+
options: { label: string; kind: 'material' | 'prefab' },
|
|
354
|
+
mutate: (entity: SceneEntity) => boolean,
|
|
355
|
+
): Promise<boolean> {
|
|
356
|
+
const history = this._history;
|
|
357
|
+
const descriptor = this._historyDescriptor;
|
|
358
|
+
if (!history || !descriptor || this._playState !== 'stopped') return false;
|
|
359
|
+
this._finishHistoryEdit();
|
|
360
|
+
const document = structuredClone(this.getDocument());
|
|
361
|
+
let changed = false;
|
|
362
|
+
walkEntities(document.entities, (entity) => {
|
|
363
|
+
if (!changed) changed = mutate(entity);
|
|
364
|
+
});
|
|
365
|
+
if (!changed) return false;
|
|
366
|
+
const documentBytes = sceneEncoder.encode(JSON.stringify(cleanDocument(document), null, 2));
|
|
367
|
+
return getProjectFileHistory(history).writeWithResources(
|
|
368
|
+
path,
|
|
369
|
+
content,
|
|
370
|
+
{ ...options, contentType: 'application/json' },
|
|
371
|
+
[{ resource: descriptor.key, bytes: documentBytes, contentType: 'application/json' }],
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
302
375
|
private _notify(): void {
|
|
376
|
+
if (!this._interactiveActive && !this._historyGestureActive) this._finishHistoryEdit();
|
|
303
377
|
this._version++;
|
|
304
378
|
for (const fn of this._listeners) fn();
|
|
305
379
|
}
|
|
306
380
|
|
|
381
|
+
attachHistory(history: HistoryService): void {
|
|
382
|
+
if (this._history === history) return;
|
|
383
|
+
if (this._history) throw new Error('EditorStore is already attached to project history.');
|
|
384
|
+
this._history = history;
|
|
385
|
+
this._bindDocumentHistory(
|
|
386
|
+
this._documentType,
|
|
387
|
+
this._savePath ??
|
|
388
|
+
(this._documentType === 'prefab'
|
|
389
|
+
? 'prefabs/__untitled__.prefab.json'
|
|
390
|
+
: 'scenes/__untitled__.vscn.json'),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private _bindDocumentHistory(kind: 'scene' | 'prefab', location: string): void {
|
|
395
|
+
const history = this._history;
|
|
396
|
+
if (!history) return;
|
|
397
|
+
const projectLocation = location.replace(/^\/+/, '');
|
|
398
|
+
const normalizedLocation = `project://${projectLocation}`;
|
|
399
|
+
if (
|
|
400
|
+
this._historyDescriptor?.kind === kind &&
|
|
401
|
+
this._historyDescriptor.location === normalizedLocation
|
|
402
|
+
) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (this._historyDescriptor) history.expireResources([this._historyDescriptor.key]);
|
|
406
|
+
this._unregisterHistoryDriver?.();
|
|
407
|
+
this._historyDescriptor = history.registry.registerProject(kind, projectLocation);
|
|
408
|
+
const driver: ResourceDriver = {
|
|
409
|
+
descriptor: this._historyDescriptor,
|
|
410
|
+
capture: async () => {
|
|
411
|
+
const bytes = this._captureDocumentBytes();
|
|
412
|
+
return {
|
|
413
|
+
revision: this._historyRevision,
|
|
414
|
+
contentType: 'application/json',
|
|
415
|
+
bytes,
|
|
416
|
+
sha256: await sha256Hex(bytes),
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
preflight: async (expected) => {
|
|
420
|
+
const actualSha256 = await sha256Hex(this._captureDocumentBytes());
|
|
421
|
+
return actualSha256 === expected.sha256
|
|
422
|
+
? { ok: true }
|
|
423
|
+
: {
|
|
424
|
+
ok: false,
|
|
425
|
+
reason: 'content-conflict',
|
|
426
|
+
actualRevision: this._historyRevision,
|
|
427
|
+
actualSha256,
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
restore: async (snapshot) => {
|
|
431
|
+
const doc = JSON.parse(
|
|
432
|
+
sceneDecoder.decode(history.snapshots.read(snapshot).bytes),
|
|
433
|
+
) as SceneFile;
|
|
434
|
+
this._pendingHistoryEdit = null;
|
|
435
|
+
this._loadFromDoc(doc);
|
|
436
|
+
this._historyRevision++;
|
|
437
|
+
this._isDirty = true;
|
|
438
|
+
this._docEpoch++;
|
|
439
|
+
this._navMeshBaked = false;
|
|
440
|
+
this._scheduleAutoSave();
|
|
441
|
+
for (const id of [...this._selectedEntityIds]) {
|
|
442
|
+
if (!this._objectMap.has(id)) this._selectedEntityIds.delete(id);
|
|
443
|
+
}
|
|
444
|
+
this._notify();
|
|
445
|
+
},
|
|
446
|
+
estimateBytes: (snapshot) => snapshot.byteLength,
|
|
447
|
+
};
|
|
448
|
+
this._unregisterHistoryDriver = history.registerDriver(driver);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private _captureDocumentBytes(): Uint8Array {
|
|
452
|
+
return sceneEncoder.encode(JSON.stringify(this.getDocument(), null, 2));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private _finishHistoryEdit(): void {
|
|
456
|
+
const pending = this._pendingHistoryEdit;
|
|
457
|
+
const history = this._history;
|
|
458
|
+
const descriptor = this._historyDescriptor;
|
|
459
|
+
if (!pending || !history || !descriptor) return;
|
|
460
|
+
this._pendingHistoryEdit = null;
|
|
461
|
+
const afterBytes = this._captureDocumentBytes();
|
|
462
|
+
this._historyRevision++;
|
|
463
|
+
void history
|
|
464
|
+
.recordAppliedTransaction(
|
|
465
|
+
{ label: pending.label, resources: [descriptor.key], scope: 'project' },
|
|
466
|
+
[
|
|
467
|
+
{
|
|
468
|
+
resource: descriptor.key,
|
|
469
|
+
beforeBytes: pending.beforeBytes,
|
|
470
|
+
afterBytes,
|
|
471
|
+
contentType: 'application/json',
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
)
|
|
475
|
+
.catch(() => {
|
|
476
|
+
// The structured error is exposed by HistoryService.getSnapshot().
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
307
480
|
// --- Viewport action dispatch ---
|
|
308
481
|
// Panels call these methods; ViewportPanel subscribes and executes them.
|
|
309
482
|
|
|
@@ -420,27 +593,16 @@ export class EditorStore {
|
|
|
420
593
|
return this._sceneMeta.environment;
|
|
421
594
|
}
|
|
422
595
|
|
|
423
|
-
/** The scene's UI roots (the `ui` field) — read by UI-edit mode (K5). */
|
|
424
|
-
getSceneUI(): import('@engine/scene/schema').UIRoot[] {
|
|
425
|
-
return this._sceneMeta.ui ?? [];
|
|
426
|
-
}
|
|
427
|
-
/** Replace the scene's UI roots (UI-edit commit), mark dirty + autosave (C9). */
|
|
428
|
-
setSceneUI(roots: import('@engine/scene/schema').UIRoot[]): void {
|
|
429
|
-
this._sceneMeta.ui = roots;
|
|
430
|
-
this._isDirty = true;
|
|
431
|
-
this._docEpoch++;
|
|
432
|
-
this._autoSave();
|
|
433
|
-
this._notify();
|
|
434
|
-
}
|
|
435
|
-
|
|
436
596
|
/** Scene name. */
|
|
437
597
|
get sceneName(): string {
|
|
438
598
|
return this._sceneMeta.name;
|
|
439
599
|
}
|
|
440
600
|
|
|
441
601
|
setSceneName(name: string): void {
|
|
602
|
+
if (name === this._sceneMeta.name) return;
|
|
603
|
+
this._beginHistoryEdit('Rename Scene');
|
|
442
604
|
this._sceneMeta.name = name;
|
|
443
|
-
|
|
605
|
+
this._notify();
|
|
444
606
|
}
|
|
445
607
|
|
|
446
608
|
// --- Selection ---
|
|
@@ -493,10 +655,10 @@ export class EditorStore {
|
|
|
493
655
|
return 'saved';
|
|
494
656
|
}
|
|
495
657
|
get canUndo(): boolean {
|
|
496
|
-
return this.
|
|
658
|
+
return this._history?.getSnapshot().canUndo ?? false;
|
|
497
659
|
}
|
|
498
660
|
get canRedo(): boolean {
|
|
499
|
-
return this.
|
|
661
|
+
return this._history?.getSnapshot().canRedo ?? false;
|
|
500
662
|
}
|
|
501
663
|
get showGrid(): boolean {
|
|
502
664
|
return this._showGrid;
|
|
@@ -507,6 +669,21 @@ export class EditorStore {
|
|
|
507
669
|
get helperVisibility(): Readonly<HelperVisibility> {
|
|
508
670
|
return this._helperVisibility;
|
|
509
671
|
}
|
|
672
|
+
isSkeletonVisible(id: string): boolean {
|
|
673
|
+
return Boolean(getUserData(this._objectMap.get(id), 'skeletonVisible'));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
setSkeletonVisible(id: string, visible: boolean): void {
|
|
677
|
+
const object = this._objectMap.get(id);
|
|
678
|
+
if (!object) return;
|
|
679
|
+
setUserData(object, 'skeletonVisible', visible);
|
|
680
|
+
object.traverse((child) => {
|
|
681
|
+
if (getUserData(child, 'editorHelperType') !== 'skeletons') return;
|
|
682
|
+
setUserData(child, 'skeletonEnabled', visible);
|
|
683
|
+
child.visible = visible || this._helperVisibility.skeletons;
|
|
684
|
+
});
|
|
685
|
+
this._notify();
|
|
686
|
+
}
|
|
510
687
|
get showStats(): boolean {
|
|
511
688
|
return this._showStats;
|
|
512
689
|
}
|
|
@@ -521,6 +698,12 @@ export class EditorStore {
|
|
|
521
698
|
}
|
|
522
699
|
setSavePath(path: string | null): void {
|
|
523
700
|
this._savePath = path;
|
|
701
|
+
// #131: the disk state of the NEW path is unknown — a stale baseline from
|
|
702
|
+
// the previous path would false-flag a save-as over an existing file as a
|
|
703
|
+
// conflict. Null keeps the guard inert until the first write establishes
|
|
704
|
+
// a fresh baseline (save-as intentionally overwrites its target).
|
|
705
|
+
this._lastSavedContent = null;
|
|
706
|
+
this._saveConflictSkips = 0;
|
|
524
707
|
this._notify();
|
|
525
708
|
}
|
|
526
709
|
get documentType(): 'scene' | 'prefab' {
|
|
@@ -548,7 +731,7 @@ export class EditorStore {
|
|
|
548
731
|
* Swap the store's active scene to the game scene. Editor scene is preserved.
|
|
549
732
|
*
|
|
550
733
|
* Also snapshots `_sceneMeta` (env/name/ui — see `getDocument`/`updateEnvironment`/
|
|
551
|
-
* `setSceneName
|
|
734
|
+
* `setSceneName`). Those setters keep mutating the SAME live
|
|
552
735
|
* `_sceneMeta` object during play so the environment inspector / scene name
|
|
553
736
|
* field keep working and `getDocument()` stays consistent while playing —
|
|
554
737
|
* but any such edit must be discarded when play stops (probe: F5 in the
|
|
@@ -676,6 +859,10 @@ export class EditorStore {
|
|
|
676
859
|
this._pendingAssetMove = null;
|
|
677
860
|
this.applyAssetMove(oldPath, newPath);
|
|
678
861
|
}
|
|
862
|
+
if (this._pendingMaterialRefresh) {
|
|
863
|
+
this._pendingMaterialRefresh = false;
|
|
864
|
+
this.refreshMaterialAssets();
|
|
865
|
+
}
|
|
679
866
|
}
|
|
680
867
|
|
|
681
868
|
// --- Viewport tab ---
|
|
@@ -709,13 +896,6 @@ export class EditorStore {
|
|
|
709
896
|
this._notify();
|
|
710
897
|
}
|
|
711
898
|
|
|
712
|
-
get activeTabKey(): string {
|
|
713
|
-
return this._activeTabKey;
|
|
714
|
-
}
|
|
715
|
-
get assetTabs(): readonly AssetTab[] {
|
|
716
|
-
return this._assetTabs;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
899
|
/** True if any entities have a navigation role. */
|
|
720
900
|
hasNavigationEntities(): boolean {
|
|
721
901
|
if (!this._scene) return false;
|
|
@@ -782,20 +962,21 @@ export class EditorStore {
|
|
|
782
962
|
return result;
|
|
783
963
|
}
|
|
784
964
|
|
|
785
|
-
// ---
|
|
965
|
+
// --- Project history ---
|
|
786
966
|
|
|
787
|
-
/**
|
|
967
|
+
/** Capture the document before a transform drag begins (called by viewport). */
|
|
788
968
|
pushUndoSnapshot(): void {
|
|
789
|
-
this.
|
|
969
|
+
this._beginHistoryEdit('Transform Selection');
|
|
970
|
+
if (this._history) this._historyGestureActive = true;
|
|
790
971
|
}
|
|
791
972
|
|
|
792
|
-
private
|
|
973
|
+
private _beginHistoryEdit(label = 'Edit Scene'): void {
|
|
793
974
|
if (!this._scene) return;
|
|
794
975
|
// Play-mode guard (T1.6): while playing/paused, `_scene`/`_objectMap` point
|
|
795
976
|
// at the LIVE game scene (see enterPlayScene) — a fresh object graph loaded
|
|
796
977
|
// from a snapshot of the document, whose entity descriptors are runtime-only
|
|
797
978
|
// copies discarded on exitPlayScene. Pushing a snapshot here would serialize
|
|
798
|
-
// that runtime state
|
|
979
|
+
// that runtime state into project history: a later edit's snapshot could
|
|
799
980
|
// capture an intermediate runtime pose, and a post-stop Ctrl+Z would then
|
|
800
981
|
// restore that runtime pose into the restored editor scene. Marking dirty /
|
|
801
982
|
// scheduling autosave would similarly persist runtime state over the on-disk
|
|
@@ -803,20 +984,19 @@ export class EditorStore {
|
|
|
803
984
|
// the caller to the live game-scene descriptor/Object3D) still takes effect
|
|
804
985
|
// for the running game — it's just never recorded in editor history or saved.
|
|
805
986
|
if (this._playState !== 'stopped') return;
|
|
987
|
+
this._history?.assertCanRecordAppliedChange();
|
|
806
988
|
// During an interactive gesture (a NumberInput scrub or color-picker drag),
|
|
807
989
|
// beginInteractiveEdit() already captured ONE pre-gesture snapshot. Skip the
|
|
808
990
|
// per-event snapshot so the whole gesture collapses to a single undo step,
|
|
809
991
|
// but still mark dirty + schedule autosave so the live value persists.
|
|
810
992
|
if (!this._interactiveActive) {
|
|
811
993
|
const doc = serializeScene(this._scene, this._sceneMeta);
|
|
812
|
-
this.
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
// beginInteractiveEdit's own _pushUndo call).
|
|
819
|
-
notifyUndoableOp(this);
|
|
994
|
+
if (this._history) {
|
|
995
|
+
this._pendingHistoryEdit = {
|
|
996
|
+
label,
|
|
997
|
+
beforeBytes: sceneEncoder.encode(JSON.stringify(cleanDocument(doc), null, 2)),
|
|
998
|
+
};
|
|
999
|
+
}
|
|
820
1000
|
}
|
|
821
1001
|
this._isDirty = true;
|
|
822
1002
|
this._docEpoch++;
|
|
@@ -831,15 +1011,16 @@ export class EditorStore {
|
|
|
831
1011
|
* onChange. Pair with endInteractiveEdit(). Mirrors the viewport gizmo's live
|
|
832
1012
|
* drag (pushUndoSnapshot at start, no-undo live updates, commit at end).
|
|
833
1013
|
*/
|
|
834
|
-
beginInteractiveEdit(): void {
|
|
1014
|
+
beginInteractiveEdit(label = 'Edit Properties'): void {
|
|
835
1015
|
if (this._interactiveActive) return;
|
|
836
|
-
this.
|
|
1016
|
+
this._beginHistoryEdit(label);
|
|
837
1017
|
this._interactiveActive = true;
|
|
838
1018
|
}
|
|
839
1019
|
|
|
840
1020
|
/** End a coalesced interactive edit gesture. */
|
|
841
1021
|
endInteractiveEdit(): void {
|
|
842
1022
|
this._interactiveActive = false;
|
|
1023
|
+
this._finishHistoryEdit();
|
|
843
1024
|
}
|
|
844
1025
|
|
|
845
1026
|
private _scheduleAutoSave(): void {
|
|
@@ -883,7 +1064,7 @@ export class EditorStore {
|
|
|
883
1064
|
private _autoSave(): void {
|
|
884
1065
|
this._autoSaveTimer = null;
|
|
885
1066
|
// Play-mode guard (T1.6, belt-and-braces): `_scene` is the live game scene
|
|
886
|
-
// while playing/paused (see enterPlayScene). `
|
|
1067
|
+
// while playing/paused (see enterPlayScene). `_beginHistoryEdit`/`commitLiveTransform`
|
|
887
1068
|
// already skip scheduling autosave during play, and play-mode.ts installs an
|
|
888
1069
|
// ephemeral persistence provider (isDirty() always false) via
|
|
889
1070
|
// `setActiveAuthoring` for the duration — so this check is now a backstop
|
|
@@ -913,6 +1094,10 @@ export class EditorStore {
|
|
|
913
1094
|
void provider.save().catch((err) => {
|
|
914
1095
|
this._lastSaveFailed = true;
|
|
915
1096
|
console.warn('[EditorStore] Auto-save threw — changes are NOT saved to disk.', err);
|
|
1097
|
+
// W4: the failure must also land in the EDITOR console — the utility
|
|
1098
|
+
// policy reveals Console on saveState → 'failed', so the entry the
|
|
1099
|
+
// user sees there is real.
|
|
1100
|
+
editorConsole.error('Auto-save failed — changes are NOT saved to disk.', 'editor');
|
|
916
1101
|
this._notify();
|
|
917
1102
|
});
|
|
918
1103
|
}
|
|
@@ -969,6 +1154,38 @@ export class EditorStore {
|
|
|
969
1154
|
if (!this._savePath || !this._scene) return false;
|
|
970
1155
|
const content = JSON.stringify(this.getDocument(), null, 2);
|
|
971
1156
|
const epoch = this._docEpoch;
|
|
1157
|
+
|
|
1158
|
+
// #131 — unobserved-external-change guard. If the bytes on disk no longer
|
|
1159
|
+
// match what this editor last wrote (or loaded — `_captureDiskBaseline`),
|
|
1160
|
+
// someone else (an agent, another tool) edited the file while our watch
|
|
1161
|
+
// channel missed it (drvfs inotify gaps, poll latency). Blind-writing the
|
|
1162
|
+
// in-memory doc would permanently destroy that edit — the exact data-loss
|
|
1163
|
+
// path found in blind run #4's investigation. Adopt the disk version
|
|
1164
|
+
// instead: worst case that reverts the human's last ~300ms gesture, while
|
|
1165
|
+
// blind-writing loses ALL of the external writer's work. Best-effort by
|
|
1166
|
+
// design: no baseline yet (null) or an unreadable disk falls through to
|
|
1167
|
+
// the normal write.
|
|
1168
|
+
if (this._lastSavedContent !== null) {
|
|
1169
|
+
let disk: string | null = null;
|
|
1170
|
+
try {
|
|
1171
|
+
disk = await readProjectTextFile(this._savePath);
|
|
1172
|
+
} catch {
|
|
1173
|
+
disk = null;
|
|
1174
|
+
}
|
|
1175
|
+
if (disk !== null && disk !== this._lastSavedContent && disk !== content) {
|
|
1176
|
+
const outcome = this._resolveSaveConflict(disk);
|
|
1177
|
+
if (outcome === 'adopted') return false; // store now matches disk; nothing to write
|
|
1178
|
+
if (outcome === 'retry') {
|
|
1179
|
+
this._scheduleAutoSave();
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
// 'overwrite': disk content is unadoptable (unparseable / non-scene
|
|
1183
|
+
// document / retries exhausted) — proceed to the loud write below so
|
|
1184
|
+
// dirty changes are never stranded off-disk forever.
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
this._saveConflictSkips = 0;
|
|
1188
|
+
|
|
972
1189
|
this._ignoreExternalUntil = Date.now() + SYNC_COOLDOWN_MS;
|
|
973
1190
|
this._lastSavedContent = content;
|
|
974
1191
|
try {
|
|
@@ -984,31 +1201,121 @@ export class EditorStore {
|
|
|
984
1201
|
this._saveEditorStateIfDue();
|
|
985
1202
|
} else {
|
|
986
1203
|
console.warn('[EditorStore] Save failed — changes are NOT saved to disk.');
|
|
1204
|
+
// W4: mirror into the editor Console — the utility policy reveals it
|
|
1205
|
+
// on saveState → 'failed', so the entry there must be real. Identical
|
|
1206
|
+
// consecutive failures collapse via count.
|
|
1207
|
+
editorConsole.error(
|
|
1208
|
+
`Save failed — changes are NOT saved to disk (${this._savePath}).`,
|
|
1209
|
+
'editor',
|
|
1210
|
+
);
|
|
987
1211
|
}
|
|
988
1212
|
return ok;
|
|
989
1213
|
} catch (err) {
|
|
990
1214
|
this._lastSaveFailed = true;
|
|
991
1215
|
this._notify();
|
|
992
1216
|
console.warn('[EditorStore] Save threw — changes are NOT saved to disk.', err);
|
|
1217
|
+
editorConsole.error(
|
|
1218
|
+
`Save failed — changes are NOT saved to disk (${this._savePath}).`,
|
|
1219
|
+
'editor',
|
|
1220
|
+
);
|
|
993
1221
|
return false;
|
|
994
1222
|
}
|
|
995
1223
|
}
|
|
996
1224
|
|
|
1225
|
+
/**
|
|
1226
|
+
* #131 — decide what `saveNow()` does about a file that changed on disk
|
|
1227
|
+
* since our last known write. Returns:
|
|
1228
|
+
* - `'adopted'` — the disk version was applied into the store (or was a
|
|
1229
|
+
* semantic no-op, e.g. an external formatter run); the doc is clean and
|
|
1230
|
+
* nothing should be written.
|
|
1231
|
+
* - `'retry'` — could not adopt right now (sync circuit breaker, a torn
|
|
1232
|
+
* mid-write read, a non-scene document); the caller reschedules autosave
|
|
1233
|
+
* and skips this write.
|
|
1234
|
+
* - `'overwrite'`— adoption failed repeatedly (`_saveConflictSkips` > 3);
|
|
1235
|
+
* write anyway, LOUDLY, so dirty changes are never stranded forever
|
|
1236
|
+
* (an invalid file on disk gets restored to validity by the write).
|
|
1237
|
+
*/
|
|
1238
|
+
private _resolveSaveConflict(disk: string): 'adopted' | 'retry' | 'overwrite' {
|
|
1239
|
+
if (this._documentType === 'scene') {
|
|
1240
|
+
let incoming: SceneFile | null = null;
|
|
1241
|
+
try {
|
|
1242
|
+
incoming = parseSceneFile(JSON.parse(disk));
|
|
1243
|
+
} catch {
|
|
1244
|
+
incoming = null; // torn mid-write read or genuinely invalid JSON
|
|
1245
|
+
}
|
|
1246
|
+
if (incoming) {
|
|
1247
|
+
// Our own save cooldown must not block adopting a conflict we just
|
|
1248
|
+
// detected ourselves.
|
|
1249
|
+
this._ignoreExternalUntil = 0;
|
|
1250
|
+
const applied = this.applyExternalUpdate(incoming, disk);
|
|
1251
|
+
if (applied) {
|
|
1252
|
+
this._lastSavedContent = disk;
|
|
1253
|
+
this._isDirty = false;
|
|
1254
|
+
this._saveConflictSkips = 0;
|
|
1255
|
+
const msg =
|
|
1256
|
+
`${this._savePath} changed on disk while the editor held unsaved changes — ` +
|
|
1257
|
+
'adopted the disk version; re-apply your last edit if it disappeared';
|
|
1258
|
+
editorConsole.warn(msg, 'scene');
|
|
1259
|
+
console.warn(`[EditorStore] ${msg}`);
|
|
1260
|
+
this._notify();
|
|
1261
|
+
return 'adopted';
|
|
1262
|
+
}
|
|
1263
|
+
// Dropped by the sync circuit breaker — its own 5s window bounds the
|
|
1264
|
+
// retries; do NOT count these toward the overwrite escalation (a sync
|
|
1265
|
+
// storm is exactly when a forced write would ping-pong).
|
|
1266
|
+
return 'retry';
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
this._saveConflictSkips++;
|
|
1270
|
+
if (this._saveConflictSkips <= 3) {
|
|
1271
|
+
console.warn(
|
|
1272
|
+
`[EditorStore] ${this._savePath} changed on disk but could not be adopted ` +
|
|
1273
|
+
`(unparseable or non-scene document) — retrying (${this._saveConflictSkips}/3) before overwriting.`,
|
|
1274
|
+
);
|
|
1275
|
+
return 'retry';
|
|
1276
|
+
}
|
|
1277
|
+
const msg =
|
|
1278
|
+
`${this._savePath} changed on disk but the disk version could not be adopted — ` +
|
|
1279
|
+
'OVERWRITING it with the editor version to avoid stranding unsaved changes';
|
|
1280
|
+
editorConsole.error(msg, 'scene');
|
|
1281
|
+
console.warn(`[EditorStore] ${msg}`);
|
|
1282
|
+
return 'overwrite';
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* #131 — baseline `_lastSavedContent` with the exact bytes on disk right
|
|
1287
|
+
* after a document load, so the save-conflict guard is armed from t=0 (an
|
|
1288
|
+
* agent's first external edit typically lands before the human's first
|
|
1289
|
+
* save). Fire-and-forget: a failed read just leaves the guard inert (null
|
|
1290
|
+
* baseline), which is the pre-#131 behavior. The `_savePath` / null checks
|
|
1291
|
+
* make a late resolution harmless if the user switched documents or a save
|
|
1292
|
+
* already established a fresher baseline. Known millisecond-scale race: an
|
|
1293
|
+
* external edit landing between the load and this read becomes the
|
|
1294
|
+
* baseline, so the next save would not flag it — the SSE/poll watch channel
|
|
1295
|
+
* covers that window.
|
|
1296
|
+
*/
|
|
1297
|
+
private async _captureDiskBaseline(path: string): Promise<void> {
|
|
1298
|
+
try {
|
|
1299
|
+
const raw = await readProjectTextFile(path);
|
|
1300
|
+
if (raw !== null && this._savePath === path && this._lastSavedContent === null) {
|
|
1301
|
+
this._lastSavedContent = raw;
|
|
1302
|
+
}
|
|
1303
|
+
} catch {
|
|
1304
|
+
/* mocked/absent backend or unreadable file — guard stays inert */
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
997
1308
|
private static readonly THUMBNAIL_INTERVAL_MS = 60_000;
|
|
998
1309
|
private static readonly THUMBNAIL_SIZE = 256;
|
|
999
1310
|
|
|
1000
1311
|
/**
|
|
1001
|
-
* Render the bound camera's current view to a PNG data URL at `size`x`size
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
* `
|
|
1006
|
-
* fresh-capture mode) — same rendering technique (offscreen render target +
|
|
1007
|
-
* `readRenderTargetPixels`), so it needs no `preserveDrawingBuffer` on the
|
|
1008
|
-
* live canvas's WebGL context. Returns null if no renderer/scene/camera is
|
|
1009
|
-
* bound yet.
|
|
1312
|
+
* Render the bound camera's current view to a PNG data URL at `size`x`size`.
|
|
1313
|
+
* Autosave thumbnails request game content only; an explicit SDK/editor
|
|
1314
|
+
* viewport capture requests every camera layer so grid/helpers/gizmos are
|
|
1315
|
+
* represented honestly. Both use the same offscreen render-target technique,
|
|
1316
|
+
* so neither needs `preserveDrawingBuffer` on the live WebGL canvas.
|
|
1010
1317
|
*/
|
|
1011
|
-
private _renderViewportImage(size: number): string | null {
|
|
1318
|
+
private _renderViewportImage(size: number, includeEditorLayers: boolean): string | null {
|
|
1012
1319
|
if (!this._renderer || !this._scene || !this._camera) return null;
|
|
1013
1320
|
|
|
1014
1321
|
const renderSize = size * 2; // render at 2x then downscale for crisp result
|
|
@@ -1017,11 +1324,14 @@ export class EditorStore {
|
|
|
1017
1324
|
const prevTarget = renderer.getRenderTarget();
|
|
1018
1325
|
const prevPixelRatio = renderer.getPixelRatio();
|
|
1019
1326
|
|
|
1020
|
-
// Render without editor-layer objects (grid, helpers, gizmos)
|
|
1021
1327
|
const camera = this._camera as THREE.PerspectiveCamera;
|
|
1022
1328
|
const prevLayers = camera.layers.mask;
|
|
1023
|
-
|
|
1024
|
-
|
|
1329
|
+
if (includeEditorLayers) {
|
|
1330
|
+
camera.layers.enableAll();
|
|
1331
|
+
} else {
|
|
1332
|
+
camera.layers.disableAll();
|
|
1333
|
+
camera.layers.enable(0); // clean project thumbnail: game content only
|
|
1334
|
+
}
|
|
1025
1335
|
|
|
1026
1336
|
renderer.setPixelRatio(1);
|
|
1027
1337
|
renderer.setRenderTarget(renderTarget);
|
|
@@ -1063,7 +1373,7 @@ export class EditorStore {
|
|
|
1063
1373
|
const now = Date.now();
|
|
1064
1374
|
if (now - this._lastThumbnailTime < EditorStore.THUMBNAIL_INTERVAL_MS) return;
|
|
1065
1375
|
this._lastThumbnailTime = now;
|
|
1066
|
-
const dataUrl = this._renderViewportImage(EditorStore.THUMBNAIL_SIZE);
|
|
1376
|
+
const dataUrl = this._renderViewportImage(EditorStore.THUMBNAIL_SIZE, false);
|
|
1067
1377
|
if (dataUrl) saveThumbnail(dataUrl);
|
|
1068
1378
|
}
|
|
1069
1379
|
|
|
@@ -1074,7 +1384,7 @@ export class EditorStore {
|
|
|
1074
1384
|
* `ViewportPanel` has mounted).
|
|
1075
1385
|
*/
|
|
1076
1386
|
captureViewportImage(size = EditorStore.THUMBNAIL_SIZE): string | null {
|
|
1077
|
-
return this._renderViewportImage(size);
|
|
1387
|
+
return this._renderViewportImage(size, true);
|
|
1078
1388
|
}
|
|
1079
1389
|
|
|
1080
1390
|
// --- Editor state persistence ---
|
|
@@ -1174,7 +1484,7 @@ export class EditorStore {
|
|
|
1174
1484
|
const obj = this._objectMap.get(id);
|
|
1175
1485
|
const entity = getUserData(obj, 'entity') as SceneEntity | undefined;
|
|
1176
1486
|
if (!entity || !obj) return;
|
|
1177
|
-
this.
|
|
1487
|
+
this._beginHistoryEdit(`Edit ${entity.name}`);
|
|
1178
1488
|
Object.assign(entity, partial);
|
|
1179
1489
|
this._rebuildEntity(id);
|
|
1180
1490
|
this._notify();
|
|
@@ -1189,7 +1499,7 @@ export class EditorStore {
|
|
|
1189
1499
|
const obj = this._objectMap.get(id);
|
|
1190
1500
|
const entity = getUserData(obj, 'entity') as SceneEntity | undefined;
|
|
1191
1501
|
if (!entity || !obj) return;
|
|
1192
|
-
this.
|
|
1502
|
+
this._beginHistoryEdit(`Transform ${entity.name}`);
|
|
1193
1503
|
entity.transform = { position: pos, rotation: rot, scale: scl };
|
|
1194
1504
|
obj.position.set(pos[0], pos[1], pos[2]);
|
|
1195
1505
|
obj.quaternion.set(rot[0], rot[1], rot[2], rot[3]);
|
|
@@ -1242,6 +1552,7 @@ export class EditorStore {
|
|
|
1242
1552
|
this._navMeshBaked = false;
|
|
1243
1553
|
this._scheduleAutoSave();
|
|
1244
1554
|
}
|
|
1555
|
+
this._historyGestureActive = false;
|
|
1245
1556
|
this._notify();
|
|
1246
1557
|
}
|
|
1247
1558
|
|
|
@@ -1250,7 +1561,7 @@ export class EditorStore {
|
|
|
1250
1561
|
if (!this._scene) return;
|
|
1251
1562
|
if (!entity.id) entity.id = crypto.randomUUID();
|
|
1252
1563
|
assignMissingIds(entity.children ?? []);
|
|
1253
|
-
this.
|
|
1564
|
+
this._beginHistoryEdit(`Add ${entity.name}`);
|
|
1254
1565
|
|
|
1255
1566
|
const obj = createEntityObject(entity);
|
|
1256
1567
|
obj.visible = entity.visible !== false;
|
|
@@ -1305,7 +1616,9 @@ export class EditorStore {
|
|
|
1305
1616
|
if (this._blockedDuringPlay('delete entity')) return;
|
|
1306
1617
|
const obj = this._objectMap.get(id);
|
|
1307
1618
|
if (!obj) return;
|
|
1308
|
-
this.
|
|
1619
|
+
this._beginHistoryEdit(
|
|
1620
|
+
`Delete ${(getUserData(obj, 'entity') as SceneEntity | undefined)?.name ?? 'Entity'}`,
|
|
1621
|
+
);
|
|
1309
1622
|
this._removeObjectFromScene(obj);
|
|
1310
1623
|
this._selectedEntityIds.delete(id);
|
|
1311
1624
|
this._notify();
|
|
@@ -1341,7 +1654,7 @@ export class EditorStore {
|
|
|
1341
1654
|
reassign(cloneEntity);
|
|
1342
1655
|
cloneEntity.name = `${entity.name} (copy)`;
|
|
1343
1656
|
|
|
1344
|
-
this.
|
|
1657
|
+
this._beginHistoryEdit(`Duplicate ${entity.name}`);
|
|
1345
1658
|
|
|
1346
1659
|
// Create object and insert after original
|
|
1347
1660
|
const cloneObj = createEntityObject(cloneEntity);
|
|
@@ -1396,7 +1709,9 @@ export class EditorStore {
|
|
|
1396
1709
|
const targetParent = parentId ? this._objectMap.get(parentId) : this._scene;
|
|
1397
1710
|
if (!targetParent) return;
|
|
1398
1711
|
|
|
1399
|
-
this.
|
|
1712
|
+
this._beginHistoryEdit(
|
|
1713
|
+
`Move ${dragIds.length === 1 ? 'Entity' : `${dragIds.length} Entities`}`,
|
|
1714
|
+
);
|
|
1400
1715
|
|
|
1401
1716
|
// Collect objects and remove from old locations
|
|
1402
1717
|
const objects: THREE.Object3D[] = [];
|
|
@@ -1431,7 +1746,7 @@ export class EditorStore {
|
|
|
1431
1746
|
const obj = this._objectMap.get(id);
|
|
1432
1747
|
const entity = getUserData(obj, 'entity') as SceneEntity | undefined;
|
|
1433
1748
|
if (!entity || !obj) return;
|
|
1434
|
-
this.
|
|
1749
|
+
this._beginHistoryEdit(`Rename ${entity.name}`);
|
|
1435
1750
|
entity.name = name;
|
|
1436
1751
|
obj.name = name;
|
|
1437
1752
|
this._notify();
|
|
@@ -1442,7 +1757,7 @@ export class EditorStore {
|
|
|
1442
1757
|
const obj = this._objectMap.get(id);
|
|
1443
1758
|
const entity = getUserData(obj, 'entity') as SceneEntity | undefined;
|
|
1444
1759
|
if (!entity || !obj) return;
|
|
1445
|
-
this.
|
|
1760
|
+
this._beginHistoryEdit(`${entity.visible === false ? 'Show' : 'Hide'} ${entity.name}`);
|
|
1446
1761
|
if (entity.visible === false) {
|
|
1447
1762
|
delete entity.visible;
|
|
1448
1763
|
obj.visible = true;
|
|
@@ -1458,7 +1773,7 @@ export class EditorStore {
|
|
|
1458
1773
|
const obj = this._objectMap.get(id);
|
|
1459
1774
|
const entity = getUserData(obj, 'entity') as SceneEntity | undefined;
|
|
1460
1775
|
if (!entity) return;
|
|
1461
|
-
this.
|
|
1776
|
+
this._beginHistoryEdit(`${entity.locked ? 'Unlock' : 'Lock'} ${entity.name}`);
|
|
1462
1777
|
if (entity.locked) {
|
|
1463
1778
|
delete entity.locked;
|
|
1464
1779
|
} else {
|
|
@@ -1488,7 +1803,7 @@ export class EditorStore {
|
|
|
1488
1803
|
// --- Bulk delete ---
|
|
1489
1804
|
deleteEntities(ids: string[]): void {
|
|
1490
1805
|
if (this._blockedDuringPlay('delete entities')) return;
|
|
1491
|
-
this.
|
|
1806
|
+
this._beginHistoryEdit(`Delete ${ids.length} ${ids.length === 1 ? 'Entity' : 'Entities'}`);
|
|
1492
1807
|
for (const id of ids) {
|
|
1493
1808
|
const obj = this._objectMap.get(id);
|
|
1494
1809
|
if (!obj) continue;
|
|
@@ -1533,7 +1848,7 @@ export class EditorStore {
|
|
|
1533
1848
|
const obj = this._objectMap.get(id);
|
|
1534
1849
|
const entity = getUserData(obj, 'entity') as SceneEntity | undefined;
|
|
1535
1850
|
if (!entity) return;
|
|
1536
|
-
this.
|
|
1851
|
+
this._beginHistoryEdit(`Set ${entity.name} Pivot`);
|
|
1537
1852
|
const isZero = pivot[0] === 0 && pivot[1] === 0 && pivot[2] === 0;
|
|
1538
1853
|
if (isZero) {
|
|
1539
1854
|
delete entity.pivot;
|
|
@@ -1591,7 +1906,7 @@ export class EditorStore {
|
|
|
1591
1906
|
if (this._blockedDuringPlay('cut entities')) return;
|
|
1592
1907
|
this.copyEntities();
|
|
1593
1908
|
const ids = [...this._selectedEntityIds];
|
|
1594
|
-
this.
|
|
1909
|
+
this._beginHistoryEdit(`Cut ${ids.length} ${ids.length === 1 ? 'Entity' : 'Entities'}`);
|
|
1595
1910
|
for (const id of ids) {
|
|
1596
1911
|
const obj = this._objectMap.get(id);
|
|
1597
1912
|
if (!obj) continue;
|
|
@@ -1604,7 +1919,9 @@ export class EditorStore {
|
|
|
1604
1919
|
pasteEntities(): void {
|
|
1605
1920
|
if (this._blockedDuringPlay('paste')) return;
|
|
1606
1921
|
if (this._clipboard.length === 0 || !this._scene) return;
|
|
1607
|
-
this.
|
|
1922
|
+
this._beginHistoryEdit(
|
|
1923
|
+
`Paste ${this._clipboard.length} ${this._clipboard.length === 1 ? 'Entity' : 'Entities'}`,
|
|
1924
|
+
);
|
|
1608
1925
|
const newIds: string[] = [];
|
|
1609
1926
|
for (const template of this._clipboard) {
|
|
1610
1927
|
const clone = deepClone(template);
|
|
@@ -1636,7 +1953,7 @@ export class EditorStore {
|
|
|
1636
1953
|
const trimmed = tag.trim();
|
|
1637
1954
|
if (!trimmed) return;
|
|
1638
1955
|
if (entity.tags?.includes(trimmed)) return;
|
|
1639
|
-
this.
|
|
1956
|
+
this._beginHistoryEdit(`Add Tag to ${entity.name}`);
|
|
1640
1957
|
if (!entity.tags) entity.tags = [];
|
|
1641
1958
|
entity.tags.push(trimmed);
|
|
1642
1959
|
this._notify();
|
|
@@ -1647,7 +1964,7 @@ export class EditorStore {
|
|
|
1647
1964
|
if (!entity?.tags) return;
|
|
1648
1965
|
const idx = entity.tags.indexOf(tag);
|
|
1649
1966
|
if (idx < 0) return;
|
|
1650
|
-
this.
|
|
1967
|
+
this._beginHistoryEdit(`Remove Tag from ${entity.name}`);
|
|
1651
1968
|
entity.tags.splice(idx, 1);
|
|
1652
1969
|
if (entity.tags.length === 0) delete entity.tags;
|
|
1653
1970
|
this._notify();
|
|
@@ -1667,7 +1984,7 @@ export class EditorStore {
|
|
|
1667
1984
|
// --- Batch mutations (multi-entity editing) ---
|
|
1668
1985
|
/** Push undo once, then mutate each entity by id and rebuild. */
|
|
1669
1986
|
batchMutate(ids: string[], mutator: (entity: SceneEntity) => void): void {
|
|
1670
|
-
this.
|
|
1987
|
+
this._beginHistoryEdit(`Edit ${ids.length} ${ids.length === 1 ? 'Entity' : 'Entities'}`);
|
|
1671
1988
|
for (const id of ids) {
|
|
1672
1989
|
const obj = this._objectMap.get(id);
|
|
1673
1990
|
const entity = getUserData(obj, 'entity') as SceneEntity | undefined;
|
|
@@ -1681,7 +1998,7 @@ export class EditorStore {
|
|
|
1681
1998
|
|
|
1682
1999
|
// --- Environment ---
|
|
1683
2000
|
updateEnvironment(partial: Partial<SceneEnvironment>): void {
|
|
1684
|
-
this.
|
|
2001
|
+
this._beginHistoryEdit('Edit Environment');
|
|
1685
2002
|
this._sceneMeta.environment = { ...this._sceneMeta.environment, ...partial };
|
|
1686
2003
|
this._applyEnvironment();
|
|
1687
2004
|
this._notify();
|
|
@@ -1689,14 +2006,59 @@ export class EditorStore {
|
|
|
1689
2006
|
|
|
1690
2007
|
/** Replace the entire environment object (for removing keys). */
|
|
1691
2008
|
setEnvironment(env: SceneEnvironment): void {
|
|
1692
|
-
this.
|
|
2009
|
+
this._beginHistoryEdit('Replace Environment');
|
|
1693
2010
|
this._sceneMeta.environment = env;
|
|
1694
2011
|
this._applyEnvironment();
|
|
1695
2012
|
this._notify();
|
|
1696
2013
|
}
|
|
1697
2014
|
|
|
2015
|
+
/**
|
|
2016
|
+
* Re-apply the AUTHORED environment to the live scene and bump
|
|
2017
|
+
* `composerVersion` — the restore half of the world-hidden eye's
|
|
2018
|
+
* environment suppression (`world-hidden-viewport.ts`'s
|
|
2019
|
+
* `suppressWorldEnvironment`). Truth is `_sceneMeta.environment`; nothing
|
|
2020
|
+
* is snapshotted at suppress time. No undo push (nothing authored changed)
|
|
2021
|
+
* and no notify — the caller (`editor-viewport.ts`'s `syncFromStore`) runs
|
|
2022
|
+
* INSIDE a notify, and `ViewportPanel`'s subscriber checks
|
|
2023
|
+
* `composerVersion` right after `syncFromStore` returns, so the composer
|
|
2024
|
+
* rebuild this triggers is picked up in the same tick.
|
|
2025
|
+
*/
|
|
2026
|
+
reapplyEnvironment(): void {
|
|
2027
|
+
this._applyEnvironment();
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/** Rebuild the stopped authoring scene after a referenced `.mat.json`
|
|
2031
|
+
* changes. This is view maintenance only: descriptors/history/dirty state
|
|
2032
|
+
* stay untouched. */
|
|
2033
|
+
refreshMaterialAssets(): void {
|
|
2034
|
+
clearMaterialRefCache();
|
|
2035
|
+
if (!this._scene) return;
|
|
2036
|
+
if (this._playState !== 'stopped') {
|
|
2037
|
+
this._pendingMaterialRefresh = true;
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
this._pendingMaterialRefresh = false;
|
|
2041
|
+
this._rebuildScene(this.getDocument());
|
|
2042
|
+
this._notify();
|
|
2043
|
+
}
|
|
2044
|
+
|
|
1698
2045
|
// --- External update (live sync from disk) ---
|
|
1699
2046
|
|
|
2047
|
+
/**
|
|
2048
|
+
* Whether `rawContent` (exact bytes from a file-watcher event) is exactly
|
|
2049
|
+
* what this editor itself last wrote to `_savePath` — i.e. an autosave
|
|
2050
|
+
* echo, not a genuine external edit. Callers that want to gate on "did
|
|
2051
|
+
* something ELSE change this file" (e.g. the dirty-notice check in
|
|
2052
|
+
* `DefaultEditorLayout`'s `onSceneChanged`, run-3 friction #1) use this to
|
|
2053
|
+
* tell the two apart before deciding whether to notify — `applyExternalUpdate`
|
|
2054
|
+
* performs the same comparison internally but only AFTER already deciding
|
|
2055
|
+
* to apply, which is too late for a caller that wants to skip notifying on
|
|
2056
|
+
* a plain echo.
|
|
2057
|
+
*/
|
|
2058
|
+
isOwnEchoContent(rawContent: string): boolean {
|
|
2059
|
+
return rawContent === this._lastSavedContent;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
1700
2062
|
/**
|
|
1701
2063
|
* Apply an external SceneFile update (e.g. from a file watcher) without
|
|
1702
2064
|
* pushing undo or triggering auto-save. Diffs against current state and
|
|
@@ -1707,9 +2069,16 @@ export class EditorStore {
|
|
|
1707
2069
|
* what we last wrote is our own save reflected back, and is skipped even
|
|
1708
2070
|
* if it arrives after `_ignoreExternalUntil` — the document may have moved
|
|
1709
2071
|
* on (undo/redo) and applying the echo would clobber the newer state.
|
|
2072
|
+
*
|
|
2073
|
+
* Returns whether the store now semantically matches `incoming`: `true`
|
|
2074
|
+
* when the diff was applied OR was a no-op (own echo / zero ops); `false`
|
|
2075
|
+
* when the update was deferred (play mode) or dropped (sync cooldown,
|
|
2076
|
+
* circuit breaker, no scene). `saveNow()`'s unobserved-external-change
|
|
2077
|
+
* guard (#131) relies on this to know whether adopting the disk version
|
|
2078
|
+
* actually happened before it marks the document clean.
|
|
1710
2079
|
*/
|
|
1711
|
-
applyExternalUpdate(incoming: SceneFile, rawContent?: string):
|
|
1712
|
-
if (rawContent !== undefined && rawContent === this._lastSavedContent) return;
|
|
2080
|
+
applyExternalUpdate(incoming: SceneFile, rawContent?: string): boolean {
|
|
2081
|
+
if (rawContent !== undefined && rawContent === this._lastSavedContent) return true;
|
|
1713
2082
|
// Play-mode / ingest guard: `_scene` is the LIVE game/ingested scene while
|
|
1714
2083
|
// playing (see enterPlayScene) — diffing and patching it here would rebuild
|
|
1715
2084
|
// the RUNNING scene out from under the game/ingested adapter, and the disk
|
|
@@ -1718,18 +2087,18 @@ export class EditorStore {
|
|
|
1718
2087
|
// latest incoming doc and apply it once play/ingest stops (see setPlayState).
|
|
1719
2088
|
if (this._playState !== 'stopped') {
|
|
1720
2089
|
this._pendingExternalUpdate = incoming;
|
|
1721
|
-
return;
|
|
2090
|
+
return false;
|
|
1722
2091
|
}
|
|
1723
|
-
if (!this._scene) return;
|
|
1724
|
-
if (Date.now() < this._ignoreExternalUntil) return;
|
|
2092
|
+
if (!this._scene) return false;
|
|
2093
|
+
if (Date.now() < this._ignoreExternalUntil) return false;
|
|
1725
2094
|
assignMissingIds(incoming.entities);
|
|
1726
2095
|
const current = this.getDocument();
|
|
1727
2096
|
const diff = diffScene(current, incoming);
|
|
1728
2097
|
|
|
1729
2098
|
// No-op polls must not count toward the circuit breaker (ED6) — only a
|
|
1730
2099
|
// poll that actually changes something is a real sync cycle.
|
|
1731
|
-
if (diff.ops.length === 0 && !diff.environmentChanged && !diff.metaChanged) return;
|
|
1732
|
-
if (this._isSyncLooping()) return;
|
|
2100
|
+
if (diff.ops.length === 0 && !diff.environmentChanged && !diff.metaChanged) return true;
|
|
2101
|
+
if (this._isSyncLooping()) return false;
|
|
1733
2102
|
|
|
1734
2103
|
this._suppressAutoSaveUntil = Date.now() + SYNC_COOLDOWN_MS;
|
|
1735
2104
|
|
|
@@ -1856,49 +2225,6 @@ export class EditorStore {
|
|
|
1856
2225
|
}
|
|
1857
2226
|
|
|
1858
2227
|
this._notify();
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
// --- Undo / Redo ---
|
|
1862
|
-
undo(): boolean {
|
|
1863
|
-
// `false` tells the undo timeline "couldn't act — put the entry back"
|
|
1864
|
-
// (see UndoAdapter in authoring/undo-timeline.ts).
|
|
1865
|
-
if (this._blockedDuringPlay('undo')) return false;
|
|
1866
|
-
if (this._undoStack.length === 0 || !this._scene) return false;
|
|
1867
|
-
const currentDoc = serializeScene(this._scene, this._sceneMeta);
|
|
1868
|
-
this._redoStack.push(JSON.stringify(currentDoc));
|
|
1869
|
-
const doc = JSON.parse(this._undoStack.pop()!) as SceneFile;
|
|
1870
|
-
this._loadFromDoc(doc);
|
|
1871
|
-
// The restored state is a document mutation like any other: mark dirty and
|
|
1872
|
-
// schedule autosave. Without this, an undo after a completed autosave left
|
|
1873
|
-
// the store claiming "saved" while the pre-undo state stayed on disk.
|
|
1874
|
-
this._isDirty = true;
|
|
1875
|
-
this._docEpoch++;
|
|
1876
|
-
this._navMeshBaked = false;
|
|
1877
|
-
this._scheduleAutoSave();
|
|
1878
|
-
// Re-validate selection
|
|
1879
|
-
for (const id of [...this._selectedEntityIds]) {
|
|
1880
|
-
if (!this._objectMap.has(id)) this._selectedEntityIds.delete(id);
|
|
1881
|
-
}
|
|
1882
|
-
this._notify();
|
|
1883
|
-
return true;
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
redo(): boolean {
|
|
1887
|
-
if (this._blockedDuringPlay('redo')) return false;
|
|
1888
|
-
if (this._redoStack.length === 0 || !this._scene) return false;
|
|
1889
|
-
const currentDoc = serializeScene(this._scene, this._sceneMeta);
|
|
1890
|
-
this._undoStack.push(JSON.stringify(currentDoc));
|
|
1891
|
-
const doc = JSON.parse(this._redoStack.pop()!) as SceneFile;
|
|
1892
|
-
this._loadFromDoc(doc);
|
|
1893
|
-
// Same as undo(): the restored state must be persisted.
|
|
1894
|
-
this._isDirty = true;
|
|
1895
|
-
this._docEpoch++;
|
|
1896
|
-
this._navMeshBaked = false;
|
|
1897
|
-
this._scheduleAutoSave();
|
|
1898
|
-
for (const id of [...this._selectedEntityIds]) {
|
|
1899
|
-
if (!this._objectMap.has(id)) this._selectedEntityIds.delete(id);
|
|
1900
|
-
}
|
|
1901
|
-
this._notify();
|
|
1902
2228
|
return true;
|
|
1903
2229
|
}
|
|
1904
2230
|
|
|
@@ -1914,22 +2240,48 @@ export class EditorStore {
|
|
|
1914
2240
|
|
|
1915
2241
|
loadDocument(doc: SceneFile, savePath?: string): void {
|
|
1916
2242
|
if (this._blockedDuringPlay('load document')) return;
|
|
1917
|
-
assignMissingIds(doc.entities);
|
|
2243
|
+
const idsWereAssigned = assignMissingIds(doc.entities);
|
|
1918
2244
|
this._sceneMeta = { version: doc.version, name: doc.name };
|
|
1919
2245
|
if (doc.environment) this._sceneMeta.environment = doc.environment;
|
|
1920
|
-
if (doc.ui) this._sceneMeta.ui = doc.ui; // preserve scene-graph UI (A1/C9)
|
|
1921
2246
|
this._documentType = 'scene';
|
|
1922
2247
|
this._selectedEntityIds.clear();
|
|
1923
|
-
this.
|
|
1924
|
-
this.
|
|
2248
|
+
this._pendingHistoryEdit = null;
|
|
2249
|
+
this._historyGestureActive = false;
|
|
1925
2250
|
this._isDirty = false;
|
|
1926
2251
|
this._savePath = savePath ?? null;
|
|
2252
|
+
// #131: reset the save-conflict baseline (a previous document's bytes
|
|
2253
|
+
// must never be compared against this document's disk file), then
|
|
2254
|
+
// re-arm it with this file's exact on-disk bytes.
|
|
2255
|
+
this._lastSavedContent = null;
|
|
2256
|
+
this._saveConflictSkips = 0;
|
|
2257
|
+
if (savePath) void this._captureDiskBaseline(savePath);
|
|
2258
|
+
this._bindDocumentHistory('scene', savePath ?? 'scenes/__untitled__.vscn.json');
|
|
1927
2259
|
|
|
1928
2260
|
if (this._scene) {
|
|
1929
2261
|
this._rebuildScene(doc);
|
|
1930
2262
|
}
|
|
1931
2263
|
this._notify();
|
|
1932
2264
|
|
|
2265
|
+
// The `apply-diff`/`vgai status` id-persistence trap: an entity loaded
|
|
2266
|
+
// from a file with no (or partial) `id` fields gets one assigned above,
|
|
2267
|
+
// IN MEMORY ONLY — `assignMissingIds` never touches disk. `vgai status`
|
|
2268
|
+
// reports these freshly-assigned ids (they're the only ids this running
|
|
2269
|
+
// session has), but until this block existed nothing ever wrote them
|
|
2270
|
+
// back to `savePath`, so the exact id `status` just handed out could
|
|
2271
|
+
// never resolve against the file `apply-diff` reads (it matches strictly
|
|
2272
|
+
// on the FILE's persisted id). Marking the document dirty and going
|
|
2273
|
+
// through the ordinary autosave path here closes the loop the same way
|
|
2274
|
+
// any other edit persists: the file catches up to the ids this session
|
|
2275
|
+
// is already handing out, automatically, ~300ms after load, with no
|
|
2276
|
+
// user/agent action required. Gated on `savePath` for the same reason
|
|
2277
|
+
// the disk-baseline capture above is: a path-less doc (e.g. the no-
|
|
2278
|
+
// project fallback scene) is explicitly never written until the user
|
|
2279
|
+
// gives it a real save target.
|
|
2280
|
+
if (idsWereAssigned && savePath) {
|
|
2281
|
+
this._isDirty = true;
|
|
2282
|
+
this._scheduleAutoSave();
|
|
2283
|
+
}
|
|
2284
|
+
|
|
1933
2285
|
// Capture a thumbnail after assets have had time to load
|
|
1934
2286
|
if (savePath) {
|
|
1935
2287
|
setTimeout(() => this._captureThumbnail(), 2000);
|
|
@@ -1965,7 +2317,7 @@ export class EditorStore {
|
|
|
1965
2317
|
const obj = this._objectMap.get(id);
|
|
1966
2318
|
const entity = getUserData(obj, 'entity') as SceneEntity | undefined;
|
|
1967
2319
|
if (!entity) return;
|
|
1968
|
-
this.
|
|
2320
|
+
this._beginHistoryEdit(`Convert ${entity.name} to Prefab`);
|
|
1969
2321
|
const keepName = entity.name;
|
|
1970
2322
|
const keepId = entity.id;
|
|
1971
2323
|
const keepTransform = entity.transform;
|
|
@@ -1997,7 +2349,7 @@ export class EditorStore {
|
|
|
1997
2349
|
console.error(`EditorStore: cannot unpack invalid prefab instance "${entity.name}":`, err);
|
|
1998
2350
|
return;
|
|
1999
2351
|
}
|
|
2000
|
-
this.
|
|
2352
|
+
this._beginHistoryEdit(`Unpack ${entity.name}`);
|
|
2001
2353
|
for (const key of Object.keys(entity) as (keyof SceneEntity)[]) {
|
|
2002
2354
|
delete entity[key];
|
|
2003
2355
|
}
|
|
@@ -2022,10 +2374,17 @@ export class EditorStore {
|
|
|
2022
2374
|
};
|
|
2023
2375
|
this._documentType = 'prefab';
|
|
2024
2376
|
this._selectedEntityIds.clear();
|
|
2025
|
-
this.
|
|
2026
|
-
this.
|
|
2377
|
+
this._pendingHistoryEdit = null;
|
|
2378
|
+
this._historyGestureActive = false;
|
|
2027
2379
|
this._isDirty = false;
|
|
2028
2380
|
this._savePath = savePath ?? null;
|
|
2381
|
+
// #131: same baseline reset+re-arm as loadDocument. Prefab conflicts are
|
|
2382
|
+
// not adoptable (applyExternalUpdate speaks SceneFile), but the baseline
|
|
2383
|
+
// still gates the loud retry/overwrite path instead of a silent clobber.
|
|
2384
|
+
this._lastSavedContent = null;
|
|
2385
|
+
this._saveConflictSkips = 0;
|
|
2386
|
+
if (savePath) void this._captureDiskBaseline(savePath);
|
|
2387
|
+
this._bindDocumentHistory('prefab', savePath ?? 'prefabs/__untitled__.prefab.json');
|
|
2029
2388
|
|
|
2030
2389
|
if (this._scene) {
|
|
2031
2390
|
this._rebuildScene(doc);
|
|
@@ -2057,76 +2416,23 @@ export class EditorStore {
|
|
|
2057
2416
|
return;
|
|
2058
2417
|
}
|
|
2059
2418
|
if (!this._scene) return;
|
|
2060
|
-
this.
|
|
2419
|
+
this._beginHistoryEdit('Update Asset References');
|
|
2061
2420
|
// Serialize, rename, rebuild
|
|
2062
2421
|
const doc = serializeScene(this._scene, this._sceneMeta);
|
|
2063
2422
|
const renamed = applyAssetRename(doc, oldPath, newPath);
|
|
2064
2423
|
this._sceneMeta = { version: renamed.version, name: renamed.name };
|
|
2065
2424
|
if (renamed.environment) this._sceneMeta.environment = renamed.environment;
|
|
2066
|
-
if (renamed.ui) this._sceneMeta.ui = renamed.ui; // preserve scene-graph UI (T1.5)
|
|
2067
2425
|
this._rebuildScene(renamed);
|
|
2068
2426
|
this._notify();
|
|
2069
2427
|
}
|
|
2070
2428
|
|
|
2071
|
-
//
|
|
2072
|
-
|
|
2073
|
-
setActiveTab
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
openAssetTab(assetPath: string, kind: AssetKind): void {
|
|
2079
|
-
const existing = this._assetTabs.find((t) => t.key === assetPath);
|
|
2080
|
-
if (existing) {
|
|
2081
|
-
this._activeTabKey = existing.key;
|
|
2082
|
-
this._notify();
|
|
2083
|
-
return;
|
|
2084
|
-
}
|
|
2085
|
-
const label = assetPath.split('/').pop() ?? assetPath;
|
|
2086
|
-
const tab: AssetTab = { key: assetPath, label, assetPath, kind };
|
|
2087
|
-
this._assetTabs.push(tab);
|
|
2088
|
-
this._activeTabKey = tab.key;
|
|
2089
|
-
this._notify();
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
openOnlineAssetTab(online: OnlineAssetInfo): void {
|
|
2093
|
-
const key = `online:${online.source}:${online.id}`;
|
|
2094
|
-
const existing = this._assetTabs.find((t) => t.key === key);
|
|
2095
|
-
if (existing) {
|
|
2096
|
-
this._activeTabKey = existing.key;
|
|
2097
|
-
this._notify();
|
|
2098
|
-
return;
|
|
2099
|
-
}
|
|
2100
|
-
const kind: AssetKind =
|
|
2101
|
-
online.type === 'model'
|
|
2102
|
-
? 'model'
|
|
2103
|
-
: online.type === 'animation' || online.type === 'source'
|
|
2104
|
-
? 'source'
|
|
2105
|
-
: 'image';
|
|
2106
|
-
const tab: AssetTab = { key, label: online.name, assetPath: '', kind, online };
|
|
2107
|
-
this._assetTabs.push(tab);
|
|
2108
|
-
this._activeTabKey = tab.key;
|
|
2109
|
-
this._notify();
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
/** Update an online asset tab to point at a local path after download. */
|
|
2113
|
-
resolveOnlineAssetTab(onlineKey: string, localPath: string): void {
|
|
2114
|
-
const tab = this._assetTabs.find((t) => t.key === onlineKey);
|
|
2115
|
-
if (!tab) return;
|
|
2116
|
-
tab.assetPath = localPath;
|
|
2117
|
-
delete tab.online;
|
|
2118
|
-
this._notify();
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
closeAssetTab(key: string): void {
|
|
2122
|
-
const idx = this._assetTabs.findIndex((t) => t.key === key);
|
|
2123
|
-
if (idx < 0) return;
|
|
2124
|
-
this._assetTabs.splice(idx, 1);
|
|
2125
|
-
if (this._activeTabKey === key) {
|
|
2126
|
-
this._activeTabKey = '__inspector__';
|
|
2127
|
-
}
|
|
2128
|
-
this._notify();
|
|
2129
|
-
}
|
|
2429
|
+
// (The former right-rail asset-tab model — `AssetTab`, `openAssetTab`,
|
|
2430
|
+
// `openEntityAssetTab`, `openOnlineAssetTab`, `resolveOnlineAssetTab`,
|
|
2431
|
+
// `closeAssetTab`, `setActiveTab` — was deleted in W2 of the workspace-
|
|
2432
|
+
// shell program: asset viewers open as CENTER workspace documents via
|
|
2433
|
+
// `components/asset-documents.tsx` + `workspace-document-registry.ts`
|
|
2434
|
+
// (docs/workspace-shell/W0-INVENTORY.md rows R2–R7), and the Inspector is
|
|
2435
|
+
// never displaced from the right rail again.)
|
|
2130
2436
|
|
|
2131
2437
|
// --- Internal helpers ---
|
|
2132
2438
|
|
|
@@ -2196,7 +2502,6 @@ export class EditorStore {
|
|
|
2196
2502
|
private _loadFromDoc(doc: SceneFile): void {
|
|
2197
2503
|
this._sceneMeta = { version: doc.version, name: doc.name };
|
|
2198
2504
|
if (doc.environment) this._sceneMeta.environment = doc.environment;
|
|
2199
|
-
if (doc.ui) this._sceneMeta.ui = doc.ui; // preserve scene-graph UI through undo/redo (T1.5)
|
|
2200
2505
|
this._rebuildScene(doc);
|
|
2201
2506
|
}
|
|
2202
2507
|
|