@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,1326 @@
|
|
|
1
|
+
import { createPerformanceProfiler } from '../performance-profiler';
|
|
2
|
+
import type { AuthoringAdapter } from '@volter/editor-project/adapter';
|
|
3
|
+
import { viewportCaptureOutputPass } from '@volter/editor-threejs/capture/output-pass';
|
|
4
|
+
import { contentWorldBounds } from '@volter/editor-threejs/viewport/content-bounds';
|
|
5
|
+
import { setUserData } from '@volter/editor-threejs/ecs/user-data';
|
|
6
|
+
import {
|
|
7
|
+
type ViewportShadingMode,
|
|
8
|
+
ViewportShadingRenderer,
|
|
9
|
+
} from '@volter/editor-threejs/render/viewport-shading';
|
|
10
|
+
import type { EffectComposer, EffectPass, RenderPass } from 'postprocessing';
|
|
11
|
+
import * as THREE from 'three';
|
|
12
|
+
import { cameraPresetDirection, type ModelCameraPreset } from '../asset-workflow/model-inspection';
|
|
13
|
+
import { editorConsole } from '../editor-console';
|
|
14
|
+
import { type EditorViewport } from '../editor-viewport';
|
|
15
|
+
import { nativeSelectionColors, subscribeNativeSelectionTheme } from '../native-selection-style';
|
|
16
|
+
import { BoneSelectionHighlight } from '../three-viewport/bone-selection-highlight';
|
|
17
|
+
import { perspectiveDistanceToFitBox } from '../three-viewport/camera-fit';
|
|
18
|
+
import {
|
|
19
|
+
createThreeSelectionOutline,
|
|
20
|
+
setThreeSelectionOutlineColors,
|
|
21
|
+
syncThreeSelectionOutline,
|
|
22
|
+
} from '../three-viewport/selection-outline';
|
|
23
|
+
import { styleEditorSkeletonHelper } from '../three-viewport/skeleton-helper';
|
|
24
|
+
import { isEditorViewportShadingTarget } from '../viewport-shading-boundary';
|
|
25
|
+
import { setAuthoringSelection } from './consumer-actions';
|
|
26
|
+
|
|
27
|
+
export type Object3DDocumentViewMode = ViewportShadingMode | 'uv' | 'vertex-colors';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How a look flight ended. Never a rejection: "the human grabbed the view
|
|
31
|
+
* mid-orbit" is an ordinary outcome of a shared camera, not an error, and the
|
|
32
|
+
* caller needs to be able to tell it apart from "it ran to the end".
|
|
33
|
+
*/
|
|
34
|
+
export interface DocumentLookOutcome {
|
|
35
|
+
readonly completed: boolean;
|
|
36
|
+
/** Only when `completed` is false. */
|
|
37
|
+
readonly cancelledBy?: 'human' | 'superseded' | 'closed';
|
|
38
|
+
/** Where the camera actually ended up, in the pivot's spherical frame. */
|
|
39
|
+
readonly azimuth: number;
|
|
40
|
+
readonly elevation: number;
|
|
41
|
+
readonly seconds: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One in-flight camera move. The pivot and radius are captured at LAUNCH from
|
|
46
|
+
* wherever the human left the view, so a flight always lerps from the current
|
|
47
|
+
* pose rather than snapping to a canonical one.
|
|
48
|
+
*/
|
|
49
|
+
interface CameraFlight {
|
|
50
|
+
readonly pivot: THREE.Vector3;
|
|
51
|
+
readonly radius: number;
|
|
52
|
+
readonly startTheta: number;
|
|
53
|
+
readonly startPhi: number;
|
|
54
|
+
readonly deltaTheta: number;
|
|
55
|
+
readonly deltaPhi: number;
|
|
56
|
+
readonly duration: number;
|
|
57
|
+
readonly ease: (t: number) => number;
|
|
58
|
+
elapsed: number;
|
|
59
|
+
theta: number;
|
|
60
|
+
phi: number;
|
|
61
|
+
settle: ((outcome: DocumentLookOutcome) => void) | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Dihedral angle below which an interior edge is dropped from the topology
|
|
66
|
+
* overlay. At 1° a PLANAR quad's triangulation diagonal disappears while every
|
|
67
|
+
* real face boundary survives — which is the entire reason the overlay exists
|
|
68
|
+
* (`toBufferGeometry` fan-triangulates, so nothing downstream of the mesh kit
|
|
69
|
+
* still knows which pairs of triangles were one quad).
|
|
70
|
+
*/
|
|
71
|
+
const TOPOLOGY_EDGE_THRESHOLD_DEGREES = 1;
|
|
72
|
+
|
|
73
|
+
/** OrbitControls' own poles-excluded range; orbiting past them flips the view. */
|
|
74
|
+
/** How long a presented-frame request waits on the host's draw loop before it
|
|
75
|
+
* gives the chrome door back `null` (see `requestPresentedFrame`). Two frames
|
|
76
|
+
* at 30fps is a loop that is running; longer means it is not. */
|
|
77
|
+
const PRESENTED_FRAME_WAIT_MS = 700;
|
|
78
|
+
|
|
79
|
+
const MIN_POLAR = 1e-3;
|
|
80
|
+
const MAX_POLAR = Math.PI - 1e-3;
|
|
81
|
+
|
|
82
|
+
const easeInOutCubic = (t: number): number => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
|
|
83
|
+
const linear = (t: number): number => t;
|
|
84
|
+
|
|
85
|
+
function finiteOr(value: unknown, fallback: number): number {
|
|
86
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Object3DDocumentPresentationState {
|
|
90
|
+
readonly mode: Object3DDocumentViewMode;
|
|
91
|
+
readonly grid: boolean;
|
|
92
|
+
readonly background: 'neutral' | 'transparent';
|
|
93
|
+
readonly exposure: number;
|
|
94
|
+
readonly projection: 'perspective' | 'orthographic';
|
|
95
|
+
readonly lighting: 'studio' | 'outdoor' | 'flat';
|
|
96
|
+
readonly skeleton: boolean;
|
|
97
|
+
readonly bounds: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const INITIAL_PRESENTATION: Object3DDocumentPresentationState = {
|
|
101
|
+
mode: 'solid',
|
|
102
|
+
grid: true,
|
|
103
|
+
background: 'neutral',
|
|
104
|
+
exposure: 1,
|
|
105
|
+
projection: 'perspective',
|
|
106
|
+
lighting: 'studio',
|
|
107
|
+
skeleton: false,
|
|
108
|
+
bounds: false,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Live presentation controls for one native Object3D document. The model
|
|
113
|
+
* graph remains truth; this session owns only editor chrome and diagnostics.
|
|
114
|
+
*/
|
|
115
|
+
export class Object3DDocumentSession {
|
|
116
|
+
readonly profiler = createPerformanceProfiler();
|
|
117
|
+
private readonly shading = new ViewportShadingRenderer();
|
|
118
|
+
private readonly helpers: THREE.Object3D[] = [];
|
|
119
|
+
private skeletonHelper: THREE.SkeletonHelper | null = null;
|
|
120
|
+
private boundsHelper: THREE.BoxHelper | null = null;
|
|
121
|
+
private boneSelectionHighlight: BoneSelectionHighlight | null = null;
|
|
122
|
+
private boneSelectionSignature = '';
|
|
123
|
+
private composer: EffectComposer | null = null;
|
|
124
|
+
/** One in-flight `import('postprocessing')` at a time — {@link ensureComposer}
|
|
125
|
+
* is called from every frame. */
|
|
126
|
+
private composerLoading = false;
|
|
127
|
+
private disposed = false;
|
|
128
|
+
private sceneRenderPass: RenderPass | null = null;
|
|
129
|
+
private selectionOutline: ReturnType<typeof createThreeSelectionOutline> | null = null;
|
|
130
|
+
private selectionOutlinePass: EffectPass | null = null;
|
|
131
|
+
private selectedObjects: THREE.Object3D[] = [];
|
|
132
|
+
private renderWidth = 1;
|
|
133
|
+
private renderHeight = 1;
|
|
134
|
+
private readonly orthographicCamera = new THREE.OrthographicCamera();
|
|
135
|
+
/** The offscreen render target's aspect, non-null ONLY inside
|
|
136
|
+
* {@link Object3DDocumentSession.captureImage} — whose buffer has its own
|
|
137
|
+
* shape (square by default, `{width, height}` when a caller asks for one)
|
|
138
|
+
* while the live camera's aspect is the panel's. */
|
|
139
|
+
private captureAspect: number | null = null;
|
|
140
|
+
/** The camera a capture door was handed, for the duration of that capture.
|
|
141
|
+
* A BLENDER RENDER IS NOT THE MODELING VIEWPORT: it photographs the scene
|
|
142
|
+
* through the scene's OWN camera, which has nothing to do with where the
|
|
143
|
+
* person is looking — see `blender-runtime-host.ts`. Before this existed the
|
|
144
|
+
* only way to photograph from another camera was to move the viewport onto
|
|
145
|
+
* it and move it back, which is why that code had a restore dance at all. */
|
|
146
|
+
private cameraOverride: THREE.Camera | null = null;
|
|
147
|
+
/** The host's mirror of the document scene onto the rendered scene — see
|
|
148
|
+
* {@link Object3DDocumentSession.setBeforeRender}. */
|
|
149
|
+
private beforeRender: (() => void) | null = null;
|
|
150
|
+
private state = INITIAL_PRESENTATION;
|
|
151
|
+
private version = 0;
|
|
152
|
+
private readonly listeners = new Set<() => void>();
|
|
153
|
+
private unsubscribeSelectionTheme: () => void = () => {};
|
|
154
|
+
/** The one in-flight look move. See {@link Object3DDocumentSession.orbit}. */
|
|
155
|
+
private flight: CameraFlight | null = null;
|
|
156
|
+
/** The camera position the live flight wrote LAST frame — the drift check
|
|
157
|
+
* in {@link Object3DDocumentSession.advanceLook} compares against it. */
|
|
158
|
+
private readonly flightPose = new THREE.Vector3();
|
|
159
|
+
/** Wireframe's edge overlay and the meshes each line set shadows. */
|
|
160
|
+
private topologyOverlay: THREE.Group | null = null;
|
|
161
|
+
private topologyFollowers: Array<{
|
|
162
|
+
readonly line: THREE.LineSegments;
|
|
163
|
+
readonly source: THREE.Object3D;
|
|
164
|
+
}> = [];
|
|
165
|
+
|
|
166
|
+
constructor(
|
|
167
|
+
readonly documentId: string,
|
|
168
|
+
public root: THREE.Object3D,
|
|
169
|
+
readonly scene: THREE.Scene,
|
|
170
|
+
readonly renderer: THREE.WebGLRenderer,
|
|
171
|
+
readonly viewport: EditorViewport,
|
|
172
|
+
private readonly editorLights: readonly THREE.Light[] = [],
|
|
173
|
+
private authoring?: AuthoringAdapter,
|
|
174
|
+
/** Asset Lab renders its neutral stage in editor chrome behind an alpha
|
|
175
|
+
* canvas; other document hosts may retain a real Three background. */
|
|
176
|
+
private neutralBackground: THREE.Color | THREE.Texture | null = new THREE.Color(0x20242a),
|
|
177
|
+
/** When set, Frame and view presets fit this box instead of the root AABB. */
|
|
178
|
+
private readonly frameBox: THREE.Box3 | null = null,
|
|
179
|
+
) {
|
|
180
|
+
this.unsubscribeSelectionTheme = subscribeNativeSelectionTheme(
|
|
181
|
+
renderer.domElement,
|
|
182
|
+
this.syncSelectionTheme,
|
|
183
|
+
);
|
|
184
|
+
// THE HUMAN ALWAYS WINS THE CAMERA. OrbitControls fires `start` on the
|
|
185
|
+
// pointer-down that begins a real drag (its `update()` only ever fires
|
|
186
|
+
// `change`), so this is the one event that means "a person just grabbed
|
|
187
|
+
// this view". An agent flight yields at that instant, leaving the camera
|
|
188
|
+
// exactly where it is: no snap-back, no two writers fighting for the pose.
|
|
189
|
+
viewport.orbitControls.addEventListener('start', this.cancelLookForHuman);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Replace authored content without replacing the editor session or camera. */
|
|
193
|
+
replaceContent(root: THREE.Object3D, authoring: AuthoringAdapter): void {
|
|
194
|
+
this.selectionOutline?.selection.clear();
|
|
195
|
+
this.selectedObjects = [];
|
|
196
|
+
this.boneSelectionSignature = '';
|
|
197
|
+
this.clearBoneSelectionHighlight();
|
|
198
|
+
this.clearSkeletonHelper();
|
|
199
|
+
this.clearBoundsHelper();
|
|
200
|
+
this.clearTopologyOverlay();
|
|
201
|
+
this.root = root;
|
|
202
|
+
this.authoring = authoring;
|
|
203
|
+
if (this.state.mode === 'wireframe') this.buildTopologyOverlay();
|
|
204
|
+
if (this.state.bounds) {
|
|
205
|
+
this.boundsHelper = new THREE.BoxHelper(root, 0xffb454);
|
|
206
|
+
this.addHelper(this.boundsHelper, 'bounds');
|
|
207
|
+
}
|
|
208
|
+
this.syncSelectionPresentation();
|
|
209
|
+
this.refreshSkeletonHelper();
|
|
210
|
+
this.notify();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private readonly cancelLookForHuman = (): void => {
|
|
214
|
+
this.settleFlight('human');
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
private readonly syncSelectionTheme = (): void => {
|
|
218
|
+
const color = nativeSelectionColors(this.renderer.domElement).visible;
|
|
219
|
+
if (this.selectionOutline) {
|
|
220
|
+
setThreeSelectionOutlineColors(
|
|
221
|
+
this.selectionOutline,
|
|
222
|
+
nativeSelectionColors(this.renderer.domElement),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
this.boneSelectionHighlight?.setColor(color);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
229
|
+
this.listeners.add(listener);
|
|
230
|
+
return () => this.listeners.delete(listener);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
getSnapshot = (): number => this.version;
|
|
234
|
+
|
|
235
|
+
presentation(): Object3DDocumentPresentationState {
|
|
236
|
+
return this.state;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private resolveFrameBounds(): THREE.Box3 {
|
|
240
|
+
const selection = this.authoring?.selection?.get() ?? [];
|
|
241
|
+
if (selection.length > 0) {
|
|
242
|
+
const selected = new THREE.Box3();
|
|
243
|
+
let found = false;
|
|
244
|
+
for (const id of selection) {
|
|
245
|
+
const object = this.authoring?.hierarchy.object3D?.(id);
|
|
246
|
+
if (!object) continue;
|
|
247
|
+
const bounds = contentWorldBounds(object);
|
|
248
|
+
if (bounds.isEmpty()) continue;
|
|
249
|
+
selected.union(bounds);
|
|
250
|
+
found = true;
|
|
251
|
+
}
|
|
252
|
+
if (found) return selected;
|
|
253
|
+
}
|
|
254
|
+
if (this.frameBox && !this.frameBox.isEmpty()) return this.frameBox;
|
|
255
|
+
return contentWorldBounds(this.root);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Frame the subject: the selection if there is one, else this document's
|
|
260
|
+
* frame box, else the whole root. `fit` scales the fitted distance — 1 is
|
|
261
|
+
* the tight fit the toolbar's Frame button has always used, >1 pulls back.
|
|
262
|
+
*/
|
|
263
|
+
frame(fit = 1): boolean {
|
|
264
|
+
const bounds = this.resolveFrameBounds();
|
|
265
|
+
if (bounds.isEmpty()) {
|
|
266
|
+
// A Frame that does nothing must say why — a model document whose
|
|
267
|
+
// mesh measures as nothing is a defect, never a quiet no-op.
|
|
268
|
+
let meshes = 0;
|
|
269
|
+
let positions = 0;
|
|
270
|
+
this.root.traverse((node) => {
|
|
271
|
+
const geometry = (node as THREE.Mesh).geometry as THREE.BufferGeometry | undefined;
|
|
272
|
+
if (!geometry) return;
|
|
273
|
+
meshes += 1;
|
|
274
|
+
positions += geometry.getAttribute('position')?.count ?? 0;
|
|
275
|
+
});
|
|
276
|
+
editorConsole.warn(
|
|
277
|
+
`frame: nothing to frame — root "${this.root.name}" (${this.root.children.length} children, ${meshes} meshes, ${positions} positions), selection ${JSON.stringify(this.authoring?.selection?.get() ?? [])}, frameBox ${this.frameBox ? (this.frameBox.isEmpty() ? 'empty' : 'set') : 'none'}`,
|
|
278
|
+
'document',
|
|
279
|
+
);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
this.settleFlight('superseded');
|
|
283
|
+
const center = bounds.getCenter(new THREE.Vector3());
|
|
284
|
+
const direction = this.viewport.camera.position
|
|
285
|
+
.clone()
|
|
286
|
+
.sub(this.viewport.orbitControls.target)
|
|
287
|
+
.normalize();
|
|
288
|
+
if (direction.lengthSq() === 0) direction.set(0.8, 0.5, -1).normalize();
|
|
289
|
+
const scale = Math.min(10, Math.max(0.1, finiteOr(fit, 1)));
|
|
290
|
+
const distance = perspectiveDistanceToFitBox(bounds, this.viewport.camera, direction) * scale;
|
|
291
|
+
this.viewport.setPose(center.clone().addScaledVector(direction, distance), center);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* THE AGENT LOOKING AT THE MODEL, as an act a person can watch.
|
|
297
|
+
*
|
|
298
|
+
* Swing this document's ONE camera — the camera the human is looking
|
|
299
|
+
* through — around the framed subject by `azimuth`/`elevation` radians,
|
|
300
|
+
* animated over `duration` seconds (advanced in {@link renderViewport}, so
|
|
301
|
+
* every intermediate pose is a frame that actually got drawn). The returned
|
|
302
|
+
* promise settles when the move ends, either because it finished or because
|
|
303
|
+
* a human grabbed the view.
|
|
304
|
+
*
|
|
305
|
+
* The pivot and radius are read from the CURRENT pose at launch, so this
|
|
306
|
+
* composes with wherever the view already is instead of homing to a canon.
|
|
307
|
+
* A second flight supersedes the first rather than blending with it.
|
|
308
|
+
*
|
|
309
|
+
* The move is drawn by the document's rAF loop, so a document that is not
|
|
310
|
+
* rendering (a background tab, an inactive dock panel) does not orbit —
|
|
311
|
+
* which is the honest behavior for a verb whose whole point is being seen.
|
|
312
|
+
*/
|
|
313
|
+
orbit(options: {
|
|
314
|
+
readonly azimuth?: number;
|
|
315
|
+
readonly elevation?: number;
|
|
316
|
+
readonly duration?: number;
|
|
317
|
+
}): Promise<DocumentLookOutcome> {
|
|
318
|
+
return this.launchFlight(
|
|
319
|
+
finiteOr(options.azimuth, 0),
|
|
320
|
+
finiteOr(options.elevation, 0),
|
|
321
|
+
Math.min(60, Math.max(0, finiteOr(options.duration, 0.6))),
|
|
322
|
+
easeInOutCubic,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A slow full orbit — {@link orbit} with the revolutions spelled in turns
|
|
328
|
+
* and a CONSTANT angular rate, because a turntable that eases in and out
|
|
329
|
+
* reads as a nervous camera rather than a rotating subject.
|
|
330
|
+
*/
|
|
331
|
+
turntable(options: {
|
|
332
|
+
readonly seconds?: number;
|
|
333
|
+
readonly revolutions?: number;
|
|
334
|
+
}): Promise<DocumentLookOutcome> {
|
|
335
|
+
const revolutions = finiteOr(options.revolutions, 1);
|
|
336
|
+
return this.launchFlight(
|
|
337
|
+
revolutions * Math.PI * 2,
|
|
338
|
+
0,
|
|
339
|
+
Math.min(60, Math.max(0, finiteOr(options.seconds, 6))),
|
|
340
|
+
linear,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Is a look flight drawing right now? */
|
|
345
|
+
looking(): boolean {
|
|
346
|
+
return this.flight !== null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private launchFlight(
|
|
350
|
+
deltaAzimuth: number,
|
|
351
|
+
deltaElevation: number,
|
|
352
|
+
duration: number,
|
|
353
|
+
ease: (t: number) => number,
|
|
354
|
+
): Promise<DocumentLookOutcome> {
|
|
355
|
+
this.settleFlight('superseded');
|
|
356
|
+
const camera = this.viewport.camera;
|
|
357
|
+
const pivot = this.viewport.orbitControls.target.clone();
|
|
358
|
+
const spherical = new THREE.Spherical().setFromVector3(camera.position.clone().sub(pivot));
|
|
359
|
+
// A degenerate radius (camera sitting on its own target) has no orbit to
|
|
360
|
+
// run; back off to something the subject's own scale justifies.
|
|
361
|
+
const radius =
|
|
362
|
+
spherical.radius > 1e-4
|
|
363
|
+
? spherical.radius
|
|
364
|
+
: Math.max(this.resolveFrameBounds().getSize(new THREE.Vector3()).length(), 1);
|
|
365
|
+
const flight: CameraFlight = {
|
|
366
|
+
pivot,
|
|
367
|
+
radius,
|
|
368
|
+
startTheta: spherical.theta,
|
|
369
|
+
startPhi: spherical.phi,
|
|
370
|
+
deltaTheta: deltaAzimuth,
|
|
371
|
+
// Elevation is measured UP from the horizon; the polar angle is measured
|
|
372
|
+
// DOWN from +Y. Raising the camera therefore shrinks phi.
|
|
373
|
+
deltaPhi: -deltaElevation,
|
|
374
|
+
duration,
|
|
375
|
+
ease,
|
|
376
|
+
elapsed: 0,
|
|
377
|
+
theta: spherical.theta,
|
|
378
|
+
phi: spherical.phi,
|
|
379
|
+
settle: null,
|
|
380
|
+
};
|
|
381
|
+
this.flight = flight;
|
|
382
|
+
const settled = new Promise<DocumentLookOutcome>((resolve) => {
|
|
383
|
+
flight.settle = resolve;
|
|
384
|
+
});
|
|
385
|
+
// A zero-length move is still a move: apply it now so the pose is correct
|
|
386
|
+
// even on a surface whose next frame never comes.
|
|
387
|
+
this.applyFlightPose(flight, 0);
|
|
388
|
+
if (duration <= 0) this.settleFlight(null);
|
|
389
|
+
return settled;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private applyFlightPose(flight: CameraFlight, progress: number): void {
|
|
393
|
+
const k = flight.ease(progress);
|
|
394
|
+
flight.theta = flight.startTheta + flight.deltaTheta * k;
|
|
395
|
+
flight.phi = Math.min(MAX_POLAR, Math.max(MIN_POLAR, flight.startPhi + flight.deltaPhi * k));
|
|
396
|
+
const camera = this.viewport.camera;
|
|
397
|
+
camera.position
|
|
398
|
+
.setFromSphericalCoords(flight.radius, flight.phi, flight.theta)
|
|
399
|
+
.add(flight.pivot);
|
|
400
|
+
this.viewport.orbitControls.target.copy(flight.pivot);
|
|
401
|
+
camera.lookAt(flight.pivot);
|
|
402
|
+
camera.updateMatrixWorld();
|
|
403
|
+
this.flightPose.copy(camera.position);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Advance the live flight. Called from {@link renderViewport} — i.e. AFTER
|
|
408
|
+
* `EditorViewport.update()` has run OrbitControls for this frame — so the
|
|
409
|
+
* flight has the final say on the pose that is about to be drawn. (Same
|
|
410
|
+
* ordering rule the viewport's own snap-to-view animation follows.)
|
|
411
|
+
*/
|
|
412
|
+
private advanceLook(deltaSeconds: number): void {
|
|
413
|
+
const flight = this.flight;
|
|
414
|
+
if (!flight) return;
|
|
415
|
+
// DID THE POSE I WROTE LAST FRAME SURVIVE? If not, something else owns the
|
|
416
|
+
// camera now, and it wins — yielding here rather than yanking the view
|
|
417
|
+
// back onto the arc is the difference between "the agent stopped" and two
|
|
418
|
+
// writers fighting over one camera.
|
|
419
|
+
//
|
|
420
|
+
// This is the door-independent half of cancellation, and it is not
|
|
421
|
+
// redundant with the OrbitControls `start` listener: three dispatches
|
|
422
|
+
// `start` for mouse, wheel and touch, but NOT for its keyboard pan, and a
|
|
423
|
+
// pointer gesture that cannot take pointer capture (every synthetic one)
|
|
424
|
+
// throws inside `onPointerDown` before reaching the dispatch. The listener
|
|
425
|
+
// names the human case early; this catches every writer that moves the
|
|
426
|
+
// camera without announcing itself.
|
|
427
|
+
const tolerance = Math.max(1e-4, flight.radius * 1e-3);
|
|
428
|
+
if (this.viewport.camera.position.distanceToSquared(this.flightPose) > tolerance * tolerance) {
|
|
429
|
+
this.settleFlight('human');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (deltaSeconds > 0 && Number.isFinite(deltaSeconds)) flight.elapsed += deltaSeconds;
|
|
433
|
+
const progress = flight.duration <= 0 ? 1 : Math.min(1, flight.elapsed / flight.duration);
|
|
434
|
+
this.applyFlightPose(flight, progress);
|
|
435
|
+
if (progress >= 1) this.settleFlight(null);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private settleFlight(cancelledBy: 'human' | 'superseded' | 'closed' | null): void {
|
|
439
|
+
const flight = this.flight;
|
|
440
|
+
if (!flight) return;
|
|
441
|
+
this.flight = null;
|
|
442
|
+
const ended = {
|
|
443
|
+
completed: cancelledBy === null,
|
|
444
|
+
azimuth: flight.theta,
|
|
445
|
+
elevation: Math.PI / 2 - flight.phi,
|
|
446
|
+
seconds: Math.min(flight.elapsed, flight.duration),
|
|
447
|
+
};
|
|
448
|
+
flight.settle?.(cancelledBy === null ? ended : { ...ended, cancelledBy });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
select(ids: readonly string[]): void {
|
|
452
|
+
if (this.authoring) setAuthoringSelection(this.authoring, [...ids]);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
selection(): string[] {
|
|
456
|
+
return [...(this.authoring?.selection?.get() ?? [])];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
idForObject(object: THREE.Object3D): string | null {
|
|
460
|
+
return this.authoring?.hierarchy.idForObject3D?.(object) ?? null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Keep native hierarchy selection legible in this document's one viewport. */
|
|
464
|
+
syncSelectionPresentation(): void {
|
|
465
|
+
const selected = (this.authoring?.selection?.get() ?? [])
|
|
466
|
+
.map((id) => this.authoring?.hierarchy.object3D?.(id) ?? null)
|
|
467
|
+
.filter((object): object is THREE.Object3D => object !== null);
|
|
468
|
+
this.selectedObjects = selected;
|
|
469
|
+
if (this.selectionOutline) {
|
|
470
|
+
syncThreeSelectionOutline(this.selectionOutline, this.selectionOutlineWanted ? selected : []);
|
|
471
|
+
this.syncComposerOutput();
|
|
472
|
+
}
|
|
473
|
+
const bones = selected.filter((object): object is THREE.Bone =>
|
|
474
|
+
Boolean((object as THREE.Bone).isBone),
|
|
475
|
+
);
|
|
476
|
+
const signature = bones.map((bone) => bone.uuid).join('\u0000');
|
|
477
|
+
if (signature === this.boneSelectionSignature) return;
|
|
478
|
+
this.boneSelectionSignature = signature;
|
|
479
|
+
this.clearBoneSelectionHighlight();
|
|
480
|
+
if (bones.length > 0) {
|
|
481
|
+
this.boneSelectionHighlight = new BoneSelectionHighlight(
|
|
482
|
+
bones,
|
|
483
|
+
nativeSelectionColors(this.renderer.domElement).visible,
|
|
484
|
+
);
|
|
485
|
+
this.scene.add(this.boneSelectionHighlight);
|
|
486
|
+
}
|
|
487
|
+
this.refreshSkeletonHelper();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
frameSelection(): boolean {
|
|
491
|
+
return this.frameIds(this.authoring?.selection?.get() ?? []);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
frameIds(ids: readonly string[]): boolean {
|
|
495
|
+
this.settleFlight('superseded');
|
|
496
|
+
const objects = ids
|
|
497
|
+
.map((id) => this.authoring?.hierarchy.object3D?.(id) ?? null)
|
|
498
|
+
.filter((object): object is THREE.Object3D => object !== null);
|
|
499
|
+
if (objects.length === 0) return false;
|
|
500
|
+
if (objects.every((object) => (object as THREE.Bone).isBone)) {
|
|
501
|
+
this.frameBones(objects as THREE.Bone[]);
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
this.viewport.focusOnMultiple(objects);
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* An AXIS view is ORTHOGRAPHIC, the way Blender's numpad 1/3/7 are: the
|
|
510
|
+
* reference frame for numpad 1 (`modeling-front-ortho.png`) says so in its
|
|
511
|
+
* own view text, "Front Orthographic". Ours left the stage in perspective,
|
|
512
|
+
* so a front view still showed the cube's side faces converging.
|
|
513
|
+
*
|
|
514
|
+
* `isometric` is the perspective preset (it is the stage's 3/4 opening
|
|
515
|
+
* direction, not an axis) and stays perspective. Both callers of this door
|
|
516
|
+
* already separate the two: the relay's `view-preset` maps its own
|
|
517
|
+
* `perspective` onto `isometric`, and the document toolbar's view menu
|
|
518
|
+
* carries the projection pair as its own second group — so nothing here
|
|
519
|
+
* reads an axis preset expecting perspective.
|
|
520
|
+
*
|
|
521
|
+
* The pose is still solved against the PERSPECTIVE camera, because the
|
|
522
|
+
* session's orthographic camera is derived from that pose every frame
|
|
523
|
+
* (`syncOrthographicCamera`) rather than posed independently.
|
|
524
|
+
*/
|
|
525
|
+
setViewPreset(preset: ModelCameraPreset): void {
|
|
526
|
+
this.settleFlight('superseded');
|
|
527
|
+
const bounds = this.resolveFrameBounds();
|
|
528
|
+
if (bounds.isEmpty()) return;
|
|
529
|
+
const center = bounds.getCenter(new THREE.Vector3());
|
|
530
|
+
const direction = cameraPresetDirection(preset);
|
|
531
|
+
const distance = perspectiveDistanceToFitBox(bounds, this.viewport.camera, direction);
|
|
532
|
+
const position = center.clone().addScaledVector(direction, distance);
|
|
533
|
+
this.viewport.camera.up.set(0, preset === 'top' ? 0 : 1, preset === 'top' ? -1 : 0);
|
|
534
|
+
this.viewport.setPose(position, center);
|
|
535
|
+
this.setProjection(preset === 'isometric' ? 'perspective' : 'orthographic');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
setCameraPose(
|
|
539
|
+
position: { x: number; y: number; z: number },
|
|
540
|
+
target: { x: number; y: number; z: number },
|
|
541
|
+
fov?: number,
|
|
542
|
+
): void {
|
|
543
|
+
this.settleFlight('superseded');
|
|
544
|
+
this.viewport.setPose(position, target, fov);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
cameraPose(): {
|
|
548
|
+
readonly position: [number, number, number];
|
|
549
|
+
readonly target: [number, number, number];
|
|
550
|
+
readonly fov: number;
|
|
551
|
+
} {
|
|
552
|
+
return {
|
|
553
|
+
position: this.viewport.camera.position.toArray(),
|
|
554
|
+
target: this.viewport.orbitControls.target.toArray(),
|
|
555
|
+
fov: this.viewport.camera.fov,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
camera(): THREE.Camera {
|
|
560
|
+
if (this.cameraOverride) return this.cameraOverride;
|
|
561
|
+
if (this.state.projection === 'perspective') return this.viewport.camera;
|
|
562
|
+
this.syncOrthographicCamera();
|
|
563
|
+
return this.orthographicCamera;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Which projection the session is drawing with. Public because a caller
|
|
567
|
+
* that switches it for ONE frame — the Blender render capture — has to put
|
|
568
|
+
* it back, and cannot read it otherwise. */
|
|
569
|
+
projection(): 'perspective' | 'orthographic' {
|
|
570
|
+
return this.state.projection;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
setProjection(projection: 'perspective' | 'orthographic'): void {
|
|
574
|
+
if (this.state.projection === projection) return;
|
|
575
|
+
this.state = { ...this.state, projection };
|
|
576
|
+
this.notify();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
lightingAvailable(): boolean {
|
|
580
|
+
return this.editorLights.length > 0;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
setLighting(lighting: 'studio' | 'outdoor' | 'flat'): void {
|
|
584
|
+
if (this.state.lighting === lighting) return;
|
|
585
|
+
this.setLightingValues(lighting);
|
|
586
|
+
this.state = { ...this.state, lighting };
|
|
587
|
+
this.notify();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
setMode(mode: Object3DDocumentViewMode): void {
|
|
591
|
+
if (this.state.mode === mode) return;
|
|
592
|
+
this.clearDiagnosticPresentation();
|
|
593
|
+
this.state = { ...this.state, mode };
|
|
594
|
+
if (mode === 'uv') {
|
|
595
|
+
this.scene.overrideMaterial = new THREE.ShaderMaterial({
|
|
596
|
+
vertexShader:
|
|
597
|
+
'varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }',
|
|
598
|
+
fragmentShader: 'varying vec2 vUv; void main(){ gl_FragColor=vec4(vUv,0.0,1.0); }',
|
|
599
|
+
});
|
|
600
|
+
} else if (mode === 'vertex-colors') {
|
|
601
|
+
this.scene.overrideMaterial = new THREE.MeshBasicMaterial({ vertexColors: true });
|
|
602
|
+
} else if (mode === 'wireframe') {
|
|
603
|
+
this.buildTopologyOverlay();
|
|
604
|
+
}
|
|
605
|
+
this.notify();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* WIREFRAME ON THE MODELING SURFACE IS TOPOLOGY, NOT TRIANGLES.
|
|
610
|
+
*
|
|
611
|
+
* The shared `wireframe` shading mode flips every material's
|
|
612
|
+
* `wireframe` flag, which draws each RENDERABLE triangle — so a quad shows
|
|
613
|
+
* its triangulation diagonal and a modeller reads noise where they expect
|
|
614
|
+
* face flow. On the Asset Lab document viewport, where the mesh IS the
|
|
615
|
+
* subject, the mode instead draws neutral clay bodies (so wires behind the
|
|
616
|
+
* form are correctly occluded) under an edge overlay built with
|
|
617
|
+
* `EdgesGeometry`: coplanar diagonals fall out, quads read as quads.
|
|
618
|
+
*
|
|
619
|
+
* The overlay is editor chrome — tagged `editorHelper`, so picking, bakes
|
|
620
|
+
* and the shading swap itself all skip it, and gizmos/outlines are
|
|
621
|
+
* untouched. One honest limit: a line set follows its mesh's NODE transform,
|
|
622
|
+
* not its skin, so a skinned mesh shows rest-pose edges while a clip plays.
|
|
623
|
+
* This surface holds content time still by policy, so that is the rare case.
|
|
624
|
+
*/
|
|
625
|
+
private buildTopologyOverlay(): void {
|
|
626
|
+
const group = new THREE.Group();
|
|
627
|
+
const followers: Array<{ line: THREE.LineSegments; source: THREE.Object3D }> = [];
|
|
628
|
+
this.root.updateMatrixWorld(true);
|
|
629
|
+
this.root.traverse((object) => {
|
|
630
|
+
const mesh = object as THREE.Mesh;
|
|
631
|
+
if (!mesh.isMesh || !mesh.geometry) return;
|
|
632
|
+
const line = new THREE.LineSegments(
|
|
633
|
+
new THREE.EdgesGeometry(mesh.geometry, TOPOLOGY_EDGE_THRESHOLD_DEGREES),
|
|
634
|
+
// Dark on the light clay body, and DEPTH-TEST OFF. An edge sits
|
|
635
|
+
// exactly on the surface it bounds, so a depth-tested line stipples
|
|
636
|
+
// itself away in z-fighting — photographed and read before this was
|
|
637
|
+
// written. Off is also the truer read: Blender's wireframe shading
|
|
638
|
+
// shows the far side too, and the far side is half the topology.
|
|
639
|
+
new THREE.LineBasicMaterial({
|
|
640
|
+
color: 0x11161d,
|
|
641
|
+
toneMapped: false,
|
|
642
|
+
transparent: true,
|
|
643
|
+
opacity: 0.85,
|
|
644
|
+
depthTest: false,
|
|
645
|
+
depthWrite: false,
|
|
646
|
+
}),
|
|
647
|
+
);
|
|
648
|
+
line.matrixAutoUpdate = false;
|
|
649
|
+
line.matrixWorldAutoUpdate = false;
|
|
650
|
+
line.matrixWorld.copy(mesh.matrixWorld);
|
|
651
|
+
line.frustumCulled = false;
|
|
652
|
+
line.renderOrder = 2;
|
|
653
|
+
group.add(line);
|
|
654
|
+
followers.push({ line, source: mesh });
|
|
655
|
+
});
|
|
656
|
+
if (followers.length === 0) return;
|
|
657
|
+
this.topologyOverlay = group;
|
|
658
|
+
this.topologyFollowers = followers;
|
|
659
|
+
this.addHelper(group, 'topology');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private syncTopologyOverlay(): void {
|
|
663
|
+
// A live module document keeps this session and SWAPS the built child on
|
|
664
|
+
// every save, which detaches the meshes these lines shadow. Rebuild from
|
|
665
|
+
// the current graph rather than drawing the previous revision's edges.
|
|
666
|
+
if (this.topologyFollowers.some(({ source }) => source.parent === null)) {
|
|
667
|
+
this.clearTopologyOverlay();
|
|
668
|
+
this.buildTopologyOverlay();
|
|
669
|
+
}
|
|
670
|
+
for (const { line, source } of this.topologyFollowers) {
|
|
671
|
+
line.matrixWorld.copy(source.matrixWorld);
|
|
672
|
+
line.visible = source.visible;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private clearTopologyOverlay(): void {
|
|
677
|
+
for (const { line } of this.topologyFollowers.splice(0)) {
|
|
678
|
+
line.geometry.dispose();
|
|
679
|
+
(line.material as THREE.Material).dispose();
|
|
680
|
+
}
|
|
681
|
+
const group = this.topologyOverlay;
|
|
682
|
+
this.topologyOverlay = null;
|
|
683
|
+
if (!group) return;
|
|
684
|
+
group.removeFromParent();
|
|
685
|
+
const index = this.helpers.indexOf(group);
|
|
686
|
+
if (index >= 0) this.helpers.splice(index, 1);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
setBounds(bounds: boolean): void {
|
|
690
|
+
if (this.state.bounds === bounds) return;
|
|
691
|
+
this.state = { ...this.state, bounds };
|
|
692
|
+
if (bounds) {
|
|
693
|
+
this.boundsHelper = new THREE.BoxHelper(this.root, 0xffb454);
|
|
694
|
+
this.addHelper(this.boundsHelper, 'bounds');
|
|
695
|
+
} else {
|
|
696
|
+
this.clearBoundsHelper();
|
|
697
|
+
}
|
|
698
|
+
this.notify();
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
setSkeleton(skeleton: boolean): void {
|
|
702
|
+
if (this.state.skeleton === skeleton) return;
|
|
703
|
+
this.state = { ...this.state, skeleton };
|
|
704
|
+
this.refreshSkeletonHelper();
|
|
705
|
+
this.notify();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
setGrid(grid: boolean): void {
|
|
709
|
+
if (this.state.grid === grid) return;
|
|
710
|
+
this.viewport.grid.visible = grid;
|
|
711
|
+
this.state = { ...this.state, grid };
|
|
712
|
+
this.notify();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** The neutral backdrop as installed — what a re-tint replaces. */
|
|
716
|
+
neutralBackgroundTexture(): THREE.Color | THREE.Texture | null {
|
|
717
|
+
return this.neutralBackground;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Re-tint the neutral backdrop (a palette switch re-derives the dressing's
|
|
721
|
+
* gradient, `standard-viewport-dressing.ts`). Repaints when it is showing. */
|
|
722
|
+
setNeutralBackground(background: THREE.Color | THREE.Texture | null): void {
|
|
723
|
+
const showing = this.scene.background === this.neutralBackground;
|
|
724
|
+
this.neutralBackground = background;
|
|
725
|
+
if (showing) this.scene.background = background;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
setBackground(background: 'neutral' | 'transparent'): void {
|
|
729
|
+
if (this.state.background === background) return;
|
|
730
|
+
this.scene.background = background === 'transparent' ? null : this.neutralBackground;
|
|
731
|
+
this.state = { ...this.state, background };
|
|
732
|
+
this.notify();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
setExposure(exposure: number): void {
|
|
736
|
+
if (!Number.isFinite(exposure) || exposure < 0.2 || exposure > 2) return;
|
|
737
|
+
if (this.state.exposure === exposure) return;
|
|
738
|
+
this.renderer.toneMappingExposure = exposure;
|
|
739
|
+
this.state = { ...this.state, exposure };
|
|
740
|
+
this.notify();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
resetPresentation(): void {
|
|
744
|
+
this.clearDiagnosticPresentation();
|
|
745
|
+
this.state = INITIAL_PRESENTATION;
|
|
746
|
+
this.refreshSkeletonHelper();
|
|
747
|
+
this.clearBoundsHelper();
|
|
748
|
+
this.viewport.grid.visible = true;
|
|
749
|
+
this.scene.background = this.neutralBackground;
|
|
750
|
+
this.renderer.toneMappingExposure = 1;
|
|
751
|
+
this.setLightingValues('studio');
|
|
752
|
+
this.viewport.camera.up.set(0, 1, 0);
|
|
753
|
+
this.frame();
|
|
754
|
+
this.notify();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* The step the document host runs before EVERY draw of the rendered scene.
|
|
759
|
+
*
|
|
760
|
+
* The host keeps the document's own `THREE.Scene` nested inside the session's
|
|
761
|
+
* rendered scene and mirrors the world dressing (environment, fog, their
|
|
762
|
+
* intensities/rotations, background) outward. That mirror used to live in the
|
|
763
|
+
* host's per-frame `animate`, so an offscreen photograph taken between two
|
|
764
|
+
* viewport ticks drew whatever the LAST tick happened to mirror — a sky set
|
|
765
|
+
* synchronously before the capture was simply missing from it. Registering it
|
|
766
|
+
* here puts `renderViewport` and `captureImage` on one path.
|
|
767
|
+
*
|
|
768
|
+
* The host owns the step and clears it in its own teardown; the session only
|
|
769
|
+
* holds the reference.
|
|
770
|
+
*/
|
|
771
|
+
setBeforeRender(step: (() => void) | null): void {
|
|
772
|
+
this.beforeRender = step;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* THE PRESENTED FRAME — the stage exactly as the person is seeing it,
|
|
777
|
+
* including everything drawn OVER the document's own render: the
|
|
778
|
+
* orientation compass (`EditorViewport.renderViewCube`) above all.
|
|
779
|
+
*
|
|
780
|
+
* Why this is not {@link captureImage}: that door is an offscreen
|
|
781
|
+
* re-render of the document's content, for thumbnails and asset previews,
|
|
782
|
+
* and it deliberately draws the subject alone. The chrome door's promise is
|
|
783
|
+
* the opposite one — "photograph the editor as the person sees it" — and
|
|
784
|
+
* routing it through `captureImage` made the door LIE about this stage: the
|
|
785
|
+
* compass renders every frame into the default framebuffer (measured: the
|
|
786
|
+
* call is reached, `_threeSurfaceShowing` true, 24 objects, target=screen)
|
|
787
|
+
* and never appeared in a single chrome frame.
|
|
788
|
+
*
|
|
789
|
+
* The canvas has no `preserveDrawingBuffer`, so the only place its pixels
|
|
790
|
+
* can be read is inside the frame that drew them — which is why the host
|
|
791
|
+
* serves the request at the end of its own `animate`
|
|
792
|
+
* ({@link servePresentedFrame}), the same shape the ingest lane's
|
|
793
|
+
* `LiveSession.snapshotFrame` uses.
|
|
794
|
+
*/
|
|
795
|
+
private presentedFrameWaiters: Array<(frame: HTMLCanvasElement | null) => void> = [];
|
|
796
|
+
/** True while a host render loop is servicing {@link servePresentedFrame}. */
|
|
797
|
+
private presentsFrames = false;
|
|
798
|
+
|
|
799
|
+
/** The host declares that its loop serves presented-frame requests. Without
|
|
800
|
+
* it a request answers `null` at once rather than waiting for a frame that
|
|
801
|
+
* is never coming (a document whose loop has stopped). */
|
|
802
|
+
setPresentsFrames(on: boolean): void {
|
|
803
|
+
this.presentsFrames = on;
|
|
804
|
+
if (!on) this.resolvePresentedFrame(null);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Ask for the next drawn frame. `null` when no loop is serving them, and
|
|
808
|
+
* `null` again when a loop that says it serves them does not draw within
|
|
809
|
+
* {@link PRESENTED_FRAME_WAIT_MS} — a hidden tab's rAF is throttled to a
|
|
810
|
+
* stop, and a photograph door may never hang on another loop's liveness. */
|
|
811
|
+
requestPresentedFrame(): Promise<HTMLCanvasElement | null> {
|
|
812
|
+
if (!this.presentsFrames) return Promise.resolve(null);
|
|
813
|
+
return new Promise((resolve) => {
|
|
814
|
+
let settled = false;
|
|
815
|
+
const once = (frame: HTMLCanvasElement | null): void => {
|
|
816
|
+
if (settled) return;
|
|
817
|
+
settled = true;
|
|
818
|
+
resolve(frame);
|
|
819
|
+
};
|
|
820
|
+
this.presentedFrameWaiters.push(once);
|
|
821
|
+
setTimeout(() => once(null), PRESENTED_FRAME_WAIT_MS);
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** Called by the host at the END of a frame, after every overlay pass. */
|
|
826
|
+
servePresentedFrame(): void {
|
|
827
|
+
if (this.presentedFrameWaiters.length === 0) return;
|
|
828
|
+
const source = this.renderer.domElement;
|
|
829
|
+
const copy = document.createElement('canvas');
|
|
830
|
+
copy.width = Math.max(1, source.width);
|
|
831
|
+
copy.height = Math.max(1, source.height);
|
|
832
|
+
const context = copy.getContext('2d');
|
|
833
|
+
if (!context) {
|
|
834
|
+
this.resolvePresentedFrame(null);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
context.drawImage(source, 0, 0);
|
|
838
|
+
this.resolvePresentedFrame(copy);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
private resolvePresentedFrame(frame: HTMLCanvasElement | null): void {
|
|
842
|
+
const waiters = this.presentedFrameWaiters;
|
|
843
|
+
this.presentedFrameWaiters = [];
|
|
844
|
+
for (const resolve of waiters) resolve(frame);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
render(renderSolid: (camera: THREE.Camera) => void): void {
|
|
848
|
+
this.beforeRender?.();
|
|
849
|
+
this.boneSelectionHighlight?.update();
|
|
850
|
+
const camera = this.camera();
|
|
851
|
+
const mode = this.state.mode;
|
|
852
|
+
this.boundsHelper?.update();
|
|
853
|
+
if (mode === 'uv' || mode === 'vertex-colors') {
|
|
854
|
+
renderSolid(camera);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (mode === 'wireframe' && this.topologyOverlay) {
|
|
858
|
+
this.syncTopologyOverlay();
|
|
859
|
+
// Clay bodies, not the triangle-wireframe swap: the overlay IS the wire,
|
|
860
|
+
// and the solid form behind it is what occludes the far side.
|
|
861
|
+
this.shading.render(
|
|
862
|
+
this.scene,
|
|
863
|
+
'clay',
|
|
864
|
+
() => renderSolid(camera),
|
|
865
|
+
isEditorViewportShadingTarget,
|
|
866
|
+
);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
this.shading.render(this.scene, mode, () => renderSolid(camera), isEditorViewportShadingTarget);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** Visible authoring draw with the editor-only native selection silhouette. */
|
|
873
|
+
renderViewport(deltaSeconds = 0): void {
|
|
874
|
+
this.advanceLook(deltaSeconds);
|
|
875
|
+
this.ensureComposer();
|
|
876
|
+
this.render((camera) => {
|
|
877
|
+
const composer = this.composer;
|
|
878
|
+
if (!composer) {
|
|
879
|
+
// The composer's library is still in flight (see `ensureComposer`).
|
|
880
|
+
// Draw straight through — the same call the offscreen capture makes —
|
|
881
|
+
// so the opening frames show the asset without its selection
|
|
882
|
+
// silhouette rather than nothing at all.
|
|
883
|
+
this.renderer.render(this.scene, camera);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
composer.setMainScene(this.scene);
|
|
887
|
+
composer.setMainCamera(camera);
|
|
888
|
+
composer.render(deltaSeconds);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
resize(width: number, height: number): void {
|
|
893
|
+
this.renderWidth = Math.max(1, width);
|
|
894
|
+
this.renderHeight = Math.max(1, height);
|
|
895
|
+
this.composer?.setSize(this.renderWidth, this.renderHeight);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private renderCapture(renderSolid: (camera: THREE.Camera) => void, transparent: boolean): void {
|
|
899
|
+
this.render((camera) => {
|
|
900
|
+
if (!transparent) {
|
|
901
|
+
renderSolid(camera);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
// Apply after the host mirrors its world into this scene. Environment
|
|
905
|
+
// lighting stays intact; only the camera background is omitted.
|
|
906
|
+
const background = this.scene.background;
|
|
907
|
+
const clearColor = this.renderer.getClearColor(new THREE.Color());
|
|
908
|
+
const clearAlpha = this.renderer.getClearAlpha();
|
|
909
|
+
try {
|
|
910
|
+
this.scene.background = null;
|
|
911
|
+
this.renderer.setClearColor(0, 0);
|
|
912
|
+
renderSolid(camera);
|
|
913
|
+
} finally {
|
|
914
|
+
this.scene.background = background;
|
|
915
|
+
this.renderer.setClearColor(clearColor, clearAlpha);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Fresh offscreen capture through this document's own renderer and
|
|
922
|
+
* diagnostic pipeline. This does not depend on preserveDrawingBuffer and
|
|
923
|
+
* therefore remains truthful after the visible canvas has presented.
|
|
924
|
+
*
|
|
925
|
+
* A NUMBER is a square of that size — the default shape, and the right one
|
|
926
|
+
* for an unstaged look at a model. `{width, height}` renders the buffer AND
|
|
927
|
+
* the camera at that aspect, so a video-shaped look comes back already
|
|
928
|
+
* shaped instead of square-and-cropped. Whatever the shape, the camera is
|
|
929
|
+
* matched to the BUFFER for the duration: rendering the panel's aspect
|
|
930
|
+
* through a square buffer is what photographed a cube as a tall thin prism
|
|
931
|
+
* on a 3.05:1 document panel, through the one look instrument an agent has.
|
|
932
|
+
* The visible viewport is never touched.
|
|
933
|
+
*/
|
|
934
|
+
captureImage(
|
|
935
|
+
size:
|
|
936
|
+
| number
|
|
937
|
+
| {
|
|
938
|
+
width: number;
|
|
939
|
+
height: number;
|
|
940
|
+
transparent?: boolean;
|
|
941
|
+
/** Photograph through this caller-owned camera without changing its projection. */
|
|
942
|
+
camera?: THREE.Camera;
|
|
943
|
+
} = 512,
|
|
944
|
+
): string | null {
|
|
945
|
+
const requestedWidth = typeof size === 'number' ? size : size.width;
|
|
946
|
+
const requestedHeight = typeof size === 'number' ? size : size.height;
|
|
947
|
+
if (!Number.isFinite(requestedWidth) || requestedWidth < 1) return null;
|
|
948
|
+
if (!Number.isFinite(requestedHeight) || requestedHeight < 1) return null;
|
|
949
|
+
const outputWidth = Math.min(2048, Math.round(requestedWidth));
|
|
950
|
+
const outputHeight = Math.min(2048, Math.round(requestedHeight));
|
|
951
|
+
const renderWidth = outputWidth * 2;
|
|
952
|
+
const renderHeight = outputHeight * 2;
|
|
953
|
+
// Three disables material tone mapping on ordinary render targets. A
|
|
954
|
+
// byte target clips lit surfaces to white before display conversion can
|
|
955
|
+
// recover them. Keep HDR values, then use the same output resolve as the
|
|
956
|
+
// scene viewport so exposure, tone mapping and color space affect captures.
|
|
957
|
+
const sceneTarget = new THREE.WebGLRenderTarget(renderWidth, renderHeight, {
|
|
958
|
+
type: THREE.HalfFloatType,
|
|
959
|
+
});
|
|
960
|
+
const target = new THREE.WebGLRenderTarget(renderWidth, renderHeight);
|
|
961
|
+
const previousTarget = this.renderer.getRenderTarget();
|
|
962
|
+
const previousPixelRatio = this.renderer.getPixelRatio();
|
|
963
|
+
const perspective = this.viewport.camera;
|
|
964
|
+
const previousAspect = perspective.aspect;
|
|
965
|
+
try {
|
|
966
|
+
this.captureAspect = outputWidth / outputHeight;
|
|
967
|
+
const override = typeof size === 'number' ? undefined : size.camera;
|
|
968
|
+
if (override) this.cameraOverride = override;
|
|
969
|
+
else {
|
|
970
|
+
perspective.aspect = outputWidth / outputHeight;
|
|
971
|
+
perspective.updateProjectionMatrix();
|
|
972
|
+
}
|
|
973
|
+
this.renderer.setPixelRatio(1);
|
|
974
|
+
this.renderer.setRenderTarget(sceneTarget);
|
|
975
|
+
this.renderCapture(
|
|
976
|
+
(camera) => this.renderSolidForCapture(camera, sceneTarget, renderWidth, renderHeight),
|
|
977
|
+
typeof size !== 'number' && size.transparent === true,
|
|
978
|
+
);
|
|
979
|
+
viewportCaptureOutputPass(typeof size !== 'number' && size.transparent === true).render(
|
|
980
|
+
this.renderer,
|
|
981
|
+
target,
|
|
982
|
+
sceneTarget,
|
|
983
|
+
0,
|
|
984
|
+
false,
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
const rowBytes = renderWidth * 4;
|
|
988
|
+
const pixels = new Uint8Array(rowBytes * renderHeight);
|
|
989
|
+
this.renderer.readRenderTargetPixels(target, 0, 0, renderWidth, renderHeight, pixels);
|
|
990
|
+
const fullCanvas = document.createElement('canvas');
|
|
991
|
+
fullCanvas.width = renderWidth;
|
|
992
|
+
fullCanvas.height = renderHeight;
|
|
993
|
+
const fullContext = fullCanvas.getContext('2d');
|
|
994
|
+
if (!fullContext) return null;
|
|
995
|
+
const imageData = fullContext.createImageData(renderWidth, renderHeight);
|
|
996
|
+
for (let y = 0; y < renderHeight; y++) {
|
|
997
|
+
const sourceRow = (renderHeight - 1 - y) * rowBytes;
|
|
998
|
+
const destinationRow = y * rowBytes;
|
|
999
|
+
imageData.data.set(pixels.subarray(sourceRow, sourceRow + rowBytes), destinationRow);
|
|
1000
|
+
}
|
|
1001
|
+
fullContext.putImageData(imageData, 0, 0);
|
|
1002
|
+
const output = document.createElement('canvas');
|
|
1003
|
+
output.width = outputWidth;
|
|
1004
|
+
output.height = outputHeight;
|
|
1005
|
+
const outputContext = output.getContext('2d');
|
|
1006
|
+
if (!outputContext) return null;
|
|
1007
|
+
outputContext.drawImage(fullCanvas, 0, 0, outputWidth, outputHeight);
|
|
1008
|
+
return output.toDataURL('image/png');
|
|
1009
|
+
} finally {
|
|
1010
|
+
this.captureAspect = null;
|
|
1011
|
+
this.cameraOverride = null;
|
|
1012
|
+
perspective.aspect = previousAspect;
|
|
1013
|
+
perspective.updateProjectionMatrix();
|
|
1014
|
+
this.renderer.setRenderTarget(previousTarget);
|
|
1015
|
+
this.renderer.setPixelRatio(previousPixelRatio);
|
|
1016
|
+
target.dispose();
|
|
1017
|
+
sceneTarget.dispose();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
dispose(): void {
|
|
1022
|
+
this.disposed = true;
|
|
1023
|
+
this.settleFlight('closed');
|
|
1024
|
+
this.viewport.orbitControls.removeEventListener('start', this.cancelLookForHuman);
|
|
1025
|
+
this.unsubscribeSelectionTheme();
|
|
1026
|
+
this.clearDiagnosticPresentation();
|
|
1027
|
+
this.clearBoneSelectionHighlight();
|
|
1028
|
+
this.clearBoundsHelper();
|
|
1029
|
+
this.clearSkeletonHelper();
|
|
1030
|
+
// OutlineEffect's Selection owns temporary render-layer bits on every
|
|
1031
|
+
// target, so clear it before disposing the pass resources.
|
|
1032
|
+
this.selectionOutline?.selection.clear();
|
|
1033
|
+
this.composer?.dispose();
|
|
1034
|
+
this.sceneRenderPass = null;
|
|
1035
|
+
this.selectionOutline = null;
|
|
1036
|
+
this.selectionOutlinePass = null;
|
|
1037
|
+
this.composer = null;
|
|
1038
|
+
this.shading.dispose();
|
|
1039
|
+
this.listeners.clear();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private addHelper(helper: THREE.Object3D, kind: string): void {
|
|
1043
|
+
helper.name = `__object3d_document_${kind}`;
|
|
1044
|
+
setUserData(helper, 'editorHelper', true);
|
|
1045
|
+
this.scene.add(helper);
|
|
1046
|
+
this.helpers.push(helper);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* THE LAZY DOOR for `postprocessing` (~200 kB of passes and shaders).
|
|
1051
|
+
*
|
|
1052
|
+
* The composer exists for ONE feature — the editor-owned selection
|
|
1053
|
+
* silhouette; `syncComposerOutput` hands the screen back to the plain scene
|
|
1054
|
+
* pass whenever nothing is outlined — so the library belongs with the first
|
|
1055
|
+
* frame of this document, not with the module that draws it. Everything an
|
|
1056
|
+
* Asset Lab document does before that first frame (mount, adopt, frame the
|
|
1057
|
+
* content) is untouched by it.
|
|
1058
|
+
*
|
|
1059
|
+
* Called from every `renderViewport`, so it must be cheap and re-entrant:
|
|
1060
|
+
* `composerLoading` keeps exactly one import in flight, and the continuation
|
|
1061
|
+
* re-checks `disposed` because a document can close mid-load.
|
|
1062
|
+
*/
|
|
1063
|
+
/**
|
|
1064
|
+
* WHETHER THIS DOCUMENT DRAWS THE SELECTION SILHOUETTE AT ALL — a LIVE
|
|
1065
|
+
* switch, not just a construction-time one.
|
|
1066
|
+
*
|
|
1067
|
+
* Two callers turn it off, for different reasons. The inspector's preview
|
|
1068
|
+
* lane shows a subject with nothing selectable inside it, so it never needs
|
|
1069
|
+
* the pass — or the effect it would build per selection — and says so once.
|
|
1070
|
+
* A modeling document says so whenever it is in EDIT mode: Blender draws no
|
|
1071
|
+
* object outline there (`modeling-edit-all.png`), and the silhouette
|
|
1072
|
+
* otherwise follows the hierarchy selection straight through the mode
|
|
1073
|
+
* change. That second caller flips it while the composer is already built,
|
|
1074
|
+
* so turning it off must drop the outline the composer is currently drawing
|
|
1075
|
+
* — gating construction alone leaves the frame exactly as it was.
|
|
1076
|
+
*/
|
|
1077
|
+
get selectionOutlineEnabled(): boolean {
|
|
1078
|
+
return this.selectionOutlineWanted;
|
|
1079
|
+
}
|
|
1080
|
+
set selectionOutlineEnabled(value: boolean) {
|
|
1081
|
+
if (value === this.selectionOutlineWanted) return;
|
|
1082
|
+
this.selectionOutlineWanted = value;
|
|
1083
|
+
if (value) this.syncSelectionPresentation();
|
|
1084
|
+
else {
|
|
1085
|
+
this.selectionOutline?.selection.clear();
|
|
1086
|
+
this.syncComposerOutput();
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
private selectionOutlineWanted = true;
|
|
1090
|
+
|
|
1091
|
+
private ensureComposer(): void {
|
|
1092
|
+
if (!this.selectionOutlineWanted) return;
|
|
1093
|
+
if (this.composer || this.composerLoading || this.disposed) return;
|
|
1094
|
+
this.composerLoading = true;
|
|
1095
|
+
void import('postprocessing')
|
|
1096
|
+
.then((postprocessing) => {
|
|
1097
|
+
this.composerLoading = false;
|
|
1098
|
+
if (this.disposed || this.composer) return;
|
|
1099
|
+
const camera = this.camera();
|
|
1100
|
+
const composer = new postprocessing.EffectComposer(this.renderer);
|
|
1101
|
+
this.sceneRenderPass = new postprocessing.RenderPass(this.scene, camera);
|
|
1102
|
+
composer.addPass(this.sceneRenderPass);
|
|
1103
|
+
this.selectionOutline = createThreeSelectionOutline(
|
|
1104
|
+
postprocessing,
|
|
1105
|
+
this.scene,
|
|
1106
|
+
camera,
|
|
1107
|
+
nativeSelectionColors(this.renderer.domElement),
|
|
1108
|
+
);
|
|
1109
|
+
this.syncSelectionTheme();
|
|
1110
|
+
syncThreeSelectionOutline(this.selectionOutline, this.selectedObjects);
|
|
1111
|
+
this.selectionOutlinePass = new postprocessing.EffectPass(camera, this.selectionOutline);
|
|
1112
|
+
composer.addPass(this.selectionOutlinePass);
|
|
1113
|
+
this.composer = composer;
|
|
1114
|
+
this.syncComposerOutput();
|
|
1115
|
+
composer.setSize(this.renderWidth, this.renderHeight);
|
|
1116
|
+
})
|
|
1117
|
+
.catch(() => {
|
|
1118
|
+
// A failed chunk load must not wedge the surface: the plain draw path
|
|
1119
|
+
// in `renderViewport` keeps the document visible, and the next frame
|
|
1120
|
+
// retries the import.
|
|
1121
|
+
this.composerLoading = false;
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* EffectComposer marks the last added pass as the screen output. Disabling
|
|
1127
|
+
* that pass without transferring screen ownership to the scene pass leaves
|
|
1128
|
+
* the unoutlined frame stranded in the composer's offscreen buffer. Keep
|
|
1129
|
+
* selection responsible only for the outline, never for whether the asset
|
|
1130
|
+
* itself reaches the canvas.
|
|
1131
|
+
*/
|
|
1132
|
+
/**
|
|
1133
|
+
* Draw the scene FOR A PHOTOGRAPH into `target` the way the screen sees it:
|
|
1134
|
+
* through the outline pass when a selection silhouette is on (the outline is
|
|
1135
|
+
* a pass, absent from a plain draw — a capture without it lied about the
|
|
1136
|
+
* screen), the plain draw otherwise.
|
|
1137
|
+
*
|
|
1138
|
+
* The two passes are driven DIRECTLY rather than through `composer.render()`,
|
|
1139
|
+
* because neither of the composer's own doors says where a frame landed:
|
|
1140
|
+
* `EffectComposer.render` ping-pongs LOCAL variables and never reassigns
|
|
1141
|
+
* `inputBuffer`/`outputBuffer`, and `CopyPass.render` ignores its
|
|
1142
|
+
* `outputBuffer` argument entirely (it always writes to its own render
|
|
1143
|
+
* target, 1x1 unless the composer sized it). Going through them wrote the
|
|
1144
|
+
* photograph nowhere — every capture of a selected object came back empty.
|
|
1145
|
+
* A pass's own contract is exact: `RenderPass` draws the scene into the
|
|
1146
|
+
* `inputBuffer` it is handed, `EffectPass` reads that and writes the
|
|
1147
|
+
* `outputBuffer` it is handed — so hand the outline pass the capture target.
|
|
1148
|
+
*/
|
|
1149
|
+
private renderSolidForCapture(
|
|
1150
|
+
camera: THREE.Camera,
|
|
1151
|
+
target: THREE.WebGLRenderTarget,
|
|
1152
|
+
width: number,
|
|
1153
|
+
height: number,
|
|
1154
|
+
): void {
|
|
1155
|
+
const composer = this.composer;
|
|
1156
|
+
const outlined = (this.selectionOutline?.selection.size ?? 0) > 0;
|
|
1157
|
+
if (!composer || !outlined || !this.sceneRenderPass || !this.selectionOutlinePass) {
|
|
1158
|
+
this.renderer.render(this.scene, camera);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const previousWidth = this.renderWidth;
|
|
1162
|
+
const previousHeight = this.renderHeight;
|
|
1163
|
+
try {
|
|
1164
|
+
this.sceneRenderPass.renderToScreen = false;
|
|
1165
|
+
this.selectionOutlinePass.renderToScreen = false;
|
|
1166
|
+
composer.setSize(width, height, false);
|
|
1167
|
+
this.sceneRenderPass.render(this.renderer, composer.inputBuffer, composer.outputBuffer, 0);
|
|
1168
|
+
this.selectionOutlinePass.render(this.renderer, composer.inputBuffer, target, 0);
|
|
1169
|
+
} finally {
|
|
1170
|
+
composer.setSize(previousWidth, previousHeight, false);
|
|
1171
|
+
this.syncComposerOutput();
|
|
1172
|
+
this.renderer.setRenderTarget(target);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
private syncComposerOutput(): void {
|
|
1177
|
+
const outlined = (this.selectionOutline?.selection.size ?? 0) > 0;
|
|
1178
|
+
if (this.sceneRenderPass) this.sceneRenderPass.renderToScreen = !outlined;
|
|
1179
|
+
if (this.selectionOutlinePass) {
|
|
1180
|
+
this.selectionOutlinePass.enabled = outlined;
|
|
1181
|
+
this.selectionOutlinePass.renderToScreen = outlined;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private refreshSkeletonHelper(): void {
|
|
1186
|
+
const visible = this.state.skeleton || this.boneSelectionHighlight !== null;
|
|
1187
|
+
if (visible && !this.skeletonHelper) {
|
|
1188
|
+
this.skeletonHelper = new THREE.SkeletonHelper(this.root);
|
|
1189
|
+
styleEditorSkeletonHelper(this.skeletonHelper);
|
|
1190
|
+
this.scene.add(this.skeletonHelper);
|
|
1191
|
+
} else if (!visible) {
|
|
1192
|
+
this.clearSkeletonHelper();
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
private clearSkeletonHelper(): void {
|
|
1197
|
+
if (!this.skeletonHelper) return;
|
|
1198
|
+
this.skeletonHelper.removeFromParent();
|
|
1199
|
+
this.skeletonHelper.dispose();
|
|
1200
|
+
this.skeletonHelper = null;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
private clearBoneSelectionHighlight(): void {
|
|
1204
|
+
if (!this.boneSelectionHighlight) return;
|
|
1205
|
+
this.boneSelectionHighlight.removeFromParent();
|
|
1206
|
+
this.boneSelectionHighlight.dispose();
|
|
1207
|
+
this.boneSelectionHighlight = null;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
private clearBoundsHelper(): void {
|
|
1211
|
+
if (!this.boundsHelper) return;
|
|
1212
|
+
const helper = this.boundsHelper;
|
|
1213
|
+
this.boundsHelper = null;
|
|
1214
|
+
const index = this.helpers.indexOf(helper);
|
|
1215
|
+
if (index >= 0) this.helpers.splice(index, 1);
|
|
1216
|
+
helper.removeFromParent();
|
|
1217
|
+
helper.geometry.dispose();
|
|
1218
|
+
if (Array.isArray(helper.material))
|
|
1219
|
+
helper.material.forEach((material) => {
|
|
1220
|
+
material.dispose();
|
|
1221
|
+
});
|
|
1222
|
+
else helper.material.dispose();
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
private frameBones(bones: readonly THREE.Bone[]): void {
|
|
1226
|
+
const bounds = new THREE.Box3();
|
|
1227
|
+
const point = new THREE.Vector3();
|
|
1228
|
+
for (const bone of bones) {
|
|
1229
|
+
bounds.expandByPoint(bone.getWorldPosition(point));
|
|
1230
|
+
if ((bone.parent as THREE.Bone | null)?.isBone) {
|
|
1231
|
+
bounds.expandByPoint(bone.parent!.getWorldPosition(point));
|
|
1232
|
+
}
|
|
1233
|
+
for (const child of bone.children) {
|
|
1234
|
+
if ((child as THREE.Bone).isBone) bounds.expandByPoint(child.getWorldPosition(point));
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const center = bounds.getCenter(new THREE.Vector3());
|
|
1238
|
+
const minimumSpan = Math.max(bounds.getSize(new THREE.Vector3()).length(), 0.2);
|
|
1239
|
+
bounds.expandByScalar(minimumSpan * 0.35);
|
|
1240
|
+
const direction = this.viewport.camera.position
|
|
1241
|
+
.clone()
|
|
1242
|
+
.sub(this.viewport.orbitControls.target)
|
|
1243
|
+
.normalize();
|
|
1244
|
+
const distance = perspectiveDistanceToFitBox(bounds, this.viewport.camera, direction);
|
|
1245
|
+
this.viewport.setPose(center.clone().addScaledVector(direction, distance), center);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
private syncOrthographicCamera(): void {
|
|
1249
|
+
const source = this.viewport.camera;
|
|
1250
|
+
const target = this.viewport.orbitControls.target;
|
|
1251
|
+
const distance = Math.max(source.position.distanceTo(target), 0.001);
|
|
1252
|
+
const halfHeight = Math.max(
|
|
1253
|
+
distance * Math.tan(THREE.MathUtils.degToRad(source.fov) / 2),
|
|
1254
|
+
0.001,
|
|
1255
|
+
);
|
|
1256
|
+
const width = Math.max(this.renderer.domElement.clientWidth, 1);
|
|
1257
|
+
const height = Math.max(this.renderer.domElement.clientHeight, 1);
|
|
1258
|
+
// The offscreen capture renders into its OWN buffer shape — see
|
|
1259
|
+
// `captureImage`; outside one, the panel's box is the aspect.
|
|
1260
|
+
const aspect = this.captureAspect ?? width / height;
|
|
1261
|
+
this.orthographicCamera.left = -halfHeight * aspect;
|
|
1262
|
+
this.orthographicCamera.right = halfHeight * aspect;
|
|
1263
|
+
this.orthographicCamera.top = halfHeight;
|
|
1264
|
+
this.orthographicCamera.bottom = -halfHeight;
|
|
1265
|
+
this.orthographicCamera.near = source.near;
|
|
1266
|
+
this.orthographicCamera.far = source.far;
|
|
1267
|
+
this.orthographicCamera.position.copy(source.position);
|
|
1268
|
+
this.orthographicCamera.quaternion.copy(source.quaternion);
|
|
1269
|
+
this.orthographicCamera.up.copy(source.up);
|
|
1270
|
+
this.orthographicCamera.layers.mask = source.layers.mask;
|
|
1271
|
+
this.orthographicCamera.updateProjectionMatrix();
|
|
1272
|
+
this.orthographicCamera.updateMatrixWorld(true);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
private setLightingValues(lighting: 'studio' | 'outdoor' | 'flat'): void {
|
|
1276
|
+
for (const light of this.editorLights) {
|
|
1277
|
+
if (light instanceof THREE.HemisphereLight) {
|
|
1278
|
+
light.intensity = lighting === 'flat' ? 2 : lighting === 'outdoor' ? 1.15 : 1.7;
|
|
1279
|
+
} else if (light instanceof THREE.DirectionalLight) {
|
|
1280
|
+
light.intensity = lighting === 'flat' ? 0 : lighting === 'outdoor' ? 2.2 : 2.4;
|
|
1281
|
+
// Every preset is NEUTRAL now, like the dressing's key this retunes
|
|
1282
|
+
// (`standard-viewport-dressing.ts` carries the measurement against
|
|
1283
|
+
// Blender's object-mode cube). Studio was `0xfff3dd`, so picking it in
|
|
1284
|
+
// the preset menu put the warm cast back on a document the dressing
|
|
1285
|
+
// had already lit white — and measured live, this whole function is
|
|
1286
|
+
// dead until someone touches that menu: the initial `lighting:
|
|
1287
|
+
// 'studio'` state never applies its own values, which is why changing
|
|
1288
|
+
// them moved nothing until the dressing changed too.
|
|
1289
|
+
light.color.set(0xffffff);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
private clearDiagnosticPresentation(): void {
|
|
1295
|
+
this.clearTopologyOverlay();
|
|
1296
|
+
this.scene.overrideMaterial?.dispose();
|
|
1297
|
+
this.scene.overrideMaterial = null;
|
|
1298
|
+
for (const helper of this.helpers.splice(0)) {
|
|
1299
|
+
helper.removeFromParent();
|
|
1300
|
+
const line = helper as THREE.Line;
|
|
1301
|
+
line.geometry?.dispose();
|
|
1302
|
+
const material = line.material;
|
|
1303
|
+
if (Array.isArray(material))
|
|
1304
|
+
material.forEach((entry) => {
|
|
1305
|
+
entry.dispose();
|
|
1306
|
+
});
|
|
1307
|
+
else material?.dispose();
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
private notify(): void {
|
|
1312
|
+
this.version++;
|
|
1313
|
+
for (const listener of this.listeners) listener();
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Compatibility for existing internal callers/tests; lookup-only consumers
|
|
1318
|
+
// import the registry directly so they do not pull this implementation graph.
|
|
1319
|
+
export {
|
|
1320
|
+
__resetObject3DDocumentSessionsForTest,
|
|
1321
|
+
allObject3DDocumentSessions,
|
|
1322
|
+
object3DDocumentSession,
|
|
1323
|
+
object3DDocumentSessionsVersion,
|
|
1324
|
+
registerObject3DDocumentSession,
|
|
1325
|
+
subscribeObject3DDocumentSessions,
|
|
1326
|
+
} from './object3d-document-session-registry';
|