@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
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
chromeSize,
|
|
4
|
+
IconButton,
|
|
5
|
+
space,
|
|
6
|
+
TextInput,
|
|
7
|
+
themeVars,
|
|
8
|
+
zIndex,
|
|
9
|
+
} from '../design-system';
|
|
1
10
|
/**
|
|
2
11
|
* GameHierarchy — the ONE hierarchy panel (A2,
|
|
3
12
|
* docs/unified-world-editor/26-unified-editor-spec.md §2 A2). Replaces the
|
|
@@ -15,12 +24,12 @@
|
|
|
15
24
|
* unpack/open/save-as) live behind `hierarchy-menu-registry.ts`'s escape
|
|
16
25
|
* hatch, registered elsewhere (`authoring/first-party-hierarchy-menu.ts`).
|
|
17
26
|
*
|
|
18
|
-
* Shape handling
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
27
|
+
* Shape handling: project authoring is a `CompositeAuthoringAdapter` rooted
|
|
28
|
+
* at Game for every N >= 1 composition. Each runtime world is a synthetic
|
|
29
|
+
* `world:<id>` group; adapter-owned surfaces such as scene UI may be nested
|
|
30
|
+
* beneath their owning world without pretending to be runtime roots. The
|
|
31
|
+
* same generic row engine also handles bare adapters used by bounded ingest
|
|
32
|
+
* and compatibility sessions. Both shapes are walked by the SAME generic row engine
|
|
24
33
|
* (`flattenIngestRows` — the format-neutral `EditorNode`-typed row
|
|
25
34
|
* flattener, not the legacy first-party-typed sibling module); a group node
|
|
26
35
|
* only ever gets a small set of cosmetic extras (kind/zOrder badge, a
|
|
@@ -35,23 +44,51 @@
|
|
|
35
44
|
* existing ingest-family e2e specs keep passing unchanged.
|
|
36
45
|
*/
|
|
37
46
|
|
|
38
|
-
import type {
|
|
47
|
+
import type {
|
|
48
|
+
AuthoringAdapter,
|
|
49
|
+
AuthoringProvenance,
|
|
50
|
+
EditorNode,
|
|
51
|
+
EditorNodeRole,
|
|
52
|
+
} from '@engine/adapter';
|
|
53
|
+
import type { IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
|
39
54
|
import {
|
|
55
|
+
faBookOpen,
|
|
56
|
+
faCamera,
|
|
40
57
|
faCaretDown,
|
|
41
58
|
faCaretRight,
|
|
59
|
+
faCircle,
|
|
60
|
+
faCircleDot,
|
|
61
|
+
faCirclePlay,
|
|
62
|
+
faCode,
|
|
42
63
|
faCrosshairs,
|
|
64
|
+
faCube,
|
|
43
65
|
faEye,
|
|
44
66
|
faEyeSlash,
|
|
67
|
+
faFileCode,
|
|
68
|
+
faFolder,
|
|
45
69
|
faHandPointer,
|
|
70
|
+
faImage,
|
|
71
|
+
faLayerGroup,
|
|
72
|
+
faLightbulb,
|
|
73
|
+
faLink,
|
|
74
|
+
faList,
|
|
46
75
|
faLock,
|
|
47
76
|
faLockOpen,
|
|
77
|
+
faMinus,
|
|
78
|
+
faObjectGroup,
|
|
48
79
|
faPlus,
|
|
80
|
+
faPuzzlePiece,
|
|
81
|
+
faRotateLeft,
|
|
82
|
+
faSquare,
|
|
83
|
+
faTriangleExclamation,
|
|
49
84
|
} from '@fortawesome/free-solid-svg-icons';
|
|
50
|
-
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
51
85
|
import { useCallback, useEffect, useReducer, useRef, useState, useSyncExternalStore } from 'react';
|
|
52
86
|
import { getActiveAuthoring, hasAuthoringOverride } from '../authoring/active-adapter';
|
|
53
87
|
import { CompositeAuthoringAdapter } from '../authoring/composite-authoring-adapter';
|
|
88
|
+
import { requestWorldFocus, worldFocusScenePath } from '../authoring/edit-mode-authoring';
|
|
89
|
+
import { provenanceForNode, unavailableReason } from '../authoring/provenance';
|
|
54
90
|
import { requestRootActivation } from '../authoring/root-activation';
|
|
91
|
+
import { resolveFocusedViewportWorldId } from '../authoring/world-hidden-viewport';
|
|
55
92
|
import {
|
|
56
93
|
isWorldHidden,
|
|
57
94
|
isWorldInteractive,
|
|
@@ -60,8 +97,15 @@ import {
|
|
|
60
97
|
toggleWorldInteractive,
|
|
61
98
|
toggleWorldPickLock,
|
|
62
99
|
} from '../authoring/world-session-state';
|
|
100
|
+
import { DisclosureIcon, DropIndicator, EditorIcon, EditorToolbar, Panel } from '../design-system';
|
|
63
101
|
import { useEditorStore } from '../EditorContext';
|
|
64
102
|
import type { EditorStore } from '../editor-store';
|
|
103
|
+
import { isSameWorldDrop } from '../hierarchy-drop';
|
|
104
|
+
import {
|
|
105
|
+
clearHierarchyExpansionPreferences,
|
|
106
|
+
hierarchyExpansionPreference,
|
|
107
|
+
setHierarchyExpansionPreferences,
|
|
108
|
+
} from '../hierarchy-expansion-state';
|
|
65
109
|
import { getHierarchyMenuItems } from '../hierarchy-menu-registry';
|
|
66
110
|
import { CHILD_CAP } from '../hierarchy-rows';
|
|
67
111
|
import { setActiveScope } from '../hotkeys';
|
|
@@ -71,12 +115,14 @@ import {
|
|
|
71
115
|
ingestRowKey,
|
|
72
116
|
ingestRowWindow,
|
|
73
117
|
} from '../ingest-hierarchy-rows';
|
|
118
|
+
import { getCurrentProject } from '../project-manager';
|
|
119
|
+
import { activateWorldDocument } from '../world-document-routing';
|
|
74
120
|
import { OverlayReportBanner } from './OverlayReportBanner';
|
|
75
|
-
import {
|
|
121
|
+
import { openStoryDocumentByStoryId } from './story-documents';
|
|
76
122
|
|
|
77
123
|
const ASSET_MIME = 'application/x-editor-asset';
|
|
78
124
|
const REORDER_MIME = 'application/x-hierarchy-reorder';
|
|
79
|
-
const ROW_HEIGHT =
|
|
125
|
+
const ROW_HEIGHT = chromeSize.treeRow;
|
|
80
126
|
const INDENT = 14;
|
|
81
127
|
|
|
82
128
|
// Wave-18-ported scroll-window sizing (docs/WAVE18-INGEST-HIERARCHY-VIRTUALIZATION-DESIGN.md),
|
|
@@ -102,19 +148,49 @@ const DEFAULT_VIEWPORT_PX = 2400;
|
|
|
102
148
|
// dungeon is ~1900 rows), which is exactly the case wave-18 targeted.
|
|
103
149
|
const VIRTUALIZE_MIN_ROWS = 300;
|
|
104
150
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
151
|
+
const ROLE_ICON: Partial<Record<EditorNodeRole, IconDefinition>> = {
|
|
152
|
+
folder: faFolder,
|
|
153
|
+
document: faFileCode,
|
|
154
|
+
story: faCirclePlay,
|
|
155
|
+
component: faPuzzlePiece,
|
|
156
|
+
boundary: faTriangleExclamation,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const KIND_ICON: Record<string, IconDefinition> = {
|
|
160
|
+
threejs: faCube,
|
|
161
|
+
react: faCode,
|
|
162
|
+
pixijs: faLayerGroup,
|
|
163
|
+
scene: faBookOpen,
|
|
164
|
+
mesh: faCube,
|
|
165
|
+
light: faLightbulb,
|
|
166
|
+
camera: faCamera,
|
|
167
|
+
group: faObjectGroup,
|
|
168
|
+
object: faCircle,
|
|
169
|
+
component: faPuzzlePiece,
|
|
170
|
+
img: faImage,
|
|
171
|
+
a: faLink,
|
|
172
|
+
ul: faList,
|
|
173
|
+
ol: faList,
|
|
174
|
+
li: faList,
|
|
175
|
+
button: faSquare,
|
|
116
176
|
};
|
|
117
177
|
|
|
178
|
+
function iconForNode(node: EditorNode): IconDefinition {
|
|
179
|
+
if (node.role === 'root') return KIND_ICON[node.kind] ?? faLayerGroup;
|
|
180
|
+
if (node.role === 'entity' || node.role === 'element') {
|
|
181
|
+
return KIND_ICON[node.kind] ?? faCircle;
|
|
182
|
+
}
|
|
183
|
+
return (node.role ? ROLE_ICON[node.role] : undefined) ?? KIND_ICON[node.kind] ?? faCircle;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const STRUCTURAL_ROLES = new Set<EditorNodeRole>([
|
|
187
|
+
'folder',
|
|
188
|
+
'root',
|
|
189
|
+
'document',
|
|
190
|
+
'story',
|
|
191
|
+
'boundary',
|
|
192
|
+
]);
|
|
193
|
+
|
|
118
194
|
type DropZone = 'before' | 'child' | 'after';
|
|
119
195
|
interface DropTarget {
|
|
120
196
|
nodeId: string;
|
|
@@ -166,11 +242,17 @@ function siblingIdsOf(adapter: AuthoringAdapter, parentId: string | null): strin
|
|
|
166
242
|
function compositeGroupBadge(
|
|
167
243
|
adapter: AuthoringAdapter,
|
|
168
244
|
nodeId: string,
|
|
169
|
-
): {
|
|
245
|
+
): {
|
|
246
|
+
zOrder: number;
|
|
247
|
+
worldId: string;
|
|
248
|
+
role: 'world' | 'surface';
|
|
249
|
+
kind: string;
|
|
250
|
+
provenance: AuthoringProvenance | null;
|
|
251
|
+
} | null {
|
|
170
252
|
if (!(adapter instanceof CompositeAuthoringAdapter)) return null;
|
|
171
253
|
for (const child of adapter.childAdapters()) {
|
|
172
254
|
if (adapter.groupNodeId(child.worldId) === nodeId) {
|
|
173
|
-
// A4 (D8) — prefer the LIVE manifest zOrder (editable on the
|
|
255
|
+
// A4 (D8) — prefer the LIVE manifest zOrder (editable on the root row,
|
|
174
256
|
// `composite.inspector.get(groupId, 'zOrder')`) over the array-index
|
|
175
257
|
// fallback `child.zOrder`, so editing zOrder through the generic
|
|
176
258
|
// inspector is reflected on this badge immediately (the write path
|
|
@@ -187,6 +269,11 @@ function compositeGroupBadge(
|
|
|
187
269
|
// — eye/interactive/pick-lock) consistently with `design-time-layers.ts`,
|
|
188
270
|
// which reads the SAME id off `composite.childAdapters()` directly.
|
|
189
271
|
worldId: child.worldId,
|
|
272
|
+
role: child.role,
|
|
273
|
+
kind: child.kind,
|
|
274
|
+
// Spec 29 §5 — the world's own seam-level provenance declaration
|
|
275
|
+
// (null when the child adapter declared none; the row shows no chip).
|
|
276
|
+
provenance: child.adapter.provenance ?? null,
|
|
190
277
|
};
|
|
191
278
|
}
|
|
192
279
|
}
|
|
@@ -200,15 +287,23 @@ function isCompositeGroupNode(adapter: AuthoringAdapter, nodeId: string): boolea
|
|
|
200
287
|
return compositeGroupBadge(adapter, nodeId) !== null;
|
|
201
288
|
}
|
|
202
289
|
|
|
290
|
+
/** Editor-only kind/semantic projection rows have no native entity owner. */
|
|
291
|
+
function isCompositeOrganizationNode(adapter: AuthoringAdapter, nodeId: string): boolean {
|
|
292
|
+
return adapter instanceof CompositeAuthoringAdapter && adapter.isOrganizationNode(nodeId);
|
|
293
|
+
}
|
|
294
|
+
|
|
203
295
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
296
|
+
* The composite child adapter that OWNS `nodeId` — the adapter whose
|
|
297
|
+
* PROVIDERS answer for it — or the active adapter itself when bare. Affordance
|
|
298
|
+
* GATES must ask the owner (the composite's own `structure`/`inspector` are
|
|
299
|
+
* never-undefined routers, so probing them says "yes" for a row whose owning
|
|
300
|
+
* child would refuse — the false-claim pattern spec 29 §3 forbids); ACTIONS
|
|
301
|
+
* still go through the ACTIVE adapter, which routes by ownership.
|
|
209
302
|
*/
|
|
210
|
-
function
|
|
211
|
-
|
|
303
|
+
function ownerAdapterOf(adapter: AuthoringAdapter, nodeId: string): AuthoringAdapter {
|
|
304
|
+
if (!(adapter instanceof CompositeAuthoringAdapter)) return adapter;
|
|
305
|
+
const worldId = adapter.ownerOf(nodeId);
|
|
306
|
+
return adapter.childAdapters().find((c) => c.worldId === worldId)?.adapter ?? adapter;
|
|
212
307
|
}
|
|
213
308
|
|
|
214
309
|
/**
|
|
@@ -230,6 +325,141 @@ function buildRootMembers(
|
|
|
230
325
|
return map;
|
|
231
326
|
}
|
|
232
327
|
|
|
328
|
+
function isStructuralHierarchyNode(
|
|
329
|
+
adapter: AuthoringAdapter,
|
|
330
|
+
nodeId: string,
|
|
331
|
+
role: EditorNodeRole | undefined,
|
|
332
|
+
rootMembers: ReadonlyMap<string, { groupId: string; active: boolean }>,
|
|
333
|
+
): boolean {
|
|
334
|
+
return (
|
|
335
|
+
isCompositeGroupNode(adapter, nodeId) ||
|
|
336
|
+
isCompositeOrganizationNode(adapter, nodeId) ||
|
|
337
|
+
rootMembers.has(nodeId) ||
|
|
338
|
+
(role !== undefined && STRUCTURAL_ROLES.has(role))
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* First-open policy: organizational rows stay open, as does the first real
|
|
344
|
+
* authored node below them. Single-child wrapper chains continue open until
|
|
345
|
+
* the first meaningful branch; branches beneath that point start folded.
|
|
346
|
+
* This is shape-based — never label- or kind-based — so the same rule covers
|
|
347
|
+
* Three.js, React, PixiJS, and arbitrary projections.
|
|
348
|
+
*/
|
|
349
|
+
function defaultExpansionByNode(
|
|
350
|
+
rows: readonly IngestRow[],
|
|
351
|
+
adapter: AuthoringAdapter,
|
|
352
|
+
rootMembers: ReadonlyMap<string, { groupId: string; active: boolean }>,
|
|
353
|
+
): Map<string, boolean> {
|
|
354
|
+
const result = new Map<string, boolean>();
|
|
355
|
+
const authoredDepthAtTreeDepth: number[] = [];
|
|
356
|
+
const nodeAtTreeDepth: IngestRow['node'][] = [];
|
|
357
|
+
for (const row of rows) {
|
|
358
|
+
if (row.more) continue;
|
|
359
|
+
const parentAuthoredDepth =
|
|
360
|
+
row.depth === 0 ? 0 : (authoredDepthAtTreeDepth[row.depth - 1] ?? 0);
|
|
361
|
+
const parentNode = row.depth === 0 ? null : nodeAtTreeDepth[row.depth - 1];
|
|
362
|
+
const structural = isStructuralHierarchyNode(adapter, row.node.id, row.node.role, rootMembers);
|
|
363
|
+
const authoredDepth = parentAuthoredDepth + (structural ? 0 : 1);
|
|
364
|
+
authoredDepthAtTreeDepth[row.depth] = authoredDepth;
|
|
365
|
+
authoredDepthAtTreeDepth.length = row.depth + 1;
|
|
366
|
+
nodeAtTreeDepth[row.depth] = row.node;
|
|
367
|
+
nodeAtTreeDepth.length = row.depth + 1;
|
|
368
|
+
result.set(row.node.id, structural || authoredDepth <= 1 || parentNode?.childIds.length === 1);
|
|
369
|
+
}
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function ancestorPathKeys(pathKey: string): string[] {
|
|
374
|
+
const parts = pathKey.split('.');
|
|
375
|
+
const ancestors: string[] = [];
|
|
376
|
+
for (let i = 1; i < parts.length; i++) ancestors.push(parts.slice(0, i).join('.'));
|
|
377
|
+
return ancestors;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function searchPathKeys(rows: readonly IngestRow[], term: string): Set<string> {
|
|
381
|
+
const visible = new Set<string>();
|
|
382
|
+
for (const row of rows) {
|
|
383
|
+
if (row.more || !row.node.label.toLowerCase().includes(term)) continue;
|
|
384
|
+
visible.add(row.pathKey);
|
|
385
|
+
for (const ancestor of ancestorPathKeys(row.pathKey)) visible.add(ancestor);
|
|
386
|
+
}
|
|
387
|
+
return visible;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Make capped browse mode include a selected node at any sibling index. */
|
|
391
|
+
function selectedRevealCounts(
|
|
392
|
+
rows: readonly IngestRow[],
|
|
393
|
+
selectedIds: ReadonlySet<string>,
|
|
394
|
+
): Map<string, number> {
|
|
395
|
+
const revealed = new Map<string, number>();
|
|
396
|
+
for (const row of rows) {
|
|
397
|
+
if (!selectedIds.has(row.node.id)) continue;
|
|
398
|
+
const parts = row.pathKey.split('.');
|
|
399
|
+
let parent = parts[0]!;
|
|
400
|
+
for (let i = 1; i < parts.length; i++) {
|
|
401
|
+
const childIndex = Number(parts[i]);
|
|
402
|
+
if (Number.isFinite(childIndex)) {
|
|
403
|
+
revealed.set(parent, Math.max(revealed.get(parent) ?? 0, childIndex + 1));
|
|
404
|
+
}
|
|
405
|
+
parent = `${parent}.${parts[i]}`;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return revealed;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function selectedAncestorNodeIds(
|
|
412
|
+
rows: readonly IngestRow[],
|
|
413
|
+
selectedIds: ReadonlySet<string>,
|
|
414
|
+
): Set<string> {
|
|
415
|
+
const ids = new Set<string>();
|
|
416
|
+
const byPath = new Map(rows.filter((row) => !row.more).map((row) => [row.pathKey, row.node.id]));
|
|
417
|
+
for (const row of rows) {
|
|
418
|
+
if (!selectedIds.has(row.node.id)) continue;
|
|
419
|
+
for (const ancestor of ancestorPathKeys(row.pathKey)) {
|
|
420
|
+
const id = byPath.get(ancestor);
|
|
421
|
+
if (id) ids.add(id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return ids;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function mergeRevealCounts(
|
|
428
|
+
persisted: ReadonlyMap<string, number>,
|
|
429
|
+
temporary: ReadonlyMap<string, number>,
|
|
430
|
+
): Map<string, number> {
|
|
431
|
+
const merged = new Map(persisted);
|
|
432
|
+
for (const [pathKey, count] of temporary) {
|
|
433
|
+
merged.set(pathKey, Math.max(merged.get(pathKey) ?? 0, count));
|
|
434
|
+
}
|
|
435
|
+
return merged;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function ViewButton({
|
|
439
|
+
label,
|
|
440
|
+
icon,
|
|
441
|
+
testId,
|
|
442
|
+
onClick,
|
|
443
|
+
}: {
|
|
444
|
+
label: string;
|
|
445
|
+
icon: IconDefinition;
|
|
446
|
+
testId: string;
|
|
447
|
+
onClick: () => void;
|
|
448
|
+
}) {
|
|
449
|
+
return (
|
|
450
|
+
<IconButton
|
|
451
|
+
title={label}
|
|
452
|
+
aria-label={label}
|
|
453
|
+
data-testid={testId}
|
|
454
|
+
onClick={onClick}
|
|
455
|
+
variant="secondary"
|
|
456
|
+
size="compact"
|
|
457
|
+
>
|
|
458
|
+
<EditorIcon icon={icon} aria-hidden="true" />
|
|
459
|
+
</IconButton>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
233
463
|
// --- Context menu ---
|
|
234
464
|
|
|
235
465
|
interface ContextMenuState {
|
|
@@ -283,54 +513,118 @@ function ContextMenu({
|
|
|
283
513
|
if (!node) return null;
|
|
284
514
|
|
|
285
515
|
const creatableKinds = adapter.structure?.creatableKinds?.(state.nodeId) ?? [];
|
|
286
|
-
|
|
516
|
+
// Gates ask the OWNING child (see ownerAdapterOf) — the composite's own
|
|
517
|
+
// structure/inspector are never-undefined routers, so probing THEM would
|
|
518
|
+
// advertise operations the owner refuses (live-caught on a Boundary row:
|
|
519
|
+
// Duplicate/Hide/Delete rendered enabled and no-op'd). `visible` is a
|
|
520
|
+
// VALUE probe like `name`/`locked`: every live adapter answers it, a
|
|
521
|
+
// Boundary answers undefined.
|
|
522
|
+
const owner = ownerAdapterOf(adapter, state.nodeId);
|
|
523
|
+
const ownerStructure = owner.structure;
|
|
524
|
+
const hasStructure = !!ownerStructure;
|
|
287
525
|
const nameKnown = adapter.inspector?.get(state.nodeId, 'name') !== undefined;
|
|
288
526
|
const lockedKnown = adapter.inspector?.get(state.nodeId, 'locked') !== undefined;
|
|
289
527
|
const locked = adapter.inspector?.get(state.nodeId, 'locked') === true;
|
|
290
|
-
const visibleKnown = adapter.inspector !== undefined;
|
|
528
|
+
const visibleKnown = adapter.inspector?.get(state.nodeId, 'visible') !== undefined;
|
|
291
529
|
const visible = adapter.inspector?.get(state.nodeId, 'visible') !== false;
|
|
292
530
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
531
|
+
// Spec 29 §5 disabled-with-reason: when the SUBJECT of an operation is
|
|
532
|
+
// present (the row exists) but the adapter can't perform it, the item
|
|
533
|
+
// renders DISABLED with the world's own provenance explanation as its
|
|
534
|
+
// tooltip — the complement of spec 28's rule (chrome whose subject is
|
|
535
|
+
// absent is hidden, not disabled).
|
|
536
|
+
const reason = unavailableReason(provenanceForNode(adapter, state.nodeId));
|
|
537
|
+
const items: {
|
|
538
|
+
label: string;
|
|
539
|
+
testId?: string;
|
|
540
|
+
action: () => void;
|
|
541
|
+
color?: string;
|
|
542
|
+
disabledReason?: string;
|
|
543
|
+
}[] = [];
|
|
544
|
+
|
|
545
|
+
// A4 (focus-switch) — a group row's ONE menu verb: make an unfocused
|
|
546
|
+
// threejs world (with a loadable scene) the live one. Same dirty-gated
|
|
547
|
+
// path as the row's double-click.
|
|
548
|
+
if (isGroupNode) {
|
|
549
|
+
const groupBadge = compositeGroupBadge(adapter, state.nodeId);
|
|
550
|
+
if (groupBadge && worldFocusScenePath(groupBadge.worldId) !== null) {
|
|
297
551
|
items.push({
|
|
298
|
-
label: '
|
|
552
|
+
label: 'Edit This World',
|
|
299
553
|
action: () => {
|
|
300
|
-
|
|
554
|
+
requestWorldFocus(store, groupBadge.worldId);
|
|
301
555
|
onClose();
|
|
302
556
|
},
|
|
303
557
|
});
|
|
304
558
|
}
|
|
305
|
-
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (!isGroupNode) {
|
|
562
|
+
items.push({
|
|
563
|
+
label: 'Rename',
|
|
564
|
+
testId: 'hierarchy-context-menu-rename',
|
|
565
|
+
...(nameKnown ? {} : { disabledReason: reason }),
|
|
566
|
+
action: () => {
|
|
567
|
+
onRename(state.nodeId);
|
|
568
|
+
onClose();
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
items.push({
|
|
572
|
+
label: 'Duplicate',
|
|
573
|
+
testId: 'hierarchy-context-menu-duplicate',
|
|
574
|
+
...(hasStructure ? {} : { disabledReason: reason }),
|
|
575
|
+
action: () => {
|
|
576
|
+
adapter.structure!.duplicate(state.nodeId);
|
|
577
|
+
onClose();
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
if (ownerStructure?.wrap) {
|
|
306
581
|
items.push({
|
|
307
|
-
label: '
|
|
582
|
+
label: 'Wrap',
|
|
583
|
+
testId: 'hierarchy-context-menu-wrap',
|
|
308
584
|
action: () => {
|
|
309
|
-
|
|
585
|
+
ownerStructure.wrap!(state.nodeId);
|
|
310
586
|
onClose();
|
|
311
587
|
},
|
|
312
588
|
});
|
|
313
589
|
}
|
|
314
|
-
if (
|
|
590
|
+
if (ownerStructure?.unwrap) {
|
|
315
591
|
items.push({
|
|
316
|
-
label:
|
|
592
|
+
label: 'Unwrap',
|
|
593
|
+
testId: 'hierarchy-context-menu-unwrap',
|
|
317
594
|
action: () => {
|
|
318
|
-
|
|
319
|
-
store.notifyIngestEdit();
|
|
595
|
+
ownerStructure.unwrap!(state.nodeId);
|
|
320
596
|
onClose();
|
|
321
597
|
},
|
|
322
598
|
});
|
|
323
599
|
}
|
|
324
|
-
if (
|
|
600
|
+
if (node.parentId !== null) {
|
|
325
601
|
items.push({
|
|
326
|
-
label:
|
|
602
|
+
label: 'Select Parent',
|
|
603
|
+
testId: 'hierarchy-context-menu-select-parent',
|
|
327
604
|
action: () => {
|
|
328
|
-
adapter.
|
|
329
|
-
store.notifyIngestEdit();
|
|
605
|
+
adapter.selection?.set([node.parentId!]);
|
|
330
606
|
onClose();
|
|
331
607
|
},
|
|
332
608
|
});
|
|
333
609
|
}
|
|
610
|
+
items.push({
|
|
611
|
+
label: visibleKnown && !visible ? 'Show' : 'Hide',
|
|
612
|
+
...(visibleKnown ? {} : { disabledReason: reason }),
|
|
613
|
+
action: () => {
|
|
614
|
+
adapter.inspector!.set(state.nodeId, 'visible', !visible);
|
|
615
|
+
store.notifyIngestEdit();
|
|
616
|
+
onClose();
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
items.push({
|
|
620
|
+
label: locked ? 'Unlock' : 'Lock',
|
|
621
|
+
...(lockedKnown ? {} : { disabledReason: reason }),
|
|
622
|
+
action: () => {
|
|
623
|
+
adapter.inspector!.set(state.nodeId, 'locked', !locked);
|
|
624
|
+
store.notifyIngestEdit();
|
|
625
|
+
onClose();
|
|
626
|
+
},
|
|
627
|
+
});
|
|
334
628
|
// First-party escape-hatch extras (prefab unpack/open/save-as, …) —
|
|
335
629
|
// permanent adapter-module contribution seam (see hierarchy-menu-registry.ts).
|
|
336
630
|
for (const extra of getHierarchyMenuItems({ nodeId: state.nodeId, store })) {
|
|
@@ -343,16 +637,16 @@ function ContextMenu({
|
|
|
343
637
|
},
|
|
344
638
|
});
|
|
345
639
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
}
|
|
640
|
+
items.push({
|
|
641
|
+
label: 'Delete',
|
|
642
|
+
testId: 'hierarchy-context-menu-delete',
|
|
643
|
+
color: themeVars.semantic.danger,
|
|
644
|
+
...(hasStructure ? {} : { disabledReason: reason }),
|
|
645
|
+
action: () => {
|
|
646
|
+
adapter.structure!.remove(state.nodeId);
|
|
647
|
+
onClose();
|
|
648
|
+
},
|
|
649
|
+
});
|
|
356
650
|
}
|
|
357
651
|
|
|
358
652
|
return (
|
|
@@ -363,39 +657,52 @@ function ContextMenu({
|
|
|
363
657
|
position: 'fixed',
|
|
364
658
|
left: state.x,
|
|
365
659
|
top: state.y,
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
660
|
+
// Menus/popovers contract (editor-style-polish §5): bg-2/border-2/
|
|
661
|
+
// radius-md/shadow-md, item padding aligned to `.vgai-menu-item`.
|
|
662
|
+
background: themeVars.surface.chrome,
|
|
663
|
+
border: `1px solid ${themeVars.boundary.strong}`,
|
|
664
|
+
borderRadius: themeVars.shape.medium,
|
|
665
|
+
padding: `${space[2]}px 0`,
|
|
666
|
+
zIndex: zIndex.sticky,
|
|
371
667
|
minWidth: 150,
|
|
372
668
|
maxWidth: 260,
|
|
373
|
-
boxShadow:
|
|
374
|
-
fontSize:
|
|
375
|
-
color:
|
|
669
|
+
boxShadow: themeVars.elevation.medium,
|
|
670
|
+
fontSize: 'var(--vgai-font-base)',
|
|
671
|
+
color: themeVars.content.primary,
|
|
376
672
|
}}
|
|
377
673
|
>
|
|
378
674
|
{/* A4 (D8): also suppressed for a root-group MEMBER row, same as an
|
|
379
675
|
existing composite group row — `isGroupNode` covers both (see the
|
|
380
|
-
caller's computation below). Create
|
|
676
|
+
caller's computation below). The Create submenu on a member id would silently
|
|
381
677
|
land at the scene root instead of doing anything meaningful. */}
|
|
382
678
|
{!isGroupNode && creatableKinds.length > 0 && (
|
|
383
679
|
<div>
|
|
384
680
|
<div
|
|
681
|
+
data-testid="hierarchy-context-menu-create"
|
|
385
682
|
role="menuitem"
|
|
386
683
|
tabIndex={0}
|
|
387
684
|
onClick={() => setCreateOpen((v) => !v)}
|
|
388
685
|
onKeyDown={(e) => {
|
|
389
686
|
if (e.key === 'Enter') setCreateOpen((v) => !v);
|
|
390
687
|
}}
|
|
391
|
-
style={{
|
|
688
|
+
style={{
|
|
689
|
+
display: 'flex',
|
|
690
|
+
alignItems: 'center',
|
|
691
|
+
justifyContent: 'space-between',
|
|
692
|
+
gap: space[4],
|
|
693
|
+
padding: `${space[2]}px ${space[4]}px`,
|
|
694
|
+
cursor: 'pointer',
|
|
695
|
+
}}
|
|
392
696
|
>
|
|
393
|
-
Create
|
|
697
|
+
<span>Create</span>
|
|
698
|
+
<DisclosureIcon direction="right" tone="dim" />
|
|
394
699
|
</div>
|
|
395
700
|
{createOpen &&
|
|
396
701
|
creatableKinds.map((k) => (
|
|
397
702
|
<div
|
|
703
|
+
className="vgai-menu-item"
|
|
398
704
|
key={k.kind}
|
|
705
|
+
data-testid={`hierarchy-context-menu-create-${k.kind}`}
|
|
399
706
|
role="menuitem"
|
|
400
707
|
tabIndex={0}
|
|
401
708
|
onClick={() => {
|
|
@@ -412,12 +719,9 @@ function ContextMenu({
|
|
|
412
719
|
onClose();
|
|
413
720
|
}
|
|
414
721
|
}}
|
|
415
|
-
style={{
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}}
|
|
419
|
-
onMouseLeave={(e) => {
|
|
420
|
-
(e.currentTarget as HTMLDivElement).style.background = 'transparent';
|
|
722
|
+
style={{
|
|
723
|
+
padding: `${space[2]}px ${space[4]}px ${space[2]}px 24px`,
|
|
724
|
+
cursor: 'pointer',
|
|
421
725
|
}}
|
|
422
726
|
>
|
|
423
727
|
{k.label}
|
|
@@ -425,33 +729,60 @@ function ContextMenu({
|
|
|
425
729
|
))}
|
|
426
730
|
</div>
|
|
427
731
|
)}
|
|
428
|
-
{items.map((item) =>
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
732
|
+
{items.map((item) =>
|
|
733
|
+
item.disabledReason !== undefined ? (
|
|
734
|
+
<div
|
|
735
|
+
className="vgai-menu-item"
|
|
736
|
+
key={item.label}
|
|
737
|
+
data-testid={item.testId}
|
|
738
|
+
role="menuitem"
|
|
739
|
+
aria-disabled="true"
|
|
740
|
+
title={item.disabledReason}
|
|
741
|
+
style={{
|
|
742
|
+
padding: `${space[2]}px ${space[4]}px`,
|
|
743
|
+
// L-21 — disabled rows get a dedicated muted color (not just
|
|
744
|
+
// opacity-faded active text) plus a not-allowed cursor.
|
|
745
|
+
cursor: 'not-allowed',
|
|
746
|
+
color: themeVars.content.dim,
|
|
747
|
+
opacity: 0.7,
|
|
748
|
+
}}
|
|
749
|
+
>
|
|
750
|
+
{item.label}
|
|
751
|
+
</div>
|
|
752
|
+
) : (
|
|
753
|
+
<div
|
|
754
|
+
key={item.label}
|
|
755
|
+
data-testid={item.testId}
|
|
756
|
+
role="menuitem"
|
|
757
|
+
tabIndex={0}
|
|
758
|
+
onClick={item.action}
|
|
759
|
+
onKeyDown={(e) => {
|
|
760
|
+
if (e.key === 'Enter') item.action();
|
|
761
|
+
}}
|
|
762
|
+
style={{
|
|
763
|
+
padding: `${space[2]}px ${space[4]}px`,
|
|
764
|
+
cursor: 'pointer',
|
|
765
|
+
color: item.color ?? themeVars.content.primary,
|
|
766
|
+
}}
|
|
767
|
+
>
|
|
768
|
+
{item.label}
|
|
769
|
+
</div>
|
|
770
|
+
),
|
|
771
|
+
)}
|
|
448
772
|
{/* Bounded-authoring honest degrade (landmine f): the ACTIVE adapter has
|
|
449
773
|
no `structure` provider at all (an ingested/foreign game owns its
|
|
450
774
|
own scene graph) — Rename/Show-Hide/Lock above still work (they're
|
|
451
775
|
gated independently, on `inspector`), but Create/Duplicate/Delete/
|
|
452
776
|
Reparent are not, so say so plainly instead of hiding the menu. */}
|
|
453
777
|
{!hasStructure && (
|
|
454
|
-
<div
|
|
778
|
+
<div
|
|
779
|
+
style={{
|
|
780
|
+
padding: `${space[4]}px ${space[6]}px`,
|
|
781
|
+
color: themeVars.semantic.warning,
|
|
782
|
+
maxWidth: 240,
|
|
783
|
+
lineHeight: 1.4,
|
|
784
|
+
}}
|
|
785
|
+
>
|
|
455
786
|
<strong>Create / Delete / Reparent — not available.</strong> This game owns its own scene
|
|
456
787
|
graph, so structural authoring isn't reapplicable. You can edit transform / material /
|
|
457
788
|
visibility, or capture a subtree into the first-party editable format.
|
|
@@ -474,6 +805,7 @@ function CreateMenu({ adapter, parentId }: { adapter: AuthoringAdapter; parentId
|
|
|
474
805
|
// the same commit; a ref reset on each open bounds it to one create per open.
|
|
475
806
|
const creatingRef = useRef(false);
|
|
476
807
|
const kinds = adapter.structure?.creatableKinds?.(parentId) ?? [];
|
|
808
|
+
const targetLabel = parentId ? adapter.hierarchy.node(parentId)?.label : null;
|
|
477
809
|
|
|
478
810
|
useEffect(() => {
|
|
479
811
|
if (!open) return;
|
|
@@ -496,42 +828,42 @@ function CreateMenu({ adapter, parentId }: { adapter: AuthoringAdapter; parentId
|
|
|
496
828
|
|
|
497
829
|
return (
|
|
498
830
|
<div ref={ref} style={{ position: 'relative', flexShrink: 0 }}>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
831
|
+
{/* L-11 — same fixed 20×20 boxed treatment as its `ViewButton` siblings
|
|
832
|
+
in the toolbar row (collapse/expand/reset), instead of an unboxed
|
|
833
|
+
inline span with mismatched padding. */}
|
|
834
|
+
<Button
|
|
835
|
+
title={targetLabel ? `Add to ${targetLabel}` : 'Add'}
|
|
836
|
+
aria-label={targetLabel ? `Add to ${targetLabel}` : 'Add'}
|
|
837
|
+
aria-expanded={open}
|
|
502
838
|
data-testid="hierarchy-add-menu"
|
|
503
839
|
onClick={() => setOpen(!open)}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
fontSize: 13,
|
|
507
|
-
padding: '1px 4px',
|
|
508
|
-
borderRadius: 3,
|
|
509
|
-
background: open ? '#3a3a3a' : 'transparent',
|
|
510
|
-
userSelect: 'none',
|
|
511
|
-
color: '#c5c8ce',
|
|
512
|
-
}}
|
|
840
|
+
variant={open ? 'primary' : 'secondary'}
|
|
841
|
+
size="compact"
|
|
513
842
|
>
|
|
514
|
-
<
|
|
515
|
-
<
|
|
516
|
-
</
|
|
843
|
+
<EditorIcon icon={faPlus} style={{ fontSize: 'var(--vgai-font-sm)' }} />{' '}
|
|
844
|
+
<EditorIcon icon={faCaretDown} style={{ fontSize: 'var(--vgai-font-xs)' }} />
|
|
845
|
+
</Button>
|
|
517
846
|
{open && (
|
|
518
847
|
<div
|
|
519
848
|
style={{
|
|
520
849
|
position: 'absolute',
|
|
521
850
|
top: '100%',
|
|
522
851
|
right: 0,
|
|
523
|
-
marginTop:
|
|
524
|
-
background:
|
|
525
|
-
border:
|
|
526
|
-
borderRadius:
|
|
527
|
-
padding:
|
|
528
|
-
zIndex:
|
|
852
|
+
marginTop: space[2],
|
|
853
|
+
background: themeVars.surface.chrome,
|
|
854
|
+
border: `1px solid ${themeVars.boundary.strong}`,
|
|
855
|
+
borderRadius: themeVars.shape.medium,
|
|
856
|
+
padding: `${space[2]}px 0`,
|
|
857
|
+
zIndex: zIndex.sticky,
|
|
529
858
|
minWidth: 130,
|
|
530
|
-
|
|
859
|
+
maxHeight: 'min(480px, 70vh)',
|
|
860
|
+
overflowY: 'auto',
|
|
861
|
+
boxShadow: themeVars.elevation.medium,
|
|
531
862
|
}}
|
|
532
863
|
>
|
|
533
864
|
{kinds.map((k) => (
|
|
534
865
|
<div
|
|
866
|
+
className="vgai-menu-item"
|
|
535
867
|
key={k.kind}
|
|
536
868
|
data-testid={`hierarchy-add-${k.kind}`}
|
|
537
869
|
onClick={() => {
|
|
@@ -540,12 +872,11 @@ function CreateMenu({ adapter, parentId }: { adapter: AuthoringAdapter; parentId
|
|
|
540
872
|
adapter.structure!.create(k.kind, parentId ?? undefined);
|
|
541
873
|
setOpen(false);
|
|
542
874
|
}}
|
|
543
|
-
style={{
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
(e.currentTarget as HTMLDivElement).style.background = 'transparent';
|
|
875
|
+
style={{
|
|
876
|
+
padding: `${space[2]}px ${space[4]}px`,
|
|
877
|
+
cursor: 'pointer',
|
|
878
|
+
fontSize: 'var(--vgai-font-base)',
|
|
879
|
+
color: themeVars.content.primary,
|
|
549
880
|
}}
|
|
550
881
|
>
|
|
551
882
|
{k.label}
|
|
@@ -562,27 +893,18 @@ function CreateMenu({ adapter, parentId }: { adapter: AuthoringAdapter; parentId
|
|
|
562
893
|
function MoreRow({ row, onReveal }: { row: IngestRow; onReveal: (parentPathKey: string) => void }) {
|
|
563
894
|
const hidden = row.more!.hidden;
|
|
564
895
|
return (
|
|
565
|
-
<
|
|
896
|
+
<Button
|
|
566
897
|
type="button"
|
|
898
|
+
variant="ghost"
|
|
899
|
+
className="vgai-hierarchy-more"
|
|
567
900
|
data-testid="ingest-row-more"
|
|
568
901
|
onClick={() => onReveal(row.more!.parentPathKey)}
|
|
569
902
|
style={{
|
|
570
|
-
display: 'flex',
|
|
571
|
-
alignItems: 'center',
|
|
572
|
-
width: '100%',
|
|
573
|
-
height: ROW_HEIGHT,
|
|
574
903
|
paddingLeft: row.depth * INDENT + INDENT,
|
|
575
|
-
border: 'none',
|
|
576
|
-
background: 'transparent',
|
|
577
|
-
color: '#7d828c',
|
|
578
|
-
fontSize: 12,
|
|
579
|
-
fontStyle: 'italic',
|
|
580
|
-
textAlign: 'left',
|
|
581
|
-
cursor: 'pointer',
|
|
582
904
|
}}
|
|
583
905
|
>
|
|
584
906
|
… {hidden} more
|
|
585
|
-
</
|
|
907
|
+
</Button>
|
|
586
908
|
);
|
|
587
909
|
}
|
|
588
910
|
|
|
@@ -608,7 +930,11 @@ function highlightSegments(label: string, term: string): React.ReactNode {
|
|
|
608
930
|
<mark
|
|
609
931
|
key={key++}
|
|
610
932
|
data-testid="ingest-row-search-highlight"
|
|
611
|
-
style={{
|
|
933
|
+
style={{
|
|
934
|
+
background: themeVars.semantic.warning,
|
|
935
|
+
color: themeVars.surface.shell,
|
|
936
|
+
borderRadius: themeVars.shape.small,
|
|
937
|
+
}}
|
|
612
938
|
>
|
|
613
939
|
{label.slice(idx, idx + needle.length)}
|
|
614
940
|
</mark>,
|
|
@@ -633,6 +959,7 @@ function Row({
|
|
|
633
959
|
searchTerm,
|
|
634
960
|
onToggleOpen,
|
|
635
961
|
onStartEditing,
|
|
962
|
+
focusedWorldId,
|
|
636
963
|
onContextMenu,
|
|
637
964
|
onDragStart,
|
|
638
965
|
onClick,
|
|
@@ -649,28 +976,36 @@ function Row({
|
|
|
649
976
|
/** A4 (D8) — set when this row is a root-group member (a scene-file
|
|
650
977
|
* root); `active` is whether it's the currently-loaded one. */
|
|
651
978
|
rootMember: { groupId: string; active: boolean } | undefined;
|
|
979
|
+
/** A4 (focus-switch) — the live/focused viewport world, recomputed by the
|
|
980
|
+
* panel per notify (`resolveFocusedViewportWorldId`); group rows render
|
|
981
|
+
* the ●/○ focus radio against it. */
|
|
982
|
+
focusedWorldId: string | null;
|
|
652
983
|
/** D4 — the trimmed, lowercased-by-the-caller search term (empty string
|
|
653
984
|
* when the search box is empty), for {@link highlightSegments}. */
|
|
654
985
|
searchTerm: string;
|
|
655
|
-
onToggleOpen: (
|
|
986
|
+
onToggleOpen: (row: IngestRow, recursive: boolean) => void;
|
|
656
987
|
onStartEditing: (id: string) => void;
|
|
657
988
|
onContextMenu: (e: React.MouseEvent, id: string) => void;
|
|
658
989
|
onDragStart: (e: React.DragEvent, id: string) => void;
|
|
659
990
|
onClick: (e: React.MouseEvent, id: string) => void;
|
|
660
991
|
onActivateRoot: (groupId: string, rootId: string) => void;
|
|
661
992
|
}) {
|
|
662
|
-
const { node, depth, hasChildren
|
|
993
|
+
const { node, depth, hasChildren } = row;
|
|
663
994
|
// A composite `world:<id>` group row (session-local eye per D9, zOrder badge,
|
|
664
995
|
// non-draggable) — NOT merely a runtime-only node (see `isCompositeGroupNode`).
|
|
665
996
|
const badge = compositeGroupBadge(adapter, node.id);
|
|
666
997
|
const isGroupNode = badge !== null;
|
|
667
|
-
// A4 (D8) —
|
|
668
|
-
//
|
|
998
|
+
// A4 (D8) — a root-group member (scene-file root) row does not own a real
|
|
999
|
+
// name/visible/locked path, isn't
|
|
669
1000
|
// draggable, and carries no `data-entity-name` (landmine: every project
|
|
670
1001
|
// ships 2+ scene-file members — they must not pollute `getEntityNames()`).
|
|
671
|
-
const isGameRootRow = isGameRootNode(adapter, node.id);
|
|
672
1002
|
const isMemberRow = rootMember !== undefined;
|
|
673
|
-
const
|
|
1003
|
+
const isOrganizationRow = isCompositeOrganizationNode(adapter, node.id);
|
|
1004
|
+
const isStructural =
|
|
1005
|
+
isGroupNode ||
|
|
1006
|
+
isOrganizationRow ||
|
|
1007
|
+
isMemberRow ||
|
|
1008
|
+
(node.role !== undefined && STRUCTURAL_ROLES.has(node.role));
|
|
674
1009
|
// Reserved-path reads, resolved ONCE per row and only for non-structural
|
|
675
1010
|
// rows: an adapter that doesn't support a reserved path returns
|
|
676
1011
|
// `undefined` (its affordance simply doesn't render). Cheap for every
|
|
@@ -679,7 +1014,11 @@ function Row({
|
|
|
679
1014
|
// reads a plain descriptor.
|
|
680
1015
|
const lockedValue = isStructural ? undefined : adapter.inspector?.get(node.id, 'locked');
|
|
681
1016
|
const nameValue = isStructural ? undefined : adapter.inspector?.get(node.id, 'name');
|
|
682
|
-
|
|
1017
|
+
// VALUE probe like `locked`/`name` above (not provider presence — the
|
|
1018
|
+
// composite's inspector is a never-undefined router, which put a no-op eye
|
|
1019
|
+
// on Boundary rows): every live adapter answers `visible`; a Boundary
|
|
1020
|
+
// answers undefined and gets no eye.
|
|
1021
|
+
const visibleKnown = !isStructural && adapter.inspector?.get(node.id, 'visible') !== undefined;
|
|
683
1022
|
// B1 (D9) — a group row's eye/interactive/pick-lock read the LIFTED,
|
|
684
1023
|
// module-level session store (`world-session-state.ts`), keyed by the
|
|
685
1024
|
// RAW manifest world id `badge.worldId` — not the group-node id itself.
|
|
@@ -691,20 +1030,57 @@ function Row({
|
|
|
691
1030
|
const locked = lockedValue === true;
|
|
692
1031
|
const nameKnown = nameValue !== undefined;
|
|
693
1032
|
|
|
694
|
-
const bg =
|
|
695
|
-
dropZone === 'child' ? 'rgba(87, 158, 255, 0.25)' : isSelected ? '#2a4a7a' : 'transparent';
|
|
696
|
-
|
|
697
1033
|
return (
|
|
698
1034
|
<div
|
|
699
|
-
|
|
1035
|
+
className="vgai-tree-row"
|
|
1036
|
+
data-entity-name={isMemberRow || isOrganizationRow ? undefined : node.label}
|
|
700
1037
|
data-testid="ingest-row"
|
|
701
1038
|
data-ingest-name={node.label}
|
|
1039
|
+
data-node-role={node.role}
|
|
1040
|
+
data-selected={isSelected ? 'true' : undefined}
|
|
1041
|
+
data-dragging={isDragged ? 'true' : undefined}
|
|
1042
|
+
data-muted={
|
|
1043
|
+
(!isStructural && !visible) || (rootMember && !rootMember.active) ? 'true' : undefined
|
|
1044
|
+
}
|
|
702
1045
|
draggable={!isStructural}
|
|
703
1046
|
onDragStart={(e) => onDragStart(e, node.id)}
|
|
704
|
-
onClick={(e) =>
|
|
1047
|
+
onClick={(e) => {
|
|
1048
|
+
if (badge?.role === 'world') {
|
|
1049
|
+
// Runtime composition belongs to Game. In edit mode a manifest
|
|
1050
|
+
// world row is a document navigator: one click reveals that
|
|
1051
|
+
// world's isolated authoring surface, matching scene/file trees in
|
|
1052
|
+
// established editors. A Three world additionally switches the
|
|
1053
|
+
// native scene slot when it is not already focused.
|
|
1054
|
+
activateWorldDocument(badge.worldId);
|
|
1055
|
+
if (
|
|
1056
|
+
badge.kind === 'threejs' &&
|
|
1057
|
+
focusedWorldId !== badge.worldId &&
|
|
1058
|
+
worldFocusScenePath(badge.worldId) !== null
|
|
1059
|
+
) {
|
|
1060
|
+
requestWorldFocus(store, badge.worldId);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (node.role === 'story') {
|
|
1064
|
+
// §4.2 (W2): clicking a story row selects it, applies it to its
|
|
1065
|
+
// React root (the existing behavior below), AND opens/activates
|
|
1066
|
+
// its CENTER story document — resolved through the live story
|
|
1067
|
+
// registry; a row whose id isn't a discovered portable story
|
|
1068
|
+
// (e.g. a legacy registry story) opens no document (nothing is
|
|
1069
|
+
// fabricated). During Play the live connector owns the root:
|
|
1070
|
+
// portable-story rows aren't projected then, and the document
|
|
1071
|
+
// path never touches the play session either way.
|
|
1072
|
+
if (node.flags.active !== true) adapter.stories?.apply(node.id, node.id);
|
|
1073
|
+
openStoryDocumentByStoryId(store, node.id);
|
|
1074
|
+
}
|
|
1075
|
+
onClick(e, node.id);
|
|
1076
|
+
}}
|
|
705
1077
|
onDoubleClick={() => {
|
|
706
1078
|
if (rootMember && !rootMember.active) {
|
|
707
1079
|
onActivateRoot(rootMember.groupId, node.id);
|
|
1080
|
+
} else if (badge?.role === 'world' && worldFocusScenePath(badge.worldId) !== null) {
|
|
1081
|
+
// A4 — same gesture as the D8 radio roots, one level up: an
|
|
1082
|
+
// inactive-but-switchable threejs world becomes the focused one.
|
|
1083
|
+
requestWorldFocus(store, badge.worldId);
|
|
708
1084
|
} else if (nameKnown) {
|
|
709
1085
|
onStartEditing(node.id);
|
|
710
1086
|
}
|
|
@@ -715,88 +1091,96 @@ function Row({
|
|
|
715
1091
|
}}
|
|
716
1092
|
style={{
|
|
717
1093
|
position: 'relative',
|
|
718
|
-
display: 'flex',
|
|
719
|
-
alignItems: 'center',
|
|
720
1094
|
height: ROW_HEIGHT,
|
|
721
1095
|
paddingLeft: depth * INDENT,
|
|
722
1096
|
paddingRight: 4,
|
|
723
1097
|
gap: 4,
|
|
724
1098
|
cursor: 'pointer',
|
|
725
|
-
|
|
726
|
-
outline: dropZone === 'child' ? '1px dashed #579EFF' : 'none',
|
|
1099
|
+
outline: dropZone === 'child' ? `1px dashed ${themeVars.accent.default}` : 'none',
|
|
727
1100
|
whiteSpace: 'nowrap',
|
|
728
1101
|
overflow: 'hidden',
|
|
729
|
-
|
|
730
|
-
isDragged || (!isStructural && !visible) || (rootMember && !rootMember.active) ? 0.45 : 1,
|
|
731
|
-
fontSize: 13,
|
|
732
|
-
color: '#c5c8ce',
|
|
733
|
-
userSelect: 'none',
|
|
1102
|
+
fontSize: 'var(--vgai-font-lg)',
|
|
734
1103
|
}}
|
|
735
1104
|
>
|
|
736
1105
|
{dropZone === 'before' && (
|
|
737
|
-
<
|
|
738
|
-
style={{
|
|
739
|
-
position: 'absolute',
|
|
740
|
-
top: 0,
|
|
741
|
-
left: depth * INDENT,
|
|
742
|
-
right: 0,
|
|
743
|
-
height: 2,
|
|
744
|
-
background: '#579EFF',
|
|
745
|
-
pointerEvents: 'none',
|
|
746
|
-
zIndex: 10,
|
|
747
|
-
}}
|
|
748
|
-
/>
|
|
1106
|
+
<DropIndicator position="before" style={{ left: depth * INDENT }} />
|
|
749
1107
|
)}
|
|
750
|
-
{dropZone === 'after' &&
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1108
|
+
{dropZone === 'after' && <DropIndicator position="after" style={{ left: depth * INDENT }} />}
|
|
1109
|
+
|
|
1110
|
+
{hasChildren ? (
|
|
1111
|
+
<IconButton
|
|
1112
|
+
type="button"
|
|
1113
|
+
variant="ghost"
|
|
1114
|
+
size="compact"
|
|
1115
|
+
aria-label={isCollapsed ? `Expand ${node.label}` : `Collapse ${node.label}`}
|
|
1116
|
+
data-testid="ingest-row-caret"
|
|
1117
|
+
onClick={(e) => {
|
|
1118
|
+
e.stopPropagation();
|
|
1119
|
+
onToggleOpen(row, e.altKey);
|
|
761
1120
|
}}
|
|
1121
|
+
title="Expand/collapse (Option/Alt-click applies to the branch)"
|
|
1122
|
+
className="vgai-tree-caret"
|
|
1123
|
+
>
|
|
1124
|
+
<EditorIcon icon={isCollapsed ? faCaretRight : faCaretDown} aria-hidden="true" />
|
|
1125
|
+
</IconButton>
|
|
1126
|
+
) : (
|
|
1127
|
+
<span
|
|
1128
|
+
data-testid="ingest-row-caret"
|
|
1129
|
+
aria-hidden="true"
|
|
1130
|
+
style={{ width: 12, flexShrink: 0 }}
|
|
762
1131
|
/>
|
|
763
1132
|
)}
|
|
764
1133
|
|
|
765
|
-
<span
|
|
766
|
-
data-testid="ingest-row-caret"
|
|
767
|
-
onClick={(e) => {
|
|
768
|
-
e.stopPropagation();
|
|
769
|
-
onToggleOpen(pathKey);
|
|
770
|
-
}}
|
|
771
|
-
style={{
|
|
772
|
-
width: 12,
|
|
773
|
-
fontSize: 8,
|
|
774
|
-
textAlign: 'center',
|
|
775
|
-
flexShrink: 0,
|
|
776
|
-
cursor: hasChildren ? 'pointer' : 'default',
|
|
777
|
-
color: '#8c8c8c',
|
|
778
|
-
}}
|
|
779
|
-
>
|
|
780
|
-
{hasChildren ? <FontAwesomeIcon icon={isCollapsed ? faCaretRight : faCaretDown} /> : ''}
|
|
781
|
-
</span>
|
|
782
|
-
|
|
783
1134
|
{rootMember && (
|
|
784
1135
|
<span
|
|
785
1136
|
data-testid="root-radio"
|
|
786
1137
|
data-root-active={rootMember.active ? 'true' : 'false'}
|
|
787
1138
|
title={rootMember.active ? 'Active root' : 'Inactive root — double-click to activate'}
|
|
788
|
-
style={{ fontSize:
|
|
1139
|
+
style={{ fontSize: 'var(--vgai-font-xs)', opacity: 0.8, flexShrink: 0 }}
|
|
789
1140
|
>
|
|
790
|
-
{rootMember.active ?
|
|
1141
|
+
<EditorIcon icon={rootMember.active ? faCircleDot : faCircle} aria-hidden="true" />
|
|
791
1142
|
</span>
|
|
792
1143
|
)}
|
|
793
1144
|
|
|
794
|
-
|
|
795
|
-
|
|
1145
|
+
{/* A4 (focus-switch) — the world-level radio, same visual grammar as the
|
|
1146
|
+
* D8 scene-root radio above: ● = the live/focused viewport world,
|
|
1147
|
+
* ○ = an unfocused threejs world with a loadable scene (double-click or
|
|
1148
|
+
* context-menu "Edit This World" to focus it). Worlds that are neither
|
|
1149
|
+
* (DOM/pixi kinds, entry-only threejs) show no dot — nothing to claim. */}
|
|
1150
|
+
{badge?.role === 'world' &&
|
|
1151
|
+
(badge.worldId === focusedWorldId || worldFocusScenePath(badge.worldId) !== null) && (
|
|
1152
|
+
<span
|
|
1153
|
+
data-testid="world-focus-radio"
|
|
1154
|
+
data-world-focused={badge.worldId === focusedWorldId ? 'true' : 'false'}
|
|
1155
|
+
title={
|
|
1156
|
+
badge.worldId === focusedWorldId
|
|
1157
|
+
? 'Focused world — live in the viewport'
|
|
1158
|
+
: 'Inactive world — double-click to edit it'
|
|
1159
|
+
}
|
|
1160
|
+
style={{ fontSize: 'var(--vgai-font-xs)', opacity: 0.8, flexShrink: 0 }}
|
|
1161
|
+
>
|
|
1162
|
+
<EditorIcon
|
|
1163
|
+
icon={badge.worldId === focusedWorldId ? faCircleDot : faCircle}
|
|
1164
|
+
aria-hidden="true"
|
|
1165
|
+
/>
|
|
1166
|
+
</span>
|
|
1167
|
+
)}
|
|
1168
|
+
|
|
1169
|
+
<span
|
|
1170
|
+
title={node.role ? `${node.role}: ${node.kind}` : node.kind}
|
|
1171
|
+
style={{
|
|
1172
|
+
width: 12,
|
|
1173
|
+
fontSize: 'var(--vgai-font-sm)',
|
|
1174
|
+
opacity: 0.65,
|
|
1175
|
+
flexShrink: 0,
|
|
1176
|
+
textAlign: 'center',
|
|
1177
|
+
}}
|
|
1178
|
+
>
|
|
1179
|
+
<EditorIcon icon={iconForNode(node)} aria-hidden="true" />
|
|
796
1180
|
</span>
|
|
797
1181
|
|
|
798
1182
|
{isEditing ? (
|
|
799
|
-
<
|
|
1183
|
+
<TextInput
|
|
800
1184
|
autoFocus
|
|
801
1185
|
defaultValue={node.label}
|
|
802
1186
|
onBlur={(e) => {
|
|
@@ -816,44 +1200,80 @@ function Row({
|
|
|
816
1200
|
style={{
|
|
817
1201
|
flex: 1,
|
|
818
1202
|
minWidth: 0,
|
|
819
|
-
background:
|
|
820
|
-
border:
|
|
821
|
-
borderRadius:
|
|
822
|
-
color:
|
|
823
|
-
fontSize:
|
|
824
|
-
padding:
|
|
1203
|
+
background: themeVars.surface.inset,
|
|
1204
|
+
border: `1px solid ${themeVars.accent.default}`,
|
|
1205
|
+
borderRadius: themeVars.shape.small,
|
|
1206
|
+
color: themeVars.content.primary,
|
|
1207
|
+
fontSize: 'var(--vgai-font-lg)',
|
|
1208
|
+
padding: `0 ${space[2]}px`,
|
|
825
1209
|
outline: 'none',
|
|
826
1210
|
}}
|
|
827
1211
|
/>
|
|
828
1212
|
) : (
|
|
829
1213
|
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
830
1214
|
{highlightSegments(node.label, searchTerm)}
|
|
831
|
-
{
|
|
832
|
-
<span style={{ marginLeft: 6, fontSize:
|
|
1215
|
+
{node.secondaryLabel && (
|
|
1216
|
+
<span style={{ marginLeft: 6, fontSize: 'var(--vgai-font-sm)', opacity: 0.5 }}>
|
|
1217
|
+
{node.secondaryLabel}
|
|
1218
|
+
</span>
|
|
1219
|
+
)}
|
|
1220
|
+
{badge?.role === 'world' && badge.zOrder !== 0 && (
|
|
1221
|
+
<span
|
|
1222
|
+
title={`Z order: ${badge.zOrder}`}
|
|
1223
|
+
style={{ marginLeft: 6, fontSize: 'var(--vgai-font-sm)', opacity: 0.6 }}
|
|
1224
|
+
>
|
|
1225
|
+
Z {badge.zOrder}
|
|
1226
|
+
</span>
|
|
1227
|
+
)}
|
|
1228
|
+
{badge?.provenance && (
|
|
1229
|
+
<span
|
|
1230
|
+
data-testid="world-provenance"
|
|
1231
|
+
data-provenance={badge.provenance.source}
|
|
1232
|
+
title={badge.provenance.detail}
|
|
1233
|
+
style={{
|
|
1234
|
+
marginLeft: 6,
|
|
1235
|
+
fontSize: 'var(--vgai-font-xs)',
|
|
1236
|
+
padding: `0 ${space[2]}px`,
|
|
1237
|
+
borderRadius: themeVars.shape.small,
|
|
1238
|
+
border: `1px solid ${themeVars.boundary.strong}`,
|
|
1239
|
+
color: themeVars.content.muted,
|
|
1240
|
+
verticalAlign: 'middle',
|
|
1241
|
+
}}
|
|
1242
|
+
>
|
|
1243
|
+
{badge.provenance.label}
|
|
1244
|
+
</span>
|
|
833
1245
|
)}
|
|
834
1246
|
</span>
|
|
835
1247
|
)}
|
|
836
1248
|
|
|
837
1249
|
{lockedKnown && (
|
|
838
|
-
<
|
|
1250
|
+
<IconButton
|
|
1251
|
+
type="button"
|
|
1252
|
+
variant="ghost"
|
|
1253
|
+
size="compact"
|
|
839
1254
|
data-testid="ingest-row-lock"
|
|
840
|
-
title=
|
|
1255
|
+
title={locked ? 'Unlock' : 'Lock'}
|
|
1256
|
+
aria-label={locked ? `Unlock ${node.label}` : `Lock ${node.label}`}
|
|
1257
|
+
aria-pressed={locked}
|
|
841
1258
|
onClick={(e) => {
|
|
842
1259
|
e.stopPropagation();
|
|
843
1260
|
adapter.inspector!.set(node.id, 'locked', !locked);
|
|
844
1261
|
store.notifyIngestEdit();
|
|
845
1262
|
}}
|
|
846
|
-
style={{ fontSize: 9, cursor: 'pointer', flexShrink: 0, opacity: locked ? 0.8 : 0.35 }}
|
|
847
1263
|
>
|
|
848
|
-
<
|
|
849
|
-
</
|
|
1264
|
+
<EditorIcon icon={locked ? faLock : faLockOpen} aria-hidden="true" />
|
|
1265
|
+
</IconButton>
|
|
850
1266
|
)}
|
|
851
1267
|
|
|
852
1268
|
{(visibleKnown || isGroupNode) && (
|
|
853
|
-
<
|
|
1269
|
+
<IconButton
|
|
854
1270
|
type="button"
|
|
1271
|
+
variant="ghost"
|
|
1272
|
+
size="compact"
|
|
855
1273
|
data-testid="ingest-row-visibility"
|
|
856
1274
|
title={visible ? 'Hide' : 'Show'}
|
|
1275
|
+
aria-label={visible ? `Hide ${node.label}` : `Show ${node.label}`}
|
|
1276
|
+
aria-pressed={!visible}
|
|
857
1277
|
onClick={(e) => {
|
|
858
1278
|
e.stopPropagation();
|
|
859
1279
|
if (badge) {
|
|
@@ -864,19 +1284,9 @@ function Row({
|
|
|
864
1284
|
store.notifyIngestEdit();
|
|
865
1285
|
}
|
|
866
1286
|
}}
|
|
867
|
-
style={{
|
|
868
|
-
width: 20,
|
|
869
|
-
height: 20,
|
|
870
|
-
border: 'none',
|
|
871
|
-
background: 'transparent',
|
|
872
|
-
color: visible ? '#9aa0a6' : '#e0a030',
|
|
873
|
-
cursor: 'pointer',
|
|
874
|
-
fontSize: 11,
|
|
875
|
-
flexShrink: 0,
|
|
876
|
-
}}
|
|
877
1287
|
>
|
|
878
|
-
<
|
|
879
|
-
</
|
|
1288
|
+
<EditorIcon icon={visible ? faEye : faEyeSlash} aria-hidden="true" />
|
|
1289
|
+
</IconButton>
|
|
880
1290
|
)}
|
|
881
1291
|
|
|
882
1292
|
{/* B1 (D9) — session-local, per-world design-time toggles: `interactive`
|
|
@@ -885,48 +1295,46 @@ function Row({
|
|
|
885
1295
|
* for B4's layered viewport picking to consume. Group rows only —
|
|
886
1296
|
* neither concept applies to an ordinary entity row. */}
|
|
887
1297
|
{badge && (
|
|
888
|
-
<
|
|
1298
|
+
<IconButton
|
|
1299
|
+
type="button"
|
|
1300
|
+
variant="ghost"
|
|
1301
|
+
size="compact"
|
|
889
1302
|
data-testid="world-interactive-toggle"
|
|
890
1303
|
title={
|
|
891
1304
|
worldInteractive
|
|
892
1305
|
? 'Interactive (layer forwards pointer events)'
|
|
893
1306
|
: 'Not interactive (layer is pointer-events:none)'
|
|
894
1307
|
}
|
|
1308
|
+
aria-label={`Forward pointer events to ${node.label}`}
|
|
1309
|
+
aria-pressed={worldInteractive}
|
|
895
1310
|
onClick={(e) => {
|
|
896
1311
|
e.stopPropagation();
|
|
897
1312
|
toggleWorldInteractive(badge.worldId);
|
|
898
1313
|
store.notifyIngestEdit();
|
|
899
1314
|
}}
|
|
900
|
-
style={{
|
|
901
|
-
fontSize: 9,
|
|
902
|
-
cursor: 'pointer',
|
|
903
|
-
flexShrink: 0,
|
|
904
|
-
opacity: worldInteractive ? 0.9 : 0.35,
|
|
905
|
-
}}
|
|
906
1315
|
>
|
|
907
|
-
<
|
|
908
|
-
</
|
|
1316
|
+
<EditorIcon icon={faHandPointer} aria-hidden="true" />
|
|
1317
|
+
</IconButton>
|
|
909
1318
|
)}
|
|
910
1319
|
{badge && (
|
|
911
|
-
<
|
|
1320
|
+
<IconButton
|
|
1321
|
+
type="button"
|
|
1322
|
+
variant="ghost"
|
|
1323
|
+
size="compact"
|
|
912
1324
|
data-testid="world-picklock-toggle"
|
|
913
1325
|
title={
|
|
914
1326
|
worldPickLocked ? 'Pick-locked (skipped by topmost-layer picking)' : 'Not pick-locked'
|
|
915
1327
|
}
|
|
1328
|
+
aria-label={`Exclude ${node.label} from viewport picking`}
|
|
1329
|
+
aria-pressed={worldPickLocked}
|
|
916
1330
|
onClick={(e) => {
|
|
917
1331
|
e.stopPropagation();
|
|
918
1332
|
toggleWorldPickLock(badge.worldId);
|
|
919
1333
|
store.notifyIngestEdit();
|
|
920
1334
|
}}
|
|
921
|
-
style={{
|
|
922
|
-
fontSize: 9,
|
|
923
|
-
cursor: 'pointer',
|
|
924
|
-
flexShrink: 0,
|
|
925
|
-
opacity: worldPickLocked ? 0.9 : 0.35,
|
|
926
|
-
}}
|
|
927
1335
|
>
|
|
928
|
-
<
|
|
929
|
-
</
|
|
1336
|
+
<EditorIcon icon={faCrosshairs} aria-hidden="true" />
|
|
1337
|
+
</IconButton>
|
|
930
1338
|
)}
|
|
931
1339
|
</div>
|
|
932
1340
|
);
|
|
@@ -938,6 +1346,9 @@ export function GameHierarchy() {
|
|
|
938
1346
|
const store = useEditorStore();
|
|
939
1347
|
useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
940
1348
|
const adapter = getActiveAuthoring(store);
|
|
1349
|
+
// A4 — recomputed per notify (never cached across notifies): drives the
|
|
1350
|
+
// world-group rows' ●/○ focus radio.
|
|
1351
|
+
const focusedViewportWorldId = resolveFocusedViewportWorldId(store);
|
|
941
1352
|
|
|
942
1353
|
const [search, setSearch] = useState('');
|
|
943
1354
|
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
|
@@ -957,28 +1368,94 @@ export function GameHierarchy() {
|
|
|
957
1368
|
const [scrollTop, setScrollTop] = useState(0);
|
|
958
1369
|
|
|
959
1370
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
960
|
-
const collapsedKeysRef = useRef(new Set<string>());
|
|
961
1371
|
const revealedRef = useRef(new Map<string, number>());
|
|
1372
|
+
const sessionExpansionRef = useRef(new Map<string, boolean>());
|
|
1373
|
+
const sessionExpansionProjectRef = useRef<string | null>(null);
|
|
1374
|
+
const lastSelectionExpansionRef = useRef<{
|
|
1375
|
+
adapter: AuthoringAdapter;
|
|
1376
|
+
selection: string;
|
|
1377
|
+
} | null>(null);
|
|
962
1378
|
const lastClickedRef = useRef<string | null>(null);
|
|
1379
|
+
const lastAutoRevealRef = useRef<{ adapter: AuthoringAdapter; selection: string } | null>(null);
|
|
963
1380
|
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
|
|
964
1381
|
|
|
965
1382
|
const term = search.trim().toLowerCase();
|
|
1383
|
+
const projectIdentity = getCurrentProject()?.rootPath ?? '__unscoped__';
|
|
1384
|
+
// A4 (D8) — every root-group member, rebuilt each render straight from
|
|
1385
|
+
// `rootGroups.groups()` (never from the id string — see `buildRootMembers`'s
|
|
1386
|
+
// doc comment).
|
|
1387
|
+
const rootMembers = buildRootMembers(adapter);
|
|
1388
|
+
const selected = adapter.selection?.get() ?? [];
|
|
1389
|
+
const selectedIds = new Set(selected);
|
|
1390
|
+
// The toolbar's generic Add menu targets the selected hierarchy node. The
|
|
1391
|
+
// composite adapter then routes the request to that node's owning world
|
|
1392
|
+
// adapter, whose `creatableKinds` is the sole source of menu contents.
|
|
1393
|
+
// Multi-selection has no unambiguous parent, so it intentionally falls
|
|
1394
|
+
// back to the active adapter's root-level insertion behavior.
|
|
1395
|
+
const createTargetId = selected.length === 1 ? selected[0]! : null;
|
|
1396
|
+
const selectionSignature = [...selectedIds].sort().join('\0');
|
|
1397
|
+
|
|
1398
|
+
if (sessionExpansionProjectRef.current !== projectIdentity) {
|
|
1399
|
+
sessionExpansionRef.current.clear();
|
|
1400
|
+
sessionExpansionProjectRef.current = projectIdentity;
|
|
1401
|
+
lastSelectionExpansionRef.current = null;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Always take one uncapped, uncollapsed structural snapshot first. It is
|
|
1405
|
+
// the source for stable defaults, search ancestry, selection reveal, and
|
|
1406
|
+
// branch operations. The final browse tree below is still capped and
|
|
1407
|
+
// virtualized, so this does not inflate rendered DOM for large ingests.
|
|
1408
|
+
const structuralRows = flattenIngestRows(adapter.hierarchy, new Set());
|
|
1409
|
+
const defaultExpansion = defaultExpansionByNode(structuralRows, adapter, rootMembers);
|
|
1410
|
+
|
|
1411
|
+
// Selection reveal behaves like an ordinary tree expansion for the rest of
|
|
1412
|
+
// this editor session. It is applied only when selection changes, so a user
|
|
1413
|
+
// can explicitly collapse the revealed branch without the current
|
|
1414
|
+
// selection immediately forcing it open again.
|
|
1415
|
+
const lastExpansion = lastSelectionExpansionRef.current;
|
|
1416
|
+
if (lastExpansion?.adapter !== adapter || lastExpansion.selection !== selectionSignature) {
|
|
1417
|
+
for (const id of selectedAncestorNodeIds(structuralRows, selectedIds)) {
|
|
1418
|
+
sessionExpansionRef.current.set(id, true);
|
|
1419
|
+
}
|
|
1420
|
+
lastSelectionExpansionRef.current = { adapter, selection: selectionSignature };
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const isExpanded = (row: IngestRow): boolean =>
|
|
1424
|
+
sessionExpansionRef.current.get(row.node.id) ??
|
|
1425
|
+
hierarchyExpansionPreference(projectIdentity, row.node.id) ??
|
|
1426
|
+
defaultExpansion.get(row.node.id) ??
|
|
1427
|
+
true;
|
|
1428
|
+
|
|
1429
|
+
const persistedCollapsedPaths = new Set(
|
|
1430
|
+
structuralRows
|
|
1431
|
+
.filter((row) => !row.more && row.hasChildren && !isExpanded(row))
|
|
1432
|
+
.map((row) => row.pathKey),
|
|
1433
|
+
);
|
|
1434
|
+
const effectiveCollapsedPaths = persistedCollapsedPaths;
|
|
1435
|
+
|
|
966
1436
|
// Cap browse-view children (D-C1) whenever a non-first-party/composite
|
|
967
1437
|
// session is active (an ingested/multi-world tree may be huge), OR while
|
|
968
1438
|
// playing (a live scene graph can also balloon) — matches the exact
|
|
969
1439
|
// capping policy the former SceneHierarchy (play-only) / IngestHierarchy
|
|
970
1440
|
// (always, unless searching) components each had on their own.
|
|
971
1441
|
const capEnabled = term.length === 0 && (hasAuthoringOverride() || store.playState !== 'stopped');
|
|
972
|
-
|
|
973
|
-
const allRows = flattenIngestRows(
|
|
974
|
-
adapter.hierarchy,
|
|
975
|
-
collapsedKeysRef.current,
|
|
1442
|
+
const temporaryRevealed = mergeRevealCounts(
|
|
976
1443
|
revealedRef.current,
|
|
1444
|
+
selectedRevealCounts(structuralRows, selectedIds),
|
|
1445
|
+
);
|
|
1446
|
+
const browseRows = flattenIngestRows(
|
|
1447
|
+
adapter.hierarchy,
|
|
1448
|
+
effectiveCollapsedPaths,
|
|
1449
|
+
temporaryRevealed,
|
|
977
1450
|
capEnabled,
|
|
978
1451
|
);
|
|
1452
|
+
const matchingPaths = term ? searchPathKeys(structuralRows, term) : null;
|
|
1453
|
+
const sourceRows = matchingPaths
|
|
1454
|
+
? structuralRows.filter((row) => matchingPaths.has(row.pathKey))
|
|
1455
|
+
: browseRows;
|
|
979
1456
|
// Session-local world-hide (D9): drop every row belonging to a hidden world
|
|
980
1457
|
// group (the group row itself stays, dimmed by its own `visible` flag above).
|
|
981
|
-
const worldFiltered =
|
|
1458
|
+
const worldFiltered = sourceRows.filter((row) => {
|
|
982
1459
|
if (isCompositeGroupNode(adapter, row.node.id)) return true; // the group row itself always shows
|
|
983
1460
|
let current: string | null = row.node.parentId;
|
|
984
1461
|
while (current) {
|
|
@@ -988,9 +1465,7 @@ export function GameHierarchy() {
|
|
|
988
1465
|
}
|
|
989
1466
|
return true;
|
|
990
1467
|
});
|
|
991
|
-
const rows = term
|
|
992
|
-
? worldFiltered.filter((r) => r.node.label.toLowerCase().includes(term))
|
|
993
|
-
: worldFiltered.filter((r) => !r.hiddenByCollapse);
|
|
1468
|
+
const rows = term ? worldFiltered : worldFiltered.filter((r) => !r.hiddenByCollapse);
|
|
994
1469
|
|
|
995
1470
|
// Wave-18-ported: a filtered-down result set shouldn't leave the viewport
|
|
996
1471
|
// parked at a stale deep scroll offset from a previous (larger) list.
|
|
@@ -1015,12 +1490,45 @@ export function GameHierarchy() {
|
|
|
1015
1490
|
const topSpacerPx = start * ROW_HEIGHT;
|
|
1016
1491
|
const bottomSpacerPx = (rows.length - end) * ROW_HEIGHT;
|
|
1017
1492
|
|
|
1018
|
-
const toggleOpen = useCallback(
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1493
|
+
const toggleOpen = useCallback(
|
|
1494
|
+
(row: IngestRow, recursive: boolean) => {
|
|
1495
|
+
const expanded = !isExpanded(row);
|
|
1496
|
+
const updates: Array<readonly [string, boolean]> = [[row.node.id, expanded]];
|
|
1497
|
+
if (recursive) {
|
|
1498
|
+
const prefix = `${row.pathKey}.`;
|
|
1499
|
+
for (const descendant of structuralRows) {
|
|
1500
|
+
if (descendant.hasChildren && descendant.pathKey.startsWith(prefix)) {
|
|
1501
|
+
updates.push([descendant.node.id, expanded]);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
for (const [id, value] of updates) sessionExpansionRef.current.set(id, value);
|
|
1506
|
+
setHierarchyExpansionPreferences(projectIdentity, updates);
|
|
1507
|
+
forceUpdate();
|
|
1508
|
+
},
|
|
1509
|
+
[isExpanded, projectIdentity, structuralRows],
|
|
1510
|
+
);
|
|
1511
|
+
|
|
1512
|
+
const setAllExpanded = useCallback(
|
|
1513
|
+
(expanded: boolean) => {
|
|
1514
|
+
const updates = structuralRows
|
|
1515
|
+
.filter((row) => row.hasChildren)
|
|
1516
|
+
.map((row) => [row.node.id, expanded] as const);
|
|
1517
|
+
for (const [id, value] of updates) sessionExpansionRef.current.set(id, value);
|
|
1518
|
+
setHierarchyExpansionPreferences(projectIdentity, updates);
|
|
1519
|
+
forceUpdate();
|
|
1520
|
+
},
|
|
1521
|
+
[projectIdentity, structuralRows],
|
|
1522
|
+
);
|
|
1523
|
+
|
|
1524
|
+
const resetExpansion = useCallback(() => {
|
|
1525
|
+
sessionExpansionRef.current.clear();
|
|
1526
|
+
for (const id of selectedAncestorNodeIds(structuralRows, selectedIds)) {
|
|
1527
|
+
sessionExpansionRef.current.set(id, true);
|
|
1528
|
+
}
|
|
1529
|
+
clearHierarchyExpansionPreferences(projectIdentity);
|
|
1022
1530
|
forceUpdate();
|
|
1023
|
-
}, []);
|
|
1531
|
+
}, [projectIdentity, selectionSignature, structuralRows]);
|
|
1024
1532
|
|
|
1025
1533
|
const revealMore = useCallback((parentPathKey: string) => {
|
|
1026
1534
|
const m = revealedRef.current;
|
|
@@ -1028,14 +1536,11 @@ export function GameHierarchy() {
|
|
|
1028
1536
|
forceUpdate();
|
|
1029
1537
|
}, []);
|
|
1030
1538
|
|
|
1031
|
-
//
|
|
1032
|
-
// `rootGroups.groups()` (never from the id string — see `buildRootMembers`'s
|
|
1033
|
-
// doc comment). `handleActivateRoot` is the ONE choke point a double-click
|
|
1539
|
+
// `handleActivateRoot` is the ONE choke point a double-click
|
|
1034
1540
|
// on an inactive member row goes through, shared with the asset-browser's
|
|
1035
1541
|
// scene double-click (`DefaultEditorLayout.tsx`'s `handleOpenScene`) via
|
|
1036
1542
|
// `requestRootActivation` so the dirty-gate/confirmation UX can't drift
|
|
1037
1543
|
// between the two call sites.
|
|
1038
|
-
const rootMembers = buildRootMembers(adapter);
|
|
1039
1544
|
const handleActivateRoot = useCallback(
|
|
1040
1545
|
(groupId: string, rootId: string) => {
|
|
1041
1546
|
requestRootActivation(adapter, groupId, rootId);
|
|
@@ -1043,7 +1548,29 @@ export function GameHierarchy() {
|
|
|
1043
1548
|
[adapter],
|
|
1044
1549
|
);
|
|
1045
1550
|
|
|
1046
|
-
|
|
1551
|
+
// Viewport picks and external editor-control selection reveal the selected
|
|
1552
|
+
// row, including through a previously collapsed path and a virtualized
|
|
1553
|
+
// list. Do this only when the selection or adapter changes; ordinary store
|
|
1554
|
+
// notifications must not fight a user's manual scroll position.
|
|
1555
|
+
useEffect(() => {
|
|
1556
|
+
if (!selectionSignature) {
|
|
1557
|
+
lastAutoRevealRef.current = null;
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const last = lastAutoRevealRef.current;
|
|
1561
|
+
if (last?.adapter === adapter && last.selection === selectionSignature) return;
|
|
1562
|
+
lastAutoRevealRef.current = { adapter, selection: selectionSignature };
|
|
1563
|
+
const index = rows.findIndex((row) => selectedIds.has(row.node.id));
|
|
1564
|
+
const container = containerRef.current;
|
|
1565
|
+
if (index < 0 || !container) return;
|
|
1566
|
+
const top = index * ROW_HEIGHT;
|
|
1567
|
+
const bottom = top + ROW_HEIGHT;
|
|
1568
|
+
if (top < container.scrollTop) container.scrollTop = top;
|
|
1569
|
+
else if (bottom > container.scrollTop + container.clientHeight) {
|
|
1570
|
+
container.scrollTop = Math.max(0, bottom - container.clientHeight);
|
|
1571
|
+
}
|
|
1572
|
+
setScrollTop(container.scrollTop);
|
|
1573
|
+
}, [adapter, rows, selectedIds, selectionSignature]);
|
|
1047
1574
|
|
|
1048
1575
|
const handleRowClick = useCallback(
|
|
1049
1576
|
(e: React.MouseEvent, nodeId: string) => {
|
|
@@ -1119,12 +1646,21 @@ export function GameHierarchy() {
|
|
|
1119
1646
|
const targetId = row.node.id;
|
|
1120
1647
|
if (draggedIds.includes(targetId)) return;
|
|
1121
1648
|
if (isDescendantOfAny(targetId, draggedIds, adapter)) return;
|
|
1649
|
+
if (!isSameWorldDrop(adapter, draggedIds, targetId)) {
|
|
1650
|
+
setDropTarget(null);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1122
1653
|
}
|
|
1123
1654
|
if (isCompositeGroupNode(adapter, row.node.id) && zone !== 'child') {
|
|
1124
1655
|
// A composite world-group row has no siblings of its own kind to
|
|
1125
1656
|
// reorder against — only "drop into this world" makes sense.
|
|
1126
1657
|
zone = 'child';
|
|
1127
1658
|
}
|
|
1659
|
+
if (row.node.role === 'document' || row.node.role === 'story') zone = 'child';
|
|
1660
|
+
if (row.node.role === 'folder' || row.node.role === 'boundary') {
|
|
1661
|
+
setDropTarget(null);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1128
1664
|
|
|
1129
1665
|
setDropTarget({ nodeId: row.node.id, zone });
|
|
1130
1666
|
},
|
|
@@ -1207,14 +1743,36 @@ export function GameHierarchy() {
|
|
|
1207
1743
|
);
|
|
1208
1744
|
|
|
1209
1745
|
const panelName = adapter.persistence?.destination ?? 'Untitled';
|
|
1746
|
+
const compositeHierarchy = adapter instanceof CompositeAuthoringAdapter;
|
|
1210
1747
|
|
|
1211
1748
|
return (
|
|
1212
1749
|
<Panel
|
|
1213
1750
|
name={
|
|
1214
1751
|
<span data-testid="scene-name" data-scene={panelName}>
|
|
1215
1752
|
{panelName}
|
|
1753
|
+
{/* Spec 29 §5 — a BARE adapter's provenance renders here (its whole
|
|
1754
|
+
panel is one world); a composite's renders per world-group row. */}
|
|
1755
|
+
{!(adapter instanceof CompositeAuthoringAdapter) && adapter.provenance && (
|
|
1756
|
+
<span
|
|
1757
|
+
data-testid="panel-provenance"
|
|
1758
|
+
data-provenance={adapter.provenance.source}
|
|
1759
|
+
title={adapter.provenance.detail}
|
|
1760
|
+
style={{
|
|
1761
|
+
marginLeft: 6,
|
|
1762
|
+
fontSize: 'var(--vgai-font-xs)',
|
|
1763
|
+
fontWeight: 400,
|
|
1764
|
+
padding: `0 ${space[2]}px`,
|
|
1765
|
+
borderRadius: themeVars.shape.small,
|
|
1766
|
+
border: `1px solid ${themeVars.boundary.strong}`,
|
|
1767
|
+
color: themeVars.content.muted,
|
|
1768
|
+
}}
|
|
1769
|
+
>
|
|
1770
|
+
{adapter.provenance.label}
|
|
1771
|
+
</span>
|
|
1772
|
+
)}
|
|
1216
1773
|
</span>
|
|
1217
1774
|
}
|
|
1775
|
+
hideHeader={compositeHierarchy}
|
|
1218
1776
|
onPointerDown={() => setActiveScope('hierarchy')}
|
|
1219
1777
|
onDragOver={(e) => {
|
|
1220
1778
|
if (e.dataTransfer.types.includes(ASSET_MIME)) {
|
|
@@ -1222,39 +1780,63 @@ export function GameHierarchy() {
|
|
|
1222
1780
|
e.dataTransfer.dropEffect = 'copy';
|
|
1223
1781
|
}
|
|
1224
1782
|
}}
|
|
1225
|
-
|
|
1783
|
+
// Dockview owns the group's resizable/collapsible persisted width; this
|
|
1784
|
+
// panel fills whatever its current group gives it.
|
|
1785
|
+
style={{
|
|
1786
|
+
flex: 1,
|
|
1787
|
+
minWidth: 0,
|
|
1788
|
+
minHeight: 0,
|
|
1789
|
+
borderRight: `1px solid ${themeVars.boundary.default}`,
|
|
1790
|
+
}}
|
|
1226
1791
|
>
|
|
1792
|
+
{compositeHierarchy && (
|
|
1793
|
+
<span data-testid="scene-name" data-scene={panelName} style={{ display: 'none' }}>
|
|
1794
|
+
{panelName}
|
|
1795
|
+
</span>
|
|
1796
|
+
)}
|
|
1227
1797
|
<OverlayReportBanner />
|
|
1228
|
-
<
|
|
1798
|
+
<EditorToolbar
|
|
1799
|
+
label="Hierarchy controls"
|
|
1800
|
+
compact
|
|
1229
1801
|
style={{
|
|
1230
|
-
padding: '4px 8px',
|
|
1231
|
-
borderBottom: '1px solid #333',
|
|
1232
|
-
display: 'flex',
|
|
1233
|
-
alignItems: 'center',
|
|
1234
1802
|
gap: 4,
|
|
1235
1803
|
flexShrink: 0,
|
|
1236
1804
|
}}
|
|
1237
1805
|
>
|
|
1238
|
-
|
|
1806
|
+
{/* L-6 — `.vgai-input` supplies a real `:focus-visible` border/outline
|
|
1807
|
+
(this box previously did `outline:'none'` with no replacement). */}
|
|
1808
|
+
<TextInput
|
|
1239
1809
|
type="text"
|
|
1240
1810
|
data-testid="ingest-search"
|
|
1241
1811
|
placeholder="Search..."
|
|
1812
|
+
className="vgai-input"
|
|
1242
1813
|
value={search}
|
|
1243
1814
|
onChange={(e) => setSearch(e.target.value)}
|
|
1244
1815
|
style={{
|
|
1245
1816
|
flex: 1,
|
|
1246
1817
|
minWidth: 0,
|
|
1247
|
-
background: '#1e1e1e',
|
|
1248
|
-
border: '1px solid #333',
|
|
1249
|
-
borderRadius: 3,
|
|
1250
|
-
color: '#c5c8ce',
|
|
1251
|
-
fontSize: 11,
|
|
1252
|
-
padding: '3px 6px',
|
|
1253
|
-
outline: 'none',
|
|
1254
1818
|
}}
|
|
1255
1819
|
/>
|
|
1256
|
-
<
|
|
1257
|
-
|
|
1820
|
+
<ViewButton
|
|
1821
|
+
label="Collapse all"
|
|
1822
|
+
icon={faMinus}
|
|
1823
|
+
testId="hierarchy-collapse-all"
|
|
1824
|
+
onClick={() => setAllExpanded(false)}
|
|
1825
|
+
/>
|
|
1826
|
+
<ViewButton
|
|
1827
|
+
label="Reset to default openness"
|
|
1828
|
+
icon={faRotateLeft}
|
|
1829
|
+
testId="hierarchy-reset-expansion"
|
|
1830
|
+
onClick={resetExpansion}
|
|
1831
|
+
/>
|
|
1832
|
+
<ViewButton
|
|
1833
|
+
label="Expand all"
|
|
1834
|
+
icon={faPlus}
|
|
1835
|
+
testId="hierarchy-expand-all"
|
|
1836
|
+
onClick={() => setAllExpanded(true)}
|
|
1837
|
+
/>
|
|
1838
|
+
<CreateMenu adapter={adapter} parentId={createTargetId} />
|
|
1839
|
+
</EditorToolbar>
|
|
1258
1840
|
|
|
1259
1841
|
<div
|
|
1260
1842
|
ref={containerRef}
|
|
@@ -1289,10 +1871,11 @@ export function GameHierarchy() {
|
|
|
1289
1871
|
store={store}
|
|
1290
1872
|
isSelected={selectedIds.has(row.node.id)}
|
|
1291
1873
|
isEditing={editingId === row.node.id}
|
|
1292
|
-
isCollapsed={
|
|
1874
|
+
isCollapsed={term ? false : effectiveCollapsedPaths.has(row.pathKey)}
|
|
1293
1875
|
dropZone={dropTarget && dropTarget.nodeId === row.node.id ? dropTarget.zone : null}
|
|
1294
1876
|
isDragged={draggedIds?.includes(row.node.id) ?? false}
|
|
1295
1877
|
rootMember={rootMembers.get(row.node.id)}
|
|
1878
|
+
focusedWorldId={focusedViewportWorldId}
|
|
1296
1879
|
searchTerm={term}
|
|
1297
1880
|
onToggleOpen={toggleOpen}
|
|
1298
1881
|
onStartEditing={setEditingId}
|
|
@@ -1313,9 +1896,9 @@ export function GameHierarchy() {
|
|
|
1313
1896
|
right: 0,
|
|
1314
1897
|
top: rows.length * ROW_HEIGHT,
|
|
1315
1898
|
height: 2,
|
|
1316
|
-
background:
|
|
1899
|
+
background: themeVars.accent.default,
|
|
1317
1900
|
pointerEvents: 'none',
|
|
1318
|
-
zIndex:
|
|
1901
|
+
zIndex: zIndex.overlayLow,
|
|
1319
1902
|
}}
|
|
1320
1903
|
/>
|
|
1321
1904
|
)}
|
|
@@ -1324,13 +1907,13 @@ export function GameHierarchy() {
|
|
|
1324
1907
|
{contextMenu &&
|
|
1325
1908
|
(() => {
|
|
1326
1909
|
const menuNode = adapter.hierarchy.node(contextMenu.nodeId);
|
|
1327
|
-
// A4 (D8): a root-group member
|
|
1910
|
+
// A4 (D8): a root-group member gets the same
|
|
1328
1911
|
// "no structural affordances" treatment as an existing composite
|
|
1329
1912
|
// group row — none of the three own a name/visible/locked path or
|
|
1330
1913
|
// a meaningful Create/Duplicate/Delete target.
|
|
1331
1914
|
const isGroupNode = menuNode
|
|
1332
1915
|
? isCompositeGroupNode(adapter, menuNode.id) ||
|
|
1333
|
-
|
|
1916
|
+
isCompositeOrganizationNode(adapter, menuNode.id) ||
|
|
1334
1917
|
rootMembers.has(menuNode.id)
|
|
1335
1918
|
: false;
|
|
1336
1919
|
return (
|