@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,1954 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ToolObject3DAuthoringProps,
|
|
3
|
+
ToolObject3DDocumentAuthoring,
|
|
4
|
+
ToolObject3DPreviewSource,
|
|
5
|
+
ToolViewportDressing,
|
|
6
|
+
} from '@volter/editor-sdk/contributions';
|
|
7
|
+
import type { StageTransportSnapshot } from '@volter/editor-sdk/host';
|
|
8
|
+
import { EditorIcon, editorIcons, IconButton, themeVars } from '@volter/editor-sdk/widgets';
|
|
9
|
+
import type { AuthoringAdapter } from '@volter/editor-project/adapter';
|
|
10
|
+
import { contentWorldBounds } from '@volter/editor-threejs/viewport/content-bounds';
|
|
11
|
+
import { isInEditorOwnedSubtree } from '@volter/editor-threejs/viewport/editor-layers';
|
|
12
|
+
import { createStandardEnvironment } from '@volter/editor-threejs/viewport/environment';
|
|
13
|
+
import {
|
|
14
|
+
acquireInspectorPreviewRenderer,
|
|
15
|
+
type InspectorPreviewLease,
|
|
16
|
+
} from '@volter/editor-threejs/viewport/preview-renderer';
|
|
17
|
+
import { setUserData } from '@volter/editor-threejs/ecs/user-data';
|
|
18
|
+
import {
|
|
19
|
+
lazy,
|
|
20
|
+
type RefObject,
|
|
21
|
+
Suspense,
|
|
22
|
+
useCallback,
|
|
23
|
+
useEffect,
|
|
24
|
+
useRef,
|
|
25
|
+
useState,
|
|
26
|
+
useSyncExternalStore,
|
|
27
|
+
} from 'react';
|
|
28
|
+
import * as THREE from 'three';
|
|
29
|
+
import { registerStageTransport, StageTransport } from '../animation/stage-transport';
|
|
30
|
+
import { scanClipSubjects } from '../animation/three-clips-subject';
|
|
31
|
+
import { liveGestureActive, whenLiveGestureIdle } from '../authoring/live-gesture-lock';
|
|
32
|
+
import {
|
|
33
|
+
type Object3DDocumentPresentationState,
|
|
34
|
+
Object3DDocumentSession,
|
|
35
|
+
} from '../authoring/object3d-document-session';
|
|
36
|
+
import {
|
|
37
|
+
announceObject3DDocumentStage,
|
|
38
|
+
registerObject3DDocumentSession,
|
|
39
|
+
} from '../authoring/object3d-document-session-registry';
|
|
40
|
+
import { Object3DGestureController } from '../authoring/object3d-gesture-controller';
|
|
41
|
+
import { SourceObject3DAuthoringAdapter } from '../authoring/source-object3d-authoring-adapter';
|
|
42
|
+
import { registerDesignTimeSurface } from '../coverage/design-time-surfaces';
|
|
43
|
+
import { DocumentRendererSession } from '../document-renderer-session';
|
|
44
|
+
import { useOptionalEditorStats, useOptionalEditorStore } from '../editor-runtime';
|
|
45
|
+
import { EditorShellStore } from '../editor-shell-store';
|
|
46
|
+
import { EditorViewport } from '../editor-viewport';
|
|
47
|
+
import {
|
|
48
|
+
lookDeclaresViewportColors,
|
|
49
|
+
nativeViewportLook,
|
|
50
|
+
subscribeNativeSelectionTheme,
|
|
51
|
+
} from '../native-selection-style';
|
|
52
|
+
import {
|
|
53
|
+
type Object3DDocumentPersistenceSession,
|
|
54
|
+
object3DDocumentWritePolicy,
|
|
55
|
+
} from '../object3d-document-write-policy';
|
|
56
|
+
import { registerPerformanceSource } from '../performance-sources';
|
|
57
|
+
import { shellStoreForHost } from '../shell-store-door';
|
|
58
|
+
import {
|
|
59
|
+
assetSubjectApplies,
|
|
60
|
+
documentStageContext,
|
|
61
|
+
focusedStageStore,
|
|
62
|
+
identityRowApplies,
|
|
63
|
+
studioStageApplies,
|
|
64
|
+
} from '../stage-context';
|
|
65
|
+
import { registerStageStore } from '../stage-store-registry';
|
|
66
|
+
import { perspectiveDistanceToFitBox } from '../three-viewport/camera-fit';
|
|
67
|
+
import {
|
|
68
|
+
acquireInteractiveViewportRenderer,
|
|
69
|
+
type InteractiveViewportRendererLease,
|
|
70
|
+
} from '../three-viewport/interactive-renderer';
|
|
71
|
+
import type { ThreeViewportProjection } from '../three-viewport-presentation';
|
|
72
|
+
import {
|
|
73
|
+
activeViewportBreakdownDocumentId,
|
|
74
|
+
markViewportConstructReady,
|
|
75
|
+
markViewportConstructStarted,
|
|
76
|
+
markViewportFirstRenderStart,
|
|
77
|
+
markViewportRafResume,
|
|
78
|
+
markViewportReactActive,
|
|
79
|
+
markViewportSegment,
|
|
80
|
+
recordViewportFirstFrame,
|
|
81
|
+
} from '../viewport-activation-timings';
|
|
82
|
+
import { bindViewportRig, runViewportFrame } from '../viewport-door';
|
|
83
|
+
import {
|
|
84
|
+
notifyWorkspaceDocumentSelectionChanged,
|
|
85
|
+
openWorkspaceDocuments,
|
|
86
|
+
registerWorkspaceDocumentSelection,
|
|
87
|
+
} from '../workspace-document-registry';
|
|
88
|
+
import { AssetEditorShell } from './AssetEditorShell';
|
|
89
|
+
import { StageOverlays } from './StageOverlays';
|
|
90
|
+
import { TransportStrip } from './TransportStrip';
|
|
91
|
+
|
|
92
|
+
// The stage's own keyboard actions, behind the same lazy boundary as the
|
|
93
|
+
// overlays and for the same reason — a bounded host pays for neither
|
|
94
|
+
// (`stage-keyboard.tsx`).
|
|
95
|
+
const LazyStageKeyboard = lazy(async () => {
|
|
96
|
+
const module = await import('./stage-keyboard');
|
|
97
|
+
return { default: module.StageKeyboardBinding };
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
import {
|
|
101
|
+
applyStandardViewportDressing,
|
|
102
|
+
createGradientBackgroundTexture,
|
|
103
|
+
STANDARD_ENVIRONMENT_INTENSITY,
|
|
104
|
+
type StandardViewportDressing,
|
|
105
|
+
watchPaletteBackdrop,
|
|
106
|
+
} from './standard-viewport-dressing';
|
|
107
|
+
import { ViewportFurniture } from './ViewportFurniture';
|
|
108
|
+
import { OBJECT3D_SURFACE_BUILDING, ViewportSurfaceStatus } from './viewport-surface-status';
|
|
109
|
+
import { workspaceHistoryService } from './workspace-history';
|
|
110
|
+
|
|
111
|
+
/** WHERE THIS DOCUMENT'S BYTES GO — the one collaborator the shell installs
|
|
112
|
+
* (`object3d-document-write-policy.ts`). The tier's source recorder, the
|
|
113
|
+
* project file history and the thumbnail manifest all live behind it, so the
|
|
114
|
+
* Asset Lab 3D document carries no editor transport in its own closure
|
|
115
|
+
* (ARCHITECTURE-CORE §Editor chrome, "the viewport stack is separable").
|
|
116
|
+
* With no shell above it every member refuses by name, which is the same
|
|
117
|
+
* refusal a read-only tier already produced. */
|
|
118
|
+
const projectWrites = () => object3DDocumentWritePolicy();
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* How far a BARE dock-panel stage bleeds past the box a person sees, so the
|
|
122
|
+
* rendered image fills the panel edge to edge under its padding. THE ONE
|
|
123
|
+
* PLACE this number lives: the stage box takes it negative, the DOM furniture
|
|
124
|
+
* takes it back positive, and the canvas-drawn orientation gizmo takes it
|
|
125
|
+
* through `EditorViewportOptions.chromeInsetPx` — three readers, one value.
|
|
126
|
+
* A chromeless mount and the Asset Editor shell fill their own box exactly
|
|
127
|
+
* and bleed nothing.
|
|
128
|
+
*/
|
|
129
|
+
const STAGE_BLEED_PX = 12;
|
|
130
|
+
|
|
131
|
+
function boxFromFrameBounds(
|
|
132
|
+
bounds:
|
|
133
|
+
| {
|
|
134
|
+
readonly min: readonly [number, number, number];
|
|
135
|
+
readonly max: readonly [number, number, number];
|
|
136
|
+
}
|
|
137
|
+
| undefined,
|
|
138
|
+
): THREE.Box3 | null {
|
|
139
|
+
if (!bounds) return null;
|
|
140
|
+
const box = new THREE.Box3(
|
|
141
|
+
new THREE.Vector3(bounds.min[0], bounds.min[1], bounds.min[2]),
|
|
142
|
+
new THREE.Vector3(bounds.max[0], bounds.max[1], bounds.max[2]),
|
|
143
|
+
);
|
|
144
|
+
return box.isEmpty() ? null : box;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Per-document overrides of the standard viewport dressing
|
|
149
|
+
* (`standard-viewport-dressing.ts`) — explicit opt-outs/opt-ins, never
|
|
150
|
+
* re-implementations. Omitted means the standard look.
|
|
151
|
+
*
|
|
152
|
+
* THE SHAPE IS THE SDK'S (`ToolViewportDressing`), because a contributed
|
|
153
|
+
* document is what asks for it: `@volter/editor-blender`'s Model document hands over
|
|
154
|
+
* its view-locked studio through this door. One declaration, so the public
|
|
155
|
+
* surface and the host cannot drift.
|
|
156
|
+
*/
|
|
157
|
+
export type StandardDressingProps = ToolViewportDressing;
|
|
158
|
+
|
|
159
|
+
/** What an editor-side source lane gets to build its authoring adapter over
|
|
160
|
+
* the document's own store and scene. */
|
|
161
|
+
export interface SourceDocumentAuthoringContext {
|
|
162
|
+
readonly store: EditorShellStore;
|
|
163
|
+
readonly scene: THREE.Scene;
|
|
164
|
+
readonly hierarchyRoots?: readonly THREE.Object3D[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type SourceDocumentAuthoringFactory = (
|
|
168
|
+
context: SourceDocumentAuthoringContext,
|
|
169
|
+
) => ToolObject3DDocumentAuthoring;
|
|
170
|
+
|
|
171
|
+
/** A document owns the Object3D returned by its build callback. Its kind
|
|
172
|
+
* supplies authoring behavior and chrome; this host owns renderer lifetime,
|
|
173
|
+
* capture and disposal. Runtime world mounting belongs to its contributing
|
|
174
|
+
* product and is not installed by the modeling host. */
|
|
175
|
+
|
|
176
|
+
/** Module-level so its identity is stable across renders, which
|
|
177
|
+
* `useSyncExternalStore` requires; the theme root is global, so no stage's
|
|
178
|
+
* own element narrows it. */
|
|
179
|
+
function subscribeThemeViewportGroup(onChange: () => void): () => void {
|
|
180
|
+
return subscribeNativeSelectionTheme(null, onChange);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface Object3DDocumentViewportProps extends ToolObject3DAuthoringProps {
|
|
184
|
+
/** See `@volter/editor-sdk`'s `ToolObject3DAuthoringProps.audit`. */
|
|
185
|
+
readonly audit?: boolean;
|
|
186
|
+
/** Reuse the canonical Asset Lab viewport with NONE of the document chrome —
|
|
187
|
+
* no toolbar, no animation widget, no workspace-document registration —
|
|
188
|
+
* so it can fill a small box (the inspector's preview section). */
|
|
189
|
+
readonly chromeless?: boolean;
|
|
190
|
+
readonly modelSource?:
|
|
191
|
+
| { readonly kind: 'project-file'; readonly path: string }
|
|
192
|
+
| { readonly kind: 'entity'; readonly entityId: string };
|
|
193
|
+
/** Asset Lab's native subject kind for this Object3D document. */
|
|
194
|
+
readonly assetType?: string;
|
|
195
|
+
/**
|
|
196
|
+
* Editor-side authoring for a document whose write authority is project
|
|
197
|
+
* SOURCE (a mounted R3F composition's stamped JSX, today). The caller that
|
|
198
|
+
* knows the source lane supplies the adapter; this document constructs
|
|
199
|
+
* none of them, so it stays mountable without any source lane in its
|
|
200
|
+
* closure (ARCHITECTURE-CORE §Editor chrome, "the viewport stack is
|
|
201
|
+
* separable"). Raw model/artifact documents omit this and remain honestly
|
|
202
|
+
* read-only through the native source adapter. Consulted only when the
|
|
203
|
+
* project `authoring` factory declined. MUST be referentially stable for
|
|
204
|
+
* the document's lifetime, like `build`: it is an activation dependency.
|
|
205
|
+
*/
|
|
206
|
+
readonly sourceAuthoring?: SourceDocumentAuthoringFactory;
|
|
207
|
+
/**
|
|
208
|
+
* Where this viewport's `WebGLRenderer` comes from.
|
|
209
|
+
*
|
|
210
|
+
* `own` (the default) constructs one for this mount — the right answer for a
|
|
211
|
+
* document that lives as long as its panel does. `inspector-preview` draws
|
|
212
|
+
* through the shared inspector-preview renderer
|
|
213
|
+
* (`inspector-preview-renderer.ts`): that lane REMOUNTS PER SELECTION, and a
|
|
214
|
+
* renderer construction there is ~1.9 s of frozen main thread plus a cold
|
|
215
|
+
* shader cache and a fresh PMREM bake, every time you click something.
|
|
216
|
+
*/
|
|
217
|
+
readonly rendererLane?: 'own' | 'inspector-preview';
|
|
218
|
+
/**
|
|
219
|
+
* Double-click on a picked node — the document's "open THIS" gesture,
|
|
220
|
+
* reported with the node's own `Object3D` so the owner can resolve whatever
|
|
221
|
+
* it means by it. Single click stays plain selection; a document that
|
|
222
|
+
* declares no handler has no open gesture at all.
|
|
223
|
+
*
|
|
224
|
+
* Sole caller: the `3D` board, where an exhibit opens that
|
|
225
|
+
* story's own turntable document (`three-board/ThreeBoardDocument.tsx`).
|
|
226
|
+
*/
|
|
227
|
+
readonly onOpenNode?: (object: THREE.Object3D) => void;
|
|
228
|
+
/**
|
|
229
|
+
* When set, Frame with no selection and the view presets fit THIS box
|
|
230
|
+
* instead of `contentWorldBounds(root)`. The 3D board passes the union of
|
|
231
|
+
* each exhibit's presence as its true-scale overview.
|
|
232
|
+
*/
|
|
233
|
+
readonly frameBounds?: {
|
|
234
|
+
readonly min: readonly [number, number, number];
|
|
235
|
+
readonly max: readonly [number, number, number];
|
|
236
|
+
};
|
|
237
|
+
/** Optional first-paint frame distinct from the document's Frame fallback.
|
|
238
|
+
* The 3D component board opens on one readable exhibit, while clearing the
|
|
239
|
+
* selection and pressing Frame still restores its true-scale overview. */
|
|
240
|
+
readonly openingFrameBounds?: {
|
|
241
|
+
readonly min: readonly [number, number, number];
|
|
242
|
+
readonly max: readonly [number, number, number];
|
|
243
|
+
};
|
|
244
|
+
/** Multiplies the opening fit distance; `1` fills the view. */
|
|
245
|
+
readonly openingFit?: number;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Distinguishes the chromeless mounts of ONE document from each other and from
|
|
250
|
+
* that document's viewport, so each publishes its own design-time surface
|
|
251
|
+
* instead of overwriting a sibling's (see the registration below).
|
|
252
|
+
*/
|
|
253
|
+
let chromelessSurfaceSequence = 0;
|
|
254
|
+
|
|
255
|
+
interface RetainedObject3DStageState {
|
|
256
|
+
readonly documentId: string;
|
|
257
|
+
readonly store: EditorShellStore;
|
|
258
|
+
inUse: boolean;
|
|
259
|
+
source: ToolObject3DPreviewSource | null;
|
|
260
|
+
camera: { position: THREE.Vector3; target: THREE.Vector3; fov: number } | null;
|
|
261
|
+
presentation: Object3DDocumentPresentationState | null;
|
|
262
|
+
transport: StageTransportSnapshot | null;
|
|
263
|
+
contentSeconds: number;
|
|
264
|
+
transportAdvances: number;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Per-pane state outlives the React surface Code-OSS mounts for the active tab.
|
|
269
|
+
* A second visible pane showing the same document claims a second state slot;
|
|
270
|
+
* an inactive slot is reused only after its former pane has unmounted.
|
|
271
|
+
*/
|
|
272
|
+
const retainedObject3DStages = new Map<string, RetainedObject3DStageState[]>();
|
|
273
|
+
|
|
274
|
+
function claimRetainedObject3DStage(documentId: string): RetainedObject3DStageState {
|
|
275
|
+
const states = retainedObject3DStages.get(documentId) ?? [];
|
|
276
|
+
let state = states.find((candidate) => !candidate.inUse);
|
|
277
|
+
if (!state) {
|
|
278
|
+
state = {
|
|
279
|
+
documentId,
|
|
280
|
+
store: new EditorShellStore(),
|
|
281
|
+
inUse: false,
|
|
282
|
+
source: null,
|
|
283
|
+
camera: null,
|
|
284
|
+
presentation: null,
|
|
285
|
+
transport: null,
|
|
286
|
+
contentSeconds: 0,
|
|
287
|
+
transportAdvances: 0,
|
|
288
|
+
};
|
|
289
|
+
states.push(state);
|
|
290
|
+
retainedObject3DStages.set(documentId, states);
|
|
291
|
+
}
|
|
292
|
+
state.inUse = true;
|
|
293
|
+
return state;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function releaseRetainedObject3DStage(state: RetainedObject3DStageState): void {
|
|
297
|
+
state.inUse = false;
|
|
298
|
+
// Switching tabs unmounts the Code-OSS pane while its document remains in
|
|
299
|
+
// the open set. An actual close removes it first; discard retained CPU state
|
|
300
|
+
// after that registry mutation so closed documents do not form a new cache.
|
|
301
|
+
queueMicrotask(() => {
|
|
302
|
+
if (state.inUse) return;
|
|
303
|
+
if (openWorkspaceDocuments().some((document) => document.descriptor.id === state.documentId))
|
|
304
|
+
return;
|
|
305
|
+
state.source?.dispose();
|
|
306
|
+
state.source = null;
|
|
307
|
+
const states = retainedObject3DStages.get(state.documentId);
|
|
308
|
+
if (!states) return;
|
|
309
|
+
const remaining = states.filter((candidate) => candidate !== state || candidate.inUse);
|
|
310
|
+
if (remaining.length > 0) retainedObject3DStages.set(state.documentId, remaining);
|
|
311
|
+
else retainedObject3DStages.delete(state.documentId);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The element the viewport draws INTO — an EMPTY host div in both lanes.
|
|
317
|
+
* React never owns the canvas. Both lanes mount a leased renderer canvas into
|
|
318
|
+
* this host imperatively, then return it when the pane hides. A lost canvas is
|
|
319
|
+
* destroyed rather than reused; a healthy one can safely serve the next pane.
|
|
320
|
+
*/
|
|
321
|
+
function ViewportSurface({
|
|
322
|
+
canvasHostRef,
|
|
323
|
+
}: {
|
|
324
|
+
readonly canvasHostRef: RefObject<HTMLDivElement | null>;
|
|
325
|
+
}) {
|
|
326
|
+
return <div ref={canvasHostRef} style={{ position: 'absolute', inset: 0 }} />;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The viewport's drawing surface for one mount: either the shared
|
|
331
|
+
* inspector-preview renderer or the interactive-document renderer pool. The
|
|
332
|
+
* lane decides which exclusive lease to mount into `canvasHost`.
|
|
333
|
+
*/
|
|
334
|
+
function mountViewportSurface(
|
|
335
|
+
canvasHost: HTMLDivElement,
|
|
336
|
+
lane: 'own' | 'inspector-preview',
|
|
337
|
+
displayName: string,
|
|
338
|
+
initialSize: { readonly width: number; readonly height: number },
|
|
339
|
+
onContextLost?: () => void,
|
|
340
|
+
): {
|
|
341
|
+
canvas: HTMLCanvasElement;
|
|
342
|
+
renderer: THREE.WebGLRenderer;
|
|
343
|
+
lease: InspectorPreviewLease | InteractiveViewportRendererLease | null;
|
|
344
|
+
} {
|
|
345
|
+
if (lane === 'inspector-preview') {
|
|
346
|
+
const lease = acquireInspectorPreviewRenderer();
|
|
347
|
+
lease.canvas.setAttribute('aria-label', `${displayName} authoring viewport`);
|
|
348
|
+
canvasHost.appendChild(lease.canvas);
|
|
349
|
+
return { canvas: lease.canvas, renderer: lease.renderer, lease };
|
|
350
|
+
}
|
|
351
|
+
const lease = acquireInteractiveViewportRenderer(
|
|
352
|
+
initialSize.width,
|
|
353
|
+
initialSize.height,
|
|
354
|
+
onContextLost,
|
|
355
|
+
);
|
|
356
|
+
const { canvas, renderer } = lease;
|
|
357
|
+
canvas.setAttribute('aria-label', `${displayName} authoring viewport`);
|
|
358
|
+
canvasHost.appendChild(canvas);
|
|
359
|
+
return { canvas, renderer, lease };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The drawing surface belongs to the document, not to a source revision.
|
|
363
|
+
* Source lifetimes borrow it; closing waits for any asynchronous gesture
|
|
364
|
+
* cleanup to return its borrow before releasing the context. */
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
interface DocumentContentBinding {
|
|
368
|
+
readonly scene: THREE.Scene;
|
|
369
|
+
settle(): Promise<void>;
|
|
370
|
+
dispose(): Promise<void>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Persistent editor state. Source revisions own only their content binding. */
|
|
374
|
+
class Object3DDocumentHost {
|
|
375
|
+
readonly scene = new THREE.Scene();
|
|
376
|
+
/** This document's store, optionally supplied by its owner. */
|
|
377
|
+
readonly store: EditorShellStore;
|
|
378
|
+
readonly cleanups: Array<() => void> = [];
|
|
379
|
+
readonly rendererSession = new DocumentRendererSession(
|
|
380
|
+
(time, resumed) => this.frame?.(time, resumed),
|
|
381
|
+
() => {},
|
|
382
|
+
);
|
|
383
|
+
viewport: EditorViewport | null = null;
|
|
384
|
+
session: Object3DDocumentSession | null = null;
|
|
385
|
+
dressing: StandardViewportDressing | null = null;
|
|
386
|
+
defaultEnvironment: THREE.Texture | null = null;
|
|
387
|
+
defaultBackground: THREE.Color | THREE.Texture | null = null;
|
|
388
|
+
adapter: AuthoringAdapter | null = null;
|
|
389
|
+
content: DocumentContentBinding | null = null;
|
|
390
|
+
frame: ((time: number, resumed: boolean) => void) | null = null;
|
|
391
|
+
/** Mirrors the document scene's world dressing onto the rendered scene. The
|
|
392
|
+
* HOST owns it; the active session holds a reference and runs it before
|
|
393
|
+
* every draw (see {@link Object3DDocumentSession.setBeforeRender}). */
|
|
394
|
+
syncHostScene: (() => void) | null = null;
|
|
395
|
+
/** The other participants on THIS stage, once its module has loaded
|
|
396
|
+
* (`stage-presence-markers.ts`). Null on a chromeless mount and until then. */
|
|
397
|
+
presence: { syncMarkers(dtSeconds: number): void } | null = null;
|
|
398
|
+
initialized = false;
|
|
399
|
+
contentSeconds = 0;
|
|
400
|
+
transportAdvances = 0;
|
|
401
|
+
private users = 0;
|
|
402
|
+
private closed = false;
|
|
403
|
+
private released = false;
|
|
404
|
+
|
|
405
|
+
/** Whether {@link close} has run — a late async install must not push a
|
|
406
|
+
* cleanup onto a host whose cleanups have already been drained. */
|
|
407
|
+
get isClosed(): boolean {
|
|
408
|
+
return this.closed;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** THIS STAGE's transport — the one holder of the content-time door below.
|
|
412
|
+
* Built from this stage's own store, because `driver` is that store's
|
|
413
|
+
* `playState` and nothing else (`animation/stage-transport.ts`). */
|
|
414
|
+
readonly transport: StageTransport;
|
|
415
|
+
|
|
416
|
+
constructor(
|
|
417
|
+
readonly lane: 'own' | 'inspector-preview',
|
|
418
|
+
readonly surface: ReturnType<typeof mountViewportSurface>,
|
|
419
|
+
store?: EditorShellStore,
|
|
420
|
+
) {
|
|
421
|
+
this.store = store ?? new EditorShellStore();
|
|
422
|
+
this.transport = new StageTransport(this.store);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
borrow(): () => void {
|
|
426
|
+
this.users++;
|
|
427
|
+
let returned = false;
|
|
428
|
+
return () => {
|
|
429
|
+
if (returned) return;
|
|
430
|
+
returned = true;
|
|
431
|
+
this.users--;
|
|
432
|
+
this.releaseIfClosed();
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
close(): void {
|
|
437
|
+
this.closed = true;
|
|
438
|
+
this.frame = null;
|
|
439
|
+
this.syncHostScene = null;
|
|
440
|
+
this.session?.setBeforeRender(null);
|
|
441
|
+
this.session?.setPresentsFrames(false);
|
|
442
|
+
this.rendererSession.setActive(false);
|
|
443
|
+
void this.content?.dispose();
|
|
444
|
+
// The one teardown path allowed to end the transport (its header states
|
|
445
|
+
// the ownership): the document that owns this stage is unmounting.
|
|
446
|
+
this.transport.dispose();
|
|
447
|
+
this.releaseIfClosed();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private releaseIfClosed(): void {
|
|
451
|
+
if (!this.closed || this.users !== 0 || this.released) return;
|
|
452
|
+
this.released = true;
|
|
453
|
+
const { lease, renderer, canvas } = this.surface;
|
|
454
|
+
for (const cleanup of [
|
|
455
|
+
...this.cleanups.splice(0),
|
|
456
|
+
() => this.session?.dispose(),
|
|
457
|
+
() => this.viewport?.dispose(),
|
|
458
|
+
() => this.rendererSession.dispose(),
|
|
459
|
+
() => this.dressing?.dispose(),
|
|
460
|
+
...(lease
|
|
461
|
+
? [() => lease.release()]
|
|
462
|
+
: [() => renderer.dispose(), () => renderer.forceContextLoss(), () => canvas.remove()]),
|
|
463
|
+
]) {
|
|
464
|
+
try {
|
|
465
|
+
cleanup();
|
|
466
|
+
} catch (error) {
|
|
467
|
+
// biome-ignore lint/suspicious/noConsole: all remaining resources must still be released
|
|
468
|
+
console.error('Object3D document cleanup failed.', error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Editor-owned authoring host for a project-supplied native Object3D factory.
|
|
476
|
+
* Project code supplies only source construction; this component owns the
|
|
477
|
+
* real EditorViewport, document-scoped AuthoringAdapter, selection, framing,
|
|
478
|
+
* animation preview, resize, and teardown.
|
|
479
|
+
*
|
|
480
|
+
* ## TIME ON THIS SURFACE — the one policy, inherited by everything mounted here
|
|
481
|
+
*
|
|
482
|
+
* This host is the shared stack: Asset Lab documents, story turntables, 3D board
|
|
483
|
+
* exhibits and the inspector's preview card all render through it. So the
|
|
484
|
+
* Edit-static rule is enforced HERE, once, rather than at each of them:
|
|
485
|
+
*
|
|
486
|
+
* - PRESENTATION time always advances — orbit damping, the view cube, the
|
|
487
|
+
* standard dressing, the editor's own helpers and overlays. It is
|
|
488
|
+
* editor-owned and it moves while the editor is in Edit.
|
|
489
|
+
* - CONTENT time does NOT. The source's own `update(dt)` and any clip a
|
|
490
|
+
* subject shows move only through the stage TRANSPORT, and only while a
|
|
491
|
+
* human is holding it (`animation/stage-transport.ts`, WORK.md §The stage
|
|
492
|
+
* transport and the animation door). A mounted
|
|
493
|
+
* document plays nothing on its own: that is the defect this closes (a GLB
|
|
494
|
+
* train animating in the Asset Lab while every instrument called the editor
|
|
495
|
+
* still, owner find 2026-08-15).
|
|
496
|
+
*
|
|
497
|
+
* Whatever content time this surface has reached is published as its content
|
|
498
|
+
* clock (`coverage/design-time-surfaces.ts`), so the Edit-static vitals row
|
|
499
|
+
* measures THIS surface by name instead of speaking only for the Scene view.
|
|
500
|
+
*/
|
|
501
|
+
export function Object3DDocumentViewport({
|
|
502
|
+
documentId,
|
|
503
|
+
sourcePath,
|
|
504
|
+
build,
|
|
505
|
+
displayName = 'Source Object3D',
|
|
506
|
+
// No default: an omitted background means the STANDARD dressing's gradient;
|
|
507
|
+
// a value is an explicit flat-color override (SDK contributions may pass it).
|
|
508
|
+
background,
|
|
509
|
+
cameraDirection,
|
|
510
|
+
persistence,
|
|
511
|
+
documentSource,
|
|
512
|
+
authoring,
|
|
513
|
+
interaction,
|
|
514
|
+
selectionOutline = true,
|
|
515
|
+
active = true,
|
|
516
|
+
audit = true,
|
|
517
|
+
chromeless = false,
|
|
518
|
+
modelSource,
|
|
519
|
+
assetType,
|
|
520
|
+
sourceAuthoring,
|
|
521
|
+
dressing,
|
|
522
|
+
rendererLane = 'own',
|
|
523
|
+
onOpenNode,
|
|
524
|
+
frameBounds,
|
|
525
|
+
openingFrameBounds,
|
|
526
|
+
openingFit,
|
|
527
|
+
statistics,
|
|
528
|
+
}: Object3DDocumentViewportProps) {
|
|
529
|
+
// ANNOUNCE THE STAGE (`authoring/object3d-document-session-registry.ts`).
|
|
530
|
+
// The lazy boundary announces for the documents that go through it; the
|
|
531
|
+
// asset viewers import THIS component directly (`asset-viewers/
|
|
532
|
+
// ModelAssetDocument.tsx:19` and its two siblings), so the announcement is
|
|
533
|
+
// made here too. Announcing is counted, so the two overlap harmlessly.
|
|
534
|
+
useEffect(() => {
|
|
535
|
+
if (chromeless) return;
|
|
536
|
+
return announceObject3DDocumentStage(documentId);
|
|
537
|
+
}, [documentId, chromeless]);
|
|
538
|
+
// The SHELL store, not this host's own: what a stage is showing is a fact
|
|
539
|
+
// about the workspace's document, and `stage-context.ts` is where every
|
|
540
|
+
// stage capability asks it (ARCHITECTURE-CORE §One stage). OPTIONAL because
|
|
541
|
+
// this host is a bounded-host surface — `@volter/editor-blender`'s Model document
|
|
542
|
+
// mounts it with no `EditorProvider` above it, and the throwing hook took
|
|
543
|
+
// that document off the screen with "useEditorStore must be used within
|
|
544
|
+
// <EditorProvider>" (measured live, 2026-09-18). No shell, no shell frame.
|
|
545
|
+
const shellStore = useOptionalEditorStore();
|
|
546
|
+
/**
|
|
547
|
+
* THE STORE THE STAGE CONTEXT IS ASKED AGAINST — the session's own, reached
|
|
548
|
+
* without a React provider.
|
|
549
|
+
*
|
|
550
|
+
* `useOptionalEditorStore()` above answers a NARROWER question than the
|
|
551
|
+
* context needs: it is whether the shell's own React tree is above this
|
|
552
|
+
* host, and for every package-contributed document it is not.
|
|
553
|
+
* `@volter/editor-blender`'s Model document mounts this host through `ToolHost`, so the
|
|
554
|
+
* context store is null there — and reading only it left `stageCtx` null,
|
|
555
|
+
* which made EVERY condition in `stage-context.ts` unanswerable for that
|
|
556
|
+
* document. That is a capability decided by where a document's React tree
|
|
557
|
+
* happens to mount, which is the origin-deciding reading §One stage
|
|
558
|
+
* retired. MEASURED 2026-09-18 on the models scaffold: the Model document's
|
|
559
|
+
* controls hint (the one overlay every three stage carries) is absent, and
|
|
560
|
+
* with it the whole context — so the studio arm of `studioStageApplies`
|
|
561
|
+
* could never fire for a model no matter what the look declared.
|
|
562
|
+
*
|
|
563
|
+
* `shellStoreForHost()` supplies the session store that
|
|
564
|
+
* `focusedStageStore()` falls back to. It is deliberately NOT used for the
|
|
565
|
+
* overlay set or the shell readout below: those are the SHELL's estate and
|
|
566
|
+
* their condition is the shell's React presence, exactly as §One stage
|
|
567
|
+
* unit 3 measured it — a stage that is not inside the shell has no shell
|
|
568
|
+
* overlays. That gap is real and named, not closed here: unit 1's bar is
|
|
569
|
+
* that the Model document's Blender frame does not move.
|
|
570
|
+
*/
|
|
571
|
+
const contextStore = shellStore ?? shellStoreForHost();
|
|
572
|
+
/**
|
|
573
|
+
* WHAT THIS STAGE IS SHOWING, asked once (ARCHITECTURE-CORE §One stage).
|
|
574
|
+
* `chrome` is the host's own answer and nothing infers it: an EMBEDDED
|
|
575
|
+
* preview has no workspace document at all, which is exactly why the
|
|
576
|
+
* subject would come back `unknown` for one.
|
|
577
|
+
*/
|
|
578
|
+
const stageCtx = contextStore
|
|
579
|
+
? documentStageContext(contextStore, documentId, chromeless ? 'embedded' : 'document')
|
|
580
|
+
: null;
|
|
581
|
+
/**
|
|
582
|
+
* THE STUDIO PRESENTATION — the alpha clear, the suppressed palette
|
|
583
|
+
* backdrop, the identity-row frame and the asset flavour of this stage's
|
|
584
|
+
* provenance. It replaced a `documentKind` prop the CALLER chose, which is
|
|
585
|
+
* exactly the origin-of-the-document reading §One stage retired: every
|
|
586
|
+
* mount that passed `documentKind="asset"` is a story, an artifact or an
|
|
587
|
+
* embedded preview, and each still answers true.
|
|
588
|
+
*/
|
|
589
|
+
// The LOOK is one of `studioStageApplies`'s live inputs (a data subject takes
|
|
590
|
+
// the studio only where the look leaves the backdrop to the editor), and the
|
|
591
|
+
// one that changes with no store write behind it: switching the style
|
|
592
|
+
// re-emits the tokens onto the theme root's inline `style`, which is exactly
|
|
593
|
+
// what this observes. Without the subscription the presentation would flip
|
|
594
|
+
// only on the next unrelated render, and a style switch is the gesture the
|
|
595
|
+
// condition exists for.
|
|
596
|
+
const lookPaintsViewport = useSyncExternalStore(
|
|
597
|
+
subscribeThemeViewportGroup,
|
|
598
|
+
lookDeclaresViewportColors,
|
|
599
|
+
);
|
|
600
|
+
const studioStage = stageCtx !== null && studioStageApplies(stageCtx, lookPaintsViewport);
|
|
601
|
+
const studioStageRef = useRef(studioStage);
|
|
602
|
+
studioStageRef.current = studioStage;
|
|
603
|
+
// An ARTIFACT document gets the identity-row shell — the frame, the context
|
|
604
|
+
// activation and the SELECTION REGISTRATION (`AssetEditorShell`'s
|
|
605
|
+
// `selection` prop). Asked here, beside the studio presentation, because the
|
|
606
|
+
// mount effect needs it too: whoever does NOT get the shell registers its own
|
|
607
|
+
// workspace selection instead, and exactly one of the two must.
|
|
608
|
+
const hasShell = stageCtx !== null && identityRowApplies(stageCtx);
|
|
609
|
+
const hasShellRef = useRef(hasShell);
|
|
610
|
+
hasShellRef.current = hasShell;
|
|
611
|
+
// The ASSET FLAVOUR of this stage's provenance: the source adapter's
|
|
612
|
+
// `documentKind` ("derived asset", `model-asset`) and the performance
|
|
613
|
+
// source's kind. It is the studio presentation MINUS the data arm — a mesh
|
|
614
|
+
// module opens in an isolation scene but is not a derived asset — and was
|
|
615
|
+
// read off `studioStage` while the two were the same answer.
|
|
616
|
+
const assetSubject = stageCtx !== null && assetSubjectApplies(stageCtx);
|
|
617
|
+
const assetSubjectRef = useRef(assetSubject);
|
|
618
|
+
assetSubjectRef.current = assetSubject;
|
|
619
|
+
// The focused document fills the shell's readout. Null without a shell.
|
|
620
|
+
const shellStats = useOptionalEditorStats();
|
|
621
|
+
const shellStatsRef = useRef(shellStats);
|
|
622
|
+
shellStatsRef.current = shellStats;
|
|
623
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
624
|
+
// The shared lane mounts the pool's own canvas here instead of rendering one.
|
|
625
|
+
const canvasHostRef = useRef<HTMLDivElement>(null);
|
|
626
|
+
const documentHostRef = useRef<Object3DDocumentHost | null>(null);
|
|
627
|
+
const retainedRef = useRef<RetainedObject3DStageState | null>(null);
|
|
628
|
+
if (!retainedRef.current || retainedRef.current.documentId !== documentId) {
|
|
629
|
+
if (retainedRef.current) releaseRetainedObject3DStage(retainedRef.current);
|
|
630
|
+
retainedRef.current = claimRetainedObject3DStage(documentId);
|
|
631
|
+
}
|
|
632
|
+
useEffect(() => {
|
|
633
|
+
const retained = retainedRef.current!;
|
|
634
|
+
return () => releaseRetainedObject3DStage(retained);
|
|
635
|
+
}, [documentId]);
|
|
636
|
+
const retainedState = retainedRef.current;
|
|
637
|
+
// Only a visible pane owns an interactive attachment. Hidden documents
|
|
638
|
+
// retain their state and return their renderer to the bounded pool.
|
|
639
|
+
const wantsSurface = active;
|
|
640
|
+
const [surfaceAttached, setSurfaceAttached] = useState(wantsSurface);
|
|
641
|
+
useEffect(() => setSurfaceAttached(wantsSurface), [wantsSurface]);
|
|
642
|
+
// The selection silhouette is a LIVE prop: a modeling document turns it off
|
|
643
|
+
// the moment it enters its own sub-object mode and back on when it leaves,
|
|
644
|
+
// and the session applies that to the composer it has already built. The ref
|
|
645
|
+
// is what the mount path reads, since the session is built inside an effect
|
|
646
|
+
// that does not depend on this prop.
|
|
647
|
+
const selectionOutlineRef = useRef(selectionOutline);
|
|
648
|
+
useEffect(() => {
|
|
649
|
+
selectionOutlineRef.current = selectionOutline;
|
|
650
|
+
const session = documentHostRef.current?.session;
|
|
651
|
+
if (session)
|
|
652
|
+
session.selectionOutlineEnabled = rendererLane !== 'inspector-preview' && selectionOutline;
|
|
653
|
+
}, [selectionOutline, rendererLane]);
|
|
654
|
+
const contentUpdateTail = useRef<Promise<void>>(Promise.resolve());
|
|
655
|
+
const retainSourceOnDisposeRef = useRef(false);
|
|
656
|
+
useEffect(() => {
|
|
657
|
+
if (!surfaceAttached) return;
|
|
658
|
+
firstFrameGateRef.current = false;
|
|
659
|
+
setSurfaceStatus('building');
|
|
660
|
+
return () => {
|
|
661
|
+
const host = documentHostRef.current;
|
|
662
|
+
if (host) {
|
|
663
|
+
// Inactive tabs stay open in the workspace. Their source remains
|
|
664
|
+
// mounted in retained CPU state while the expensive attachment goes
|
|
665
|
+
// back to the pool. A source revision or final close still disposes it.
|
|
666
|
+
retainSourceOnDisposeRef.current = !activeRef.current;
|
|
667
|
+
retainedState.camera = host.store.cameraPose;
|
|
668
|
+
retainedState.presentation = host.session?.presentation() ?? null;
|
|
669
|
+
retainedState.transport = host.transport.snapshot();
|
|
670
|
+
retainedState.contentSeconds = host.contentSeconds;
|
|
671
|
+
retainedState.transportAdvances = host.transportAdvances;
|
|
672
|
+
}
|
|
673
|
+
documentStateRef.current = null;
|
|
674
|
+
persistenceSessionRef.current = null;
|
|
675
|
+
interactionExtensionRef.current = null;
|
|
676
|
+
rendererSessionRef.current = null;
|
|
677
|
+
host?.close();
|
|
678
|
+
documentHostRef.current = null;
|
|
679
|
+
};
|
|
680
|
+
}, [documentId, rendererLane, retainedState, surfaceAttached]);
|
|
681
|
+
// Read through a ref: the one big lifetime effect below owns the canvas
|
|
682
|
+
// listener, and re-running it on a new handler identity would tear the whole
|
|
683
|
+
// renderer down and rebuild it.
|
|
684
|
+
const onOpenNodeRef = useRef(onOpenNode);
|
|
685
|
+
onOpenNodeRef.current = onOpenNode;
|
|
686
|
+
const frameBoundsRef = useRef(frameBounds);
|
|
687
|
+
frameBoundsRef.current = frameBounds;
|
|
688
|
+
const openingFrameBoundsRef = useRef(openingFrameBounds);
|
|
689
|
+
openingFrameBoundsRef.current = openingFrameBounds;
|
|
690
|
+
// The current prop is still read by asynchronous installation. Attachment
|
|
691
|
+
// lifetime is controlled separately by `surfaceAttached` above.
|
|
692
|
+
const activeRef = useRef(active);
|
|
693
|
+
activeRef.current = active;
|
|
694
|
+
const previousActiveRef = useRef(active);
|
|
695
|
+
const sourceIdentityRef = useRef({ build, sourcePath });
|
|
696
|
+
useEffect(() => {
|
|
697
|
+
const previous = sourceIdentityRef.current;
|
|
698
|
+
sourceIdentityRef.current = { build, sourcePath };
|
|
699
|
+
if (previous.build === build && previous.sourcePath === sourcePath) return;
|
|
700
|
+
// An HMR revision received while hidden invalidates the prepared source.
|
|
701
|
+
// The next reveal constructs the current revision while retaining the
|
|
702
|
+
// pane's camera, tool, helpers, selection, and transport state.
|
|
703
|
+
if (!surfaceAttached && retainedState.source) {
|
|
704
|
+
retainedState.source.dispose();
|
|
705
|
+
retainedState.source = null;
|
|
706
|
+
}
|
|
707
|
+
}, [build, retainedState, sourcePath, surfaceAttached]);
|
|
708
|
+
const rendererSessionRef = useRef<DocumentRendererSession | null>(null);
|
|
709
|
+
const firstFrameGateRef = useRef(false);
|
|
710
|
+
const interactionExtensionRef = useRef<ReturnType<
|
|
711
|
+
NonNullable<typeof interaction>['setup']
|
|
712
|
+
> | null>(null);
|
|
713
|
+
const documentStateRef = useRef<{
|
|
714
|
+
root: THREE.Object3D;
|
|
715
|
+
animations: readonly THREE.AnimationClip[];
|
|
716
|
+
} | null>(null);
|
|
717
|
+
const persistenceSessionRef = useRef<Promise<Object3DDocumentPersistenceSession> | null>(null);
|
|
718
|
+
const [error, setError] = useState<string | null>(null);
|
|
719
|
+
const [surfaceStatus, setSurfaceStatus] = useState<'building' | 'ready'>('building');
|
|
720
|
+
const [projection, setProjection] = useState<ThreeViewportProjection>('perspective');
|
|
721
|
+
// The live authoring adapter this document's inspection composes from. It
|
|
722
|
+
// only exists once the graph is built, so it is state rather than a ref:
|
|
723
|
+
// its arrival is what re-renders the aside below.
|
|
724
|
+
const [documentAdapter, setDocumentAdapter] = useState<AuthoringAdapter | null>(null);
|
|
725
|
+
const documentAdapterRef = useRef<AuthoringAdapter | null>(null);
|
|
726
|
+
documentAdapterRef.current = documentAdapter;
|
|
727
|
+
const cameraX = cameraDirection?.[0];
|
|
728
|
+
const cameraY = cameraDirection?.[1];
|
|
729
|
+
const cameraZ = cameraDirection?.[2];
|
|
730
|
+
const modelSourceKind = modelSource?.kind;
|
|
731
|
+
const modelSourcePath = modelSource?.kind === 'project-file' ? modelSource.path : undefined;
|
|
732
|
+
const modelSourceEntityId = modelSource?.kind === 'entity' ? modelSource.entityId : undefined;
|
|
733
|
+
const dressingEnvironment = dressing?.environment;
|
|
734
|
+
const dressingBackground = dressing?.background;
|
|
735
|
+
const dressingKeyLight = dressing?.keyLight;
|
|
736
|
+
const dressingGrid = dressing?.grid;
|
|
737
|
+
const dressingViewLocked = dressing?.viewLocked;
|
|
738
|
+
const dressingToneMapping = dressing?.toneMapping;
|
|
739
|
+
if (active) markViewportReactActive(documentId);
|
|
740
|
+
|
|
741
|
+
// Document identity owns the host. Source changes prepare a separate native
|
|
742
|
+
// graph, then publish it into that host without recreating editor state.
|
|
743
|
+
useEffect(() => {
|
|
744
|
+
let cancelled = false;
|
|
745
|
+
if (!surfaceAttached) return;
|
|
746
|
+
const install = async () => {
|
|
747
|
+
await documentHostRef.current?.content?.settle();
|
|
748
|
+
while (liveGestureActive()) await whenLiveGestureIdle();
|
|
749
|
+
if (cancelled) return;
|
|
750
|
+
const container = containerRef.current;
|
|
751
|
+
const canvasHost = canvasHostRef.current;
|
|
752
|
+
if (!container || !canvasHost) return;
|
|
753
|
+
if (!build || sourcePath === undefined) {
|
|
754
|
+
// `build` content is one Object3D the caller constructs from one file.
|
|
755
|
+
// Both are required by the SDK; validate callers at the runtime boundary.
|
|
756
|
+
setError('This stage shows `build` content and was given no `build()`/`sourcePath`.');
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
let source: ReturnType<NonNullable<typeof build>>;
|
|
760
|
+
try {
|
|
761
|
+
source = retainedState.source ?? build();
|
|
762
|
+
retainedState.source = null;
|
|
763
|
+
} catch (caught) {
|
|
764
|
+
setError(caught instanceof Error ? caught.message : String(caught));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
if (!documentHostRef.current) {
|
|
769
|
+
markViewportConstructStarted();
|
|
770
|
+
documentHostRef.current = new Object3DDocumentHost(
|
|
771
|
+
rendererLane,
|
|
772
|
+
mountViewportSurface(
|
|
773
|
+
canvasHost,
|
|
774
|
+
rendererLane,
|
|
775
|
+
displayName,
|
|
776
|
+
{
|
|
777
|
+
width: Math.max(1, container.clientWidth),
|
|
778
|
+
height: Math.max(1, container.clientHeight),
|
|
779
|
+
},
|
|
780
|
+
() => {
|
|
781
|
+
// A lost context is never returned to the pool. Rebuild this
|
|
782
|
+
// visible attachment from the retained document state.
|
|
783
|
+
setSurfaceAttached(false);
|
|
784
|
+
window.setTimeout(() => {
|
|
785
|
+
if (activeRef.current) setSurfaceAttached(true);
|
|
786
|
+
}, 0);
|
|
787
|
+
},
|
|
788
|
+
),
|
|
789
|
+
retainedState.store,
|
|
790
|
+
);
|
|
791
|
+
documentHostRef.current.contentSeconds = retainedState.contentSeconds;
|
|
792
|
+
documentHostRef.current.transportAdvances = retainedState.transportAdvances;
|
|
793
|
+
}
|
|
794
|
+
} catch (caught) {
|
|
795
|
+
source.dispose();
|
|
796
|
+
throw caught;
|
|
797
|
+
}
|
|
798
|
+
const host = documentHostRef.current!;
|
|
799
|
+
const returnSurface = host.borrow();
|
|
800
|
+
const { renderer, canvas, lease } = host.surface;
|
|
801
|
+
const store = host.store;
|
|
802
|
+
// This stage's store, reachable by the shared panels through
|
|
803
|
+
// `focusedStageStore()` (ARCHITECTURE-CORE §One stage unit 4). A
|
|
804
|
+
// CHROMELESS preview is not a stage anything can focus — it has no
|
|
805
|
+
// workspace document — so it registers nothing.
|
|
806
|
+
if (!chromeless) {
|
|
807
|
+
host.cleanups.push(registerStageStore(documentId, store));
|
|
808
|
+
host.cleanups.push(registerStageTransport(documentId, host.transport));
|
|
809
|
+
}
|
|
810
|
+
const history = workspaceHistoryService();
|
|
811
|
+
if (history) store.attachHistory(history);
|
|
812
|
+
const shared = rendererLane === 'inspector-preview';
|
|
813
|
+
const studioStage = studioStageRef.current;
|
|
814
|
+
const hasShell = hasShellRef.current;
|
|
815
|
+
const assetSubject = assetSubjectRef.current;
|
|
816
|
+
const scene = new THREE.Scene();
|
|
817
|
+
// The document's content scene is the HOST's own object here, and the
|
|
818
|
+
// host's `syncHostScene` mirrors its environment fields onto the
|
|
819
|
+
// rendered scene before every draw. So the studio IBL's strength is
|
|
820
|
+
// set on THIS scene, at creation: a value set on the rendered scene, or
|
|
821
|
+
// by the dressing (which dresses the rendered scene), is overwritten
|
|
822
|
+
// each frame by this scene's untouched default of 1 — measured twice,
|
|
823
|
+
// once by the long-tail pass and once by the orchestrator, with zero
|
|
824
|
+
// movement. The value is the dressing's own, fitted with its key light.
|
|
825
|
+
// A document whose adapter authors an intensity sets it after this and
|
|
826
|
+
// wins, as authored lighting always does.
|
|
827
|
+
//
|
|
828
|
+
// `environment: false` sets NEITHER: no map and no strength for it. The
|
|
829
|
+
// strength alone is inert with no map — but the mirror above copies
|
|
830
|
+
// this field onto the rendered scene every draw, so leaving a studio
|
|
831
|
+
// number here would scale ANY environment the document's own adapter
|
|
832
|
+
// later applies (a Blender render's world, which is applied to the
|
|
833
|
+
// presented root's scene) by a value that document opted out of.
|
|
834
|
+
if (dressingEnvironment !== false)
|
|
835
|
+
scene.environmentIntensity = STANDARD_ENVIRONMENT_INTENSITY;
|
|
836
|
+
const sourceParent = source.root.parent;
|
|
837
|
+
scene.add(source.root);
|
|
838
|
+
if (source.animations) source.root.animations = [...source.animations];
|
|
839
|
+
const documentState = { root: source.root, animations: source.root.animations };
|
|
840
|
+
let defaultAdapter: SourceObject3DAuthoringAdapter | null = null;
|
|
841
|
+
let projectAuthoring: ToolObject3DDocumentAuthoring | null = null;
|
|
842
|
+
let activePersistenceSession: Promise<Object3DDocumentPersistenceSession> | null = null;
|
|
843
|
+
let interactionExtension: ReturnType<NonNullable<typeof interaction>['setup']> | null = null;
|
|
844
|
+
let gestureController: Object3DGestureController | null = null;
|
|
845
|
+
let pointerDownListener: ((event: PointerEvent) => void) | null = null;
|
|
846
|
+
let pointerMoveListener: ((event: PointerEvent) => void) | null = null;
|
|
847
|
+
let pointerUpListener: ((event: PointerEvent) => void) | null = null;
|
|
848
|
+
let pointerCancelListener: ((event: PointerEvent) => void) | null = null;
|
|
849
|
+
let escapeListener: ((event: KeyboardEvent) => void) | null = null;
|
|
850
|
+
let activateInteraction: (() => void) | null = null;
|
|
851
|
+
let rollback: (() => void) | null = null;
|
|
852
|
+
let published = false;
|
|
853
|
+
let disposed = false;
|
|
854
|
+
let cleanup: Promise<void> | null = null;
|
|
855
|
+
const binding: DocumentContentBinding = {
|
|
856
|
+
scene,
|
|
857
|
+
settle: () => gestureController?.settle() ?? Promise.resolve(),
|
|
858
|
+
dispose: () => {
|
|
859
|
+
if (cleanup) return cleanup;
|
|
860
|
+
disposed = true;
|
|
861
|
+
if (pointerDownListener)
|
|
862
|
+
container.removeEventListener('pointerdown', pointerDownListener, true);
|
|
863
|
+
if (pointerMoveListener)
|
|
864
|
+
container.removeEventListener('pointermove', pointerMoveListener, true);
|
|
865
|
+
if (pointerUpListener)
|
|
866
|
+
container.removeEventListener('pointerup', pointerUpListener, true);
|
|
867
|
+
if (pointerCancelListener)
|
|
868
|
+
container.removeEventListener('pointercancel', pointerCancelListener, true);
|
|
869
|
+
if (escapeListener) window.removeEventListener('keydown', escapeListener);
|
|
870
|
+
cleanup = (async () => {
|
|
871
|
+
await gestureController?.settle();
|
|
872
|
+
const failures: unknown[] = [];
|
|
873
|
+
for (const release of [
|
|
874
|
+
() => interactionExtension?.dispose(),
|
|
875
|
+
() => projectAuthoring?.dispose?.(),
|
|
876
|
+
() => defaultAdapter?.dispose(),
|
|
877
|
+
() => scene.removeFromParent(),
|
|
878
|
+
() => {
|
|
879
|
+
if (retainSourceOnDisposeRef.current) retainedState.source = source;
|
|
880
|
+
else source.dispose();
|
|
881
|
+
retainSourceOnDisposeRef.current = false;
|
|
882
|
+
},
|
|
883
|
+
returnSurface,
|
|
884
|
+
]) {
|
|
885
|
+
try {
|
|
886
|
+
release();
|
|
887
|
+
} catch (error) {
|
|
888
|
+
failures.push(error);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (failures.length) {
|
|
892
|
+
// biome-ignore lint/suspicious/noConsole: teardown must remain diagnosable
|
|
893
|
+
console.error('Object3D source cleanup failed.', ...failures);
|
|
894
|
+
}
|
|
895
|
+
})();
|
|
896
|
+
return cleanup;
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
try {
|
|
900
|
+
const commitProjectDocument = async (label?: string): Promise<boolean> => {
|
|
901
|
+
try {
|
|
902
|
+
const document = documentState;
|
|
903
|
+
if (documentSource && !persistence) {
|
|
904
|
+
// The whole document is ONE source module: the same seam the
|
|
905
|
+
// animation binding writes through — checksum-guarded whole-file
|
|
906
|
+
// replace, recorded in canonical history — never the resource
|
|
907
|
+
// route, which refuses executable source by rule.
|
|
908
|
+
if (!document)
|
|
909
|
+
throw new Error('Project history is unavailable; the edit was not saved.');
|
|
910
|
+
const history = workspaceHistoryService();
|
|
911
|
+
if (!history) {
|
|
912
|
+
throw new Error('Project history is unavailable; the edit was not saved.');
|
|
913
|
+
}
|
|
914
|
+
const source = documentSource.serialize(document);
|
|
915
|
+
if (source === null) {
|
|
916
|
+
// The document says nothing changed: a real no-change outcome,
|
|
917
|
+
// never a write of the emitted form over a hand-written file.
|
|
918
|
+
setError(null);
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
const changed = await projectWrites().replaceSource(history, {
|
|
922
|
+
file: documentSource.path,
|
|
923
|
+
source,
|
|
924
|
+
label: label ?? documentSource.label ?? `Edit ${displayName}`,
|
|
925
|
+
});
|
|
926
|
+
setError(null);
|
|
927
|
+
return changed;
|
|
928
|
+
}
|
|
929
|
+
if (!persistence) {
|
|
930
|
+
throw new Error('This Asset Lab document has no persistence binding.');
|
|
931
|
+
}
|
|
932
|
+
if (!activePersistenceSession || !document) {
|
|
933
|
+
throw new Error('Project history is unavailable; the edit was not saved.');
|
|
934
|
+
}
|
|
935
|
+
// The pipe's ack for THIS commit: `persisted` is whether a byte moved
|
|
936
|
+
// (a serializer that reproduced the bytes already on disk is a real
|
|
937
|
+
// no-change outcome; a genuine failure throws below).
|
|
938
|
+
const { persisted } = await (await activePersistenceSession).commit(document, label);
|
|
939
|
+
setError(null);
|
|
940
|
+
return persisted;
|
|
941
|
+
} catch (caught) {
|
|
942
|
+
setError(caught instanceof Error ? caught.message : JSON.stringify(caught));
|
|
943
|
+
throw caught;
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
defaultAdapter = new SourceObject3DAuthoringAdapter(store, scene, {
|
|
947
|
+
documentId,
|
|
948
|
+
title: displayName,
|
|
949
|
+
sourcePath,
|
|
950
|
+
documentKind: assetSubject ? 'asset' : 'source',
|
|
951
|
+
audit,
|
|
952
|
+
...(source.hierarchyRoots ? { hierarchyRoots: source.hierarchyRoots } : {}),
|
|
953
|
+
...(modelSourceKind === 'project-file' && modelSourcePath
|
|
954
|
+
? { modelSource: { kind: modelSourceKind, path: modelSourcePath } as const }
|
|
955
|
+
: modelSourceKind === 'entity' && modelSourceEntityId
|
|
956
|
+
? { modelSource: { kind: modelSourceKind, entityId: modelSourceEntityId } as const }
|
|
957
|
+
: {}),
|
|
958
|
+
});
|
|
959
|
+
const authoringContext = {
|
|
960
|
+
documentId,
|
|
961
|
+
sourcePath,
|
|
962
|
+
root: source.root,
|
|
963
|
+
animations: source.root.animations,
|
|
964
|
+
scene,
|
|
965
|
+
defaultAdapter,
|
|
966
|
+
commit: commitProjectDocument,
|
|
967
|
+
};
|
|
968
|
+
projectAuthoring = authoring?.(authoringContext) ?? null;
|
|
969
|
+
if (!projectAuthoring && sourceAuthoring) {
|
|
970
|
+
projectAuthoring = sourceAuthoring({
|
|
971
|
+
store,
|
|
972
|
+
scene,
|
|
973
|
+
...(source.hierarchyRoots ? { hierarchyRoots: source.hierarchyRoots } : {}),
|
|
974
|
+
});
|
|
975
|
+
defaultAdapter.dispose();
|
|
976
|
+
}
|
|
977
|
+
// Project authoring may establish the module-local artifact that its
|
|
978
|
+
// serializers read (a rig document is the concrete example). Open the
|
|
979
|
+
// persistence baseline only after that factory has adopted the root;
|
|
980
|
+
// opening it before authoring made every clean rig mount reject with
|
|
981
|
+
// "Object3D is not a project rig artifact."
|
|
982
|
+
activePersistenceSession =
|
|
983
|
+
persistence && history
|
|
984
|
+
? projectWrites().openPersistence({
|
|
985
|
+
binding: persistence,
|
|
986
|
+
document: documentState,
|
|
987
|
+
history,
|
|
988
|
+
})
|
|
989
|
+
: null;
|
|
990
|
+
if (activePersistenceSession) {
|
|
991
|
+
void activePersistenceSession.catch((caught) => {
|
|
992
|
+
if (!disposed && documentHostRef.current?.content === binding) {
|
|
993
|
+
setError(caught instanceof Error ? caught.message : JSON.stringify(caught));
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
const adapter: AuthoringAdapter = projectAuthoring?.adapter ?? defaultAdapter;
|
|
998
|
+
|
|
999
|
+
// All editor furniture belongs to the persistent scene. Adapters and
|
|
1000
|
+
// project extensions see only this revision's isolated content scene.
|
|
1001
|
+
if (!host.dressing) {
|
|
1002
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
1003
|
+
// The stage's VIEW TRANSFORM. ACES unless the document states its
|
|
1004
|
+
// own — see `ToolViewportDressing.toneMapping`. It is set here, with
|
|
1005
|
+
// the rest of the renderer's fixed state, because it belongs to the
|
|
1006
|
+
// surface rather than to any one material: three compiles the
|
|
1007
|
+
// operator into every `toneMapped` program.
|
|
1008
|
+
renderer.toneMapping = dressingToneMapping ?? THREE.ACESFilmicToneMapping;
|
|
1009
|
+
renderer.shadowMap.enabled = true;
|
|
1010
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
1011
|
+
renderer.info.autoReset = false;
|
|
1012
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1013
|
+
if (studioStage) renderer.setClearColor(0x000000, 0);
|
|
1014
|
+
const environment =
|
|
1015
|
+
dressingEnvironment === false
|
|
1016
|
+
? null
|
|
1017
|
+
: lease && 'environment' in lease
|
|
1018
|
+
? lease.environment()
|
|
1019
|
+
: createStandardEnvironment(renderer);
|
|
1020
|
+
// A palette that carries a viewport background (Blender's flat grey)
|
|
1021
|
+
// paints it in place of the dressing's; the viewport follows theme
|
|
1022
|
+
// changes from there (`EditorViewport`'s look subscription).
|
|
1023
|
+
const look = nativeViewportLook(canvas);
|
|
1024
|
+
host.dressing = applyStandardViewportDressing(host.scene, {
|
|
1025
|
+
environment,
|
|
1026
|
+
background:
|
|
1027
|
+
!studioStage &&
|
|
1028
|
+
background === undefined &&
|
|
1029
|
+
dressingBackground !== false &&
|
|
1030
|
+
look.background === null,
|
|
1031
|
+
keyLight: dressingKeyLight !== false,
|
|
1032
|
+
grid: dressingGrid === true,
|
|
1033
|
+
content: source.root,
|
|
1034
|
+
});
|
|
1035
|
+
if (!studioStage && background !== undefined)
|
|
1036
|
+
host.scene.background = new THREE.Color(background);
|
|
1037
|
+
else if (!studioStage && look.background !== null)
|
|
1038
|
+
host.scene.background = new THREE.Color(look.background);
|
|
1039
|
+
host.defaultEnvironment = host.scene.environment;
|
|
1040
|
+
host.defaultBackground = host.scene.background;
|
|
1041
|
+
host.cleanups.push(() => {
|
|
1042
|
+
if (
|
|
1043
|
+
host.defaultBackground instanceof THREE.Texture &&
|
|
1044
|
+
host.defaultBackground !== host.dressing?.backgroundTexture
|
|
1045
|
+
)
|
|
1046
|
+
host.defaultBackground.dispose();
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
const pick = (clientX: number, clientY: number): string | null => {
|
|
1050
|
+
const current = host.adapter;
|
|
1051
|
+
const viewport = host.viewport;
|
|
1052
|
+
if (!current || !viewport) return null;
|
|
1053
|
+
const rect = canvas.getBoundingClientRect();
|
|
1054
|
+
const pointer = new THREE.Vector2(
|
|
1055
|
+
((clientX - rect.left) / Math.max(rect.width, 1)) * 2 - 1,
|
|
1056
|
+
-((clientY - rect.top) / Math.max(rect.height, 1)) * 2 + 1,
|
|
1057
|
+
);
|
|
1058
|
+
const raycaster = new THREE.Raycaster();
|
|
1059
|
+
raycaster.layers.enableAll();
|
|
1060
|
+
raycaster.setFromCamera(pointer, host.session?.camera() ?? viewport.camera);
|
|
1061
|
+
for (const hit of raycaster.intersectObjects([...store.objectMap.values()], true)) {
|
|
1062
|
+
if (isInEditorOwnedSubtree(hit.object)) continue;
|
|
1063
|
+
let object: THREE.Object3D | null = hit.object;
|
|
1064
|
+
while (object) {
|
|
1065
|
+
const id = current.hierarchy.idForObject3D?.(object);
|
|
1066
|
+
if (id) return id;
|
|
1067
|
+
object = object.parent;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return null;
|
|
1071
|
+
};
|
|
1072
|
+
if (!host.viewport) {
|
|
1073
|
+
host.viewport = new EditorViewport(canvas, host.scene, store, container, {
|
|
1074
|
+
renderer,
|
|
1075
|
+
authoring: () => host.adapter ?? adapter,
|
|
1076
|
+
pick,
|
|
1077
|
+
publishPickContext: false,
|
|
1078
|
+
onProjectionChange: setProjection,
|
|
1079
|
+
// The same expression that places the DOM furniture, for the one
|
|
1080
|
+
// piece of furniture that is drawn on the canvas instead.
|
|
1081
|
+
chromeInsetPx: chromeless || hasShell ? 0 : STAGE_BLEED_PX,
|
|
1082
|
+
// A document that turns the dressing's key light off has said it
|
|
1083
|
+
// lights itself, and the editor's design-time rig answers exactly
|
|
1084
|
+
// the same question. MEASURED on the Model stage before this
|
|
1085
|
+
// existed: the rig's ambient 0.5 + directional 1.0 sit on
|
|
1086
|
+
// `EDITOR_LAYER`, but three filters lights by the CAMERA's layers
|
|
1087
|
+
// (`WebGLRenderer.projectObject`) and the one camera that draws
|
|
1088
|
+
// this stage enables that layer to see the grid and the gizmo —
|
|
1089
|
+
// so the layer scopes them away from nothing at all and they lit
|
|
1090
|
+
// the content.
|
|
1091
|
+
lightRig: dressingKeyLight !== false,
|
|
1092
|
+
});
|
|
1093
|
+
if (dressingGrid === true) host.viewport.grid.removeFromParent();
|
|
1094
|
+
const open = (event: MouseEvent) => {
|
|
1095
|
+
const id = pick(event.clientX, event.clientY);
|
|
1096
|
+
const object = id ? host.adapter?.hierarchy.object3D?.(id) : null;
|
|
1097
|
+
if (object) onOpenNodeRef.current?.(object);
|
|
1098
|
+
};
|
|
1099
|
+
container.addEventListener('dblclick', open);
|
|
1100
|
+
host.cleanups.push(() => container.removeEventListener('dblclick', open));
|
|
1101
|
+
}
|
|
1102
|
+
const viewport = host.viewport;
|
|
1103
|
+
if (dressingViewLocked) {
|
|
1104
|
+
// VIEW-LOCKED DRESSING. The object turns with the view because it
|
|
1105
|
+
// hangs off the camera, and the camera joins the RENDERED scene for
|
|
1106
|
+
// the same reason: three collects lights while it walks the scene
|
|
1107
|
+
// (`WebGLRenderer.projectObject`), so a light parented to a camera
|
|
1108
|
+
// that is not in the scene is never collected at all. A camera in
|
|
1109
|
+
// the graph draws nothing and picks nothing — it carries no
|
|
1110
|
+
// geometry — and the scene's own `updateMatrixWorld` is then what
|
|
1111
|
+
// carries the view pose down to its children each frame.
|
|
1112
|
+
//
|
|
1113
|
+
// ONE camera is the view pose for this stage: the session's
|
|
1114
|
+
// orthographic camera copies `viewport.camera`'s position and
|
|
1115
|
+
// quaternion before every draw it makes (`object3d-document-
|
|
1116
|
+
// session.ts::syncOrthographicCamera`), so locking to the
|
|
1117
|
+
// perspective camera locks to both projections. A capture with its
|
|
1118
|
+
// OWN camera (a Blender render photographs from the scene's camera)
|
|
1119
|
+
// is deliberately not followed: a render is lit by the scene.
|
|
1120
|
+
const camera = viewport.camera;
|
|
1121
|
+
host.scene.add(camera);
|
|
1122
|
+
camera.add(dressingViewLocked);
|
|
1123
|
+
host.cleanups.push(() => {
|
|
1124
|
+
dressingViewLocked.removeFromParent();
|
|
1125
|
+
camera.removeFromParent();
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
if (interaction) {
|
|
1129
|
+
const overlay = new THREE.Group();
|
|
1130
|
+
overlay.name = '__object3d_document_overlay';
|
|
1131
|
+
setUserData(overlay, 'editorHelper', true);
|
|
1132
|
+
scene.add(overlay);
|
|
1133
|
+
interactionExtension = interaction.setup({
|
|
1134
|
+
root: source.root,
|
|
1135
|
+
scene,
|
|
1136
|
+
renderer,
|
|
1137
|
+
overlay,
|
|
1138
|
+
camera: () => host.session?.camera() ?? viewport.camera,
|
|
1139
|
+
writable: persistence !== undefined || documentSource !== undefined,
|
|
1140
|
+
});
|
|
1141
|
+
const pointerEvent = (event: PointerEvent) => {
|
|
1142
|
+
const rect = canvas.getBoundingClientRect();
|
|
1143
|
+
const pointer = new THREE.Vector2(
|
|
1144
|
+
((event.clientX - rect.left) / Math.max(rect.width, 1)) * 2 - 1,
|
|
1145
|
+
-((event.clientY - rect.top) / Math.max(rect.height, 1)) * 2 + 1,
|
|
1146
|
+
);
|
|
1147
|
+
const raycaster = new THREE.Raycaster();
|
|
1148
|
+
raycaster.layers.enableAll();
|
|
1149
|
+
raycaster.setFromCamera(pointer, host.session?.camera() ?? viewport.camera);
|
|
1150
|
+
return {
|
|
1151
|
+
pointerId: event.pointerId,
|
|
1152
|
+
button: event.button,
|
|
1153
|
+
buttons: event.buttons,
|
|
1154
|
+
clientX: event.clientX,
|
|
1155
|
+
clientY: event.clientY,
|
|
1156
|
+
altKey: event.altKey,
|
|
1157
|
+
ctrlKey: event.ctrlKey,
|
|
1158
|
+
metaKey: event.metaKey,
|
|
1159
|
+
shiftKey: event.shiftKey,
|
|
1160
|
+
ray: raycaster.ray.clone(),
|
|
1161
|
+
hits: raycaster.intersectObjects([source!.root, overlay], true),
|
|
1162
|
+
};
|
|
1163
|
+
};
|
|
1164
|
+
gestureController = new Object3DGestureController({
|
|
1165
|
+
begin: (event) => {
|
|
1166
|
+
if (!persistence && !documentSource) {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
'This Asset Lab document has no persistence binding; project gestures are read-only.',
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
return interactionExtension!.begin(event);
|
|
1172
|
+
},
|
|
1173
|
+
persist: async (label) => {
|
|
1174
|
+
await commitProjectDocument(label);
|
|
1175
|
+
},
|
|
1176
|
+
reportError: (caught) =>
|
|
1177
|
+
setError(caught instanceof Error ? caught.message : JSON.stringify(caught)),
|
|
1178
|
+
});
|
|
1179
|
+
const claim = (event: PointerEvent) => {
|
|
1180
|
+
event.preventDefault();
|
|
1181
|
+
event.stopPropagation();
|
|
1182
|
+
};
|
|
1183
|
+
pointerDownListener = (event) => {
|
|
1184
|
+
// A control painted OVER the stage owns its own press. These
|
|
1185
|
+
// listeners sit on the CONTAINER in the capture phase, so they run
|
|
1186
|
+
// before that control's own handler and a `stopPropagation` there
|
|
1187
|
+
// would be too late: pressing the rolled-back-operation notice's
|
|
1188
|
+
// Dismiss also started a box-select on the mesh (measured — the
|
|
1189
|
+
// notice's text changed from the failed op to "Box select").
|
|
1190
|
+
// The viewport's navigation cluster (`ViewportFurniture`) is the
|
|
1191
|
+
// same case and was missing here: its Pan button drives the camera
|
|
1192
|
+
// from `onPointerDown`, which React dispatches at the root, so
|
|
1193
|
+
// claiming the press here meant `startPan` never ran and the
|
|
1194
|
+
// control was inert (measured — the camera floats were identical
|
|
1195
|
+
// before and after a full pointer gesture, while the cluster's
|
|
1196
|
+
// `onClick` buttons beside it moved the camera every time).
|
|
1197
|
+
if (
|
|
1198
|
+
event.target instanceof Element &&
|
|
1199
|
+
event.target.closest('[role="alert"], [role="toolbar"]') !== null
|
|
1200
|
+
) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (!gestureController?.begin(pointerEvent(event))) return;
|
|
1204
|
+
claim(event);
|
|
1205
|
+
// DOM-only editor automation dispatches honest untrusted pointer
|
|
1206
|
+
// events, which have no browser-owned active pointer and therefore
|
|
1207
|
+
// cannot be captured. Hardware pointers still take capture so a
|
|
1208
|
+
// gesture can leave the viewport without getting stranded.
|
|
1209
|
+
if (event.isTrusted) canvas.setPointerCapture?.(event.pointerId);
|
|
1210
|
+
};
|
|
1211
|
+
pointerMoveListener = (event) => {
|
|
1212
|
+
if (!gestureController?.hasActiveGesture()) return;
|
|
1213
|
+
claim(event);
|
|
1214
|
+
gestureController.update(pointerEvent(event));
|
|
1215
|
+
};
|
|
1216
|
+
pointerUpListener = (event) => {
|
|
1217
|
+
if (!gestureController?.hasActiveGesture()) return;
|
|
1218
|
+
claim(event);
|
|
1219
|
+
if (canvas.hasPointerCapture?.(event.pointerId)) {
|
|
1220
|
+
canvas.releasePointerCapture?.(event.pointerId);
|
|
1221
|
+
}
|
|
1222
|
+
void gestureController.commit(pointerEvent(event));
|
|
1223
|
+
};
|
|
1224
|
+
pointerCancelListener = (event) => {
|
|
1225
|
+
if (!gestureController?.hasActiveGesture()) return;
|
|
1226
|
+
claim(event);
|
|
1227
|
+
void gestureController.cancel(event.pointerId);
|
|
1228
|
+
};
|
|
1229
|
+
escapeListener = (event) => {
|
|
1230
|
+
if (event.key !== 'Escape' || !gestureController?.hasActiveGesture()) return;
|
|
1231
|
+
event.preventDefault();
|
|
1232
|
+
void gestureController.cancel();
|
|
1233
|
+
};
|
|
1234
|
+
activateInteraction = () => {
|
|
1235
|
+
container.addEventListener('pointerdown', pointerDownListener!, true);
|
|
1236
|
+
container.addEventListener('pointermove', pointerMoveListener!, true);
|
|
1237
|
+
container.addEventListener('pointerup', pointerUpListener!, true);
|
|
1238
|
+
container.addEventListener('pointercancel', pointerCancelListener!, true);
|
|
1239
|
+
window.addEventListener('keydown', escapeListener!);
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
// Candidate construction has succeeded. Publish all source references in
|
|
1243
|
+
// one synchronous turn; no frame or input event can see a half-swap.
|
|
1244
|
+
const previous = host.content;
|
|
1245
|
+
const selected = [...store.selectedEntityIds];
|
|
1246
|
+
const previousAdapter = host.adapter;
|
|
1247
|
+
const previousObjects = new Map(store.objectMap);
|
|
1248
|
+
const previousRoot = host.session?.root;
|
|
1249
|
+
const previousFrame = host.frame;
|
|
1250
|
+
const previousSyncHostScene = host.syncHostScene;
|
|
1251
|
+
const previousDocument = documentStateRef.current;
|
|
1252
|
+
const previousPersistence = persistenceSessionRef.current;
|
|
1253
|
+
const previousInteraction = interactionExtensionRef.current;
|
|
1254
|
+
const previousBackground = host.session?.neutralBackgroundTexture();
|
|
1255
|
+
const previousLights = host.dressing.lights.map((light) => light.visible);
|
|
1256
|
+
const cleanupCount = host.cleanups.length;
|
|
1257
|
+
const wasInitialized = host.initialized;
|
|
1258
|
+
rollback = () => {
|
|
1259
|
+
scene.removeFromParent();
|
|
1260
|
+
if (sourceParent) sourceParent.add(source.root);
|
|
1261
|
+
if (previous) host.scene.add(previous.scene);
|
|
1262
|
+
host.adapter = previousAdapter;
|
|
1263
|
+
host.content = previous;
|
|
1264
|
+
host.frame = previousFrame;
|
|
1265
|
+
host.syncHostScene = previousSyncHostScene;
|
|
1266
|
+
host.initialized = wasInitialized;
|
|
1267
|
+
for (const cleanup of host.cleanups.splice(cleanupCount)) cleanup();
|
|
1268
|
+
if (!previousRoot && host.session) {
|
|
1269
|
+
host.session.dispose();
|
|
1270
|
+
host.session = null;
|
|
1271
|
+
}
|
|
1272
|
+
store.objectMap.clear();
|
|
1273
|
+
for (const [id, object] of previousObjects) store.objectMap.set(id, object);
|
|
1274
|
+
if (previous)
|
|
1275
|
+
store.bindScene(previous.scene, renderer, viewport.batchedRenderer, viewport.camera);
|
|
1276
|
+
if (previousRoot && previousAdapter)
|
|
1277
|
+
host.session?.replaceContent(previousRoot, previousAdapter);
|
|
1278
|
+
documentStateRef.current = previousDocument;
|
|
1279
|
+
persistenceSessionRef.current = previousPersistence;
|
|
1280
|
+
interactionExtensionRef.current = previousInteraction;
|
|
1281
|
+
documentAdapterRef.current = previousAdapter;
|
|
1282
|
+
setDocumentAdapter(previousAdapter);
|
|
1283
|
+
if (previousBackground !== undefined)
|
|
1284
|
+
host.session?.setNeutralBackground(previousBackground);
|
|
1285
|
+
host.dressing?.lights.forEach((light, index) => {
|
|
1286
|
+
light.visible = previousLights[index] ?? true;
|
|
1287
|
+
});
|
|
1288
|
+
if (previousRoot) host.dressing?.frameContent(previousRoot);
|
|
1289
|
+
store.selectMultiple(selected);
|
|
1290
|
+
store.notifyIngestObjectMapEdit();
|
|
1291
|
+
};
|
|
1292
|
+
previous?.scene.removeFromParent();
|
|
1293
|
+
host.scene.add(scene);
|
|
1294
|
+
host.adapter = adapter;
|
|
1295
|
+
host.content = binding;
|
|
1296
|
+
store.bindScene(scene, renderer, viewport.batchedRenderer, viewport.camera);
|
|
1297
|
+
store.setOrbitTarget(viewport.orbitControls.target);
|
|
1298
|
+
store.objectMap.clear();
|
|
1299
|
+
scene.traverse((object) => {
|
|
1300
|
+
const id = adapter.hierarchy.idForObject3D?.(object);
|
|
1301
|
+
if (id) store.objectMap.set(id, object);
|
|
1302
|
+
});
|
|
1303
|
+
if (!host.session) {
|
|
1304
|
+
const overviewFrame = boxFromFrameBounds(frameBoundsRef.current);
|
|
1305
|
+
const openingFrame = boxFromFrameBounds(openingFrameBoundsRef.current) ?? overviewFrame;
|
|
1306
|
+
if (!openingFrame) viewport.focusOn(source.root);
|
|
1307
|
+
if (cameraX !== undefined && cameraY !== undefined && cameraZ !== undefined) {
|
|
1308
|
+
const box = openingFrame ?? contentWorldBounds(source.root);
|
|
1309
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
1310
|
+
const direction = new THREE.Vector3(cameraX, cameraY, cameraZ).normalize();
|
|
1311
|
+
const distance =
|
|
1312
|
+
perspectiveDistanceToFitBox(box, viewport.camera, direction) *
|
|
1313
|
+
Math.min(10, Math.max(0.1, openingFit ?? 1));
|
|
1314
|
+
viewport.camera.position.copy(center).add(direction.multiplyScalar(distance));
|
|
1315
|
+
viewport.orbitControls.target.copy(center);
|
|
1316
|
+
}
|
|
1317
|
+
host.session = new Object3DDocumentSession(
|
|
1318
|
+
documentId,
|
|
1319
|
+
source.root,
|
|
1320
|
+
host.scene,
|
|
1321
|
+
renderer,
|
|
1322
|
+
viewport,
|
|
1323
|
+
host.dressing.lights,
|
|
1324
|
+
adapter,
|
|
1325
|
+
studioStage
|
|
1326
|
+
? null
|
|
1327
|
+
: background === undefined
|
|
1328
|
+
? host.dressing.backgroundTexture
|
|
1329
|
+
: new THREE.Color(background),
|
|
1330
|
+
overviewFrame,
|
|
1331
|
+
);
|
|
1332
|
+
host.session.selectionOutlineEnabled = !shared && selectionOutlineRef.current;
|
|
1333
|
+
if (!studioStage && background === undefined && host.dressing.backgroundTexture) {
|
|
1334
|
+
const session = host.session;
|
|
1335
|
+
host.cleanups.push(
|
|
1336
|
+
watchPaletteBackdrop(() => {
|
|
1337
|
+
// A palette that names a flat viewport background owns the
|
|
1338
|
+
// backdrop; the gradient is for palettes that name none.
|
|
1339
|
+
const flat = nativeViewportLook(canvas).background;
|
|
1340
|
+
if (flat !== null) {
|
|
1341
|
+
host.scene.background = new THREE.Color(flat);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const previous = host.defaultBackground;
|
|
1345
|
+
host.defaultBackground = createGradientBackgroundTexture();
|
|
1346
|
+
if (session.neutralBackgroundTexture() === previous)
|
|
1347
|
+
session.setNeutralBackground(host.defaultBackground);
|
|
1348
|
+
if (
|
|
1349
|
+
previous instanceof THREE.Texture &&
|
|
1350
|
+
previous !== host.dressing?.backgroundTexture
|
|
1351
|
+
)
|
|
1352
|
+
previous.dispose();
|
|
1353
|
+
}),
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
} else host.session.replaceContent(source.root, adapter);
|
|
1357
|
+
const retainedCamera = retainedState.camera;
|
|
1358
|
+
if (retainedCamera) {
|
|
1359
|
+
viewport.setProjection(projection);
|
|
1360
|
+
viewport.setPose(
|
|
1361
|
+
retainedCamera.position,
|
|
1362
|
+
retainedCamera.target,
|
|
1363
|
+
retainedCamera.fov || undefined,
|
|
1364
|
+
);
|
|
1365
|
+
retainedState.camera = null;
|
|
1366
|
+
}
|
|
1367
|
+
const documentSession = host.session;
|
|
1368
|
+
documentStateRef.current = documentState;
|
|
1369
|
+
persistenceSessionRef.current = activePersistenceSession;
|
|
1370
|
+
interactionExtensionRef.current = interactionExtension;
|
|
1371
|
+
documentAdapterRef.current = adapter;
|
|
1372
|
+
setDocumentAdapter(adapter);
|
|
1373
|
+
let sourceHasLight = false;
|
|
1374
|
+
source.root.traverse((object) => {
|
|
1375
|
+
if ((object as THREE.Light).isLight) sourceHasLight = true;
|
|
1376
|
+
});
|
|
1377
|
+
for (const light of host.dressing.lights) light.visible = !sourceHasLight;
|
|
1378
|
+
host.dressing.frameContent(source.root);
|
|
1379
|
+
store.selectMultiple(selected.filter((id) => store.objectMap.has(id)));
|
|
1380
|
+
store.notifyIngestObjectMapEdit();
|
|
1381
|
+
documentSession.syncSelectionPresentation();
|
|
1382
|
+
activateInteraction?.();
|
|
1383
|
+
setError(null);
|
|
1384
|
+
if (!chromeless) notifyWorkspaceDocumentSelectionChanged(documentId);
|
|
1385
|
+
let nativeBackground = scene.background;
|
|
1386
|
+
documentSession.setNeutralBackground(nativeBackground ?? host.defaultBackground);
|
|
1387
|
+
const retainedPresentation = retainedState.presentation;
|
|
1388
|
+
if (retainedPresentation) {
|
|
1389
|
+
documentSession.setMode(retainedPresentation.mode);
|
|
1390
|
+
documentSession.setGrid(retainedPresentation.grid);
|
|
1391
|
+
documentSession.setBackground(retainedPresentation.background);
|
|
1392
|
+
documentSession.setExposure(retainedPresentation.exposure);
|
|
1393
|
+
documentSession.setProjection(retainedPresentation.projection);
|
|
1394
|
+
documentSession.setLighting(retainedPresentation.lighting);
|
|
1395
|
+
documentSession.setSkeleton(retainedPresentation.skeleton);
|
|
1396
|
+
documentSession.setBounds(retainedPresentation.bounds);
|
|
1397
|
+
retainedState.presentation = null;
|
|
1398
|
+
}
|
|
1399
|
+
// The document's world dressing lives on its OWN scene, nested inside the
|
|
1400
|
+
// rendered `host.scene`. Every draw of the rendered scene mirrors it out
|
|
1401
|
+
// first — the viewport frame and every offscreen photograph alike — so a
|
|
1402
|
+
// capture can never show the dressing the last viewport tick happened to
|
|
1403
|
+
// leave behind. The session runs this before it renders; nothing else may
|
|
1404
|
+
// hold a second copy of this field list.
|
|
1405
|
+
const syncHostScene = () => {
|
|
1406
|
+
host.scene.environment = scene.environment ?? host.defaultEnvironment;
|
|
1407
|
+
host.scene.fog = scene.fog;
|
|
1408
|
+
host.scene.environmentIntensity = scene.environmentIntensity;
|
|
1409
|
+
host.scene.environmentRotation.copy(scene.environmentRotation);
|
|
1410
|
+
host.scene.backgroundIntensity = scene.backgroundIntensity;
|
|
1411
|
+
host.scene.backgroundBlurriness = scene.backgroundBlurriness;
|
|
1412
|
+
host.scene.backgroundRotation.copy(scene.backgroundRotation);
|
|
1413
|
+
if (nativeBackground !== scene.background) {
|
|
1414
|
+
nativeBackground = scene.background;
|
|
1415
|
+
documentSession.setNeutralBackground(nativeBackground ?? host.defaultBackground);
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
host.syncHostScene = syncHostScene;
|
|
1419
|
+
documentSession.setBeforeRender(() => host.syncHostScene?.());
|
|
1420
|
+
// This loop draws the compass and every other overlay pass over the
|
|
1421
|
+
// document's own render, so it — and only it — can serve the chrome
|
|
1422
|
+
// door a frame that matches the screen.
|
|
1423
|
+
documentSession.setPresentsFrames(true);
|
|
1424
|
+
// THE CONTENT-TIME DOOR, re-created in Step 3 with exactly one caller
|
|
1425
|
+
// (the transport's `tick` below). The mixer line the deleted estate
|
|
1426
|
+
// had here is gone for good: subjects seek THEMSELVES, so content time
|
|
1427
|
+
// is only ever the bookkeeping plus the source's own update.
|
|
1428
|
+
const advanceContent = (deltaSeconds: number): void => {
|
|
1429
|
+
if (disposed || !(deltaSeconds > 0) || !Number.isFinite(deltaSeconds)) return;
|
|
1430
|
+
host.contentSeconds += deltaSeconds;
|
|
1431
|
+
host.transportAdvances++;
|
|
1432
|
+
source.update?.(deltaSeconds);
|
|
1433
|
+
};
|
|
1434
|
+
// What this stage can show at a time. Scanned once here; the world
|
|
1435
|
+
// root's stage rescans (below) because its tree arrives later.
|
|
1436
|
+
const clipScan = scanClipSubjects(source.root, host.transport);
|
|
1437
|
+
host.cleanups.push(() => clipScan.dispose());
|
|
1438
|
+
const retainedTransport = retainedState.transport;
|
|
1439
|
+
if (retainedTransport) {
|
|
1440
|
+
if (retainedTransport.activeSubject)
|
|
1441
|
+
host.transport.setActiveSubject(retainedTransport.activeSubject);
|
|
1442
|
+
host.transport.setLoop(retainedTransport.range.loop);
|
|
1443
|
+
host.transport.setTimeScale(retainedTransport.timeScale);
|
|
1444
|
+
host.transport.seek(retainedTransport.time);
|
|
1445
|
+
if (retainedTransport.playbackState === 'playing') host.transport.play();
|
|
1446
|
+
else if (retainedTransport.playbackState === 'paused') host.transport.pause();
|
|
1447
|
+
retainedState.transport = null;
|
|
1448
|
+
}
|
|
1449
|
+
let previousTime = performance.now();
|
|
1450
|
+
const animate = (time: number, resumed: boolean) => {
|
|
1451
|
+
if (disposed || !renderer || !viewport) return;
|
|
1452
|
+
const activeRenderer = renderer;
|
|
1453
|
+
const activeViewport = viewport;
|
|
1454
|
+
const profiler = documentSession?.profiler;
|
|
1455
|
+
profiler?.beginFrame();
|
|
1456
|
+
if (resumed) previousTime = time;
|
|
1457
|
+
const delta = Math.min((time - previousTime) / 1000, 0.1);
|
|
1458
|
+
previousTime = time;
|
|
1459
|
+
profiler?.beginPhase();
|
|
1460
|
+
// CONTENT TIME moves ONLY through the stage transport, and only
|
|
1461
|
+
// while a human is holding it: `tick` is inert unless playing and
|
|
1462
|
+
// the editor drives, and it returns exactly the seconds it advanced.
|
|
1463
|
+
const advanced = host.transport.tick(delta);
|
|
1464
|
+
if (advanced > 0) advanceContent(advanced);
|
|
1465
|
+
interactionExtension?.prepareFrame?.(delta);
|
|
1466
|
+
interactionExtension?.update?.(delta);
|
|
1467
|
+
profiler?.endPhase('animation');
|
|
1468
|
+
profiler?.beginPhase();
|
|
1469
|
+
host.presence?.syncMarkers(delta);
|
|
1470
|
+
viewport.update(delta);
|
|
1471
|
+
// This stage's frame hook. After viewport.update, the same order the
|
|
1472
|
+
// scene panel uses, so anything riding the loop (a camera flight)
|
|
1473
|
+
// has final say over the pose and orbit damping never fights it.
|
|
1474
|
+
if (!chromeless) runViewportFrame(documentId, delta);
|
|
1475
|
+
viewport.batchedRenderer.update(delta);
|
|
1476
|
+
profiler?.endPhase('editor');
|
|
1477
|
+
profiler?.beginPhase();
|
|
1478
|
+
activeRenderer.info.reset();
|
|
1479
|
+
const timeFirstRender =
|
|
1480
|
+
!firstFrameGateRef.current && activeViewportBreakdownDocumentId() === documentId;
|
|
1481
|
+
if (timeFirstRender) markViewportFirstRenderStart(documentId);
|
|
1482
|
+
const tFirstRender = timeFirstRender ? Date.now() : 0;
|
|
1483
|
+
const presentationOverride = host.scene.overrideMaterial;
|
|
1484
|
+
const mode = documentSession.presentation().mode;
|
|
1485
|
+
if (mode !== 'uv' && mode !== 'vertex-colors')
|
|
1486
|
+
host.scene.overrideMaterial = scene.overrideMaterial;
|
|
1487
|
+
try {
|
|
1488
|
+
activeViewport.renderWithInfrastructure(() => {
|
|
1489
|
+
documentSession.renderViewport(delta);
|
|
1490
|
+
activeViewport.renderViewCube(activeRenderer);
|
|
1491
|
+
});
|
|
1492
|
+
} finally {
|
|
1493
|
+
host.scene.overrideMaterial = presentationOverride;
|
|
1494
|
+
}
|
|
1495
|
+
// Inside the frame that drew them — the canvas has no
|
|
1496
|
+
// preserveDrawingBuffer, so this is the only moment its pixels exist.
|
|
1497
|
+
documentSession.servePresentedFrame();
|
|
1498
|
+
if (!firstFrameGateRef.current) {
|
|
1499
|
+
firstFrameGateRef.current = true;
|
|
1500
|
+
if (timeFirstRender) markViewportSegment('first-render', Date.now() - tFirstRender);
|
|
1501
|
+
recordViewportFirstFrame(documentId);
|
|
1502
|
+
setSurfaceStatus('ready');
|
|
1503
|
+
}
|
|
1504
|
+
profiler?.reportRender({
|
|
1505
|
+
gpuMs: null,
|
|
1506
|
+
drawCalls: activeRenderer.info.render.calls,
|
|
1507
|
+
triangles: activeRenderer.info.render.triangles,
|
|
1508
|
+
geometries: activeRenderer.info.memory.geometries,
|
|
1509
|
+
textures: activeRenderer.info.memory.textures,
|
|
1510
|
+
});
|
|
1511
|
+
profiler?.endPhase('render');
|
|
1512
|
+
profiler?.endFrame();
|
|
1513
|
+
// THE SHELL'S READOUT IS THE FOCUSED STAGE'S (ARCHITECTURE-CORE
|
|
1514
|
+
// §One stage unit 4). `CameraInfo` and `StatsOverlay` read one
|
|
1515
|
+
// struct the shell owns; the stage the panels are following fills
|
|
1516
|
+
// it, so a model or prefab document reports ITS frame and ITS
|
|
1517
|
+
// camera. Inactive documents leave the shared readout alone.
|
|
1518
|
+
const shellReadout = shellStatsRef.current;
|
|
1519
|
+
if (shellReadout && shellStore && focusedStageStore(shellStore) === store) {
|
|
1520
|
+
const pose = documentSession.cameraPose();
|
|
1521
|
+
shellReadout.frameTime = delta * 1000;
|
|
1522
|
+
shellReadout.fps = delta > 0 ? 1 / delta : 0;
|
|
1523
|
+
shellReadout.drawCalls = activeRenderer.info.render.calls;
|
|
1524
|
+
shellReadout.triangles = activeRenderer.info.render.triangles;
|
|
1525
|
+
shellReadout.cameraPosition.x = pose.position[0];
|
|
1526
|
+
shellReadout.cameraPosition.y = pose.position[1];
|
|
1527
|
+
shellReadout.cameraPosition.z = pose.position[2];
|
|
1528
|
+
shellReadout.cameraTarget.x = pose.target[0];
|
|
1529
|
+
shellReadout.cameraTarget.y = pose.target[1];
|
|
1530
|
+
shellReadout.cameraTarget.z = pose.target[2];
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
host.frame = animate;
|
|
1535
|
+
rendererSessionRef.current = host.rendererSession;
|
|
1536
|
+
if (!host.initialized) {
|
|
1537
|
+
if (!chromeless) {
|
|
1538
|
+
// --- The viewport door (viewport-door.ts) ---
|
|
1539
|
+
// This host is ONE STAGE among the mounted 3D documents
|
|
1540
|
+
// (ARCHITECTURE-CORE §One stage), and it binds under the id of the
|
|
1541
|
+
// document it draws, so an SDK reader reaches THIS stage's rig,
|
|
1542
|
+
// helper sink and frame loop through `host.viewport.stages()`.
|
|
1543
|
+
// It presents no live roots yet — Play's adoption is the world
|
|
1544
|
+
// root's, and moves onto this host in unit 3 — so the presenter
|
|
1545
|
+
// declines every root rather than pretending to a subject.
|
|
1546
|
+
// A CHROMELESS mount (the inspector's object preview) binds
|
|
1547
|
+
// nothing: it is a thumbnail of a document, not a stage of its own,
|
|
1548
|
+
// and several can be alive for one document id at once.
|
|
1549
|
+
host.cleanups.push(
|
|
1550
|
+
bindViewportRig(
|
|
1551
|
+
{
|
|
1552
|
+
camera: viewport.camera,
|
|
1553
|
+
orbit: viewport.orbitControls,
|
|
1554
|
+
scene: host.scene,
|
|
1555
|
+
},
|
|
1556
|
+
() => null,
|
|
1557
|
+
(kind, object) => viewport.setHelper(kind, object),
|
|
1558
|
+
{ documentId },
|
|
1559
|
+
),
|
|
1560
|
+
);
|
|
1561
|
+
// --- The other participants, on THIS stage ---
|
|
1562
|
+
// Presence was the scene panel's, so only the world root had it.
|
|
1563
|
+
// It is a capability with a condition — a stage painting a three
|
|
1564
|
+
// surface — and a `build` stage always paints one, so every
|
|
1565
|
+
// document with chrome mounts it and a prefab story shows the
|
|
1566
|
+
// same camera frusta, selection boxes and pointer rays the Scene
|
|
1567
|
+
// does (ARCHITECTURE-CORE §One stage; WORK.md §Presence and the
|
|
1568
|
+
// substrate, presence unit 4). Dynamically imported: it reaches
|
|
1569
|
+
// the collaboration client, which a bounded host must not pay for.
|
|
1570
|
+
void import('./stage-presence-markers').then((markers) => {
|
|
1571
|
+
if (host.isClosed || documentHostRef.current !== host || host.presence) return;
|
|
1572
|
+
const binding = markers.bindStagePresenceMarkers({
|
|
1573
|
+
// The HOST's scene, not this revision's content scene: a
|
|
1574
|
+
// source update swaps the content and the markers must not go
|
|
1575
|
+
// with it.
|
|
1576
|
+
scene: host.scene,
|
|
1577
|
+
store,
|
|
1578
|
+
documentId,
|
|
1579
|
+
container,
|
|
1580
|
+
canvas,
|
|
1581
|
+
viewport,
|
|
1582
|
+
readVisibleCameraPose: () => {
|
|
1583
|
+
const camera = host.session?.camera() ?? viewport.renderCamera;
|
|
1584
|
+
const position = camera.getWorldPosition(new THREE.Vector3());
|
|
1585
|
+
const fov = (camera as THREE.PerspectiveCamera).fov;
|
|
1586
|
+
return {
|
|
1587
|
+
position,
|
|
1588
|
+
target: viewport.orbitControls.target,
|
|
1589
|
+
fov: typeof fov === 'number' ? fov : 0,
|
|
1590
|
+
};
|
|
1591
|
+
},
|
|
1592
|
+
});
|
|
1593
|
+
host.presence = binding;
|
|
1594
|
+
host.cleanups.push(() => binding.dispose());
|
|
1595
|
+
});
|
|
1596
|
+
host.cleanups.push(registerObject3DDocumentSession(documentSession));
|
|
1597
|
+
host.cleanups.push(
|
|
1598
|
+
registerPerformanceSource({
|
|
1599
|
+
id: documentId,
|
|
1600
|
+
label: displayName,
|
|
1601
|
+
kind: assetSubject ? 'asset' : 'source',
|
|
1602
|
+
profiler: documentSession.profiler,
|
|
1603
|
+
}),
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
// EXACTLY ONE registrar of this document's workspace selection: the
|
|
1607
|
+
// identity-row shell does it through its own `selection` prop, so
|
|
1608
|
+
// the host does it wherever that shell is absent. The condition was
|
|
1609
|
+
// `!studioStage` while the shell and the studio were the same
|
|
1610
|
+
// answer; they parted when a DATA subject gained the studio
|
|
1611
|
+
// (§A model is data), and reading the old one left the Model
|
|
1612
|
+
// document with NEITHER registrar — the Hierarchy said "No
|
|
1613
|
+
// authoring adapter" and the mesh inspector emptied (measured live
|
|
1614
|
+
// on the models scaffold, 2026-09-18).
|
|
1615
|
+
if (!chromeless && !hasShell) {
|
|
1616
|
+
host.cleanups.push(
|
|
1617
|
+
registerWorkspaceDocumentSelection(documentId, () => ({
|
|
1618
|
+
adapter: host.adapter!,
|
|
1619
|
+
nodeId: host.adapter?.selection?.get()[0] ?? null,
|
|
1620
|
+
})),
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
const designTimeSurfaceId = chromeless
|
|
1624
|
+
? `${documentId}#chromeless-${++chromelessSurfaceSequence}`
|
|
1625
|
+
: documentId;
|
|
1626
|
+
host.cleanups.push(
|
|
1627
|
+
registerDesignTimeSurface({
|
|
1628
|
+
id: designTimeSurfaceId,
|
|
1629
|
+
label: displayName,
|
|
1630
|
+
contentClock: () => host.contentSeconds,
|
|
1631
|
+
transportAdvances: () => host.transportAdvances,
|
|
1632
|
+
}),
|
|
1633
|
+
);
|
|
1634
|
+
// THE VIEWPORT ACTION BUS, on a document stage. The world root has
|
|
1635
|
+
// answered these since the scene panel existed; a document stage
|
|
1636
|
+
// never did, which is why Frame (`F`) was inert on a prefab or a
|
|
1637
|
+
// model — the hotkey pushes `focus-selection` onto the stage's own
|
|
1638
|
+
// store and nothing was listening (ARCHITECTURE-CORE §One stage:
|
|
1639
|
+
// the capability is present wherever its condition holds).
|
|
1640
|
+
host.cleanups.push(
|
|
1641
|
+
store.onViewportAction((action) => {
|
|
1642
|
+
switch (action.type) {
|
|
1643
|
+
case 'focus-selection':
|
|
1644
|
+
case 'focus-scene':
|
|
1645
|
+
// The session frames the selection, else the whole subject,
|
|
1646
|
+
// and says so on the console when there is nothing to frame.
|
|
1647
|
+
if (!host.session?.frame() && host.session?.root)
|
|
1648
|
+
viewport.focusOn(host.session.root);
|
|
1649
|
+
break;
|
|
1650
|
+
case 'focus-entity': {
|
|
1651
|
+
const object = host.adapter?.hierarchy.object3D?.(action.id) ?? null;
|
|
1652
|
+
if (object) viewport.focusOn(object);
|
|
1653
|
+
break;
|
|
1654
|
+
}
|
|
1655
|
+
case 'snap-selection-to-floor':
|
|
1656
|
+
viewport.snapSelectionToFloor();
|
|
1657
|
+
break;
|
|
1658
|
+
case 'set-view-preset':
|
|
1659
|
+
viewport.setViewPreset(action.preset);
|
|
1660
|
+
break;
|
|
1661
|
+
case 'set-camera-pose':
|
|
1662
|
+
viewport.setPose(action.position, action.target, action.fov);
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1665
|
+
}),
|
|
1666
|
+
);
|
|
1667
|
+
let selectionSignature = '';
|
|
1668
|
+
host.cleanups.push(
|
|
1669
|
+
store.subscribe(() => {
|
|
1670
|
+
viewport.objectMap = store.objectMap;
|
|
1671
|
+
viewport.syncFromStore();
|
|
1672
|
+
host.session?.syncSelectionPresentation();
|
|
1673
|
+
const next = (host.adapter?.selection?.get() ?? []).join('\u0000');
|
|
1674
|
+
if (next !== selectionSignature) {
|
|
1675
|
+
selectionSignature = next;
|
|
1676
|
+
if (!chromeless) notifyWorkspaceDocumentSelectionChanged(documentId);
|
|
1677
|
+
}
|
|
1678
|
+
}),
|
|
1679
|
+
);
|
|
1680
|
+
const resize = () => {
|
|
1681
|
+
const width = Math.max(1, container.clientWidth);
|
|
1682
|
+
const height = Math.max(1, container.clientHeight);
|
|
1683
|
+
const ratio = renderer.getPixelRatio();
|
|
1684
|
+
if (
|
|
1685
|
+
canvas.width !== Math.floor(width * ratio) ||
|
|
1686
|
+
canvas.height !== Math.floor(height * ratio)
|
|
1687
|
+
)
|
|
1688
|
+
renderer.setSize(width, height, false);
|
|
1689
|
+
viewport.resize(width, height);
|
|
1690
|
+
host.session?.resize(width, height);
|
|
1691
|
+
host.rendererSession.redraw();
|
|
1692
|
+
};
|
|
1693
|
+
const observer = new ResizeObserver(resize);
|
|
1694
|
+
observer.observe(container);
|
|
1695
|
+
host.cleanups.push(() => observer.disconnect());
|
|
1696
|
+
resize();
|
|
1697
|
+
host.initialized = true;
|
|
1698
|
+
markViewportConstructReady();
|
|
1699
|
+
}
|
|
1700
|
+
viewport.objectMap = store.objectMap;
|
|
1701
|
+
viewport.syncFromStore();
|
|
1702
|
+
host.rendererSession.setActive(activeRef.current);
|
|
1703
|
+
host.rendererSession.redraw();
|
|
1704
|
+
published = true;
|
|
1705
|
+
// Old graph stays owned until the new binding is fully published.
|
|
1706
|
+
void previous?.dispose();
|
|
1707
|
+
} catch (caught) {
|
|
1708
|
+
if (!published) {
|
|
1709
|
+
try {
|
|
1710
|
+
rollback?.();
|
|
1711
|
+
} finally {
|
|
1712
|
+
if (sourceParent && source.root.parent === scene) sourceParent.add(source.root);
|
|
1713
|
+
void binding.dispose();
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
// Candidate construction leaves the last good binding and pixels live.
|
|
1717
|
+
// biome-ignore lint/suspicious/noConsole: source failure must remain diagnosable
|
|
1718
|
+
console.error(`[object3d document] ${displayName}: source update failed`, caught);
|
|
1719
|
+
setError(caught instanceof Error ? caught.message : String(caught));
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
const update = contentUpdateTail.current.then(install);
|
|
1723
|
+
contentUpdateTail.current = update.catch((caught) => {
|
|
1724
|
+
if (!cancelled) setError(caught instanceof Error ? caught.message : String(caught));
|
|
1725
|
+
});
|
|
1726
|
+
return () => {
|
|
1727
|
+
cancelled = true;
|
|
1728
|
+
};
|
|
1729
|
+
}, [
|
|
1730
|
+
authoring,
|
|
1731
|
+
background,
|
|
1732
|
+
build,
|
|
1733
|
+
cameraX,
|
|
1734
|
+
cameraY,
|
|
1735
|
+
cameraZ,
|
|
1736
|
+
displayName,
|
|
1737
|
+
documentId,
|
|
1738
|
+
studioStage,
|
|
1739
|
+
dressingBackground,
|
|
1740
|
+
dressingEnvironment,
|
|
1741
|
+
dressingGrid,
|
|
1742
|
+
dressingKeyLight,
|
|
1743
|
+
dressingViewLocked,
|
|
1744
|
+
dressingToneMapping,
|
|
1745
|
+
chromeless,
|
|
1746
|
+
modelSourceEntityId,
|
|
1747
|
+
modelSourceKind,
|
|
1748
|
+
modelSourcePath,
|
|
1749
|
+
interaction,
|
|
1750
|
+
persistence,
|
|
1751
|
+
documentSource,
|
|
1752
|
+
rendererLane,
|
|
1753
|
+
retainedState,
|
|
1754
|
+
surfaceAttached,
|
|
1755
|
+
sourceAuthoring,
|
|
1756
|
+
sourcePath,
|
|
1757
|
+
]);
|
|
1758
|
+
|
|
1759
|
+
useEffect(() => {
|
|
1760
|
+
const session = rendererSessionRef.current;
|
|
1761
|
+
const becameActive = active && !previousActiveRef.current;
|
|
1762
|
+
previousActiveRef.current = active;
|
|
1763
|
+
if (!session) return;
|
|
1764
|
+
if (becameActive) {
|
|
1765
|
+
firstFrameGateRef.current = false;
|
|
1766
|
+
markViewportRafResume(documentId);
|
|
1767
|
+
}
|
|
1768
|
+
session.setActive(active);
|
|
1769
|
+
}, [active, documentId]);
|
|
1770
|
+
|
|
1771
|
+
// The stage's own handle for the capabilities that drive it directly (the
|
|
1772
|
+
// viewport hotkeys). `surfaceStatus` is what re-renders once the stage has
|
|
1773
|
+
// finished building, so reading the ref here is safe and never stale.
|
|
1774
|
+
const host = documentHostRef.current;
|
|
1775
|
+
const stageHandle =
|
|
1776
|
+
surfaceStatus === 'ready' && host?.viewport
|
|
1777
|
+
? { store: host.store, viewport: host.viewport, canvas: host.surface.canvas }
|
|
1778
|
+
: null;
|
|
1779
|
+
// The document's own selection changes as you pick inside it; the registry
|
|
1780
|
+
// reads this on demand, so the picked part follows without re-registering.
|
|
1781
|
+
// With nothing picked the DOCUMENT NODE is the subject — the same reading
|
|
1782
|
+
// as clicking a model's root in the scene.
|
|
1783
|
+
const readSelection = useCallback(() => {
|
|
1784
|
+
const owner = documentAdapterRef.current;
|
|
1785
|
+
return owner
|
|
1786
|
+
? {
|
|
1787
|
+
adapter: owner,
|
|
1788
|
+
nodeId: owner.selection?.get()[0] ?? owner.hierarchy.roots()[0]?.id ?? null,
|
|
1789
|
+
}
|
|
1790
|
+
: null;
|
|
1791
|
+
}, []);
|
|
1792
|
+
const viewport = (
|
|
1793
|
+
<div
|
|
1794
|
+
data-testid="tool-object3d-authoring"
|
|
1795
|
+
data-vgai-chromeless={chromeless || undefined}
|
|
1796
|
+
className={studioStage ? 'vgai-object3d-studio-stage' : undefined}
|
|
1797
|
+
style={{
|
|
1798
|
+
position: chromeless || hasShell ? 'relative' : 'absolute',
|
|
1799
|
+
// Inside the Asset Editor shell the stage fills its own box exactly;
|
|
1800
|
+
// in a bare dock panel it bleeds under the panel's padding.
|
|
1801
|
+
...(chromeless
|
|
1802
|
+
? { width: '100%', height: '100%' }
|
|
1803
|
+
: hasShell
|
|
1804
|
+
? { flex: 1, minHeight: 0 }
|
|
1805
|
+
: { inset: -STAGE_BLEED_PX }),
|
|
1806
|
+
overflow: 'hidden',
|
|
1807
|
+
// Asset documents paint the shared studio stage through their class.
|
|
1808
|
+
// An inline background here would win the cascade and hide it.
|
|
1809
|
+
background: studioStage ? undefined : themeVars.surface.raised,
|
|
1810
|
+
pointerEvents: 'auto',
|
|
1811
|
+
}}
|
|
1812
|
+
>
|
|
1813
|
+
<div
|
|
1814
|
+
ref={containerRef}
|
|
1815
|
+
style={{ position: 'absolute', inset: 0, minHeight: 0, overflow: 'hidden' }}
|
|
1816
|
+
>
|
|
1817
|
+
<ViewportSurface canvasHostRef={canvasHostRef} />
|
|
1818
|
+
{/* WHICH KEYS THIS STAGE ANSWERS — the transform modes, the view
|
|
1819
|
+
presets, Frame, pivot, snap. Beside the stage rather than inside
|
|
1820
|
+
the overlay set, because a package-contributed document mounts
|
|
1821
|
+
with no shell above it and reached none of them
|
|
1822
|
+
(`stage-keyboard.tsx`). The registry holds ONE stage's set, so the
|
|
1823
|
+
condition is the ACTIVE document's stage. */}
|
|
1824
|
+
{active && stageHandle && !chromeless ? (
|
|
1825
|
+
<Suspense fallback={null}>
|
|
1826
|
+
<LazyStageKeyboard stage={stageHandle} />
|
|
1827
|
+
</Suspense>
|
|
1828
|
+
) : null}
|
|
1829
|
+
{/* The stage's own overlays, co-located with the canvas the way
|
|
1830
|
+
`RootSelectionOverlay`'s DOM contract requires (its marquee is
|
|
1831
|
+
measured against this container's box). A stage with no shell
|
|
1832
|
+
above it — a bounded host — renders none of them and never loads
|
|
1833
|
+
them. */}
|
|
1834
|
+
{shellStore && !chromeless ? (
|
|
1835
|
+
<StageOverlays
|
|
1836
|
+
store={shellStore}
|
|
1837
|
+
documentId={documentId}
|
|
1838
|
+
stage={stageHandle}
|
|
1839
|
+
active={active}
|
|
1840
|
+
chrome="document"
|
|
1841
|
+
/>
|
|
1842
|
+
) : null}
|
|
1843
|
+
{/* A ROLLED-BACK operation (an animation write, a capture, a source
|
|
1844
|
+
write the history guarded) — every writer of `error` restores the
|
|
1845
|
+
prior state, so the surface below is live and must stay visible.
|
|
1846
|
+
It used to mount a full-bleed opaque curtain with no dismiss: the
|
|
1847
|
+
stage read as dead until the session restarted (measured). A
|
|
1848
|
+
notice over the stage, dismissible, and cleared by the next
|
|
1849
|
+
gesture that succeeds. */}
|
|
1850
|
+
{error && (
|
|
1851
|
+
<div
|
|
1852
|
+
role="alert"
|
|
1853
|
+
style={{
|
|
1854
|
+
position: 'absolute',
|
|
1855
|
+
insetInline: 0,
|
|
1856
|
+
top: 0,
|
|
1857
|
+
display: 'flex',
|
|
1858
|
+
justifyContent: 'center',
|
|
1859
|
+
padding: 'var(--vgai-space-3)',
|
|
1860
|
+
pointerEvents: 'none',
|
|
1861
|
+
}}
|
|
1862
|
+
>
|
|
1863
|
+
<div
|
|
1864
|
+
className="vgai-chrome-island"
|
|
1865
|
+
style={{
|
|
1866
|
+
display: 'flex',
|
|
1867
|
+
alignItems: 'center',
|
|
1868
|
+
gap: 'var(--vgai-space-3)',
|
|
1869
|
+
maxWidth: '80%',
|
|
1870
|
+
padding: 'var(--vgai-space-2) var(--vgai-space-3)',
|
|
1871
|
+
borderRadius: 'var(--vgai-radius-md)',
|
|
1872
|
+
border: `var(--vgai-stroke-resting) solid ${themeVars.semantic.danger}`,
|
|
1873
|
+
background: themeVars.surface.raised,
|
|
1874
|
+
color: themeVars.semantic.danger,
|
|
1875
|
+
pointerEvents: 'auto',
|
|
1876
|
+
}}
|
|
1877
|
+
>
|
|
1878
|
+
<span>{error}</span>
|
|
1879
|
+
<IconButton
|
|
1880
|
+
size="compact"
|
|
1881
|
+
aria-label="Dismiss"
|
|
1882
|
+
onClick={() => setError(null)}
|
|
1883
|
+
data-testid="object3d-document-error-dismiss"
|
|
1884
|
+
>
|
|
1885
|
+
<EditorIcon icon={editorIcons.action.close} />
|
|
1886
|
+
</IconButton>
|
|
1887
|
+
</div>
|
|
1888
|
+
</div>
|
|
1889
|
+
)}
|
|
1890
|
+
{!error && surfaceStatus === 'building' && (
|
|
1891
|
+
<ViewportSurfaceStatus testId="object3d-document-status">
|
|
1892
|
+
{OBJECT3D_SURFACE_BUILDING}
|
|
1893
|
+
</ViewportSurfaceStatus>
|
|
1894
|
+
)}
|
|
1895
|
+
{/* The stage BLEEDS 12px under the dock panel's padding (`inset: -12`
|
|
1896
|
+
above) so the rendered image fills it edge to edge. The furniture is
|
|
1897
|
+
CHROME, not image: it has to sit inside the panel the person can
|
|
1898
|
+
actually see, so it gets that bleed back. Without this the view text
|
|
1899
|
+
started at page x=-6 — the "U" of "User Perspective" was clipped off
|
|
1900
|
+
the window — and the navigation cluster's right edge landed 4px past
|
|
1901
|
+
the panel, under the Inspector's section rail (both measured). */}
|
|
1902
|
+
<div
|
|
1903
|
+
style={{
|
|
1904
|
+
position: 'absolute',
|
|
1905
|
+
inset: chromeless || hasShell ? 0 : STAGE_BLEED_PX,
|
|
1906
|
+
pointerEvents: 'none',
|
|
1907
|
+
}}
|
|
1908
|
+
>
|
|
1909
|
+
{!chromeless && surfaceStatus === 'ready' && documentHostRef.current && (
|
|
1910
|
+
<ViewportFurniture
|
|
1911
|
+
viewport={documentHostRef.current.viewport}
|
|
1912
|
+
session={documentHostRef.current.session}
|
|
1913
|
+
store={documentHostRef.current.store}
|
|
1914
|
+
projection={projection}
|
|
1915
|
+
displayName={displayName}
|
|
1916
|
+
{...(statistics ? { statistics } : {})}
|
|
1917
|
+
objectName={(id) =>
|
|
1918
|
+
documentHostRef.current?.adapter?.hierarchy.object3D?.(id)?.name ??
|
|
1919
|
+
// Document builders may supply names through their store index.
|
|
1920
|
+
documentHostRef.current?.store.objectMap.get(id)?.name ??
|
|
1921
|
+
null
|
|
1922
|
+
}
|
|
1923
|
+
/>
|
|
1924
|
+
)}
|
|
1925
|
+
</div>
|
|
1926
|
+
</div>
|
|
1927
|
+
</div>
|
|
1928
|
+
);
|
|
1929
|
+
if (!hasShell) return viewport;
|
|
1930
|
+
return (
|
|
1931
|
+
<AssetEditorShell
|
|
1932
|
+
documentId={documentId}
|
|
1933
|
+
active={active}
|
|
1934
|
+
type={assetType ?? (modelSourceEntityId ? 'entity' : 'model')}
|
|
1935
|
+
title={displayName}
|
|
1936
|
+
status={sourcePath}
|
|
1937
|
+
selection={readSelection}
|
|
1938
|
+
fill
|
|
1939
|
+
>
|
|
1940
|
+
<div className="vgai-object3d-asset-workspace">
|
|
1941
|
+
{viewport}
|
|
1942
|
+
{/* Where the deleted preview strip was. It draws nothing until this
|
|
1943
|
+
stage's transport has a subject, so a document with nothing to show
|
|
1944
|
+
at a time is unchanged. */}
|
|
1945
|
+
<TransportStrip transport={documentHostRef.current?.transport ?? null} />
|
|
1946
|
+
</div>
|
|
1947
|
+
</AssetEditorShell>
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/** Public project contribution surface over the editor's native Object3D document host. */
|
|
1952
|
+
export function ToolObject3DAuthoring(props: ToolObject3DAuthoringProps) {
|
|
1953
|
+
return <Object3DDocumentViewport {...props} />;
|
|
1954
|
+
}
|