@volter/editor-core 0.5.57
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/BUNDLED_NOTICES +1527 -0
- package/LICENSE +686 -0
- package/LICENSE-APACHE +202 -0
- package/NOTICE +18 -0
- package/README.md +24 -0
- package/dist/build/chunk-JS4D4XZ3.js +175 -0
- package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
- package/dist/build/vite-plugin-product-contributions.js +100 -0
- package/dist/build/vite-plugin-product-contributions.js.map +7 -0
- package/dist/build/vite-plugin-shared-react.js +25 -0
- package/dist/build/vite-plugin-shared-react.js.map +7 -0
- package/dist/build/vite-plugin-shared-three.js +86 -0
- package/dist/build/vite-plugin-shared-three.js.map +7 -0
- package/dist/server/chunk-KMJC7AGO.js +98 -0
- package/dist/server/chunk-KMJC7AGO.js.map +7 -0
- package/dist/server/chunk-PSILUMGS.js +46 -0
- package/dist/server/chunk-PSILUMGS.js.map +7 -0
- package/dist/server/frame-proxy.js +255 -0
- package/dist/server/frame-proxy.js.map +7 -0
- package/dist/server/open-browser.js +53 -0
- package/dist/server/open-browser.js.map +7 -0
- package/dist/server/process-shutdown.js +127 -0
- package/dist/server/process-shutdown.js.map +7 -0
- package/dist/server/session-registry.js +141 -0
- package/dist/server/session-registry.js.map +7 -0
- package/dist/server/spawn-opener.js +11 -0
- package/dist/server/spawn-opener.js.map +7 -0
- package/dist/server/worktree-identity.js +11 -0
- package/dist/server/worktree-identity.js.map +7 -0
- package/dist-server/packaged.mjs +88816 -0
- package/package.json +118 -0
- package/scripts/build-plugins.mjs +14 -0
- package/scripts/build-server.mjs +26 -0
- package/scripts/build-session.mjs +9 -0
- package/server/account-credentials.ts +60 -0
- package/server/account-service.ts +1434 -0
- package/server/adapter-region-includes.ts +47 -0
- package/server/asset-catalog-v2.ts +175 -0
- package/server/asset-history-snapshots.ts +196 -0
- package/server/asset-ledger-store.ts +195 -0
- package/server/asset-library-routes.ts +1595 -0
- package/server/blender-wasm-artifact.ts +430 -0
- package/server/boot-timings.ts +106 -0
- package/server/canonical-path.ts +50 -0
- package/server/checkout-workspace-preflight.ts +349 -0
- package/server/cloud-asset-catalog.ts +158 -0
- package/server/coding-inference-launch.ts +237 -0
- package/server/collaboration-account-client.ts +202 -0
- package/server/collaboration-attribution.ts +122 -0
- package/server/collaboration-session.ts +934 -0
- package/server/comfyui-bridge.ts +248 -0
- package/server/console-ledger.ts +499 -0
- package/server/creation-site-transform.ts +304 -0
- package/server/creation-site-write.ts +648 -0
- package/server/data-file-serialize.ts +158 -0
- package/server/editor-brand-html.ts +72 -0
- package/server/editor-control-socket.ts +366 -0
- package/server/editor-server-options.ts +190 -0
- package/server/editor-server.ts +1483 -0
- package/server/editor-sse.ts +352 -0
- package/server/engine-provenance.ts +169 -0
- package/server/engine-source-restart.ts +117 -0
- package/server/frame-bridge.ts +115 -0
- package/server/frame-proxy.ts +490 -0
- package/server/frame-workbench.ts +259 -0
- package/server/frontend-handoff.ts +229 -0
- package/server/game-globals-shadow.ts +113 -0
- package/server/gameplay-session-retention.ts +149 -0
- package/server/gameplay-sessions.ts +123 -0
- package/server/generation-jobs.ts +201 -0
- package/server/generation-reconciler.ts +137 -0
- package/server/generative-execution-context.ts +59 -0
- package/server/git-workflow.ts +612 -0
- package/server/harness-chat-caller.ts +36 -0
- package/server/harness-chat-service.ts +1681 -0
- package/server/idle-shutdown.ts +170 -0
- package/server/js-profiling-policy.ts +52 -0
- package/server/launcher-settings.ts +84 -0
- package/server/local-asset-catalog.ts +410 -0
- package/server/managed-account-defaults.ts +22 -0
- package/server/mock-control-plane.ts +540 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/native-credential-store.ts +162 -0
- package/server/open-browser.ts +118 -0
- package/server/packaged.ts +1192 -0
- package/server/play-stall.ts +126 -0
- package/server/process-shutdown.ts +218 -0
- package/server/product-presets.ts +55 -0
- package/server/project-build-artifact.ts +95 -0
- package/server/project-components.ts +100 -0
- package/server/project-dependency-invalidation.ts +114 -0
- package/server/project-file-scan.ts +235 -0
- package/server/project-hmr-files.ts +215 -0
- package/server/project-install-roots.ts +210 -0
- package/server/project-kinds.ts +71 -0
- package/server/project-mcp-servers.ts +82 -0
- package/server/project-module-freshness.ts +109 -0
- package/server/project-module-instance.ts +375 -0
- package/server/project-optimize-deps-entries.ts +827 -0
- package/server/project-output-writer.ts +679 -0
- package/server/project-package-origin.ts +63 -0
- package/server/project-root-surface.ts +831 -0
- package/server/project-scratch-path.ts +47 -0
- package/server/project-script-hmr.ts +619 -0
- package/server/project-serving-plugins.ts +236 -0
- package/server/project-tools.ts +653 -0
- package/server/project-validation.ts +250 -0
- package/server/project-verbs.ts +127 -0
- package/server/project-view.ts +52 -0
- package/server/project-watch.ts +1134 -0
- package/server/project-work-coordinator.ts +288 -0
- package/server/provider-credentials.ts +331 -0
- package/server/recent-projects-store.ts +98 -0
- package/server/redact-secrets.ts +71 -0
- package/server/repository-presence.ts +267 -0
- package/server/routes/account.ts +357 -0
- package/server/routes/agents.ts +60 -0
- package/server/routes/assets.ts +154 -0
- package/server/routes/build.ts +13 -0
- package/server/routes/collaboration.ts +595 -0
- package/server/routes/configurations.ts +419 -0
- package/server/routes/context.ts +264 -0
- package/server/routes/control-plane.ts +1811 -0
- package/server/routes/logs.ts +272 -0
- package/server/routes/project-identity.ts +302 -0
- package/server/routes/project-open.ts +429 -0
- package/server/routes/project-source.ts +686 -0
- package/server/routes/project-state.ts +383 -0
- package/server/routes/relay.ts +1029 -0
- package/server/routes/route-helpers.ts +29 -0
- package/server/routes/served-modules.ts +138 -0
- package/server/routes/session-tabs.ts +196 -0
- package/server/routes/settings.ts +121 -0
- package/server/routes/share-control.ts +486 -0
- package/server/routes/themes.ts +116 -0
- package/server/routes/tools.ts +230 -0
- package/server/routes/worktrees.ts +595 -0
- package/server/scoped-game-css.ts +461 -0
- package/server/server-utils.ts +1194 -0
- package/server/session-product.ts +112 -0
- package/server/session-registry.ts +230 -0
- package/server/share-claims.ts +138 -0
- package/server/share-host.ts +526 -0
- package/server/share-session-gateway.ts +1362 -0
- package/server/share-tunnel.ts +123 -0
- package/server/spawn-opener.ts +74 -0
- package/server/support/play/log-format.ts +76 -0
- package/server/support/play/session-record.ts +199 -0
- package/server/support/project/build-discipline.ts +718 -0
- package/server/support/project/inspection-node.ts +119 -0
- package/server/support/project/log-naming.ts +54 -0
- package/server/support/project/provenance.ts +290 -0
- package/server/support/project/run-name.ts +18 -0
- package/server/support/project/session-journal.ts +830 -0
- package/server/support/project/shared.ts +327 -0
- package/server/tab-bootstrap.ts +62 -0
- package/server/tab-heartbeat.ts +477 -0
- package/server/tab-lifecycle.ts +550 -0
- package/server/tab-presence.ts +1508 -0
- package/server/team-agent-mirror.ts +95 -0
- package/server/twin-auth.ts +138 -0
- package/server/vendored-lock-recorder.ts +968 -0
- package/server/worktree-identity.ts +121 -0
- package/server/worktree-management.ts +398 -0
- package/server/worktree-retention.d.mts +13 -0
- package/server/worktree-retention.mjs +205 -0
- package/src/EditorContext.tsx +169 -0
- package/src/account.ts +333 -0
- package/src/action-registry.ts +532 -0
- package/src/active-product.ts +74 -0
- package/src/active-project.ts +155 -0
- package/src/adapter-editor-config.ts +25 -0
- package/src/adapter-observation.ts +49 -0
- package/src/animation/stage-transport.ts +379 -0
- package/src/animation/three-clips-subject.ts +172 -0
- package/src/api/asset-library-wire.ts +45 -0
- package/src/api/assets.ts +365 -0
- package/src/api/base.ts +10 -0
- package/src/api/build.ts +99 -0
- package/src/api/git-wire.ts +56 -0
- package/src/api/logs.ts +92 -0
- package/src/api/project-identity.ts +74 -0
- package/src/api/project-open.ts +355 -0
- package/src/api/project-source.ts +162 -0
- package/src/api/project-state.ts +107 -0
- package/src/api/relay.ts +268 -0
- package/src/api/settings.ts +36 -0
- package/src/api/themes.ts +45 -0
- package/src/api/worktrees.ts +257 -0
- package/src/asset-compare-core.ts +171 -0
- package/src/asset-compare.ts +294 -0
- package/src/asset-editor-context.tsx +100 -0
- package/src/asset-events.ts +97 -0
- package/src/asset-inspector-actions.ts +87 -0
- package/src/asset-preview-framing.ts +357 -0
- package/src/asset-preview.ts +2802 -0
- package/src/asset-selection-viewer-registry.ts +113 -0
- package/src/asset-selection.ts +133 -0
- package/src/asset-workflow/asset-capabilities.ts +344 -0
- package/src/asset-workflow/asset-import-jobs.ts +106 -0
- package/src/asset-workflow/asset-ledger-backend.ts +126 -0
- package/src/asset-workflow/asset-ledger.ts +156 -0
- package/src/asset-workflow/asset-materialization-report.ts +140 -0
- package/src/asset-workflow/asset-pack-manifest.ts +320 -0
- package/src/asset-workflow/asset-types.ts +142 -0
- package/src/asset-workflow/asset-workflow-quality.ts +61 -0
- package/src/asset-workflow/audio-preview-player.ts +193 -0
- package/src/asset-workflow/audio-waveform.ts +22 -0
- package/src/asset-workflow/cloud-asset-client.ts +263 -0
- package/src/asset-workflow/folder-preview.ts +345 -0
- package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
- package/src/asset-workflow/image-view-scale.ts +32 -0
- package/src/asset-workflow/import-contract.ts +124 -0
- package/src/asset-workflow/ledger-write-lock.ts +244 -0
- package/src/asset-workflow/model-inspection.ts +818 -0
- package/src/asset-workflow/pixi-spritesheet.ts +197 -0
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +139 -0
- package/src/asset-workflow/project-asset-operations.ts +154 -0
- package/src/asset-workflow/project-asset-roots.ts +68 -0
- package/src/asset-workflow/project-content.ts +288 -0
- package/src/asset-workflow/project-source-index.ts +550 -0
- package/src/asset-workflow/thumbnail-system.ts +256 -0
- package/src/authoring/active-adapter.ts +199 -0
- package/src/authoring/active-systems.ts +422 -0
- package/src/authoring/adapter-key.ts +18 -0
- package/src/authoring/authoring-asset-url.ts +27 -0
- package/src/authoring/bootstrap-state.ts +49 -0
- package/src/authoring/boundary-authoring-adapter.ts +184 -0
- package/src/authoring/breakpoint-state.ts +43 -0
- package/src/authoring/canvas-scene-guides.ts +76 -0
- package/src/authoring/component-instance-root.ts +171 -0
- package/src/authoring/composite-authoring-adapter.ts +2110 -0
- package/src/authoring/consumer-actions.ts +520 -0
- package/src/authoring/css-numeric-style.ts +97 -0
- package/src/authoring/design-time-layers.ts +849 -0
- package/src/authoring/design-time-mount-registry.ts +235 -0
- package/src/authoring/design-time-settle.ts +343 -0
- package/src/authoring/edit-mode-authoring.ts +619 -0
- package/src/authoring/eyedropper-session.ts +60 -0
- package/src/authoring/instance-source-menu-register.ts +13 -0
- package/src/authoring/instance-source-menu.ts +135 -0
- package/src/authoring/layered-pick.ts +183 -0
- package/src/authoring/live-gesture-lock.ts +50 -0
- package/src/authoring/live-object-transform.ts +62 -0
- package/src/authoring/mount-failure-report.ts +154 -0
- package/src/authoring/mounted-root-subjects.ts +144 -0
- package/src/authoring/no-authoring-adapter.ts +55 -0
- package/src/authoring/null-inspection-subjects.tsx +75 -0
- package/src/authoring/object3d-document-persistence.ts +106 -0
- package/src/authoring/object3d-document-session-registry.ts +120 -0
- package/src/authoring/object3d-document-session.ts +1326 -0
- package/src/authoring/object3d-gesture-controller.ts +113 -0
- package/src/authoring/panel-authoring.ts +121 -0
- package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
- package/src/authoring/prefab-instance-section-model.ts +55 -0
- package/src/authoring/project-authoring-session.ts +105 -0
- package/src/authoring/provenance.ts +99 -0
- package/src/authoring/quarks-particle-systems.ts +19 -0
- package/src/authoring/react-canvas-navigation.ts +255 -0
- package/src/authoring/react-design-canvas-style.ts +20 -0
- package/src/authoring/react-story-board.ts +937 -0
- package/src/authoring/selection-scope.ts +195 -0
- package/src/authoring/shell-document-ops.ts +169 -0
- package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
- package/src/authoring/shell-viewport-policy.ts +48 -0
- package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/authoring/stories-scope.ts +35 -0
- package/src/authoring/story-board-chrome-fit.ts +107 -0
- package/src/authoring/story-board-presentation.ts +111 -0
- package/src/authoring/three-projection-core.ts +226 -0
- package/src/authoring/viewport-pick-context.ts +40 -0
- package/src/authoring/viewport-raycast.ts +240 -0
- package/src/authoring/viewport-tool-context.ts +73 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +152 -0
- package/src/authoring/world-pan-state.ts +198 -0
- package/src/authoring/world-session-state.ts +101 -0
- package/src/authoring/write-pipe.ts +173 -0
- package/src/availability-tick.ts +66 -0
- package/src/bitmap-label.ts +120 -0
- package/src/blender-tab-metrics.ts +161 -0
- package/src/board-open-actions.ts +20 -0
- package/src/boot-routing.ts +391 -0
- package/src/build-identity.ts +15 -0
- package/src/bytes-codec.ts +62 -0
- package/src/camera-authoring.ts +175 -0
- package/src/cancellation-reason.ts +58 -0
- package/src/canvas-preview-frames.ts +482 -0
- package/src/capture-camera-pose.ts +77 -0
- package/src/chrome-registry.ts +159 -0
- package/src/chrome-slot-registry.ts +91 -0
- package/src/collaboration-client.ts +264 -0
- package/src/collaboration-presence.ts +41 -0
- package/src/command-dispatch.ts +19 -0
- package/src/command-listener.ts +2743 -0
- package/src/command-registry.ts +70 -0
- package/src/component-board-registry.ts +205 -0
- package/src/component-states-registry.ts +200 -0
- package/src/components/AgentPresentationNotice.tsx +75 -0
- package/src/components/AlignToolbar.tsx +204 -0
- package/src/components/AppRoot.tsx +394 -0
- package/src/components/ApplicationMenus.tsx +415 -0
- package/src/components/AssetBrowser.tsx +2843 -0
- package/src/components/AssetEditorShell.tsx +216 -0
- package/src/components/AssetInspectorToolSection.tsx +124 -0
- package/src/components/BoardRulers.tsx +354 -0
- package/src/components/CameraInfo.tsx +81 -0
- package/src/components/CanvasSceneViewport.tsx +556 -0
- package/src/components/CapabilityCoverageSection.tsx +185 -0
- package/src/components/CenterDocuments.tsx +22 -0
- package/src/components/ChromeSlot.tsx +20 -0
- package/src/components/CodeView.tsx +470 -0
- package/src/components/CollaborationHeaderControl.css +74 -0
- package/src/components/CompactInspectorCard.tsx +190 -0
- package/src/components/CompactInspectorShell.tsx +39 -0
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +270 -0
- package/src/components/DefaultEditorLayout.tsx +106 -0
- package/src/components/DocumentHeaderStrip.tsx +96 -0
- package/src/components/DocumentShelfRail.tsx +45 -0
- package/src/components/DocumentThumbnail.tsx +345 -0
- package/src/components/EditorLeaseGuard.tsx +372 -0
- package/src/components/ErrorBanner.tsx +123 -0
- package/src/components/FolderPreviewTile.tsx +159 -0
- package/src/components/GameHierarchy.tsx +3842 -0
- package/src/components/GameplaySessionTimeline.tsx +295 -0
- package/src/components/InspectionProjection.tsx +968 -0
- package/src/components/Inspector.tsx +233 -0
- package/src/components/InspectorCanvasPreview.tsx +35 -0
- package/src/components/InspectorFieldsSection.tsx +290 -0
- package/src/components/InspectorObjectPreview.tsx +57 -0
- package/src/components/InspectorStoriesSection.tsx +92 -0
- package/src/components/InspectorToolSection.tsx +96 -0
- package/src/components/InspectorTransformSection.tsx +245 -0
- package/src/components/LightExplorerPanel.tsx +433 -0
- package/src/components/MediaProperties.tsx +145 -0
- package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
- package/src/components/Object3DDocumentToolbar.css +100 -0
- package/src/components/Object3DDocumentToolbar.tsx +454 -0
- package/src/components/Object3DDocumentViewport.tsx +58 -0
- package/src/components/ProjectHeader.css +617 -0
- package/src/components/ProjectHeader.tsx +328 -0
- package/src/components/ProjectLayout.tsx +249 -0
- package/src/components/ReactCanvasControls.tsx +284 -0
- package/src/components/RootSelectionOverlay.tsx +3162 -0
- package/src/components/RootTextEditor.tsx +79 -0
- package/src/components/SaveStatus.tsx +70 -0
- package/src/components/StageHost.tsx +1954 -0
- package/src/components/StageOverlays.tsx +21 -0
- package/src/components/StartupErrorScreen.tsx +76 -0
- package/src/components/StartupLoadingScreen.tsx +65 -0
- package/src/components/StatsOverlay.tsx +78 -0
- package/src/components/SurfaceStateOverlay.tsx +24 -0
- package/src/components/ToolContributionSurfaces.tsx +65 -0
- package/src/components/ToolHost.tsx +370 -0
- package/src/components/ToolObject3DPreview.tsx +39 -0
- package/src/components/Toolbar.tsx +595 -0
- package/src/components/TransientHint.tsx +44 -0
- package/src/components/TransportStrip.tsx +174 -0
- package/src/components/VersionControlSection.tsx +470 -0
- package/src/components/VgaiLogo.css +83 -0
- package/src/components/VgaiLogo.tsx +35 -0
- package/src/components/ViewportControlsHint.tsx +60 -0
- package/src/components/ViewportFurniture.tsx +401 -0
- package/src/components/ViewportOverlay.tsx +145 -0
- package/src/components/ViewportOverlaysMenu.tsx +153 -0
- package/src/components/ViewportShadingMenu.tsx +300 -0
- package/src/components/ViewportViewMenu.tsx +101 -0
- package/src/components/WorkspaceDocumentSurface.tsx +204 -0
- package/src/components/WorkspaceUtilitySurface.tsx +22 -0
- package/src/components/WorktreeSwitcher.css +239 -0
- package/src/components/WorktreeSwitcher.tsx +890 -0
- package/src/components/account-documents.tsx +1162 -0
- package/src/components/asset-documents.tsx +812 -0
- package/src/components/asset-editor-persistence.ts +216 -0
- package/src/components/asset-selection-section.tsx +535 -0
- package/src/components/asset-thumbnails.tsx +312 -0
- package/src/components/asset-viewers/AudioViewer.tsx +201 -0
- package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
- package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
- package/src/components/asset-viewers/ImageViewer.tsx +300 -0
- package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
- package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
- package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
- package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
- package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
- package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
- package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
- package/src/components/asset-viewers/VideoViewer.tsx +101 -0
- package/src/components/asset-viewers/shader-source.ts +144 -0
- package/src/components/asset-workflow.css +731 -0
- package/src/components/board-guides.ts +150 -0
- package/src/components/compact-inspector.css +539 -0
- package/src/components/core-utilities.tsx +90 -0
- package/src/components/editor-notifications.css +40 -0
- package/src/components/engine-workspace.css +274 -0
- package/src/components/inspector-preview-section.tsx +238 -0
- package/src/components/inspector-property-grouping.ts +64 -0
- package/src/components/inspector-revert-label.ts +20 -0
- package/src/components/inspector-selection.ts +42 -0
- package/src/components/inspector-stories-gating.ts +171 -0
- package/src/components/inspector-transform-subject.ts +11 -0
- package/src/components/inspector-transform.ts +75 -0
- package/src/components/kind-documents.tsx +500 -0
- package/src/components/palette-action-publisher.tsx +140 -0
- package/src/components/primitives/DraftColorInput.tsx +74 -0
- package/src/components/product-shell.css +1829 -0
- package/src/components/project-tool-documents.tsx +410 -0
- package/src/components/scene-documents.tsx +223 -0
- package/src/components/stage-keyboard.tsx +37 -0
- package/src/components/stage-overlay-set.tsx +107 -0
- package/src/components/stage-presence-markers.ts +478 -0
- package/src/components/standard-viewport-dressing.ts +417 -0
- package/src/components/status-contributions.tsx +403 -0
- package/src/components/tool-documents.tsx +309 -0
- package/src/components/tool-schema-form.tsx +262 -0
- package/src/components/use-after-paint.ts +41 -0
- package/src/components/use-project-image-assets.ts +86 -0
- package/src/components/viewport-header-controls.css +90 -0
- package/src/components/viewport-surface-status.tsx +55 -0
- package/src/components/workspace-history.ts +32 -0
- package/src/components/workspace-static-panel-registry.tsx +152 -0
- package/src/components/workspace-surfaces.css +621 -0
- package/src/components/world-documents.tsx +586 -0
- package/src/components/world-overlay-gestures.ts +1694 -0
- package/src/composite-screenshot.ts +1641 -0
- package/src/console-sync.ts +131 -0
- package/src/constraint-helper.ts +338 -0
- package/src/content-entry-source-registry.ts +184 -0
- package/src/coverage/authoring-seam-evidence.ts +300 -0
- package/src/coverage/canvas-reveal.ts +192 -0
- package/src/coverage/design-time-surfaces.ts +101 -0
- package/src/coverage/live-seam-evidence.ts +11 -0
- package/src/coverage/ontology-invariants.ts +466 -0
- package/src/coverage/session-vitals.ts +501 -0
- package/src/coverage/system-seam-evidence.ts +72 -0
- package/src/crash-null-boundary.ts +36 -0
- package/src/creation-site-edit.ts +1479 -0
- package/src/creation-site-registry.ts +160 -0
- package/src/delegate-harness-registry.ts +143 -0
- package/src/document-context-registry.ts +177 -0
- package/src/document-open-registry.ts +200 -0
- package/src/document-preview-source.ts +20 -0
- package/src/document-renderer-session.ts +138 -0
- package/src/editor-api.ts +46 -0
- package/src/editor-chrome-capture.ts +139 -0
- package/src/editor-commands.ts +164 -0
- package/src/editor-console.ts +563 -0
- package/src/editor-current-view.ts +82 -0
- package/src/editor-document-probe.ts +881 -0
- package/src/editor-git-client.ts +115 -0
- package/src/editor-host-door.ts +462 -0
- package/src/editor-hotkeys.ts +842 -0
- package/src/editor-lease-view.ts +39 -0
- package/src/editor-lease.ts +415 -0
- package/src/editor-mode.ts +19 -0
- package/src/editor-notifications.ts +140 -0
- package/src/editor-presence.ts +563 -0
- package/src/editor-presentation-activity.ts +50 -0
- package/src/editor-presentation-notice.ts +42 -0
- package/src/editor-runtime.tsx +145 -0
- package/src/editor-server-response.ts +86 -0
- package/src/editor-session-attribution.ts +75 -0
- package/src/editor-session-mode.ts +54 -0
- package/src/editor-shell-store.ts +1304 -0
- package/src/editor-state-facets.ts +74 -0
- package/src/editor-styles.css +33 -0
- package/src/editor-view-presentation.ts +810 -0
- package/src/editor-viewport.ts +5302 -0
- package/src/entity-lod.ts +31 -0
- package/src/entity-object.ts +91 -0
- package/src/files/file-provider.ts +62 -0
- package/src/files/project-files.ts +264 -0
- package/src/finders/index.ts +136 -0
- package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
- package/src/frame/bridge.tsx +1408 -0
- package/src/frame/product.ts +77 -0
- package/src/gameplay-dom-recording.ts +319 -0
- package/src/gameplay-export-state.ts +14 -0
- package/src/gameplay-replay.ts +417 -0
- package/src/gameplay-session-time.ts +9 -0
- package/src/gameplay-sessions.ts +204 -0
- package/src/harness-chat-types.ts +253 -0
- package/src/hierarchy-component-marks.ts +298 -0
- package/src/hierarchy-drop.ts +91 -0
- package/src/hierarchy-expansion-state.ts +80 -0
- package/src/hierarchy-header-slot.ts +52 -0
- package/src/hierarchy-internals.ts +197 -0
- package/src/hierarchy-kind-icon.ts +217 -0
- package/src/hierarchy-mark-reader.ts +73 -0
- package/src/hierarchy-menu-registry.ts +67 -0
- package/src/hierarchy-node-rows.ts +307 -0
- package/src/hierarchy-panel-view.ts +280 -0
- package/src/hierarchy-projection.ts +76 -0
- package/src/hierarchy-row-cache.ts +243 -0
- package/src/hierarchy-row-model.ts +308 -0
- package/src/hierarchy-rows.ts +11 -0
- package/src/hierarchy-walk.ts +86 -0
- package/src/history/editor-session.ts +25 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-delegate.ts +163 -0
- package/src/history/history-limit-notices.ts +43 -0
- package/src/history/history-service.ts +1173 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +386 -0
- package/src/history/project-root-history-backends.ts +139 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +103 -0
- package/src/history/source-history-backend.ts +546 -0
- package/src/history/types.ts +124 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hotkeys.ts +188 -0
- package/src/inference-diagnostics.ts +69 -0
- package/src/initial-project.ts +80 -0
- package/src/inspection/active-subject.ts +578 -0
- package/src/inspection/active-surface.ts +142 -0
- package/src/inspection/compose.ts +1064 -0
- package/src/inspection/display.ts +170 -0
- package/src/inspection/document-subject.ts +109 -0
- package/src/inspection/game-subject.ts +85 -0
- package/src/inspection/model.ts +542 -0
- package/src/inspection/null-subject.ts +115 -0
- package/src/inspection/serialize.ts +357 -0
- package/src/inspection/use-active-inspection.ts +180 -0
- package/src/inspector-presentation.ts +201 -0
- package/src/inspector-section-registry.ts +221 -0
- package/src/instance-source-actions.ts +163 -0
- package/src/instanced-presentation.ts +164 -0
- package/src/js-heap.ts +71 -0
- package/src/key-actions.ts +91 -0
- package/src/keymap-presets.ts +401 -0
- package/src/layout-policy.ts +31 -0
- package/src/learn-links.ts +73 -0
- package/src/light-explorer-model.ts +134 -0
- package/src/live-canvas-frame.ts +55 -0
- package/src/live-document.ts +279 -0
- package/src/live-module-source.ts +230 -0
- package/src/live-session-registry.ts +220 -0
- package/src/live-transition.ts +633 -0
- package/src/manifest-project.ts +107 -0
- package/src/model-thumbnail.ts +523 -0
- package/src/native-selection-style.ts +202 -0
- package/src/object3d-document-write-policy.ts +137 -0
- package/src/packaged-runtime.ts +108 -0
- package/src/palettes/maya.palette.json +57 -0
- package/src/palettes/substance.palette.json +57 -0
- package/src/pasteboard-module.ts +81 -0
- package/src/performance-profiler.ts +367 -0
- package/src/performance-sources.ts +69 -0
- package/src/play-boot-phase.ts +145 -0
- package/src/presentation-surface.ts +248 -0
- package/src/project-adapter.ts +1143 -0
- package/src/project-asset-refresh.ts +26 -0
- package/src/project-declaration-refresh.ts +31 -0
- package/src/project-local-state.ts +118 -0
- package/src/project-manager.ts +243 -0
- package/src/project-module-changes.ts +163 -0
- package/src/project-module-split.ts +266 -0
- package/src/project-provenance.ts +115 -0
- package/src/project-ready.ts +42 -0
- package/src/project-session-reset.ts +67 -0
- package/src/project-shape.ts +68 -0
- package/src/project-tool-discovery.ts +66 -0
- package/src/project-tools.ts +107 -0
- package/src/project-work-types.ts +149 -0
- package/src/projection/three.ts +898 -0
- package/src/projection/types.ts +44 -0
- package/src/readiness.ts +113 -0
- package/src/reflection-probe-helper.ts +142 -0
- package/src/reported-play-state.ts +90 -0
- package/src/resolve-relative-specifier.ts +33 -0
- package/src/scene-document-plan.ts +315 -0
- package/src/scene-framing.ts +315 -0
- package/src/scene-live-open.ts +210 -0
- package/src/scene-view-fog.ts +89 -0
- package/src/scoped-game-css.ts +152 -0
- package/src/session-orphan-record.ts +193 -0
- package/src/session-tombstone.ts +126 -0
- package/src/settings/settings-provider.ts +82 -0
- package/src/settings-store.ts +345 -0
- package/src/shared-view-restore.ts +42 -0
- package/src/shell-store-door.ts +45 -0
- package/src/source-conflict.ts +122 -0
- package/src/spatial-handle-visuals.ts +332 -0
- package/src/stage-context.ts +341 -0
- package/src/stage-store-registry.ts +63 -0
- package/src/stale-chunk-recovery.ts +34 -0
- package/src/state-report-deferral.ts +73 -0
- package/src/storage/host-files-storage.ts +97 -0
- package/src/storage/http-storage.ts +174 -0
- package/src/storage/index.ts +75 -0
- package/src/storage/mem-storage.ts +149 -0
- package/src/storage/path-lock.ts +44 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/types.ts +116 -0
- package/src/stories/StoryComponentThumbnail.tsx +184 -0
- package/src/stories/StoryPreviewMount.tsx +306 -0
- package/src/stories/component-content-source.tsx +106 -0
- package/src/stories/component-states-source.ts +78 -0
- package/src/stories/compose-project-stories.ts +255 -0
- package/src/stories/pixi-story-model.ts +30 -0
- package/src/stories/prefabs-finder.ts +54 -0
- package/src/stories/prefabs-from-stories.ts +182 -0
- package/src/stories/project-story-discovery.ts +78 -0
- package/src/stories/project-story-regions.ts +24 -0
- package/src/stories/story-actions.ts +54 -0
- package/src/stories/story-capture-command.ts +124 -0
- package/src/stories/story-capture.ts +584 -0
- package/src/stories/story-declared-medium.ts +126 -0
- package/src/stories/story-discovery.ts +176 -0
- package/src/stories/story-document-openers.ts +36 -0
- package/src/stories/story-dom-runtime.ts +78 -0
- package/src/stories/story-grouping.ts +111 -0
- package/src/stories/story-lane.ts +88 -0
- package/src/stories/story-mount-turn.ts +27 -0
- package/src/stories/story-opener.ts +260 -0
- package/src/stories/story-pixi-preview.ts +408 -0
- package/src/stories/story-presentation.ts +215 -0
- package/src/stories/story-registry.ts +517 -0
- package/src/stories/story-three-preview.ts +807 -0
- package/src/stories/three-story-model.ts +96 -0
- package/src/story-three-preview-runtime.ts +56 -0
- package/src/surface-keyboard.ts +112 -0
- package/src/surface-state.ts +124 -0
- package/src/tab-bootstrap.js +628 -0
- package/src/tab-census.ts +198 -0
- package/src/tab-lifecycle-client.ts +196 -0
- package/src/theme-library.ts +844 -0
- package/src/theme-preference.ts +378 -0
- package/src/theme.css +3437 -0
- package/src/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/three-viewport/camera-fit.ts +41 -0
- package/src/three-viewport/interactive-renderer.ts +132 -0
- package/src/three-viewport/selection-brackets.ts +310 -0
- package/src/three-viewport/selection-outline.ts +183 -0
- package/src/three-viewport/skeleton-helper.ts +61 -0
- package/src/three-viewport/source-color.ts +167 -0
- package/src/three-viewport/studio-environment.ts +96 -0
- package/src/three-viewport-presentation.ts +23 -0
- package/src/tool-contribution-play.ts +74 -0
- package/src/tool-loader.ts +1798 -0
- package/src/transform-mode-request.ts +57 -0
- package/src/transient-hint.ts +78 -0
- package/src/trigger-volume-helper.ts +116 -0
- package/src/ui-source/adapter-region-includes.ts +241 -0
- package/src/ui-source/ensure-import.ts +132 -0
- package/src/ui-source/file-region-resolver.ts +302 -0
- package/src/ui-source/inspect.ts +775 -0
- package/src/ui-source/oid-transform.ts +1845 -0
- package/src/ui-source/plan-csf-story.ts +196 -0
- package/src/ui-source/plan-extract-component.ts +421 -0
- package/src/ui-source/plan-fork-component.ts +621 -0
- package/src/ui-source/plan-named-style.ts +95 -0
- package/src/ui-source/plan-source-edit.ts +353 -0
- package/src/ui-source/r3f-contract-resolver.ts +327 -0
- package/src/ui-source/r3f-diagnostic-index.ts +165 -0
- package/src/ui-source/r3f-environment-binding.ts +133 -0
- package/src/ui-source/r3f-joint-binding.ts +251 -0
- package/src/ui-source/r3f-lod-binding.ts +91 -0
- package/src/ui-source/r3f-particle-binding.ts +235 -0
- package/src/ui-source/r3f-physics-binding.ts +483 -0
- package/src/ui-source/r3f-project-contracts.ts +444 -0
- package/src/ui-source/relative-import-specifier.ts +34 -0
- package/src/ui-source/reparent-guard.ts +544 -0
- package/src/ui-source/source-edit-request.ts +97 -0
- package/src/ui-source/source-write-backend.ts +618 -0
- package/src/ui-source/syntactic-prop-specs.ts +296 -0
- package/src/ui-source/tier-source-write-backend.ts +279 -0
- package/src/ui-source/ts-ast.ts +171 -0
- package/src/ui-source/utility-class-support.ts +169 -0
- package/src/ui-source/write-component-default.ts +133 -0
- package/src/ui-source/writer.ts +1989 -0
- package/src/viewport-activation-timings.ts +840 -0
- package/src/viewport-authoring-policy.ts +186 -0
- package/src/viewport-controls-hint.ts +57 -0
- package/src/viewport-door.ts +237 -0
- package/src/viewport-shading-boundary.ts +12 -0
- package/src/vite-error-surface.ts +141 -0
- package/src/wait-until.ts +37 -0
- package/src/workspace-areas.ts +156 -0
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-available-documents.ts +127 -0
- package/src/workspace-core-utilities.ts +31 -0
- package/src/workspace-document-ids.ts +59 -0
- package/src/workspace-document-registry.ts +612 -0
- package/src/workspace-document-restore.ts +145 -0
- package/src/workspace-host-commands.ts +141 -0
- package/src/workspace-persistence-gate.ts +40 -0
- package/src/workspace-play-utilities.ts +44 -0
- package/src/workspace-presets.ts +425 -0
- package/src/workspace-regions.ts +294 -0
- package/src/workspace-state-persistence.ts +536 -0
- package/src/workspace-static-panels.ts +73 -0
- package/src/workspace-status-registry.ts +121 -0
- package/src/workspace-style.ts +239 -0
- package/src/workspace-utility-commands.ts +74 -0
- package/src/workspace-utility-registry.ts +263 -0
- package/src/workspace-viewport-rect.ts +97 -0
- package/src/world-adoption.ts +115 -0
- package/src/world-document-routing.ts +104 -0
- package/vite-plugin-creation-site-write.ts +184 -0
- package/vite-plugin-creation-site.ts +80 -0
- package/vite-plugin-game-static.ts +303 -0
- package/vite-plugin-module-doorways.ts +336 -0
- package/vite-plugin-product-contributions.ts +197 -0
- package/vite-plugin-project-game-static.ts +125 -0
- package/vite-plugin-project-jsx-js.ts +65 -0
- package/vite-plugin-project-root-absolute-assets.ts +359 -0
- package/vite-plugin-shared-react.ts +420 -0
- package/vite-plugin-shared-three.ts +203 -0
- package/vite-plugin-ui-oid.ts +2106 -0
|
@@ -0,0 +1,2110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CompositeAuthoringAdapter — merges N per-world {@link AuthoringAdapter}s into ONE authoring
|
|
3
|
+
* tree so the editor's hierarchy lists EVERY mounted world's entities and selection/inspection
|
|
4
|
+
* routes to the right backend. This is the editor-multi-root-selection seam, generalized to N-ary
|
|
5
|
+
* (T6.1 slice 3): one top-level GROUP NODE per world
|
|
6
|
+
* (`world:<id> (<kind>)`), the child adapter's own roots nested beneath it. The project/game
|
|
7
|
+
* identity already lives in the editor chrome, so the authoring projection starts at those real
|
|
8
|
+
* world roots — it does not add a redundant synthetic Game row. Routing is by GROUP-NODE
|
|
9
|
+
* OWNERSHIP: the child adapter whose tree contains a given node id.
|
|
10
|
+
*
|
|
11
|
+
* The editor still talks to a single {@link AuthoringAdapter}; selection +
|
|
12
|
+
* inspection work in every world; the 3D transform gizmo stays threejs-only (a
|
|
13
|
+
* viewport concern, outside this adapter); play/pause stays game-level (T7.6).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
AssetDropProvider,
|
|
18
|
+
AssetSubjectProvider,
|
|
19
|
+
AuthoringAdapter,
|
|
20
|
+
AuthoringCapabilities,
|
|
21
|
+
ComponentInstancesProvider,
|
|
22
|
+
EditorNode,
|
|
23
|
+
HierarchyProvider,
|
|
24
|
+
InspectorProvider,
|
|
25
|
+
NodeCreationSite,
|
|
26
|
+
PersistenceProvider,
|
|
27
|
+
PropertyDescriptor,
|
|
28
|
+
RelatedSubjectsProvider,
|
|
29
|
+
SelectionProvider,
|
|
30
|
+
SpatialHandlesProvider,
|
|
31
|
+
StoriesProvider,
|
|
32
|
+
StoryRef,
|
|
33
|
+
StructureProvider,
|
|
34
|
+
Transform,
|
|
35
|
+
TransformProvider,
|
|
36
|
+
TruthProvider,
|
|
37
|
+
WriteAck,
|
|
38
|
+
} from '@volter/editor-project/adapter';
|
|
39
|
+
import type * as THREE from 'three';
|
|
40
|
+
import { recordAuthoringConsumerUse } from '../coverage/authoring-seam-evidence';
|
|
41
|
+
import { NO_OBJECT_REASON } from '../creation-site-registry';
|
|
42
|
+
import { editorConsole } from '../editor-console';
|
|
43
|
+
import type { HierarchyProjection, HierarchyProjectionGroup } from '../hierarchy-projection';
|
|
44
|
+
import { forEachHierarchyNode } from '../hierarchy-walk';
|
|
45
|
+
import {
|
|
46
|
+
applyAuthoringInstanceToComponent,
|
|
47
|
+
applyAuthoringTransform,
|
|
48
|
+
beginAuthoringTransformEdit,
|
|
49
|
+
commitAuthoringTransformSource,
|
|
50
|
+
copyAuthoringNodes,
|
|
51
|
+
createAuthoringNode,
|
|
52
|
+
cutAuthoringNodes,
|
|
53
|
+
dropAuthoringAsset,
|
|
54
|
+
duplicateAuthoringNode,
|
|
55
|
+
endAuthoringTransformEdit,
|
|
56
|
+
groupAuthoringNodes,
|
|
57
|
+
pasteAuthoringNodes,
|
|
58
|
+
removeAuthoringNode,
|
|
59
|
+
removeAuthoringTransform,
|
|
60
|
+
removeManyAuthoringNodes,
|
|
61
|
+
reorderAuthoringNode,
|
|
62
|
+
reparentAuthoringNode,
|
|
63
|
+
revertAuthoringInstance,
|
|
64
|
+
saveAuthoringDocument,
|
|
65
|
+
setAuthoringSelection,
|
|
66
|
+
ungroupAuthoringNode,
|
|
67
|
+
unwrapAuthoringNode,
|
|
68
|
+
wrapAuthoringNode,
|
|
69
|
+
} from './consumer-actions';
|
|
70
|
+
import { WORLD_SCOPE_NODE_ID } from './stories-scope';
|
|
71
|
+
import { LIVE_ONLY_ACK, NO_PERSISTABLE_CHILD_DESTINATION } from './write-pipe';
|
|
72
|
+
|
|
73
|
+
/** One child world's authoring adapter, ordered as given to the constructor. */
|
|
74
|
+
export interface CompositeChild {
|
|
75
|
+
/** Manifest world id (T3.1) — becomes the group node id `world:<worldId>`. */
|
|
76
|
+
readonly worldId: string;
|
|
77
|
+
/** World render substrate kind — shown in the group node's label. */
|
|
78
|
+
readonly kind: string;
|
|
79
|
+
/** Runtime manifest roots are the default. A nested authoring surface is
|
|
80
|
+
* adapter-owned content of its parent world, not another runtime world. */
|
|
81
|
+
readonly role?: 'world' | 'surface';
|
|
82
|
+
/** Optional user-facing label for a nested authoring surface. */
|
|
83
|
+
readonly label?: string;
|
|
84
|
+
readonly adapter: AuthoringAdapter;
|
|
85
|
+
/**
|
|
86
|
+
* D12 (B4) — the world's INSTALL-TIME manifest zOrder (`world.zOrder ?? 0`),
|
|
87
|
+
* independent of this child's position in the constructor's array. Absent
|
|
88
|
+
* (every pre-B4 call site) ⇒ `childAdapters()` falls back to the array
|
|
89
|
+
* index, today's behavior unchanged. See `childAdapters()`'s own doc
|
|
90
|
+
* comment for why this stopped being "array order doubles as z-order" and
|
|
91
|
+
* what "install-time" means here.
|
|
92
|
+
*/
|
|
93
|
+
readonly zOrder?: number;
|
|
94
|
+
/** Manifest pausing policy, retained for read-only play-mode inspection. */
|
|
95
|
+
readonly pausable?: boolean;
|
|
96
|
+
/** Adapter-owned content declaration, retained for play-mode inspection. */
|
|
97
|
+
readonly content?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Nest this child's group node UNDER another child's group node instead
|
|
100
|
+
* of directly in the hierarchy root list. Used by the scene-UI child
|
|
101
|
+
* (`edit-mode-authoring.ts`'s UI child, `kind: 'scene-ui'`) to appear
|
|
102
|
+
* "under its owning world" in the ONE hierarchy, per the C3 spec text.
|
|
103
|
+
* Absent ⇒ today's flat top-level shape, unchanged.
|
|
104
|
+
*/
|
|
105
|
+
readonly parentRootId?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function childAssetEntries(adapter: AuthoringAdapter) {
|
|
109
|
+
const provider = adapter.assetSubject;
|
|
110
|
+
if (!provider) return [];
|
|
111
|
+
const declared = provider.entries?.();
|
|
112
|
+
if (declared) return declared;
|
|
113
|
+
const entries: Array<{
|
|
114
|
+
id: string;
|
|
115
|
+
subject: NonNullable<ReturnType<AssetSubjectProvider['get']>>;
|
|
116
|
+
}> = [];
|
|
117
|
+
forEachHierarchyNode(adapter.hierarchy, (node) => {
|
|
118
|
+
const subject = provider.get(node.id);
|
|
119
|
+
if (subject) entries.push({ id: node.id, subject });
|
|
120
|
+
});
|
|
121
|
+
return entries;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const GROUP_PREFIX = 'world:';
|
|
125
|
+
const KIND_PREFIX = 'kind:';
|
|
126
|
+
const PROJECTION_PREFIX = 'projection:';
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Display label per root KIND — what the host hands the root, never which
|
|
130
|
+
* library the root chose.
|
|
131
|
+
*
|
|
132
|
+
* This used to read `pixijs: 'PixiJS'` / `react: 'React'`, which the kind
|
|
133
|
+
* vocabulary makes a lie: a `canvas` root may be Pixi, Phaser, Babylon, raw
|
|
134
|
+
* WebGL/WebGPU or a 2D context, and a `dom` root may be React, Vue, Svelte or
|
|
135
|
+
* plain HTML. The host cannot know, and labelling it "PixiJS" would tell a
|
|
136
|
+
* Phaser author something false about their own game.
|
|
137
|
+
*
|
|
138
|
+
* `three` is the exception on purpose — that kind is DEFINED by the shared
|
|
139
|
+
* `three` instance the host provides, so naming it is honest.
|
|
140
|
+
*
|
|
141
|
+
* An adapter that wants to say "Pixi" here can: it knows what it is, and the
|
|
142
|
+
* label belongs to it rather than to this table.
|
|
143
|
+
*/
|
|
144
|
+
const KIND_LABELS: Readonly<Record<string, string>> = {
|
|
145
|
+
three: 'Three.js',
|
|
146
|
+
canvas: 'Canvas',
|
|
147
|
+
dom: 'DOM',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
function groupNodeId(worldId: string): string {
|
|
151
|
+
return `${GROUP_PREFIX}${worldId}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The refusal sentence for an id no child owns — named here (not inlined at
|
|
156
|
+
* each call site) so the console message, the thrown `transforms.get` error and
|
|
157
|
+
* the `editor.select` door all say the SAME thing: which resolver rejected it,
|
|
158
|
+
* which id, and which worlds actually exist.
|
|
159
|
+
*/
|
|
160
|
+
export function unownedIdRefusal(
|
|
161
|
+
operation: string,
|
|
162
|
+
id: string,
|
|
163
|
+
children: ReadonlyArray<{ readonly worldId: string }>,
|
|
164
|
+
): string {
|
|
165
|
+
return (
|
|
166
|
+
`[CompositeAuthoringAdapter] ${operation}: no root owns entity id "${id}". ` +
|
|
167
|
+
`Roots in this composition: ${children.map((c) => c.worldId).join(', ')}. ` +
|
|
168
|
+
`Refusing rather than answering from the first root — an id nothing owns has no subject.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The refusal sentence for an id a child DOES own but has no transform truth
|
|
174
|
+
* for — the other half of {@link unownedIdRefusal}. Ownership answers "which
|
|
175
|
+
* root", not "does that root pose this node": a child adapter may expose no
|
|
176
|
+
* `TransformProvider` at all (the interface makes it optional), and answering
|
|
177
|
+
* an identity pose there is a fabricated subject, not a degrade. `dimensions`
|
|
178
|
+
* already returns `null` for this case so the Transform section never renders;
|
|
179
|
+
* this is what the non-nullable `get` says when a caller reaches it anyway.
|
|
180
|
+
*/
|
|
181
|
+
export function untransformedIdRefusal(operation: string, id: string, worldId: string): string {
|
|
182
|
+
return (
|
|
183
|
+
`[CompositeAuthoringAdapter] ${operation}: root "${worldId}" owns entity id "${id}" but ` +
|
|
184
|
+
'exposes no transform provider for it. Refusing rather than answering an identity pose — ' +
|
|
185
|
+
'a node with no transform truth has no pose to report.'
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** True for a synthetic group-node id this Composite itself manufactures (never
|
|
190
|
+
* produced by a child adapter — the `world:` prefix is reserved for this use). */
|
|
191
|
+
function isGroupNodeId(id: string): boolean {
|
|
192
|
+
return id.startsWith(GROUP_PREFIX);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function kindNodeId(kind: string): string {
|
|
196
|
+
return `${KIND_PREFIX}${kind}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function projectionNodeId(id: string): string {
|
|
200
|
+
return `${PROJECTION_PREFIX}${id}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isOrganizationNodeId(id: string): boolean {
|
|
204
|
+
return id.startsWith(KIND_PREFIX) || id.startsWith(PROJECTION_PREFIX);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function humanizeId(id: string): string {
|
|
208
|
+
const words = id.replace(/[-_]+/g, ' ').trim();
|
|
209
|
+
return words ? words.replace(/\b\w/g, (letter) => letter.toUpperCase()) : id;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Manifest-backed properties on each authored root, implemented by
|
|
214
|
+
* `edit-mode-authoring.ts`'s `ManifestAuthoring` over the RAW `vgai.project.json`
|
|
215
|
+
* (read-modify-write, never round-tripped through Zod). Defined here (not
|
|
216
|
+
* imported from `edit-mode-authoring.ts`) to avoid a circular import — that
|
|
217
|
+
* module already imports `CompositeAuthoringAdapter`.
|
|
218
|
+
*/
|
|
219
|
+
export interface RootManifestProvider {
|
|
220
|
+
/** Adapter-owned content declaration shown on the world boundary (for
|
|
221
|
+
* example `Entry · src/world.tsx` or `Entry · src/ui/App.tsx`). */
|
|
222
|
+
getRootContentSource?(worldId: string): string | undefined;
|
|
223
|
+
getRootZOrder(worldId: string): number;
|
|
224
|
+
setRootZOrder(worldId: string, value: number): void;
|
|
225
|
+
getRootPausable(worldId: string): boolean;
|
|
226
|
+
setRootPausable(worldId: string, value: boolean): void;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** OR every child's capabilities together — a feature is offered if ANY
|
|
230
|
+
* child supports it (the editor gates per-node via the routed adapter
|
|
231
|
+
* anyway). Factored out of the constructor (B1) so {@link
|
|
232
|
+
* CompositeAuthoringAdapter.replaceChild} can recompute it after an
|
|
233
|
+
* in-place child swap without duplicating the reduce. */
|
|
234
|
+
function computeCapabilities(children: ReadonlyArray<CompositeChild>): AuthoringCapabilities {
|
|
235
|
+
return children.reduce<AuthoringCapabilities>(
|
|
236
|
+
(acc, c) => ({
|
|
237
|
+
transform: acc.transform || c.adapter.capabilities.transform,
|
|
238
|
+
inspectorFields: acc.inspectorFields || c.adapter.capabilities.inspectorFields,
|
|
239
|
+
persist: acc.persist || c.adapter.capabilities.persist,
|
|
240
|
+
}),
|
|
241
|
+
{
|
|
242
|
+
transform: false,
|
|
243
|
+
inspectorFields: false,
|
|
244
|
+
persist: false,
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export class CompositeAuthoringAdapter implements AuthoringAdapter {
|
|
250
|
+
/** NOT `readonly` (interface-level `readonly` is a consumer contract, not a
|
|
251
|
+
* ban on internal reassignment) — B1's {@link replaceChild} recomputes
|
|
252
|
+
* this after an in-place child swap (e.g. a world's design-time layer
|
|
253
|
+
* mounting successfully upgrades its Boundary adapter to a live one). */
|
|
254
|
+
capabilities: AuthoringCapabilities;
|
|
255
|
+
/**
|
|
256
|
+
* NOT `ReadonlyArray` (B1): {@link replaceChild} mutates ONE element of
|
|
257
|
+
* this array in place, preserving both the array's own identity-adjacent
|
|
258
|
+
* invariants (length, index/z-order) and — critically — THIS composite's
|
|
259
|
+
* OWN object identity, which `world-root-stage.ts`'s teardown-identity
|
|
260
|
+
* capture, `exitEditModeAuthoring`'s installed-instance check, and the
|
|
261
|
+
* `markEditModeOverride` brand all key off. A REBUILT composite (a new
|
|
262
|
+
* `CompositeAuthoringAdapter` instance) would silently break all three —
|
|
263
|
+
* see this class's own doc comment and `edit-mode-authoring.ts`'s. Copied
|
|
264
|
+
* (`[...children]`) out of the constructor's `ReadonlyArray` parameter so
|
|
265
|
+
* mutating it here never aliases a caller's own array.
|
|
266
|
+
*/
|
|
267
|
+
private readonly children: CompositeChild[];
|
|
268
|
+
/** Optional writable manifest surface for world composition properties.
|
|
269
|
+
* Runtime still has one semantic Game root; the authoring hierarchy does not
|
|
270
|
+
* mirror that project-level wrapper because the editor chrome already owns it. */
|
|
271
|
+
private readonly manifest: RootManifestProvider | null;
|
|
272
|
+
/** Optional editor-only organization; never consulted by runtime mounting. */
|
|
273
|
+
private readonly projection: HierarchyProjection | null;
|
|
274
|
+
private readonly resolvedProjectionGroups: Array<HierarchyProjectionGroup & { roots: string[] }>;
|
|
275
|
+
/** Which child owns the session's last successful structural copy/cut. The
|
|
276
|
+
* payload itself stays inside that adapter; the composite remembers only the
|
|
277
|
+
* routing fact needed after selection moves. */
|
|
278
|
+
private structureClipboardWorldId: string | null = null;
|
|
279
|
+
/** Cross-root authored edges rebuilt once at the start of each hierarchy read. */
|
|
280
|
+
private crossSurfaceParents = new Map<string, string>();
|
|
281
|
+
private crossSurfaceChildren = new Map<string, string[]>();
|
|
282
|
+
private crossSurfaceOrders = new Map<string, number>();
|
|
283
|
+
private crossSurfaceRootParents = new Map<string, string>();
|
|
284
|
+
private crossSurfaceRootChildren = new Map<string, string[]>();
|
|
285
|
+
private crossSurfaceGroupLabels = new Map<string, string>();
|
|
286
|
+
/** A DOM semantic host may render OID-stamped implementation elements
|
|
287
|
+
* between source-owned semantic rows. These maps make those wrappers
|
|
288
|
+
* transparent only inside an explicitly identified cross-surface subtree. */
|
|
289
|
+
private crossSurfaceDomParents = new Map<string, string>();
|
|
290
|
+
private crossSurfaceDomChildren = new Map<string, string[]>();
|
|
291
|
+
private crossSurfaceInputSignatures: ReadonlyArray<string | null> | null = null;
|
|
292
|
+
/** Stable panel views for a native root that belongs to one explicit
|
|
293
|
+
* cross-surface semantic document. The proxy changes only `hierarchy`; every
|
|
294
|
+
* provider and capability remains this composite's live routed seam. */
|
|
295
|
+
private readonly projectedDocumentAdapters = new Map<string, AuthoringAdapter>();
|
|
296
|
+
|
|
297
|
+
/** Stable routing object. A composite can gain/lose a capable child through
|
|
298
|
+
* `replaceChild`, so its provider remains present and honestly returns no
|
|
299
|
+
* layers while none of the current children implements the seam. */
|
|
300
|
+
readonly spatialHandles: SpatialHandlesProvider = {
|
|
301
|
+
layers: (id) => this.routeOwned(id, 'spatialHandles.layers')?.spatialHandles?.layers(id) ?? [],
|
|
302
|
+
preview: (id, handleId, worldPosition) => {
|
|
303
|
+
this.routeOwned(id, 'spatialHandles.preview')?.spatialHandles?.preview(
|
|
304
|
+
id,
|
|
305
|
+
handleId,
|
|
306
|
+
worldPosition,
|
|
307
|
+
);
|
|
308
|
+
},
|
|
309
|
+
commit: (id, handleId, worldPosition) =>
|
|
310
|
+
this.routeOwned(id, 'spatialHandles.commit')?.spatialHandles?.commit(
|
|
311
|
+
id,
|
|
312
|
+
handleId,
|
|
313
|
+
worldPosition,
|
|
314
|
+
),
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* N-ary (T6.1 slice 3): an ordered `{ worldId, kind, adapter }[]` — one group
|
|
319
|
+
* node per entry, in array order.
|
|
320
|
+
*
|
|
321
|
+
* `manifest` is an optional writable provider. Omitting it leaves root
|
|
322
|
+
* composition properties read-only.
|
|
323
|
+
*/
|
|
324
|
+
constructor(
|
|
325
|
+
children: ReadonlyArray<CompositeChild>,
|
|
326
|
+
manifest?: RootManifestProvider,
|
|
327
|
+
projection?: HierarchyProjection,
|
|
328
|
+
) {
|
|
329
|
+
this.children = [...children];
|
|
330
|
+
this.manifest = manifest ?? null;
|
|
331
|
+
this.projection = projection ?? null;
|
|
332
|
+
// Zero children is a real state (ARCHITECTURE-CORE §Roots: a project may
|
|
333
|
+
// declare no roots); every answer below degrades to its honest empty one.
|
|
334
|
+
this.resolvedProjectionGroups = this.resolveProjectionGroups();
|
|
335
|
+
this.capabilities = computeCapabilities(this.children);
|
|
336
|
+
this.refreshTruth();
|
|
337
|
+
this.refreshRelated();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* B1 — swap `worldId`'s child adapter IN PLACE (the design-time layer
|
|
342
|
+
* mount's "adapter upgrade": a world's read-only `BoundaryAuthoringAdapter`
|
|
343
|
+
* is replaced by a live one — react: `ReactRootAuthoringAdapter` — once its
|
|
344
|
+
* layer mounts successfully; a layer that fails/throws rebuilds the
|
|
345
|
+
* Boundary with the caught reason, also through this method). Mutates the
|
|
346
|
+
* existing children array (preserving array order — z-order/`childAdapters()`
|
|
347
|
+
* index is unaffected) and recomputes `capabilities`, rather than
|
|
348
|
+
* constructing a new `CompositeAuthoringAdapter` — a rebuild would break
|
|
349
|
+
* `world-root-stage.ts`'s captured teardown identity, `exitEditModeAuthoring`'s
|
|
350
|
+
* installed-instance check, and the `markEditModeOverride` brand, all of
|
|
351
|
+
* which key off THIS instance's identity (see the class doc comment).
|
|
352
|
+
*
|
|
353
|
+
* No-op (loud warn, #18 discipline) when `worldId` doesn't name an existing
|
|
354
|
+
* child — never silently routes to the wrong world or grows the array.
|
|
355
|
+
* Callers are responsible for their own side effects around the swap
|
|
356
|
+
* (`store.notifyIngestEdit()` so panels re-render against the new child) —
|
|
357
|
+
* this method only owns the swap + capability recompute.
|
|
358
|
+
*/
|
|
359
|
+
replaceChild(worldId: string, adapter: AuthoringAdapter): void {
|
|
360
|
+
const index = this.children.findIndex((c) => c.worldId === worldId);
|
|
361
|
+
if (index === -1) {
|
|
362
|
+
editorConsole.warn(
|
|
363
|
+
`[CompositeAuthoringAdapter] replaceChild: no child with worldId "${worldId}" — ignoring.`,
|
|
364
|
+
'authoring',
|
|
365
|
+
);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// D12 (B4) — carry the outgoing child's `zOrder` forward too (not just
|
|
369
|
+
// `kind`): a swap must not silently reset a world's install-time zOrder
|
|
370
|
+
// back to "no override" (array-index fallback). Built conditionally (not
|
|
371
|
+
// `{ ..., zOrder }`) because `zOrder` is an OPTIONAL property under this
|
|
372
|
+
// repo's `exactOptionalPropertyTypes` — an explicit `zOrder: undefined`
|
|
373
|
+
// is not the same as an absent property to the type checker.
|
|
374
|
+
const previous = this.children[index]!;
|
|
375
|
+
this.children[index] = { ...previous, worldId, adapter };
|
|
376
|
+
this.crossSurfaceInputSignatures = null;
|
|
377
|
+
this.capabilities = computeCapabilities(this.children);
|
|
378
|
+
// The swap can bring a truth resolver in (a Boundary upgrading to a
|
|
379
|
+
// live adapter) or take the last one away — same recompute, same reason.
|
|
380
|
+
this.refreshTruth();
|
|
381
|
+
this.refreshRelated();
|
|
382
|
+
// An id the outgoing child did not own may be owned by the incoming one.
|
|
383
|
+
this.refusedIds.clear();
|
|
384
|
+
// Re-point every live subscription at the NEW child set, then tell the
|
|
385
|
+
// subscribers — the swap is itself the "this world finished mounting"
|
|
386
|
+
// structure change (see the change fan-out section below).
|
|
387
|
+
this.syncChildFanOut();
|
|
388
|
+
this.fanOutStructure();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** The group node id for a given child (exposed for callers building
|
|
392
|
+
* post-construction selection/expansion state — e.g. tests, e2e hooks). */
|
|
393
|
+
groupNodeId(worldId: string): string {
|
|
394
|
+
return groupNodeId(worldId);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** True for a read-only shell node manufactured only for hierarchy layout. */
|
|
398
|
+
isOrganizationNode(nodeId: string): boolean {
|
|
399
|
+
return isOrganizationNodeId(nodeId);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** The child that owns `id` — by ACTUAL ownership (its hierarchy resolves the
|
|
403
|
+
* id), not by any prefix convention. `null` for a group-node id or an id no
|
|
404
|
+
* child recognizes. O(children) per call; fine at editor-UI scale, and avoids
|
|
405
|
+
* a cache that could go stale as a live tree adds/removes nodes. */
|
|
406
|
+
private findOwnerChild(id: string): CompositeChild | null {
|
|
407
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return null;
|
|
408
|
+
for (const child of this.children) {
|
|
409
|
+
if (child.adapter.hierarchy.node(id)) return child;
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Ids already refused, so a bogus selection that survives across renders logs
|
|
415
|
+
* its refusal ONCE instead of once per row per notify. Cleared whenever the
|
|
416
|
+
* child set changes ({@link replaceChild}) — the same id can become real. */
|
|
417
|
+
private readonly refusedIds = new Set<string>();
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Every real id this composite has ever OBSERVED or ROUTED successfully — the memory that
|
|
421
|
+
* separates "a row from a previous mount epoch" from "a subject that never
|
|
422
|
+
* existed", so only the second one is an error.
|
|
423
|
+
*
|
|
424
|
+
* The refusal below exists for a subject that does not exist
|
|
425
|
+
* (`editor.select('Player')`). An id that a PREVIOUS mount epoch really did
|
|
426
|
+
* own is a different fact: the row existed, the caller read it from this
|
|
427
|
+
* composite's own hierarchy, and then Play (or a design-time layer upgrade)
|
|
428
|
+
* swapped the child underneath it. That is the ordinary shape of a remount,
|
|
429
|
+
* and ARCHITECTURE-CORE says so directly — "receipts from an old adapter
|
|
430
|
+
* object/mount epoch never grade the replacement".
|
|
431
|
+
*
|
|
432
|
+
* It is unavoidable for an ingest root specifically, and that is what made it
|
|
433
|
+
* visible: an ingested world has no source oids, so `projection/three.ts`
|
|
434
|
+
* mints `live:<worldId>:<n>` from a session counter against the Object3D
|
|
435
|
+
* IDENTITIES it walked. A remount builds new Object3Ds, so every id is new by
|
|
436
|
+
* construction — nothing can carry them across, the way `r3f:<worldId>:<oid>`
|
|
437
|
+
* carries across for a source-stamped world.
|
|
438
|
+
*
|
|
439
|
+
* MEASURED on the vendored `racing-game` ingest: six
|
|
440
|
+
* `[CompositeAuthoringAdapter] inspector.get: no root owns entity id
|
|
441
|
+
* "live:racing-game:N"` errors in three of four `vgai doctor` runs, every one
|
|
442
|
+
* of them within two seconds of ▶ — `GameHierarchy` re-rendering rows it read
|
|
443
|
+
* before the swap against the children that came after it, one render before
|
|
444
|
+
* its own row rebuild lands. (Flaky precisely because it is a race.)
|
|
445
|
+
*
|
|
446
|
+
* Recorded when this hierarchy resolves a row and when a provider routes it,
|
|
447
|
+
* rather than snapshotted from the outgoing child at {@link replaceChild},
|
|
448
|
+
* and that is not a shortcut: both swap paths dispose
|
|
449
|
+
* the old mount BEFORE they replace it (`design-time-layers.ts`'s
|
|
450
|
+
* `suspendForPlay` calls `disposeEverything()` first; `r3f-design-session.ts`
|
|
451
|
+
* calls `disposeMounted()` first), so walking the outgoing adapter's
|
|
452
|
+
* hierarchy at swap time would read a disposed stage. Hierarchy-time capture
|
|
453
|
+
* is essential: a consumer can resolve/render a row and lose the race to the
|
|
454
|
+
* swap before its FIRST provider request (`hierarchy.object3D` exposed this
|
|
455
|
+
* exact ordering in Doctor). Provider-time capture remains for callers that
|
|
456
|
+
* route a real id without first resolving its row. Both cost one `Set.add`.
|
|
457
|
+
*
|
|
458
|
+
* Never cleared: an id this composite has SEEN is a real id forever, and the
|
|
459
|
+
* set is bounded by the rows one session actually mounted.
|
|
460
|
+
*/
|
|
461
|
+
private readonly supersededIds = new Set<string>();
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* ONE resolver for "which child owns this id?", and the ONLY door into a
|
|
465
|
+
* child adapter for a caller holding a raw id.
|
|
466
|
+
*
|
|
467
|
+
* There used to be a floor here: an id no child owned fell through to
|
|
468
|
+
* `children[0]`, "so callers that pass a bogus id get inert/default responses
|
|
469
|
+
* rather than a throw". They did not. The first child answered AS IF it owned
|
|
470
|
+
* the id, and the composite's own `transforms.get` `??`-default then handed
|
|
471
|
+
* back `{position:[0,0,0], rotation:[0,0,0,1], scale:[1,1,1]}` — so
|
|
472
|
+
* `editor.select('Player')`, a string no world has ever heard of, produced a
|
|
473
|
+
* fully-populated inspector reading all zeros. That is the anti-shim rule's
|
|
474
|
+
* exact failure: the adapter fabricated first-party data for a subject that
|
|
475
|
+
* does not exist. An id nothing owns is now REFUSED, loudly and by name, and
|
|
476
|
+
* every caller below degrades to its honest empty answer.
|
|
477
|
+
*/
|
|
478
|
+
private routeOwned(id: string, operation: string): AuthoringAdapter | null {
|
|
479
|
+
const owner = this.findOwnerChild(id);
|
|
480
|
+
if (owner) {
|
|
481
|
+
// This id is real, now. Remembering that is what lets a later miss on it
|
|
482
|
+
// be read as a superseded row rather than a bogus subject — see
|
|
483
|
+
// {@link supersededIds}.
|
|
484
|
+
this.supersededIds.add(id);
|
|
485
|
+
return owner.adapter;
|
|
486
|
+
}
|
|
487
|
+
// A row THIS COMPOSITE MANUFACTURED is not a bogus id. A group node has no
|
|
488
|
+
// live object by construction — that is what makes it a group node — so
|
|
489
|
+
// "no child owns it" is the expected answer here, and the caller's own
|
|
490
|
+
// empty degrade is the right one. The refusal above exists for a subject
|
|
491
|
+
// that does not exist (`editor.select('Player')`); shouting it for the
|
|
492
|
+
// composite's own root row says something false about the editor to the
|
|
493
|
+
// person who merely clicked that row. Every provider with a real answer
|
|
494
|
+
// for these rows already intercepts them ahead of this call
|
|
495
|
+
// (`rootProperties`, `inspector.get`/`set`/`editability`); the rest —
|
|
496
|
+
// `spatialHandles`, `transforms`, `instances`, `assetDrop`, `stories` —
|
|
497
|
+
// have nothing to hand back, which is exactly `null`.
|
|
498
|
+
//
|
|
499
|
+
// MEASURED as two console errors in one session on a packaged build over
|
|
500
|
+
// a canvas ingest root with a DOM UI beside it — `inspector.editability`
|
|
501
|
+
// and then, once that one answered for itself, `spatialHandles.layers` —
|
|
502
|
+
// on the same id, `world:probe-canvas:canvas`. Fixing the providers one at
|
|
503
|
+
// a time was fixing instances of this.
|
|
504
|
+
//
|
|
505
|
+
// A synthetic id naming a row this composition does NOT have still
|
|
506
|
+
// refuses: a stale selection outliving a root removal, or a projection
|
|
507
|
+
// group deleted from the manifest, is a subject nothing owns — exactly
|
|
508
|
+
// what the refusal is for. So the quiet answer is gated on the row
|
|
509
|
+
// EXISTING, not on the id's prefix: `world:` against the current children,
|
|
510
|
+
// `kind:`/`projection:` against `hierarchy.node`, which is this class's own
|
|
511
|
+
// answer to "is this a row of mine?" (a `kind:` node exists only where a
|
|
512
|
+
// kind has >1 unprojected root; a `projection:` node only for a group the
|
|
513
|
+
// manifest still declares). Asking the hierarchy rather than restating its
|
|
514
|
+
// rules is what keeps the two from drifting; both branches are O(children)
|
|
515
|
+
// and walk no child scene.
|
|
516
|
+
if (this.groupChild(id) || (isOrganizationNodeId(id) && this.hierarchy.node(id))) return null;
|
|
517
|
+
// An id a replaced child really did own is a superseded row, not a subject
|
|
518
|
+
// that never existed — quiet, and the caller's own empty degrade is right.
|
|
519
|
+
if (this.supersededIds.has(id)) return null;
|
|
520
|
+
if (!this.refusedIds.has(id)) {
|
|
521
|
+
this.refusedIds.add(id);
|
|
522
|
+
editorConsole.error(unownedIdRefusal(operation, id, this.children), 'authoring');
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** PUBLIC ownership query (T0 — A3's inspector dispatch and B4's pick routing
|
|
528
|
+
* both need it): the `worldId` of the child that owns `id`, or `null` for a
|
|
529
|
+
* group-node id or an id no child recognizes. */
|
|
530
|
+
ownerOf(id: string): string | null {
|
|
531
|
+
return this.findOwnerChild(id)?.worldId ?? null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* PUBLIC read-only view of the wrapped children (T0), ordered as constructed.
|
|
536
|
+
*
|
|
537
|
+
* `zOrder` is EACH CHILD'S OWN `CompositeChild.zOrder` (the world's
|
|
538
|
+
* install-time manifest zOrder) when supplied, falling back to the array
|
|
539
|
+
* index otherwise. Array order is NOT z-order (D12/B4 correction of this
|
|
540
|
+
* doc comment's former claim): children are built in MANIFEST DECLARATION
|
|
541
|
+
* order (`edit-mode-authoring.ts`), not stacking order — paint/pick order is
|
|
542
|
+
* `stackOrder` over each child's manifest zOrder (mirroring
|
|
543
|
+
* `create-runtime.ts`'s real stacking pass and `design-time-layers.ts`'s
|
|
544
|
+
* layer z-index assignment), which is exactly why this field exists.
|
|
545
|
+
* "Install-time" — the zOrder captured when this composite/child was built,
|
|
546
|
+
* not a live subscription: a world's zOrder edited afterward (the generic
|
|
547
|
+
* inspector's `zOrder` property, `A4`/D8) does NOT retroactively reorder an
|
|
548
|
+
* already-mounted design-time layer stack or an already-computed pick walk
|
|
549
|
+
* order this call returns; both settle to the new order the next time the
|
|
550
|
+
* composite/layers are rebuilt. Callers that need the LIVE manifest value
|
|
551
|
+
* (e.g. `GameHierarchy.tsx`'s zOrder badge) read it from the manifest
|
|
552
|
+
* surface directly instead of trusting this field alone — see
|
|
553
|
+
* `compositeGroupBadge`'s own comment.
|
|
554
|
+
*/
|
|
555
|
+
childAdapters(): ReadonlyArray<{
|
|
556
|
+
worldId: string;
|
|
557
|
+
adapter: AuthoringAdapter;
|
|
558
|
+
kind: string;
|
|
559
|
+
zOrder: number;
|
|
560
|
+
role: 'world' | 'surface';
|
|
561
|
+
label?: string;
|
|
562
|
+
parentRootId?: string;
|
|
563
|
+
}> {
|
|
564
|
+
return this.children.map((c, index) => ({
|
|
565
|
+
worldId: c.worldId,
|
|
566
|
+
adapter: c.adapter,
|
|
567
|
+
kind: c.kind,
|
|
568
|
+
zOrder: c.zOrder ?? index,
|
|
569
|
+
role: c.role ?? 'world',
|
|
570
|
+
...(c.label !== undefined ? { label: c.label } : {}),
|
|
571
|
+
...(c.parentRootId !== undefined ? { parentRootId: c.parentRootId } : {}),
|
|
572
|
+
}));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* The semantic document containing `worldId`, when the manifest explicitly
|
|
577
|
+
* groups that native root with at least one other surface.
|
|
578
|
+
*
|
|
579
|
+
* A Scene panel ordinarily scopes to its native Three child. That loses an
|
|
580
|
+
* authored DOM/canvas child whose semantic parent is in Three, even though
|
|
581
|
+
* this composite already owns the exact cross-surface edge. This view keeps
|
|
582
|
+
* all provider routing on the composite and narrows only the hierarchy roots
|
|
583
|
+
* to the declared group, so unrelated scenes remain outside the document.
|
|
584
|
+
*/
|
|
585
|
+
projectedDocumentAdapter(worldId: string): AuthoringAdapter | null {
|
|
586
|
+
const group = this.resolvedProjectionGroups.find(
|
|
587
|
+
(candidate) => candidate.roots.length > 1 && candidate.roots.includes(worldId),
|
|
588
|
+
);
|
|
589
|
+
if (!group) return null;
|
|
590
|
+
const cached = this.projectedDocumentAdapters.get(group.id);
|
|
591
|
+
if (cached) return cached;
|
|
592
|
+
|
|
593
|
+
const hierarchy: HierarchyProvider = {
|
|
594
|
+
...this.hierarchy,
|
|
595
|
+
roots: () => {
|
|
596
|
+
this.ensureCrossSurfaceEdges();
|
|
597
|
+
return [this.projectionGroupNode(group)];
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
const projected = new Proxy(this, {
|
|
601
|
+
get: (target, property) =>
|
|
602
|
+
property === 'hierarchy' ? hierarchy : Reflect.get(target, property, target),
|
|
603
|
+
});
|
|
604
|
+
this.projectedDocumentAdapters.set(group.id, projected);
|
|
605
|
+
return projected;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* C3 — a child declared with `parentRootId` nests its group node UNDER
|
|
610
|
+
* that parent's group node instead of at the top level: the parent's
|
|
611
|
+
* `parentId` resolution below picks up the nested group id automatically
|
|
612
|
+
* (`this.children.find(...).parentRootId`), and this method also appends
|
|
613
|
+
* every child NESTED under `worldId` (in declaration order) to the END of
|
|
614
|
+
* `childIds`, after `worldId`'s own adapter roots — so e.g. a `scene-ui`
|
|
615
|
+
* child appears as a trailing "ui (scene-ui)" row under its owning world.
|
|
616
|
+
*/
|
|
617
|
+
private groupNode(worldId: string, kind: string, childRoots: EditorNode[]): EditorNode {
|
|
618
|
+
const self = this.children.find((c) => c.worldId === worldId);
|
|
619
|
+
const parentId = self?.parentRootId
|
|
620
|
+
? groupNodeId(self.parentRootId)
|
|
621
|
+
: this.parentForTopLevelRoot(worldId, kind);
|
|
622
|
+
const nestedGroupIds = this.children
|
|
623
|
+
.filter((c) => c.parentRootId === worldId)
|
|
624
|
+
.map((c) => groupNodeId(c.worldId));
|
|
625
|
+
return {
|
|
626
|
+
id: groupNodeId(worldId),
|
|
627
|
+
label:
|
|
628
|
+
self?.role === 'surface'
|
|
629
|
+
? (self.label ?? `${worldId} (${kind})`)
|
|
630
|
+
: this.labelForRoot(worldId, kind),
|
|
631
|
+
role: self?.role === 'surface' ? 'document' : 'root',
|
|
632
|
+
kind,
|
|
633
|
+
parentId,
|
|
634
|
+
childIds: [...childRoots.map((r) => r.id), ...nestedGroupIds],
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** C3 — every child NOT nested under another (`parentRootId` unset): the
|
|
639
|
+
* top-level group set. A nested child is reached only via its
|
|
640
|
+
* parent's `childIds` (`groupNode` above), never listed here too — a node
|
|
641
|
+
* in two parents' `childIds` would render twice / cycle the row walk. */
|
|
642
|
+
private topLevelChildren(): CompositeChild[] {
|
|
643
|
+
return this.children.filter((c) => !c.parentRootId);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private runtimeRootChildren(): CompositeChild[] {
|
|
647
|
+
return this.topLevelChildren().filter((c) => (c.role ?? 'world') === 'world');
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private resolveProjectionGroups(): Array<HierarchyProjectionGroup & { roots: string[] }> {
|
|
651
|
+
const known = new Set(this.runtimeRootChildren().map((child) => child.worldId));
|
|
652
|
+
const placed = new Set<string>();
|
|
653
|
+
return (this.projection?.groups ?? []).flatMap((group) => {
|
|
654
|
+
const roots = group.roots.filter((rootId) => {
|
|
655
|
+
if (!known.has(rootId)) {
|
|
656
|
+
editorConsole.warn(
|
|
657
|
+
`[CompositeAuthoringAdapter] hierarchy group "${group.label}" references unknown root "${rootId}" — ignoring.`,
|
|
658
|
+
'authoring',
|
|
659
|
+
);
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
if (placed.has(rootId)) {
|
|
663
|
+
editorConsole.warn(
|
|
664
|
+
`[CompositeAuthoringAdapter] hierarchy root "${rootId}" is referenced by more than one group; the tree projection uses its first placement.`,
|
|
665
|
+
'authoring',
|
|
666
|
+
);
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
placed.add(rootId);
|
|
670
|
+
return true;
|
|
671
|
+
});
|
|
672
|
+
return roots.length > 0 ? [{ ...group, roots }] : [];
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private projectionGroups(): Array<HierarchyProjectionGroup & { roots: string[] }> {
|
|
677
|
+
return this.resolvedProjectionGroups;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private projectionGroupFor(worldId: string): HierarchyProjectionGroup | undefined {
|
|
681
|
+
return this.projectionGroups().find((group) => group.roots.includes(worldId));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private unprojectedRoots(): CompositeChild[] {
|
|
685
|
+
const projected = new Set(this.projectionGroups().flatMap((group) => group.roots));
|
|
686
|
+
return this.runtimeRootChildren().filter((child) => !projected.has(child.worldId));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private rootsOfKind(kind: string): CompositeChild[] {
|
|
690
|
+
return this.unprojectedRoots().filter((child) => child.kind === kind);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private parentForTopLevelRoot(worldId: string, kind: string): string | null {
|
|
694
|
+
const group = this.projectionGroupFor(worldId);
|
|
695
|
+
if (group) return projectionNodeId(group.id);
|
|
696
|
+
return this.rootsOfKind(kind).length > 1 ? kindNodeId(kind) : null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private labelForRoot(worldId: string, kind: string): string {
|
|
700
|
+
const configured = this.projection?.rootLabels?.[worldId]?.trim();
|
|
701
|
+
if (configured) return configured;
|
|
702
|
+
return this.rootsOfKind(kind).length === 1 && !this.projectionGroupFor(worldId)
|
|
703
|
+
? (KIND_LABELS[kind] ?? humanizeId(kind))
|
|
704
|
+
: humanizeId(worldId);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
private kindNode(kind: string): EditorNode {
|
|
708
|
+
return {
|
|
709
|
+
id: kindNodeId(kind),
|
|
710
|
+
label: KIND_LABELS[kind] ?? humanizeId(kind),
|
|
711
|
+
role: 'folder',
|
|
712
|
+
kind: 'group',
|
|
713
|
+
parentId: null,
|
|
714
|
+
childIds: this.rootsOfKind(kind).map((child) => groupNodeId(child.worldId)),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private projectionGroupNode(group: HierarchyProjectionGroup & { roots: string[] }): EditorNode {
|
|
719
|
+
return {
|
|
720
|
+
id: projectionNodeId(group.id),
|
|
721
|
+
label: this.crossSurfaceGroupLabels.get(group.id) ?? group.label,
|
|
722
|
+
role: 'folder',
|
|
723
|
+
kind: 'group',
|
|
724
|
+
parentId: null,
|
|
725
|
+
childIds: this.crossSurfaceRootChildren.get(group.id) ?? group.roots.map(groupNodeId),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private orderJoinedSiblings(childIds: readonly string[]): string[] {
|
|
730
|
+
return childIds
|
|
731
|
+
.map((id, index) => ({ id, index, order: this.crossSurfaceOrders.get(id) }))
|
|
732
|
+
.sort((left, right) => {
|
|
733
|
+
if (left.order === undefined && right.order === undefined) return left.index - right.index;
|
|
734
|
+
if (left.order === undefined) return 1;
|
|
735
|
+
if (right.order === undefined) return -1;
|
|
736
|
+
return left.order - right.order || left.index - right.index;
|
|
737
|
+
})
|
|
738
|
+
.map(({ id }) => id);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* A projected group may span several native adapters while its source owns
|
|
743
|
+
* one sibling list. Lift only rows that explicitly carry a semantic identity
|
|
744
|
+
* and ordinal, and only after every contributing adapter has mounted one;
|
|
745
|
+
* until then the ordinary adapter group rows remain intact.
|
|
746
|
+
*/
|
|
747
|
+
private refreshCrossSurfaceRoots(
|
|
748
|
+
nodes: ReadonlyArray<{ child: CompositeChild; node: EditorNode }>,
|
|
749
|
+
semanticIds: ReadonlyMap<string, { child: CompositeChild; nodeId: string } | null>,
|
|
750
|
+
): void {
|
|
751
|
+
this.crossSurfaceRootParents.clear();
|
|
752
|
+
this.crossSurfaceRootChildren.clear();
|
|
753
|
+
this.crossSurfaceGroupLabels.clear();
|
|
754
|
+
const nodesByChild = new Map<CompositeChild, Map<string, EditorNode>>();
|
|
755
|
+
for (const { child, node } of nodes) {
|
|
756
|
+
const owned = nodesByChild.get(child) ?? new Map<string, EditorNode>();
|
|
757
|
+
owned.set(node.id, node);
|
|
758
|
+
nodesByChild.set(child, owned);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
for (const group of this.projectionGroups()) {
|
|
762
|
+
const participatingWorlds = new Set(group.roots);
|
|
763
|
+
const liveLabels = new Set(
|
|
764
|
+
nodes.flatMap(({ child, node }) => {
|
|
765
|
+
const label = node.crossSurfaceGroupLabel?.trim();
|
|
766
|
+
return participatingWorlds.has(child.worldId) && label ? [label] : [];
|
|
767
|
+
}),
|
|
768
|
+
);
|
|
769
|
+
if (liveLabels.size === 1) this.crossSurfaceGroupLabels.set(group.id, [...liveLabels][0]!);
|
|
770
|
+
if (group.roots.length < 2) continue;
|
|
771
|
+
const candidates = nodes.filter(({ child, node }) => {
|
|
772
|
+
if (
|
|
773
|
+
!participatingWorlds.has(child.worldId) ||
|
|
774
|
+
node.crossSurfaceId === undefined ||
|
|
775
|
+
node.crossSurfaceOrder === undefined ||
|
|
776
|
+
node.crossSurfaceParentId !== undefined ||
|
|
777
|
+
semanticIds.get(node.crossSurfaceId)?.nodeId !== node.id
|
|
778
|
+
) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
let parentId = node.parentId;
|
|
782
|
+
const owned = nodesByChild.get(child);
|
|
783
|
+
while (parentId !== null) {
|
|
784
|
+
const parent = owned?.get(parentId);
|
|
785
|
+
if (parent === undefined) break;
|
|
786
|
+
if (parent.crossSurfaceId !== undefined) return false;
|
|
787
|
+
parentId = parent.parentId;
|
|
788
|
+
}
|
|
789
|
+
return true;
|
|
790
|
+
});
|
|
791
|
+
// A surface may contribute only descendants whose authored parents live on another
|
|
792
|
+
// surface. Requiring a top-level candidate from every adapter leaves those already-joined
|
|
793
|
+
// rows hidden behind synthetic native-root folders. Participation is therefore proven by
|
|
794
|
+
// any unique semantic row; only the union-level roots themselves become group children.
|
|
795
|
+
const mountedWorlds = new Set(
|
|
796
|
+
nodes.flatMap(({ child, node }) =>
|
|
797
|
+
participatingWorlds.has(child.worldId) &&
|
|
798
|
+
node.crossSurfaceId !== undefined &&
|
|
799
|
+
semanticIds.get(node.crossSurfaceId)?.nodeId === node.id
|
|
800
|
+
? [child.worldId]
|
|
801
|
+
: [],
|
|
802
|
+
),
|
|
803
|
+
);
|
|
804
|
+
if (group.roots.some((worldId) => !mountedWorlds.has(worldId))) continue;
|
|
805
|
+
if (candidates.length === 0) continue;
|
|
806
|
+
const childIds = this.orderJoinedSiblings(candidates.map(({ node }) => node.id));
|
|
807
|
+
this.crossSurfaceRootChildren.set(group.id, childIds);
|
|
808
|
+
const parentId = projectionNodeId(group.id);
|
|
809
|
+
for (const childId of childIds) this.crossSurfaceRootParents.set(childId, parentId);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Project the source-owned DOM semantic tree rather than the implementation
|
|
815
|
+
* DOM a semantic host component happens to render.
|
|
816
|
+
*
|
|
817
|
+
* Ordinary React/HTML authoring is unchanged: folding starts only below a
|
|
818
|
+
* DOM row carrying an explicit `crossSurfaceId`. Within that subtree, the
|
|
819
|
+
* nearest descendant rows carrying their own identities become its authored
|
|
820
|
+
* children and intervening OID rows are transparent. This is the DOM half of
|
|
821
|
+
* the same cross-surface contract the edge join already consumes; it does
|
|
822
|
+
* not infer a framework or library from tag names.
|
|
823
|
+
*/
|
|
824
|
+
private refreshCrossSurfaceDomTrees(
|
|
825
|
+
nodes: ReadonlyArray<{ child: CompositeChild; node: EditorNode }>,
|
|
826
|
+
): void {
|
|
827
|
+
this.crossSurfaceDomParents.clear();
|
|
828
|
+
this.crossSurfaceDomChildren.clear();
|
|
829
|
+
|
|
830
|
+
for (const child of this.children) {
|
|
831
|
+
if (child.kind !== 'dom') continue;
|
|
832
|
+
const owned = new Map(
|
|
833
|
+
nodes
|
|
834
|
+
.filter((candidate) => candidate.child === child)
|
|
835
|
+
.map(({ node }) => [node.id, node] as const),
|
|
836
|
+
);
|
|
837
|
+
const roots = [...owned.values()].filter((node) => node.parentId === null);
|
|
838
|
+
const visited = new Set<string>();
|
|
839
|
+
const visit = (node: EditorNode, semanticParentId: string | null): void => {
|
|
840
|
+
if (visited.has(node.id)) return;
|
|
841
|
+
visited.add(node.id);
|
|
842
|
+
let nextSemanticParentId = semanticParentId;
|
|
843
|
+
if (node.crossSurfaceId !== undefined) {
|
|
844
|
+
this.crossSurfaceDomChildren.set(node.id, []);
|
|
845
|
+
if (semanticParentId !== null) {
|
|
846
|
+
this.crossSurfaceDomParents.set(node.id, semanticParentId);
|
|
847
|
+
this.crossSurfaceDomChildren.get(semanticParentId)?.push(node.id);
|
|
848
|
+
}
|
|
849
|
+
nextSemanticParentId = node.id;
|
|
850
|
+
}
|
|
851
|
+
for (const childId of node.childIds) {
|
|
852
|
+
const descendant = owned.get(childId);
|
|
853
|
+
if (descendant) visit(descendant, nextSemanticParentId);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
for (const root of roots) visit(root, null);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Join only explicit project-owned semantic edges. Native parentage stays
|
|
862
|
+
* adapter-owned; this adds the one relationship a substrate cannot express:
|
|
863
|
+
* a DOM/canvas row authored beneath a Three row (or any other pair of roots).
|
|
864
|
+
*/
|
|
865
|
+
private refreshCrossSurfaceEdges(): void {
|
|
866
|
+
const nodes: Array<{ child: CompositeChild; node: EditorNode }> = [];
|
|
867
|
+
const ids = new Map<string, { child: CompositeChild; nodeId: string } | null>();
|
|
868
|
+
for (const child of this.children) {
|
|
869
|
+
forEachHierarchyNode(child.adapter.hierarchy, (node) => {
|
|
870
|
+
nodes.push({ child, node });
|
|
871
|
+
if (node.crossSurfaceId === undefined) return;
|
|
872
|
+
const previous = ids.get(node.crossSurfaceId);
|
|
873
|
+
ids.set(node.crossSurfaceId, previous === undefined ? { child, nodeId: node.id } : null);
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
this.crossSurfaceParents.clear();
|
|
877
|
+
this.crossSurfaceChildren.clear();
|
|
878
|
+
this.crossSurfaceOrders.clear();
|
|
879
|
+
for (const { node } of nodes) {
|
|
880
|
+
if (node.crossSurfaceOrder !== undefined) {
|
|
881
|
+
this.crossSurfaceOrders.set(node.id, node.crossSurfaceOrder);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
this.refreshCrossSurfaceDomTrees(nodes);
|
|
885
|
+
for (const { child, node } of nodes) {
|
|
886
|
+
if (node.crossSurfaceParentId === undefined) continue;
|
|
887
|
+
const parent = ids.get(node.crossSurfaceParentId);
|
|
888
|
+
if (parent === undefined || parent === null || parent.child === child) continue;
|
|
889
|
+
this.crossSurfaceParents.set(node.id, parent.nodeId);
|
|
890
|
+
const children = this.crossSurfaceChildren.get(parent.nodeId) ?? [];
|
|
891
|
+
children.push(node.id);
|
|
892
|
+
this.crossSurfaceChildren.set(parent.nodeId, children);
|
|
893
|
+
}
|
|
894
|
+
this.refreshCrossSurfaceRoots(nodes, ids);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private clearCrossSurfaceEdges(): void {
|
|
898
|
+
this.crossSurfaceParents.clear();
|
|
899
|
+
this.crossSurfaceChildren.clear();
|
|
900
|
+
this.crossSurfaceOrders.clear();
|
|
901
|
+
this.crossSurfaceRootParents.clear();
|
|
902
|
+
this.crossSurfaceRootChildren.clear();
|
|
903
|
+
this.crossSurfaceGroupLabels.clear();
|
|
904
|
+
this.crossSurfaceDomParents.clear();
|
|
905
|
+
this.crossSurfaceDomChildren.clear();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** Refresh semantic joins only when a child reports that the fields which
|
|
909
|
+
* define those joins changed. Ordinary native hierarchy churn still flows
|
|
910
|
+
* through each child's own provider; it simply cannot affect these maps. */
|
|
911
|
+
private ensureCrossSurfaceEdges(): void {
|
|
912
|
+
const signatures: Array<string | null> = [];
|
|
913
|
+
for (const child of this.children) {
|
|
914
|
+
const signature = child.adapter.hierarchy.crossSurfaceStructureSignature;
|
|
915
|
+
if (!signature) {
|
|
916
|
+
this.crossSurfaceInputSignatures = null;
|
|
917
|
+
this.refreshCrossSurfaceEdges();
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
signatures.push(signature.call(child.adapter.hierarchy));
|
|
921
|
+
}
|
|
922
|
+
if (
|
|
923
|
+
this.crossSurfaceInputSignatures !== null &&
|
|
924
|
+
signatures.length === this.crossSurfaceInputSignatures.length &&
|
|
925
|
+
signatures.every((value, index) => value === this.crossSurfaceInputSignatures?.[index])
|
|
926
|
+
) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
this.crossSurfaceInputSignatures = signatures;
|
|
930
|
+
if (signatures.every((signature) => signature === null)) {
|
|
931
|
+
this.clearCrossSurfaceEdges();
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
this.refreshCrossSurfaceEdges();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private projectCrossSurfaceNode(node: EditorNode, defaultParentId: string | null): EditorNode {
|
|
938
|
+
const parentId =
|
|
939
|
+
this.crossSurfaceParents.get(node.id) ??
|
|
940
|
+
this.crossSurfaceDomParents.get(node.id) ??
|
|
941
|
+
this.crossSurfaceRootParents.get(node.id) ??
|
|
942
|
+
defaultParentId;
|
|
943
|
+
const nativeChildIds = this.crossSurfaceDomChildren.get(node.id) ?? node.childIds;
|
|
944
|
+
const childIds = this.orderJoinedSiblings(
|
|
945
|
+
nativeChildIds
|
|
946
|
+
.filter(
|
|
947
|
+
(childId) =>
|
|
948
|
+
!this.crossSurfaceParents.has(childId) && !this.crossSurfaceRootParents.has(childId),
|
|
949
|
+
)
|
|
950
|
+
.concat(this.crossSurfaceChildren.get(node.id) ?? []),
|
|
951
|
+
);
|
|
952
|
+
return parentId === node.parentId &&
|
|
953
|
+
childIds.length === node.childIds.length &&
|
|
954
|
+
childIds.every((childId, index) => childId === node.childIds[index])
|
|
955
|
+
? node
|
|
956
|
+
: { ...node, parentId, childIds };
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
private defaultProjectionNodeIds(): string[] {
|
|
960
|
+
const ids: string[] = [];
|
|
961
|
+
const seenKinds = new Set<string>();
|
|
962
|
+
for (const child of this.unprojectedRoots()) {
|
|
963
|
+
if (seenKinds.has(child.kind)) continue;
|
|
964
|
+
seenKinds.add(child.kind);
|
|
965
|
+
const peers = this.rootsOfKind(child.kind);
|
|
966
|
+
ids.push(peers.length > 1 ? kindNodeId(child.kind) : groupNodeId(child.worldId));
|
|
967
|
+
}
|
|
968
|
+
return ids;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
readonly hierarchy: HierarchyProvider = {
|
|
972
|
+
roots: (): EditorNode[] => {
|
|
973
|
+
this.ensureCrossSurfaceEdges();
|
|
974
|
+
return [
|
|
975
|
+
...this.projectionGroups().map((group) => this.projectionGroupNode(group)),
|
|
976
|
+
...this.defaultProjectionNodeIds()
|
|
977
|
+
.map((id) => this.hierarchy.node(id))
|
|
978
|
+
.filter((node): node is EditorNode => node !== null),
|
|
979
|
+
];
|
|
980
|
+
},
|
|
981
|
+
node: (id) => {
|
|
982
|
+
if (id.startsWith(KIND_PREFIX)) {
|
|
983
|
+
const kind = id.slice(KIND_PREFIX.length);
|
|
984
|
+
return this.rootsOfKind(kind).length > 1 ? this.kindNode(kind) : null;
|
|
985
|
+
}
|
|
986
|
+
if (id.startsWith(PROJECTION_PREFIX)) {
|
|
987
|
+
const groupId = id.slice(PROJECTION_PREFIX.length);
|
|
988
|
+
const group = this.projectionGroups().find((candidate) => candidate.id === groupId);
|
|
989
|
+
return group ? this.projectionGroupNode(group) : null;
|
|
990
|
+
}
|
|
991
|
+
if (isGroupNodeId(id)) {
|
|
992
|
+
const worldId = id.slice(GROUP_PREFIX.length);
|
|
993
|
+
const child = this.children.find((c) => c.worldId === worldId);
|
|
994
|
+
if (!child) return null;
|
|
995
|
+
return this.groupNode(child.worldId, child.kind, child.adapter.hierarchy.roots());
|
|
996
|
+
}
|
|
997
|
+
const owner = this.findOwnerChild(id);
|
|
998
|
+
if (!owner) return null;
|
|
999
|
+
const node = owner.adapter.hierarchy.node(id);
|
|
1000
|
+
if (!node) return null;
|
|
1001
|
+
// Resolving the row is already proof that this mount epoch genuinely
|
|
1002
|
+
// owned the id. Remember it before any provider lookup: Play/remount can
|
|
1003
|
+
// replace the child between this read and that lookup, and the stale row
|
|
1004
|
+
// must then degrade quietly rather than be mislabeled as fabricated.
|
|
1005
|
+
this.supersededIds.add(id);
|
|
1006
|
+
// A child's OWN root has `parentId: null` in its adapter's tree — rewrite
|
|
1007
|
+
// it to point at this world's group node so the merged tree is one
|
|
1008
|
+
// connected forest instead of the child roots looking parentless again.
|
|
1009
|
+
return this.projectCrossSurfaceNode(
|
|
1010
|
+
node,
|
|
1011
|
+
node.parentId === null ? groupNodeId(owner.worldId) : node.parentId,
|
|
1012
|
+
);
|
|
1013
|
+
},
|
|
1014
|
+
// The composite spans mixed substrates, so it always answers these — but
|
|
1015
|
+
// only three-backed children implement them; the rest are absent (P-5).
|
|
1016
|
+
object3D: (id) =>
|
|
1017
|
+
isGroupNodeId(id) || isOrganizationNodeId(id)
|
|
1018
|
+
? null
|
|
1019
|
+
: (this.routeOwned(id, 'hierarchy.object3D')?.hierarchy.object3D?.(id) ?? null),
|
|
1020
|
+
idForObject3D: (o: THREE.Object3D) => {
|
|
1021
|
+
for (const child of this.children) {
|
|
1022
|
+
const id = child.adapter.hierarchy.idForObject3D?.(o);
|
|
1023
|
+
if (id) return id;
|
|
1024
|
+
}
|
|
1025
|
+
return null;
|
|
1026
|
+
},
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
readonly selection: SelectionProvider = {
|
|
1030
|
+
// Union of every child's own selection.
|
|
1031
|
+
get: () => {
|
|
1032
|
+
const seen = new Set<string>();
|
|
1033
|
+
for (const child of this.children) {
|
|
1034
|
+
for (const id of child.adapter.selection?.get() ?? []) seen.add(id);
|
|
1035
|
+
}
|
|
1036
|
+
return [...seen];
|
|
1037
|
+
},
|
|
1038
|
+
set: (ids, options) => {
|
|
1039
|
+
// A group-node id is not a real entity in any
|
|
1040
|
+
// child's tree — route it specially rather than forwarding a bogus id
|
|
1041
|
+
// nobody owns. Route each REAL id to its owning child.
|
|
1042
|
+
//
|
|
1043
|
+
// Every existing adapter (first-party/ingest/canvas) delegates
|
|
1044
|
+
// selection to the SAME editor-global `EditorShellStore` — `selectMultiple`
|
|
1045
|
+
// REPLACES the store's whole selected-id set, it does not merge. So
|
|
1046
|
+
// calling `.set([])` on every non-owning child (the naive "clear the
|
|
1047
|
+
// rest" approach) would call `selectMultiple` a second time on the
|
|
1048
|
+
// SAME shared store and immediately clear out whatever the owning
|
|
1049
|
+
// child's `.set()` call just wrote — a single click could never
|
|
1050
|
+
// actually select anything. Only forward to children that own at
|
|
1051
|
+
// least one of `ids`.
|
|
1052
|
+
//
|
|
1053
|
+
// A4 (D8): when NONE do (an empty selection, OR a selection that is
|
|
1054
|
+
// PURELY a synthetic id), forward to exactly ONE child (any child
|
|
1055
|
+
// sharing the backing store reaches every other child too) — but
|
|
1056
|
+
// forward the SYNTHETIC ID ITSELF (not an empty clear) when one was
|
|
1057
|
+
// given. This is why `RightPanel.tsx`'s `[...store.selectedEntityIds][0]`
|
|
1058
|
+
// (and `Inspector.tsx`'s identical read) can show a world group's own
|
|
1059
|
+
// properties: every stock adapter shares the SAME `EditorShellStore`, so
|
|
1060
|
+
// writing `'world:<id>'` into any one
|
|
1061
|
+
// child's selection lands in that one shared `selectedEntityIds` set,
|
|
1062
|
+
// which both those raw-store reads AND this composite's OWN `get()`
|
|
1063
|
+
// (via the owning-adapter's `selection.get()`) then see identically —
|
|
1064
|
+
// no separate synthetic-id bookkeeping needed on this class at all.
|
|
1065
|
+
const byChild = new Map<AuthoringAdapter, string[]>();
|
|
1066
|
+
let synthetic: string | null = null;
|
|
1067
|
+
for (const id of ids) {
|
|
1068
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) {
|
|
1069
|
+
synthetic = id; // last one wins — only used when NOTHING else claims byChild
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const owner = this.findOwnerChild(id);
|
|
1073
|
+
if (!owner) continue;
|
|
1074
|
+
const arr = byChild.get(owner.adapter);
|
|
1075
|
+
if (arr) arr.push(id);
|
|
1076
|
+
else byChild.set(owner.adapter, [id]);
|
|
1077
|
+
}
|
|
1078
|
+
const setChildSelection = (adapter: AuthoringAdapter, childIds: string[]): void => {
|
|
1079
|
+
setAuthoringSelection(adapter, childIds, options);
|
|
1080
|
+
};
|
|
1081
|
+
if (byChild.size === 0) {
|
|
1082
|
+
const first = this.children[0];
|
|
1083
|
+
if (first) setChildSelection(first.adapter, synthetic ? [synthetic] : []);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
for (const [adapter, childIds] of byChild) setChildSelection(adapter, childIds);
|
|
1087
|
+
},
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
readonly transforms: TransformProvider = {
|
|
1091
|
+
// `null` for an unowned id is what keeps the fabricated all-zero Transform
|
|
1092
|
+
// section off the inspector: `inspection/compose.ts` renders that section
|
|
1093
|
+
// only when `dimensions` answers, so the refusal below is the gate every
|
|
1094
|
+
// shell path already respects. `get` can only THROW (its contract returns a
|
|
1095
|
+
// non-nullable `Transform`), which is correct for a caller that reaches it
|
|
1096
|
+
// anyway — for an id nothing owns, and equally for an id whose OWNING root
|
|
1097
|
+
// exposes no transform provider. A named error beats a pose in both cases;
|
|
1098
|
+
// `the world root's stage`'s camera-authoring gestures catch and report it.
|
|
1099
|
+
dimensions: (id) => {
|
|
1100
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return null;
|
|
1101
|
+
const transforms = this.routeOwned(id, 'transforms.dimensions')?.transforms;
|
|
1102
|
+
if (!transforms) return null;
|
|
1103
|
+
const dimensions = transforms.dimensions?.(id);
|
|
1104
|
+
return dimensions === undefined ? '3d' : dimensions;
|
|
1105
|
+
},
|
|
1106
|
+
get: (id): Transform => {
|
|
1107
|
+
const owner = this.routeOwned(id, 'transforms.get');
|
|
1108
|
+
if (!owner) throw new Error(unownedIdRefusal('transforms.get', id, this.children));
|
|
1109
|
+
const transforms = owner.transforms;
|
|
1110
|
+
if (!transforms) {
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
untransformedIdRefusal('transforms.get', id, this.ownerOf(id) ?? 'unknown'),
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
return transforms.get(id);
|
|
1116
|
+
},
|
|
1117
|
+
editability: (id, channel) => {
|
|
1118
|
+
const owner = this.routeOwned(id, 'transforms.editability');
|
|
1119
|
+
if (!owner) return { writable: false, reason: `No root owns entity id "${id}".` };
|
|
1120
|
+
return owner.transforms?.editability?.(id, channel) ?? { writable: true };
|
|
1121
|
+
},
|
|
1122
|
+
beginEdit: (id) => {
|
|
1123
|
+
const owner = this.routeOwned(id, 'transforms.beginEdit');
|
|
1124
|
+
if (owner) beginAuthoringTransformEdit(owner, id);
|
|
1125
|
+
},
|
|
1126
|
+
apply: (id, transform) => {
|
|
1127
|
+
const owner = this.routeOwned(id, 'transforms.apply');
|
|
1128
|
+
if (owner) applyAuthoringTransform(owner, id, transform);
|
|
1129
|
+
},
|
|
1130
|
+
endEdit: (id) => {
|
|
1131
|
+
const owner = this.routeOwned(id, 'transforms.endEdit');
|
|
1132
|
+
return owner ? endAuthoringTransformEdit(owner, id) : undefined;
|
|
1133
|
+
},
|
|
1134
|
+
// The OWNING child's ack, passed straight through — same reason `endEdit`
|
|
1135
|
+
// above must never synthesize one. Whether the channel HAS an override to
|
|
1136
|
+
// drop is `editability`'s answer (`removable`), routed per id the same way,
|
|
1137
|
+
// so a composite over a child with no removal door still refuses by name.
|
|
1138
|
+
remove: (id, channel) => {
|
|
1139
|
+
const owner = this.routeOwned(id, 'transforms.remove');
|
|
1140
|
+
return owner ? removeAuthoringTransform(owner, id, channel) : undefined;
|
|
1141
|
+
},
|
|
1142
|
+
sourceCommit: {
|
|
1143
|
+
availability: (id) => {
|
|
1144
|
+
const owner = this.routeOwned(id, 'transforms.sourceCommit');
|
|
1145
|
+
return (
|
|
1146
|
+
owner?.transforms?.sourceCommit?.availability(id) ?? {
|
|
1147
|
+
available: false,
|
|
1148
|
+
reason: 'The owning root exposes no explicit live-transform source commit.',
|
|
1149
|
+
}
|
|
1150
|
+
);
|
|
1151
|
+
},
|
|
1152
|
+
commit: async (id) => {
|
|
1153
|
+
const owner = this.routeOwned(id, 'transforms.sourceCommit');
|
|
1154
|
+
const outcome = owner ? commitAuthoringTransformSource(owner, id) : undefined;
|
|
1155
|
+
const ack = await outcome;
|
|
1156
|
+
return ack ?? { destination: 'no owning source-commit provider', persisted: false };
|
|
1157
|
+
},
|
|
1158
|
+
},
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* H5 — route a row's authorability warnings to the child that owns it.
|
|
1163
|
+
*
|
|
1164
|
+
* NOT part of `AuthoringAdapter`: like H3's source accessors this is an
|
|
1165
|
+
* OPTIONAL capability a child either has or does not, probed structurally by
|
|
1166
|
+
* `hierarchy-row-model.ts`'s `RowDiagnosticsSource`. Forwarding it here (as
|
|
1167
|
+
* opposed to letting the hierarchy walk `childAdapters()` itself, the way the
|
|
1168
|
+
* H3 context menu does) is what keeps the per-row cost identical to the
|
|
1169
|
+
* neighbouring `transforms.editability` probe — one `route(id)` per row, and
|
|
1170
|
+
* `RowWarningCache` pays even that at most once per row per version.
|
|
1171
|
+
*/
|
|
1172
|
+
diagnosticsFor(id: string): readonly { code: string; message: string }[] | undefined {
|
|
1173
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return undefined;
|
|
1174
|
+
const owner = (this.routeOwned(id, 'diagnosticsFor') ?? {}) as {
|
|
1175
|
+
diagnosticsFor?: (nodeId: string) => unknown;
|
|
1176
|
+
};
|
|
1177
|
+
return typeof owner.diagnosticsFor === 'function'
|
|
1178
|
+
? (owner.diagnosticsFor(id) as readonly { code: string; message: string }[] | undefined)
|
|
1179
|
+
: undefined;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* H6 — route "what does this row's projection hide?" to the child that owns
|
|
1184
|
+
* the row, exactly as {@link diagnosticsFor} routes its warnings and for the
|
|
1185
|
+
* same reason: it is an OPTIONAL capability probed structurally
|
|
1186
|
+
* (`hierarchy-internals.ts`'s `InternalsSource`), not part of
|
|
1187
|
+
* `AuthoringAdapter`, so a child without it simply reveals nothing.
|
|
1188
|
+
*
|
|
1189
|
+
* The synthetic group/organization ids are answered here rather than routed:
|
|
1190
|
+
* they are shell rows with no live object at all, so "reveal its internals"
|
|
1191
|
+
* has no referent — and `route()`'s unknown-id floor would otherwise ask the
|
|
1192
|
+
* FIRST child about an id it has never heard of.
|
|
1193
|
+
*/
|
|
1194
|
+
internalChildren(id: string): EditorNode[] | undefined {
|
|
1195
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return undefined;
|
|
1196
|
+
// Reveal state is session-only and deliberately survives a child remount. During that remount
|
|
1197
|
+
// an id which belonged to the old live graph may temporarily (or permanently) have no owner;
|
|
1198
|
+
// hierarchy-internals.ts's contract says that stale reveal is silently inert. This is an
|
|
1199
|
+
// optional read-only projection probe, not an authoring operation, so it must not go through
|
|
1200
|
+
// routeOwned's loud anti-shim refusal.
|
|
1201
|
+
const owner = (this.findOwnerChild(id)?.adapter ?? {}) as {
|
|
1202
|
+
internalChildren?: (nodeId: string) => EditorNode[] | undefined;
|
|
1203
|
+
};
|
|
1204
|
+
return typeof owner.internalChildren === 'function' ? owner.internalChildren(id) : undefined;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/** H6 — the companion cheap probe; same routing rules as
|
|
1208
|
+
* {@link internalChildren}. */
|
|
1209
|
+
hasInternals(id: string): boolean {
|
|
1210
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return false;
|
|
1211
|
+
// Same stale-reveal rule as internalChildren above.
|
|
1212
|
+
const owner = (this.findOwnerChild(id)?.adapter ?? {}) as {
|
|
1213
|
+
hasInternals?: (nodeId: string) => boolean;
|
|
1214
|
+
};
|
|
1215
|
+
return typeof owner.hasInternals === 'function' ? owner.hasInternals(id) : false;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* A4 (D8) — each adapter root's stable `id`, `zOrder`/`pausable`/`kind` are
|
|
1220
|
+
* a MANIFEST concept, not something any child
|
|
1221
|
+
* adapter's own inspector knows about — intercepted here, BEFORE `route()`,
|
|
1222
|
+
* so a real per-world adapter is never asked about an id it doesn't own.
|
|
1223
|
+
* Without a writable provider, these synthetic nodes still expose read-only
|
|
1224
|
+
* composition facts instead of being routed into a child that cannot own them.
|
|
1225
|
+
*/
|
|
1226
|
+
private rootProperties(id: string): PropertyDescriptor[] | null {
|
|
1227
|
+
if (isOrganizationNodeId(id)) {
|
|
1228
|
+
return [{ path: 'organization', label: 'Authoring Group', type: 'string', readonly: true }];
|
|
1229
|
+
}
|
|
1230
|
+
const child = this.groupChild(id);
|
|
1231
|
+
if (!child) return null;
|
|
1232
|
+
const surfaceRow = (child.role ?? 'world') === 'surface';
|
|
1233
|
+
return [
|
|
1234
|
+
{ path: 'id', label: 'Root ID', type: 'string', readonly: true },
|
|
1235
|
+
{ path: 'kind', label: 'Kind', type: 'string', readonly: true },
|
|
1236
|
+
...(this.manifest?.getRootContentSource || child.content
|
|
1237
|
+
? ([{ path: 'content', label: 'Content', type: 'string', readonly: true }] as const)
|
|
1238
|
+
: []),
|
|
1239
|
+
// A `surface` row is a seam this composite manufactured INSIDE a root
|
|
1240
|
+
// (an ingested game's DOM UI beside its canvas), not a manifest root of
|
|
1241
|
+
// its own. `zOrder`/`pausable` are manifest-root facts keyed by root id,
|
|
1242
|
+
// and there is no manifest root under this worldId to read or write — so
|
|
1243
|
+
// the row answers what it honestly knows (`id`, `kind`, `content`) and
|
|
1244
|
+
// does not manufacture the two it does not.
|
|
1245
|
+
...(surfaceRow
|
|
1246
|
+
? []
|
|
1247
|
+
: ([
|
|
1248
|
+
{
|
|
1249
|
+
path: 'zOrder',
|
|
1250
|
+
label: 'Z Order',
|
|
1251
|
+
type: 'number',
|
|
1252
|
+
...(this.manifest ? {} : { readonly: true }),
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
path: 'pausable',
|
|
1256
|
+
label: 'Pausable',
|
|
1257
|
+
type: 'boolean',
|
|
1258
|
+
...(this.manifest ? {} : { readonly: true }),
|
|
1259
|
+
},
|
|
1260
|
+
] as PropertyDescriptor[])),
|
|
1261
|
+
];
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* The child a GROUP-node id names, whatever its `role`.
|
|
1266
|
+
*
|
|
1267
|
+
* The role gates used to live here, so a `role: 'surface'` child's group row
|
|
1268
|
+
* answered nothing: `rootProperties` returned `null`, the inspector then
|
|
1269
|
+
* routed the `world:`-prefixed id into {@link findOwnerChild}, which refuses
|
|
1270
|
+
* every group id by construction (a group node has NO live object — that is
|
|
1271
|
+
* what makes it a group node), and the row logged an unowned-id refusal
|
|
1272
|
+
* instead of its own facts. Measured on an ingested game's `…:dom-ui` seam
|
|
1273
|
+
* row while `…:canvas` beside it answered normally.
|
|
1274
|
+
*
|
|
1275
|
+
* Widening the gate is the right half to move, not `findOwnerChild`: entity
|
|
1276
|
+
* ownership genuinely is "some child's hierarchy resolves this id", and a
|
|
1277
|
+
* group id resolves in nobody's. `role` distinguishes how a row is PROJECTED
|
|
1278
|
+
* ({@link groupNode}'s `document` vs `root`, {@link runtimeRootChildren}'s
|
|
1279
|
+
* top-level set) — it was never meant to decide whether a row exists to be
|
|
1280
|
+
* inspected. `hierarchy.node()` already treats both roles alike.
|
|
1281
|
+
*/
|
|
1282
|
+
private groupChild(id: string): CompositeChild | null {
|
|
1283
|
+
if (!isGroupNodeId(id)) return null;
|
|
1284
|
+
return this.children.find((c) => groupNodeId(c.worldId) === id) ?? null;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
readonly inspector: InspectorProvider = {
|
|
1288
|
+
properties: (id): PropertyDescriptor[] =>
|
|
1289
|
+
this.rootProperties(id) ??
|
|
1290
|
+
this.routeOwned(id, 'inspector.properties')?.inspector?.properties(id) ??
|
|
1291
|
+
[],
|
|
1292
|
+
get: (id, path) => {
|
|
1293
|
+
if (isOrganizationNodeId(id)) {
|
|
1294
|
+
return path === 'organization' ? 'Editor-only hierarchy projection' : undefined;
|
|
1295
|
+
}
|
|
1296
|
+
const child = this.groupChild(id);
|
|
1297
|
+
if (child) {
|
|
1298
|
+
const worldId = child.worldId;
|
|
1299
|
+
if (path === 'id') return worldId;
|
|
1300
|
+
if (path === 'kind') return child.kind;
|
|
1301
|
+
if (path === 'content') {
|
|
1302
|
+
return this.manifest?.getRootContentSource?.(worldId) ?? child.content;
|
|
1303
|
+
}
|
|
1304
|
+
// Surface rows declare neither (see `rootProperties`) — answering
|
|
1305
|
+
// `undefined` keeps read and describe agreeing.
|
|
1306
|
+
if ((child.role ?? 'world') === 'surface') return undefined;
|
|
1307
|
+
if (path === 'zOrder') {
|
|
1308
|
+
return this.manifest?.getRootZOrder(worldId) ?? child.zOrder ?? 0;
|
|
1309
|
+
}
|
|
1310
|
+
if (path === 'pausable') {
|
|
1311
|
+
return this.manifest?.getRootPausable(worldId) ?? child.pausable ?? true;
|
|
1312
|
+
}
|
|
1313
|
+
return undefined;
|
|
1314
|
+
}
|
|
1315
|
+
return this.routeOwned(id, 'inspector.get')?.inspector?.get(id, path);
|
|
1316
|
+
},
|
|
1317
|
+
// A synthetic row answers for ITSELF here, exactly as `properties`/`get`/
|
|
1318
|
+
// `set` above already do. Routing a `world:`-prefixed id into
|
|
1319
|
+
// `routeOwned` refuses every group id by construction (a group node has NO
|
|
1320
|
+
// live object — that is what makes it a group node), so the inspector
|
|
1321
|
+
// logged an unowned-id refusal for a row it had just described. MEASURED
|
|
1322
|
+
// on a packaged build against a canvas ingest root with a DOM UI beside
|
|
1323
|
+
// it: `inspector.editability: no root owns entity id
|
|
1324
|
+
// "world:probe-canvas:canvas"` on every selection of that root's own row.
|
|
1325
|
+
//
|
|
1326
|
+
// The descriptor is the ONE source of truth for whether a synthetic row's
|
|
1327
|
+
// field is writable (`rootProperties` decides `readonly` per path — the
|
|
1328
|
+
// manifest provider gates `zOrder`/`pausable`, `id`/`kind`/`content` are
|
|
1329
|
+
// always read-only, and a `surface` row declares neither of the first
|
|
1330
|
+
// two), so this reads the answer back off it instead of restating the
|
|
1331
|
+
// rules and drifting from them. An unknown path on a synthetic row is not
|
|
1332
|
+
// writable: nothing would receive the write.
|
|
1333
|
+
editability: (id, path) => {
|
|
1334
|
+
const synthetic = this.rootProperties(id);
|
|
1335
|
+
if (synthetic) {
|
|
1336
|
+
const descriptor = synthetic.find((property) => property.path === path);
|
|
1337
|
+
if (!descriptor) {
|
|
1338
|
+
return {
|
|
1339
|
+
writable: false,
|
|
1340
|
+
reason: `this root row has no "${path}" field`,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
return descriptor.readonly
|
|
1344
|
+
? {
|
|
1345
|
+
writable: false,
|
|
1346
|
+
reason: 'a composition fact, read from the manifest — not an authored value',
|
|
1347
|
+
}
|
|
1348
|
+
: { writable: true };
|
|
1349
|
+
}
|
|
1350
|
+
return (
|
|
1351
|
+
this.routeOwned(id, 'inspector.editability')?.inspector?.editability?.(id, path) ?? {
|
|
1352
|
+
writable: true,
|
|
1353
|
+
}
|
|
1354
|
+
);
|
|
1355
|
+
},
|
|
1356
|
+
set: (id, path, value) => {
|
|
1357
|
+
const child = this.groupChild(id);
|
|
1358
|
+
if (child) {
|
|
1359
|
+
if (this.manifest && (child.role ?? 'world') === 'world') {
|
|
1360
|
+
if (path === 'zOrder') this.manifest.setRootZOrder(child.worldId, Number(value));
|
|
1361
|
+
else if (path === 'pausable')
|
|
1362
|
+
this.manifest.setRootPausable(child.worldId, Boolean(value));
|
|
1363
|
+
}
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
// The OWNING child's ack, passed straight through. The composite has no
|
|
1367
|
+
// ack of its own to give and must never synthesize one: joining every
|
|
1368
|
+
// child's persistence is how a three-root edit came to report the DOM
|
|
1369
|
+
// root's destination with `persisted: true`.
|
|
1370
|
+
return this.routeOwned(id, 'inspector.set')?.inspector?.set(id, path, value);
|
|
1371
|
+
},
|
|
1372
|
+
remove: (id, path) => {
|
|
1373
|
+
// Root-group synthetic props have no removable override; everything else
|
|
1374
|
+
// routes to the owning child (which optional-omits `remove` when its
|
|
1375
|
+
// dialect has no way to express absence).
|
|
1376
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return;
|
|
1377
|
+
// The OWNING child's ack, passed straight through — same reason `set`
|
|
1378
|
+
// above must never synthesize one.
|
|
1379
|
+
return this.routeOwned(id, 'inspector.remove')?.inspector?.remove?.(id, path);
|
|
1380
|
+
},
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
/** Source-derived component instances, routed by the same ownership answer
|
|
1384
|
+
* as the generic Inspector. Synthetic root/group rows have no instance. */
|
|
1385
|
+
readonly instances: ComponentInstancesProvider = {
|
|
1386
|
+
describe: (id) => {
|
|
1387
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return null;
|
|
1388
|
+
return this.routeOwned(id, 'instances.describe')?.instances?.describe(id) ?? null;
|
|
1389
|
+
},
|
|
1390
|
+
revert: async (id, paths) => {
|
|
1391
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return;
|
|
1392
|
+
const adapter = this.routeOwned(id, 'instances.revert');
|
|
1393
|
+
if (!adapter?.instances) return;
|
|
1394
|
+
return revertAuthoringInstance(adapter, id, paths);
|
|
1395
|
+
},
|
|
1396
|
+
applyToComponent: async (id, path) => {
|
|
1397
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) {
|
|
1398
|
+
return { changed: false, message: 'A composition row is not a component instance.' };
|
|
1399
|
+
}
|
|
1400
|
+
const adapter = this.routeOwned(id, 'instances.applyToComponent');
|
|
1401
|
+
const provider = adapter?.instances;
|
|
1402
|
+
if (!provider) {
|
|
1403
|
+
return { changed: false, message: 'This subject has no writable component source.' };
|
|
1404
|
+
}
|
|
1405
|
+
return applyAuthoringInstanceToComponent(adapter, id, path);
|
|
1406
|
+
},
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Resolve a `create`/`creatableKinds` target: a group-node id
|
|
1411
|
+
* (`world:<w>`) targets that world's OWN root (forwarded parentId
|
|
1412
|
+
* `undefined`); a real id targets its owning child (forwarded as-is);
|
|
1413
|
+
* `null`/`undefined` (no parent — the toolbar's root-level "Add") targets
|
|
1414
|
+
* the FIRST child that actually has a structure provider — in edit mode
|
|
1415
|
+
* that is the one focused, live world adapter; every other
|
|
1416
|
+
* child is a read-only `BoundaryAuthoringAdapter` with no `structure` at
|
|
1417
|
+
* all. `null` when nothing can serve the request, so a create is never
|
|
1418
|
+
* silently routed to the wrong world.
|
|
1419
|
+
*/
|
|
1420
|
+
private resolveStructureTarget(
|
|
1421
|
+
parentId: string | null | undefined,
|
|
1422
|
+
): { child: CompositeChild; forwardParentId: string | undefined } | null {
|
|
1423
|
+
if (parentId != null && isOrganizationNodeId(parentId)) return null;
|
|
1424
|
+
if (parentId != null && isGroupNodeId(parentId)) {
|
|
1425
|
+
const worldId = parentId.slice(GROUP_PREFIX.length);
|
|
1426
|
+
const child = this.children.find((c) => c.worldId === worldId);
|
|
1427
|
+
return child ? { child, forwardParentId: undefined } : null;
|
|
1428
|
+
}
|
|
1429
|
+
if (parentId != null) {
|
|
1430
|
+
const child = this.findOwnerChild(parentId);
|
|
1431
|
+
if (!child) return null;
|
|
1432
|
+
const parent = child.adapter.hierarchy.node(parentId);
|
|
1433
|
+
return {
|
|
1434
|
+
child,
|
|
1435
|
+
// Documents and design states organize an adapter's native roots;
|
|
1436
|
+
// they are not native parent ids. The structure contract's root
|
|
1437
|
+
// spelling is `undefined`/`null`, so forward that explicitly.
|
|
1438
|
+
forwardParentId:
|
|
1439
|
+
parent?.role === 'document' || parent?.role === 'story' ? undefined : parentId,
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
const child = this.children.find((c) => c.adapter.structure);
|
|
1443
|
+
return child ? { child, forwardParentId: undefined } : null;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* A2 (map §5) — structural pass-through, routed by ownership. Never a
|
|
1448
|
+
* silent no-op when a request can't be served: `create`/`remove`/
|
|
1449
|
+
* `duplicate` log loudly via `editorConsole.warn` and return an honest
|
|
1450
|
+
* empty/unchanged result (#18) rather than pretending to route to the
|
|
1451
|
+
* wrong world. `reorder` is the one exception — it mirrors the base
|
|
1452
|
+
* contract's OWN "absent means no reorder UI" convention, so a missing
|
|
1453
|
+
* `reorder` on the owning child is a silent no-op, not a warning.
|
|
1454
|
+
*/
|
|
1455
|
+
readonly structure: StructureProvider = {
|
|
1456
|
+
// The owning child's WHOLE answer is forwarded — id and ack together. A
|
|
1457
|
+
// route that found no owner attempted no write, so its ack is `undefined`
|
|
1458
|
+
// (the honest `void` of `StructuralWriteOutcome`), never a fabricated one.
|
|
1459
|
+
create: (kind, parentId) => {
|
|
1460
|
+
const target = this.resolveStructureTarget(parentId ?? null);
|
|
1461
|
+
if (!target?.child.adapter.structure) {
|
|
1462
|
+
editorConsole.warn(
|
|
1463
|
+
`[CompositeAuthoringAdapter] create: no owning child with a structure ` +
|
|
1464
|
+
`provider for parent "${parentId ?? '(root)'}"`,
|
|
1465
|
+
'authoring',
|
|
1466
|
+
);
|
|
1467
|
+
return { id: '', ack: undefined };
|
|
1468
|
+
}
|
|
1469
|
+
return createAuthoringNode(target.child.adapter, kind, target.forwardParentId);
|
|
1470
|
+
},
|
|
1471
|
+
remove: (id) => {
|
|
1472
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return;
|
|
1473
|
+
const owner = this.findOwnerChild(id);
|
|
1474
|
+
if (!owner?.adapter.structure) {
|
|
1475
|
+
editorConsole.warn(
|
|
1476
|
+
`[CompositeAuthoringAdapter] remove: no owner/structure for id "${id}"`,
|
|
1477
|
+
'authoring',
|
|
1478
|
+
);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
// Propagate the owning child's return value (a react-world child
|
|
1482
|
+
// returns an awaitable) so `deleteSelection` can still serialize a
|
|
1483
|
+
// multi-delete THROUGH the composite the same way it does for a
|
|
1484
|
+
// directly-active override adapter (see that adapter's own `remove`
|
|
1485
|
+
// doc comment).
|
|
1486
|
+
return removeAuthoringNode(owner.adapter, id);
|
|
1487
|
+
},
|
|
1488
|
+
removeMany: async (ids) => {
|
|
1489
|
+
const groups = new Map<CompositeChild, string[]>();
|
|
1490
|
+
for (const id of ids) {
|
|
1491
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) continue;
|
|
1492
|
+
const owner = this.findOwnerChild(id);
|
|
1493
|
+
if (!owner?.adapter.structure) {
|
|
1494
|
+
editorConsole.warn(
|
|
1495
|
+
`[CompositeAuthoringAdapter] removeMany: no owner/structure for id "${id}"`,
|
|
1496
|
+
'authoring',
|
|
1497
|
+
);
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
const ownedIds = groups.get(owner);
|
|
1501
|
+
if (ownedIds) ownedIds.push(id);
|
|
1502
|
+
else groups.set(owner, [id]);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Preserve each child's strongest atomicity guarantee. In the common case
|
|
1506
|
+
// (one world's multi-selection), this is one child batch and therefore one
|
|
1507
|
+
// history transaction. Cross-world selections remain one ordered batch per
|
|
1508
|
+
// owner because no child can soundly mutate another world's document.
|
|
1509
|
+
// One gesture, one answer. A cross-world batch cannot honestly name TWO
|
|
1510
|
+
// destinations, so the composite reports the last owner's ack only when
|
|
1511
|
+
// EVERY owner persisted; the moment one did not, the whole batch degrades
|
|
1512
|
+
// to the live-only floor. Naming a file that carried only part of the
|
|
1513
|
+
// selection is the blanket ack the persistence pipe exists to kill.
|
|
1514
|
+
let ack: WriteAck = LIVE_ONLY_ACK;
|
|
1515
|
+
let allPersisted = true;
|
|
1516
|
+
for (const [owner, ownedIds] of groups) {
|
|
1517
|
+
const structure = owner.adapter.structure!;
|
|
1518
|
+
// biome-ignore lint/complexity/noUselessUndefinedInitialization: not useless — `void | WriteAck` is not definitely assigned by either branch below, and tsc reads the two reads that follow as use-before-assignment without it.
|
|
1519
|
+
let last: void | WriteAck = undefined;
|
|
1520
|
+
if (structure.removeMany) last = await removeManyAuthoringNodes(owner.adapter, ownedIds);
|
|
1521
|
+
else for (const id of ownedIds) last = await removeAuthoringNode(owner.adapter, id);
|
|
1522
|
+
if (last) ack = last;
|
|
1523
|
+
if (!last?.persisted) allPersisted = false;
|
|
1524
|
+
}
|
|
1525
|
+
return allPersisted ? ack : LIVE_ONLY_ACK;
|
|
1526
|
+
},
|
|
1527
|
+
canCopy: (ids) => {
|
|
1528
|
+
const owners = ids.map((id) => this.findOwnerChild(id));
|
|
1529
|
+
const owner = owners[0];
|
|
1530
|
+
return !!(
|
|
1531
|
+
owner?.adapter.structure?.copy &&
|
|
1532
|
+
!owners.some((candidate) => candidate?.worldId !== owner.worldId) &&
|
|
1533
|
+
(owner.adapter.structure.canCopy?.(ids) ?? true)
|
|
1534
|
+
);
|
|
1535
|
+
},
|
|
1536
|
+
copy: async (ids) => {
|
|
1537
|
+
const owners = ids.map((id) => this.findOwnerChild(id));
|
|
1538
|
+
const owner = owners[0];
|
|
1539
|
+
if (!this.structure.canCopy?.(ids) || !owner?.adapter.structure?.copy) {
|
|
1540
|
+
editorConsole.warn(
|
|
1541
|
+
'[CompositeAuthoringAdapter] copy requires source-addressable entities from one world.',
|
|
1542
|
+
'authoring',
|
|
1543
|
+
);
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
if (!(await copyAuthoringNodes(owner.adapter, ids))) return false;
|
|
1547
|
+
this.structureClipboardWorldId = owner.worldId;
|
|
1548
|
+
return true;
|
|
1549
|
+
},
|
|
1550
|
+
cut: async (ids) => {
|
|
1551
|
+
const owners = ids.map((id) => this.findOwnerChild(id));
|
|
1552
|
+
const owner = owners[0];
|
|
1553
|
+
if (!this.structure.canCopy?.(ids) || !owner?.adapter.structure?.cut) {
|
|
1554
|
+
editorConsole.warn(
|
|
1555
|
+
'[CompositeAuthoringAdapter] cut requires source-addressable entities from one world.',
|
|
1556
|
+
'authoring',
|
|
1557
|
+
);
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
const ack = await cutAuthoringNodes(owner.adapter, ids);
|
|
1561
|
+
if (ack === false) return false;
|
|
1562
|
+
this.structureClipboardWorldId = owner.worldId;
|
|
1563
|
+
return ack;
|
|
1564
|
+
},
|
|
1565
|
+
canPaste: (parentId) => {
|
|
1566
|
+
const clipboardOwner = this.children.find(
|
|
1567
|
+
(child) => child.worldId === this.structureClipboardWorldId,
|
|
1568
|
+
);
|
|
1569
|
+
if (!clipboardOwner?.adapter.structure?.paste) return false;
|
|
1570
|
+
const target =
|
|
1571
|
+
parentId === null
|
|
1572
|
+
? { child: clipboardOwner, forwardParentId: null }
|
|
1573
|
+
: this.resolveStructureTarget(parentId);
|
|
1574
|
+
return !!(
|
|
1575
|
+
target &&
|
|
1576
|
+
target.child.worldId === clipboardOwner.worldId &&
|
|
1577
|
+
(clipboardOwner.adapter.structure.canPaste?.(target.forwardParentId ?? null) ?? true)
|
|
1578
|
+
);
|
|
1579
|
+
},
|
|
1580
|
+
paste: async (parentId) => {
|
|
1581
|
+
const clipboardOwner = this.children.find(
|
|
1582
|
+
(child) => child.worldId === this.structureClipboardWorldId,
|
|
1583
|
+
);
|
|
1584
|
+
if (!clipboardOwner?.adapter.structure?.paste) {
|
|
1585
|
+
editorConsole.warn(
|
|
1586
|
+
'[CompositeAuthoringAdapter] paste has no copied entity payload to route.',
|
|
1587
|
+
'authoring',
|
|
1588
|
+
);
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
const target =
|
|
1592
|
+
parentId === null
|
|
1593
|
+
? { child: clipboardOwner, forwardParentId: undefined }
|
|
1594
|
+
: this.resolveStructureTarget(parentId);
|
|
1595
|
+
if (!target || target.child.worldId !== clipboardOwner.worldId) {
|
|
1596
|
+
editorConsole.warn(
|
|
1597
|
+
'[CompositeAuthoringAdapter] refusing to paste an entity across world roots.',
|
|
1598
|
+
'authoring',
|
|
1599
|
+
);
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
return await pasteAuthoringNodes(clipboardOwner.adapter, target.forwardParentId ?? null);
|
|
1603
|
+
},
|
|
1604
|
+
duplicate: (id) => {
|
|
1605
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return { id, ack: undefined };
|
|
1606
|
+
const owner = this.findOwnerChild(id);
|
|
1607
|
+
if (!owner?.adapter.structure) {
|
|
1608
|
+
editorConsole.warn(
|
|
1609
|
+
`[CompositeAuthoringAdapter] duplicate: no owner/structure for id "${id}"`,
|
|
1610
|
+
'authoring',
|
|
1611
|
+
);
|
|
1612
|
+
return { id, ack: undefined };
|
|
1613
|
+
}
|
|
1614
|
+
return duplicateAuthoringNode(owner.adapter, id);
|
|
1615
|
+
},
|
|
1616
|
+
reparent: (id, newParentId) => {
|
|
1617
|
+
const owner = this.findOwnerChild(id);
|
|
1618
|
+
if (!owner?.adapter.structure) {
|
|
1619
|
+
editorConsole.warn(
|
|
1620
|
+
`[CompositeAuthoringAdapter] reparent: no owner/structure for id "${id}"`,
|
|
1621
|
+
'authoring',
|
|
1622
|
+
);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
if (newParentId === null || newParentId === groupNodeId(owner.worldId)) {
|
|
1626
|
+
return reparentAuthoringNode(owner.adapter, id, null); // -> a root of its own world
|
|
1627
|
+
}
|
|
1628
|
+
const newOwner = this.findOwnerChild(newParentId);
|
|
1629
|
+
if (!newOwner || newOwner.worldId !== owner.worldId) {
|
|
1630
|
+
editorConsole.warn(
|
|
1631
|
+
`[CompositeAuthoringAdapter] reparent: refusing to move "${id}" across roots ` +
|
|
1632
|
+
`(target parent "${newParentId}" is not in world "${owner.worldId}")`,
|
|
1633
|
+
'authoring',
|
|
1634
|
+
);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const newParent = newOwner.adapter.hierarchy.node(newParentId);
|
|
1638
|
+
if (newParent?.role === 'document' || newParent?.role === 'story') {
|
|
1639
|
+
return reparentAuthoringNode(owner.adapter, id, null);
|
|
1640
|
+
}
|
|
1641
|
+
return reparentAuthoringNode(owner.adapter, id, newParentId);
|
|
1642
|
+
},
|
|
1643
|
+
reorder: (id, beforeSiblingId) => {
|
|
1644
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return;
|
|
1645
|
+
const owner = this.findOwnerChild(id);
|
|
1646
|
+
if (!owner) return;
|
|
1647
|
+
// Cross-world guard: a non-null sibling anchor MUST belong to the same
|
|
1648
|
+
// child as `id`. Otherwise (e.g. a drag onto another world's row whose
|
|
1649
|
+
// reparent was already refused) the child maps the foreign sibling id to
|
|
1650
|
+
// "append to end" and silently moves the entity within its own list on an
|
|
1651
|
+
// operation the composite just rejected. A null anchor (move to end) is
|
|
1652
|
+
// always valid.
|
|
1653
|
+
if (beforeSiblingId !== null && this.findOwnerChild(beforeSiblingId) !== owner) return;
|
|
1654
|
+
return reorderAuthoringNode(owner.adapter, id, beforeSiblingId);
|
|
1655
|
+
},
|
|
1656
|
+
creatableKinds: (parentId) => {
|
|
1657
|
+
const target = this.resolveStructureTarget(parentId);
|
|
1658
|
+
return (
|
|
1659
|
+
target?.child.adapter.structure?.creatableKinds?.(target.forwardParentId ?? null) ?? []
|
|
1660
|
+
);
|
|
1661
|
+
},
|
|
1662
|
+
// D3.a (spec 27 §6) — forward wrap/unwrap to the owning child, same
|
|
1663
|
+
// owner-lookup + loud-warn-on-miss shape as `duplicate`/`remove` above
|
|
1664
|
+
// (unlike `reorder`, which mirrors the base contract's OWN "absent means
|
|
1665
|
+
// no UI" silent-no-op convention — wrap/unwrap follow the LOUD group
|
|
1666
|
+
// instead since they're triggered from an explicit, always-visible menu
|
|
1667
|
+
// item, same as duplicate/delete).
|
|
1668
|
+
wrap: (id, wrapperTag) => {
|
|
1669
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return;
|
|
1670
|
+
const owner = this.findOwnerChild(id);
|
|
1671
|
+
if (!owner?.adapter.structure?.wrap) {
|
|
1672
|
+
editorConsole.warn(
|
|
1673
|
+
`[CompositeAuthoringAdapter] wrap: no owner/structure.wrap for id "${id}"`,
|
|
1674
|
+
'authoring',
|
|
1675
|
+
);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
return wrapAuthoringNode(owner.adapter, id, wrapperTag);
|
|
1679
|
+
},
|
|
1680
|
+
unwrap: (id) => {
|
|
1681
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return;
|
|
1682
|
+
const owner = this.findOwnerChild(id);
|
|
1683
|
+
if (!owner?.adapter.structure?.unwrap) {
|
|
1684
|
+
editorConsole.warn(
|
|
1685
|
+
`[CompositeAuthoringAdapter] unwrap: no owner/structure.unwrap for id "${id}"`,
|
|
1686
|
+
'authoring',
|
|
1687
|
+
);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
return unwrapAuthoringNode(owner.adapter, id);
|
|
1691
|
+
},
|
|
1692
|
+
// A cross-world selection is REFUSED whole rather than grouped in whichever
|
|
1693
|
+
// world came first — the same degrade `removeMany` makes for a batch whose
|
|
1694
|
+
// owners disagree. Nothing was written, so there is nothing to ack.
|
|
1695
|
+
group: (ids) => {
|
|
1696
|
+
if (ids.length === 0) return { id: null, ack: undefined };
|
|
1697
|
+
const owners = ids.map((id) => this.findOwnerChild(id));
|
|
1698
|
+
const owner = owners[0];
|
|
1699
|
+
if (
|
|
1700
|
+
!owner?.adapter.structure?.group ||
|
|
1701
|
+
owners.some((candidate) => candidate?.adapter !== owner.adapter)
|
|
1702
|
+
) {
|
|
1703
|
+
return { id: null, ack: undefined };
|
|
1704
|
+
}
|
|
1705
|
+
return groupAuthoringNodes(owner.adapter, ids);
|
|
1706
|
+
},
|
|
1707
|
+
ungroup: (id) => {
|
|
1708
|
+
const owner = this.findOwnerChild(id);
|
|
1709
|
+
return owner ? ungroupAuthoringNode(owner.adapter, id) : { ids: [], ack: undefined };
|
|
1710
|
+
},
|
|
1711
|
+
canUngroup: (id) => {
|
|
1712
|
+
const owner = this.findOwnerChild(id);
|
|
1713
|
+
return owner?.adapter.structure?.canUngroup?.(id) ?? false;
|
|
1714
|
+
},
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Asset-drop pass-through. A group-node id (`world:<w>`) must NOT fall
|
|
1719
|
+
* through `route()`'s "unknown id -> first child" floor: that would target
|
|
1720
|
+
* whatever child happened to be FIRST in the array regardless of which
|
|
1721
|
+
* world's row was actually under the cursor. Translate
|
|
1722
|
+
* the group-node id to that world's child directly, forwarding `''` (the
|
|
1723
|
+
* root-drop sentinel) as the node id, since the synthetic `world:<w>`
|
|
1724
|
+
* string means nothing to the child's own hierarchy.
|
|
1725
|
+
*/
|
|
1726
|
+
private resolveAssetDropTarget(
|
|
1727
|
+
nodeId: string,
|
|
1728
|
+
): { adapter: AuthoringAdapter; forwardId: string } | null {
|
|
1729
|
+
// A VIEWPORT drop names no node (`''`): it means "into the spatial world
|
|
1730
|
+
// under the pointer". Route it to the one three-surface child that accepts
|
|
1731
|
+
// drops — every design session is a composite (world + UI), and the
|
|
1732
|
+
// unknown-id refusal below made a drag from Content onto the 3D view die
|
|
1733
|
+
// silently on all of them (runhuman pass 45). Two spatial worlds would be
|
|
1734
|
+
// ambiguous, and the refusal stands for that case.
|
|
1735
|
+
if (nodeId === '') {
|
|
1736
|
+
const spatial = this.children.filter(
|
|
1737
|
+
(c) => c.kind === 'three' && c.role !== 'surface' && c.adapter.assetDrop !== undefined,
|
|
1738
|
+
);
|
|
1739
|
+
return spatial.length === 1 && spatial[0]
|
|
1740
|
+
? { adapter: spatial[0].adapter, forwardId: '' }
|
|
1741
|
+
: null;
|
|
1742
|
+
}
|
|
1743
|
+
if (isGroupNodeId(nodeId)) {
|
|
1744
|
+
const worldId = nodeId.slice(GROUP_PREFIX.length);
|
|
1745
|
+
const child = this.children.find((c) => c.worldId === worldId);
|
|
1746
|
+
return child ? { adapter: child.adapter, forwardId: '' } : null;
|
|
1747
|
+
}
|
|
1748
|
+
const owner = this.findOwnerChild(nodeId);
|
|
1749
|
+
// Unknown real id: REFUSED (`routeOwned`), not floored onto the first child —
|
|
1750
|
+
// dropping an asset onto a row nothing owns must not land it in whatever
|
|
1751
|
+
// world happens to be first.
|
|
1752
|
+
if (!owner) {
|
|
1753
|
+
this.routeOwned(nodeId, 'assetDrop');
|
|
1754
|
+
return null;
|
|
1755
|
+
}
|
|
1756
|
+
const node = owner.adapter.hierarchy.node(nodeId);
|
|
1757
|
+
return {
|
|
1758
|
+
adapter: owner.adapter,
|
|
1759
|
+
forwardId: node?.role === 'document' || node?.role === 'story' ? '' : nodeId,
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* D4 (B2) — resolves a `stories` call THE SAME WAY `resolveAssetDropTarget`
|
|
1765
|
+
* (above) resolves an asset drop: a group-node id (`world:<w>`) used to
|
|
1766
|
+
* fall through `route()`'s "unknown id -> first child" floor, so
|
|
1767
|
+
* `storiesFor('world:hud')` silently asked whatever child happened to be
|
|
1768
|
+
* FIRST in the array (never the actual owner), meaning the story picker
|
|
1769
|
+
* could NEVER appear on the world's own group row — the one place B2's
|
|
1770
|
+
* shell (`Inspector.tsx`) actually calls it. Translated to `<w>`'s own
|
|
1771
|
+
* child adapter. Unlike the asset-drop translation, the node id is
|
|
1772
|
+
* forwarded UNCHANGED (not blanked to `''`) — B2's `ReactRootAuthoringAdapter.stories`
|
|
1773
|
+
* is world-level and ignores `nodeId` entirely (see that class's doc
|
|
1774
|
+
* comment), but a future per-node catalog (B3) will want the real id, so
|
|
1775
|
+
* there is no sentinel to invent here.
|
|
1776
|
+
*/
|
|
1777
|
+
private resolveStoriesTarget(
|
|
1778
|
+
nodeId: string,
|
|
1779
|
+
): { adapter: AuthoringAdapter; forwardId: string } | null {
|
|
1780
|
+
if (isGroupNodeId(nodeId)) {
|
|
1781
|
+
const worldId = nodeId.slice(GROUP_PREFIX.length);
|
|
1782
|
+
const child = this.children.find((c) => c.worldId === worldId);
|
|
1783
|
+
return child ? { adapter: child.adapter, forwardId: nodeId } : null;
|
|
1784
|
+
}
|
|
1785
|
+
const adapter = this.routeOwned(nodeId, 'stories');
|
|
1786
|
+
return adapter ? { adapter, forwardId: nodeId } : null;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/** D4 — storybook stories, routed by node ownership (group-node ids
|
|
1790
|
+
* translated — see `resolveStoriesTarget` above, the same fix
|
|
1791
|
+
* `resolveAssetDropTarget` applied for asset drops). A node whose owning
|
|
1792
|
+
* adapter has no `stories` provider reports/does nothing (empty list, null
|
|
1793
|
+
* active, no-op apply/isolate) rather than throwing. */
|
|
1794
|
+
readonly stories: StoriesProvider = {
|
|
1795
|
+
storiesFor: (nodeId): StoryRef[] => {
|
|
1796
|
+
// THE SCOPE PROBE IS NOT AN UNOWNED ID. `storiesFor(WORLD_SCOPE_NODE_ID)`
|
|
1797
|
+
// is the protocol's world-level question ("list YOUR stories", asked with
|
|
1798
|
+
// no node in hand — `authoring/stories-scope.ts`), and this provider is
|
|
1799
|
+
// node-scoped BY CONSTRUCTION: it routes every call by node ownership, so
|
|
1800
|
+
// its honest answer with no node is the empty list. Routing the probe
|
|
1801
|
+
// through `routeOwned` produced that same empty list plus a loud
|
|
1802
|
+
// unowned-id error on every probe — noise for the one answer the protocol
|
|
1803
|
+
// asks for. Only this verb short-circuits: `active`/`apply`/`isolate` are
|
|
1804
|
+
// reached with the sentinel only after `storiesFor` answered rows, which
|
|
1805
|
+
// this provider never does, so an empty id arriving there IS a caller
|
|
1806
|
+
// error and keeps its error.
|
|
1807
|
+
if (nodeId === WORLD_SCOPE_NODE_ID) return [];
|
|
1808
|
+
const target = this.resolveStoriesTarget(nodeId);
|
|
1809
|
+
return target?.adapter.stories?.storiesFor(target.forwardId) ?? [];
|
|
1810
|
+
},
|
|
1811
|
+
active: (nodeId) => {
|
|
1812
|
+
const target = this.resolveStoriesTarget(nodeId);
|
|
1813
|
+
return target?.adapter.stories?.active(target.forwardId) ?? null;
|
|
1814
|
+
},
|
|
1815
|
+
apply: (nodeId, storyId) => {
|
|
1816
|
+
const target = this.resolveStoriesTarget(nodeId);
|
|
1817
|
+
const provider = target?.adapter.stories;
|
|
1818
|
+
if (!target || !provider) return;
|
|
1819
|
+
recordAuthoringConsumerUse({
|
|
1820
|
+
adapter: target.adapter,
|
|
1821
|
+
seam: 'editor.stories.apply',
|
|
1822
|
+
stage: 'effect',
|
|
1823
|
+
detail: `the composite applied story ${storyId ?? 'default'} through its owning adapter`,
|
|
1824
|
+
run: () => provider.apply(target.forwardId, storyId),
|
|
1825
|
+
});
|
|
1826
|
+
},
|
|
1827
|
+
isolate: (nodeId, storyId) => {
|
|
1828
|
+
// A null nodeId (exit isolation) has no owner to route by — forward the
|
|
1829
|
+
// exit to every child that implements `isolate` so nothing is left stuck
|
|
1830
|
+
// in an isolated state.
|
|
1831
|
+
if (nodeId === null) {
|
|
1832
|
+
for (const child of this.children) child.adapter.stories?.isolate?.(null, storyId);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
const target = this.resolveStoriesTarget(nodeId);
|
|
1836
|
+
target?.adapter.stories?.isolate?.(target.forwardId, storyId);
|
|
1837
|
+
},
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1840
|
+
/** Atomic authored assets, routed by the same node ownership as Inspector. */
|
|
1841
|
+
readonly assetSubject: AssetSubjectProvider = {
|
|
1842
|
+
get: (id) => this.findOwnerChild(id)?.adapter.assetSubject?.get(id) ?? null,
|
|
1843
|
+
entries: () =>
|
|
1844
|
+
this.children.flatMap((child) =>
|
|
1845
|
+
childAssetEntries(child.adapter).map(({ id, subject }) => ({
|
|
1846
|
+
id: `${child.worldId}:${id}`,
|
|
1847
|
+
subject,
|
|
1848
|
+
})),
|
|
1849
|
+
),
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
/** Asset drop, routed by node ownership (group-node ids translated — see
|
|
1853
|
+
* `resolveAssetDropTarget` above). */
|
|
1854
|
+
readonly assetDrop: AssetDropProvider = {
|
|
1855
|
+
accepts: (nodeId, assetPath, context) => {
|
|
1856
|
+
const target = this.resolveAssetDropTarget(nodeId);
|
|
1857
|
+
const provider = target?.adapter.assetDrop;
|
|
1858
|
+
if (!target || !provider) return false;
|
|
1859
|
+
return context === undefined
|
|
1860
|
+
? provider.accepts(target.forwardId, assetPath)
|
|
1861
|
+
: provider.accepts(target.forwardId, assetPath, context);
|
|
1862
|
+
},
|
|
1863
|
+
drop: (nodeId, assetPath, context) => {
|
|
1864
|
+
const target = this.resolveAssetDropTarget(nodeId);
|
|
1865
|
+
const provider = target?.adapter.assetDrop;
|
|
1866
|
+
if (!target || !provider) {
|
|
1867
|
+
// A human gesture never dies mutely: name what the composition holds
|
|
1868
|
+
// so the refusal is a fact, not a mystery (runhuman pass 45).
|
|
1869
|
+
const worlds = this.children
|
|
1870
|
+
.map((c) => `${c.worldId}:${c.kind}${c.adapter.assetDrop ? '' : ' (no drop target)'}`)
|
|
1871
|
+
.join(', ');
|
|
1872
|
+
editorConsole.warn(
|
|
1873
|
+
`Dropped ${assetPath} was not placed: ${
|
|
1874
|
+
nodeId === ''
|
|
1875
|
+
? 'no single spatial world accepts a viewport drop'
|
|
1876
|
+
: `“${nodeId}” has no owning world`
|
|
1877
|
+
} — worlds here: ${worlds}.`,
|
|
1878
|
+
'editor',
|
|
1879
|
+
);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
return dropAuthoringAsset(target.adapter, target.forwardId, assetPath, context);
|
|
1883
|
+
},
|
|
1884
|
+
};
|
|
1885
|
+
|
|
1886
|
+
// NOTE: `pickable` is deliberately NOT implemented here — layered viewport
|
|
1887
|
+
// picking (D12) across children is shell policy (B4), not something this
|
|
1888
|
+
// composite merges. Callers needing per-layer pick should go through
|
|
1889
|
+
// `childAdapters()` and pick each child's own `pickable` themselves.
|
|
1890
|
+
//
|
|
1891
|
+
// NOTE: `provenance` is deliberately NOT implemented here either, for the
|
|
1892
|
+
// complementary reason: provenance is a PER-WORLD declaration, and a
|
|
1893
|
+
// composite spans worlds whose declarations differ (a stamped ingest canvas
|
|
1894
|
+
// beside a live DOM HUD) — one adapter-level value would assert one world's
|
|
1895
|
+
// truth over another's rows, which is the fabrication the anti-shim rule
|
|
1896
|
+
// forbids. The shell resolves it per node through
|
|
1897
|
+
// `authoring/provenance.ts`'s governing-adapter hop, and the hierarchy's
|
|
1898
|
+
// world group rows read each child's own declaration off `childAdapters()`.
|
|
1899
|
+
//
|
|
1900
|
+
// Both absences are DELIVERED capabilities measured at a different door;
|
|
1901
|
+
// `adapter-reach.ts`'s `measureAdapter` states this so the coverage report
|
|
1902
|
+
// does not read them as gaps.
|
|
1903
|
+
|
|
1904
|
+
/**
|
|
1905
|
+
* Truth resolution, routed by SUBJECT OWNERSHIP exactly as
|
|
1906
|
+
* {@link inspector} routes: the child that actually owns the node answers
|
|
1907
|
+
* for it.
|
|
1908
|
+
*
|
|
1909
|
+
* PRESENT ONLY when at least one child actually indexes, because ABSENCE is
|
|
1910
|
+
* itself read as a fact by the shell and a composite must not answer for a
|
|
1911
|
+
* composition whose children index nothing: `shell-document-ops.ts`'s
|
|
1912
|
+
* `activeSelectionCreationSite` treats absence as "nothing to ask" (never a
|
|
1913
|
+
* fabricated unanchored verdict). It reads the ACTIVE adapter, which is this
|
|
1914
|
+
* composite for every promoted session (ingest with a detected DOM UI,
|
|
1915
|
+
* ingest+siblings, and ALL first-party play) — so without this, those
|
|
1916
|
+
* sessions lost creation-site reveal entirely even when the child holding
|
|
1917
|
+
* the running objects had a full index.
|
|
1918
|
+
*
|
|
1919
|
+
* A plain conditionally-assigned field rather than a getter, for the same
|
|
1920
|
+
* reason `BoundaryAuthoringAdapter.pickable` is one: under
|
|
1921
|
+
* `exactOptionalPropertyTypes` the key must be genuinely ABSENT, and a
|
|
1922
|
+
* getter's declared type would have to include `undefined`, which no longer
|
|
1923
|
+
* satisfies `AuthoringAdapter.truth?: TruthProvider`. NOT
|
|
1924
|
+
* `readonly` for the same reason `capabilities` is not — {@link
|
|
1925
|
+
* replaceChild} recomputes it after an in-place child swap (a design-time
|
|
1926
|
+
* layer mount upgrading a Boundary to a live adapter is exactly a swap that
|
|
1927
|
+
* can bring an index in, or take one away).
|
|
1928
|
+
*/
|
|
1929
|
+
truth?: TruthProvider;
|
|
1930
|
+
|
|
1931
|
+
/** The routing provider itself never changes — only whether this adapter
|
|
1932
|
+
* offers it at all does. Unlike `route()`, an unowned id is NOT floored
|
|
1933
|
+
* onto the first child: a synthetic group/organization row has no live
|
|
1934
|
+
* object, and an id no child recognizes must not be answered by a world
|
|
1935
|
+
* that never saw it (#18). Both get the same honest `NO_OBJECT_REASON` the
|
|
1936
|
+
* per-world adapters give for an id with nothing behind it. */
|
|
1937
|
+
private readonly truthProvider: TruthProvider = {
|
|
1938
|
+
resolve: (id, property) => {
|
|
1939
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) {
|
|
1940
|
+
return {
|
|
1941
|
+
site: { anchored: false, reason: NO_OBJECT_REASON } as NodeCreationSite,
|
|
1942
|
+
writeAnchorKind: undefined,
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
return (
|
|
1946
|
+
this.findOwnerChild(id)?.adapter.truth?.resolve(id, property) ?? {
|
|
1947
|
+
site: { anchored: false, reason: NO_OBJECT_REASON },
|
|
1948
|
+
writeAnchorKind: undefined,
|
|
1949
|
+
}
|
|
1950
|
+
);
|
|
1951
|
+
},
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
private refreshTruth(): void {
|
|
1955
|
+
if (this.children.some((c) => c.adapter.truth)) {
|
|
1956
|
+
this.truth = this.truthProvider;
|
|
1957
|
+
} else {
|
|
1958
|
+
// `delete`, not `= undefined`: an explicit `undefined` is not an absent
|
|
1959
|
+
// property under `exactOptionalPropertyTypes`, and absence is the fact
|
|
1960
|
+
// both shell readers are checking for.
|
|
1961
|
+
delete this.truth;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Related-subject links, routed by SUBJECT OWNERSHIP exactly as
|
|
1967
|
+
* {@link truth} routes — the inspector reads `adapter.related` off the
|
|
1968
|
+
* ACTIVE adapter (`inspection/compose.ts`), which is this composite for
|
|
1969
|
+
* every promoted session, so without this route every subject in a
|
|
1970
|
+
* composite session lost its jump links even when the owning child could
|
|
1971
|
+
* answer. Same conditionally-assigned shape as {@link truth}, recomputed by
|
|
1972
|
+
* {@link replaceChild} (a Boundary upgrading to a live adapter is exactly a
|
|
1973
|
+
* swap that can bring a provider in, or take one away). A synthetic
|
|
1974
|
+
* group/organization row has no defining document and honestly reports no
|
|
1975
|
+
* links.
|
|
1976
|
+
*/
|
|
1977
|
+
related?: RelatedSubjectsProvider;
|
|
1978
|
+
|
|
1979
|
+
private readonly relatedProvider: RelatedSubjectsProvider = {
|
|
1980
|
+
links: (id) => {
|
|
1981
|
+
if (isGroupNodeId(id) || isOrganizationNodeId(id)) return [];
|
|
1982
|
+
return this.findOwnerChild(id)?.adapter.related?.links(id) ?? [];
|
|
1983
|
+
},
|
|
1984
|
+
};
|
|
1985
|
+
|
|
1986
|
+
private refreshRelated(): void {
|
|
1987
|
+
if (this.children.some((c) => c.adapter.related)) {
|
|
1988
|
+
this.related = this.relatedProvider;
|
|
1989
|
+
} else {
|
|
1990
|
+
delete this.related;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// ------------------------------------------------------- change fan-out
|
|
1995
|
+
//
|
|
1996
|
+
// A consumer subscribes to THIS composite, not to a child — and the child set
|
|
1997
|
+
// is not fixed. `replaceChild` swaps a world's adapter mid-session (a
|
|
1998
|
+
// suspended R3F world finishing its async mount replaces its read-only
|
|
1999
|
+
// Boundary with the live source adapter; play suspend/Stop swap it back), and
|
|
2000
|
+
// the previous implementation bound each subscriber directly to the children
|
|
2001
|
+
// that existed AT SUBSCRIBE TIME. Those bindings survived the swap pointing at
|
|
2002
|
+
// the OUTGOING adapter, so every consumer that subscribed before a world
|
|
2003
|
+
// finished mounting was permanently deaf to the world that actually mounted —
|
|
2004
|
+
// the hierarchy panel among them, whose only adapter-side signal is
|
|
2005
|
+
// `adapter.subscribe`. Callers papered over it by pushing a store
|
|
2006
|
+
// notification alongside every swap (`store.notifyIngestEdit()`), which is a
|
|
2007
|
+
// side channel around a broken seam, not the seam working.
|
|
2008
|
+
//
|
|
2009
|
+
// Now the composite keeps the LISTENERS and re-derives its child bindings
|
|
2010
|
+
// whenever the child set changes, and the swap itself notifies: replacing a
|
|
2011
|
+
// world's adapter IS a structure change, so a subscriber hears about the
|
|
2012
|
+
// mounted world from the composite rather than from a store call the caller
|
|
2013
|
+
// has to remember.
|
|
2014
|
+
private readonly structureListeners = new Set<() => void>();
|
|
2015
|
+
/** Live child bindings, or `null` when nothing is listening. */
|
|
2016
|
+
private childFanOutUnsubs: Array<() => void> | null = null;
|
|
2017
|
+
|
|
2018
|
+
private readonly fanOutStructure = (): void => {
|
|
2019
|
+
for (const listener of [...this.structureListeners]) listener();
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
/** (Re)bind child subscriptions to match the CURRENT children and the current
|
|
2023
|
+
* listener demand. Idempotent; the only mutator of `childFanOutUnsubs`. */
|
|
2024
|
+
private syncChildFanOut(): void {
|
|
2025
|
+
if (this.childFanOutUnsubs) {
|
|
2026
|
+
for (const unsub of this.childFanOutUnsubs) unsub();
|
|
2027
|
+
this.childFanOutUnsubs = null;
|
|
2028
|
+
}
|
|
2029
|
+
if (this.structureListeners.size === 0) return;
|
|
2030
|
+
const unsubs: Array<() => void> = [];
|
|
2031
|
+
for (const child of this.children) {
|
|
2032
|
+
if (this.structureListeners.size > 0) {
|
|
2033
|
+
const subscribe = child.adapter.subscribe;
|
|
2034
|
+
const unsub = subscribe
|
|
2035
|
+
? recordAuthoringConsumerUse({
|
|
2036
|
+
adapter: child.adapter,
|
|
2037
|
+
seam: 'editor.subscribe',
|
|
2038
|
+
stage: 'effect',
|
|
2039
|
+
detail: 'the composite subscribed to child authoring changes',
|
|
2040
|
+
run: () => subscribe.call(child.adapter, this.fanOutStructure),
|
|
2041
|
+
})
|
|
2042
|
+
: undefined;
|
|
2043
|
+
if (unsub) unsubs.push(unsub);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
this.childFanOutUnsubs = unsubs;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
subscribe(listener: () => void): () => void {
|
|
2050
|
+
this.structureListeners.add(listener);
|
|
2051
|
+
this.syncChildFanOut();
|
|
2052
|
+
return () => {
|
|
2053
|
+
this.structureListeners.delete(listener);
|
|
2054
|
+
this.syncChildFanOut();
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* Persistence (T3.2 slice 3, generalized 2→N unchanged in semantics): `isDirty`
|
|
2060
|
+
* = OR of children that actually have a provider (a child without one, e.g. an
|
|
2061
|
+
* no-authoring adapter, is simply skipped — it has nothing to be dirty about);
|
|
2062
|
+
* `save()` saves the dirty children SEQUENTIALLY, logging loudly per-child on
|
|
2063
|
+
* failure without aborting the rest; `destination` joins child destinations
|
|
2064
|
+
* with `' + '`.
|
|
2065
|
+
*
|
|
2066
|
+
* History is resource-driven, so the composite needs no ordering or inverse
|
|
2067
|
+
* logic of its own; its children journal into the open project's service.
|
|
2068
|
+
*/
|
|
2069
|
+
get persistence(): PersistenceProvider {
|
|
2070
|
+
// Always a REAL (never `undefined`) provider — see composite-authoring-adapter's
|
|
2071
|
+
// 2-child precedent / `ephemeral-persistence.ts` for the same pattern.
|
|
2072
|
+
const persistable = this.children.filter(
|
|
2073
|
+
(
|
|
2074
|
+
c,
|
|
2075
|
+
): c is CompositeChild & {
|
|
2076
|
+
adapter: AuthoringAdapter & { persistence: PersistenceProvider };
|
|
2077
|
+
} => c.adapter.capabilities.persist && !!c.adapter.persistence,
|
|
2078
|
+
);
|
|
2079
|
+
return {
|
|
2080
|
+
isDirty: () => persistable.some((c) => c.adapter.persistence.isDirty()),
|
|
2081
|
+
lastError: () => {
|
|
2082
|
+
const errors = new Set<string>();
|
|
2083
|
+
for (const child of persistable) {
|
|
2084
|
+
const error = child.adapter.persistence.lastError?.();
|
|
2085
|
+
if (error) errors.add(error);
|
|
2086
|
+
}
|
|
2087
|
+
return errors.size > 0 ? [...errors].join(' · ') : null;
|
|
2088
|
+
},
|
|
2089
|
+
save: async () => {
|
|
2090
|
+
for (const c of persistable) {
|
|
2091
|
+
const p = c.adapter.persistence;
|
|
2092
|
+
if (!p.isDirty()) continue;
|
|
2093
|
+
try {
|
|
2094
|
+
await saveAuthoringDocument(c.adapter, `the composite saved dirty child ${c.worldId}`);
|
|
2095
|
+
} catch (err) {
|
|
2096
|
+
console.error(
|
|
2097
|
+
`[CompositeAuthoringAdapter] save failed for world "${c.worldId}" ` +
|
|
2098
|
+
`(destination: ${p.destination}) — continuing with remaining roots:`,
|
|
2099
|
+
err,
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
},
|
|
2104
|
+
destination:
|
|
2105
|
+
persistable.length > 0
|
|
2106
|
+
? persistable.map((c) => c.adapter.persistence.destination).join(' + ')
|
|
2107
|
+
: NO_PERSISTABLE_CHILD_DESTINATION,
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
}
|