@volter/sdk 0.0.0-stage → 0.5.203
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/LICENSE +202 -0
- package/NOTICE +20 -0
- package/README.md +38 -3
- package/package.json +510 -4
- package/src/account.ts +210 -0
- package/src/chrome.ts +88 -0
- package/src/client.ts +1646 -0
- package/src/commands.ts +66 -0
- package/src/contributions.ts +619 -0
- package/src/css-numeric-style.ts +97 -0
- package/src/document-probe.ts +282 -0
- package/src/editor-view.ts +225 -0
- package/src/extension.ts +40 -0
- package/src/generations.ts +178 -0
- package/src/host.ts +1157 -0
- package/src/http-transport.browser.ts +14 -0
- package/src/http-transport.node.ts +19 -0
- package/src/index.ts +131 -0
- package/src/kit/CapabilityCoverageSection.tsx +185 -0
- package/src/kit/account-client.ts +333 -0
- package/src/kit/action-registry.ts +317 -0
- package/src/kit/active-product.ts +76 -0
- package/src/kit/active-project.ts +155 -0
- package/src/kit/adapter-editor-config.ts +25 -0
- package/src/kit/adapter-module.ts +7 -0
- package/src/kit/adapter-observation.ts +49 -0
- package/src/kit/animation/animation-clock.ts +479 -0
- package/src/kit/animation/stage-transport.ts +385 -0
- package/src/kit/api/assets.ts +365 -0
- package/src/kit/api/project-open.ts +355 -0
- package/src/kit/api/project-source.ts +180 -0
- package/src/kit/api/project-state.ts +110 -0
- package/src/kit/api/relay.ts +270 -0
- package/src/kit/api/themes.ts +45 -0
- package/src/kit/api-asset-library-wire.ts +45 -0
- package/src/kit/api-base.ts +10 -0
- package/src/kit/api-build.ts +99 -0
- package/src/kit/api-git-wire.ts +56 -0
- package/src/kit/api-logs.ts +92 -0
- package/src/kit/api-project-identity.ts +74 -0
- package/src/kit/api-settings.ts +36 -0
- package/src/kit/api-worktrees.ts +205 -0
- package/src/kit/asset-capabilities.ts +344 -0
- package/src/kit/asset-compare-core.ts +171 -0
- package/src/kit/asset-editor-context.tsx +101 -0
- package/src/kit/asset-events.ts +96 -0
- package/src/kit/asset-inspector-actions.ts +87 -0
- package/src/kit/asset-selection-viewer-registry.ts +113 -0
- package/src/kit/asset-selection.ts +146 -0
- package/src/kit/asset-thumbnails.ts +25 -0
- package/src/kit/asset-viewers.ts +115 -0
- package/src/kit/asset-workflow/asset-import-jobs.ts +106 -0
- package/src/kit/asset-workflow/asset-ledger-backend.ts +126 -0
- package/src/kit/asset-workflow/asset-ledger.ts +156 -0
- package/src/kit/asset-workflow/asset-materialization-report.ts +140 -0
- package/src/kit/asset-workflow/asset-pack-manifest.ts +320 -0
- package/src/kit/asset-workflow/asset-types.ts +142 -0
- package/src/kit/asset-workflow/audio-preview-player.ts +193 -0
- package/src/kit/asset-workflow/audio-waveform.ts +22 -0
- package/src/kit/asset-workflow/cloud-asset-client.ts +263 -0
- package/src/kit/asset-workflow/hosted-asset-materialization.ts +236 -0
- package/src/kit/asset-workflow/image-view-scale.ts +31 -0
- package/src/kit/asset-workflow/import-contract.ts +124 -0
- package/src/kit/asset-workflow/ledger-write-lock.ts +244 -0
- package/src/kit/asset-workflow/pixi-spritesheet.ts +197 -0
- package/src/kit/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/kit/asset-workflow/project-asset-commands.ts +20 -0
- package/src/kit/asset-workflow/project-content.ts +288 -0
- package/src/kit/asset-workflow/project-source-index.ts +545 -0
- package/src/kit/asset-workflow/thumbnail-system.ts +256 -0
- package/src/kit/authoring/active-adapter.ts +200 -0
- package/src/kit/authoring/active-systems.ts +422 -0
- package/src/kit/authoring/adapter-key.ts +18 -0
- package/src/kit/authoring/authoring-asset-url.ts +27 -0
- package/src/kit/authoring/bootstrap-state.ts +49 -0
- package/src/kit/authoring/boundary-authoring-adapter.ts +189 -0
- package/src/kit/authoring/canvas-scene-guides.ts +84 -0
- package/src/kit/authoring/composite-authoring-adapter.ts +2109 -0
- package/src/kit/authoring/consumer-actions.ts +531 -0
- package/src/kit/authoring/design-time-layers.ts +852 -0
- package/src/kit/authoring/design-time-mount-registry.ts +244 -0
- package/src/kit/authoring/edit-mode-authoring.ts +637 -0
- package/src/kit/authoring/empty-project-authoring.ts +22 -0
- package/src/kit/authoring/instance-source-menu.ts +135 -0
- package/src/kit/authoring/layered-pick.ts +185 -0
- package/src/kit/authoring/mounted-root-subjects.ts +146 -0
- package/src/kit/authoring/no-authoring-adapter.ts +59 -0
- package/src/kit/authoring/object3d-document-persistence.ts +122 -0
- package/src/kit/authoring/panel-authoring.ts +121 -0
- package/src/kit/authoring/project-authoring-session.ts +105 -0
- package/src/kit/authoring/provenance.ts +99 -0
- package/src/kit/authoring/react-canvas-navigation.ts +259 -0
- package/src/kit/authoring/react-design-canvas-style.ts +20 -0
- package/src/kit/authoring/react-story-board.ts +917 -0
- package/src/kit/authoring/selection-scope.ts +195 -0
- package/src/kit/authoring/shell-document-ops.ts +169 -0
- package/src/kit/authoring/story-board-chrome-fit.ts +107 -0
- package/src/kit/authoring/story-board-presentation.ts +111 -0
- package/src/kit/authoring/three-root.ts +67 -0
- package/src/kit/authoring/viewport-tool-context.ts +73 -0
- package/src/kit/authoring/viewport-tool-owner.ts +38 -0
- package/src/kit/authoring/world-session-state.ts +101 -0
- package/src/kit/authoring-seam-evidence.ts +300 -0
- package/src/kit/availability-tick.ts +66 -0
- package/src/kit/bitmap-label.ts +120 -0
- package/src/kit/boot-routing.ts +392 -0
- package/src/kit/breakpoint-state.ts +43 -0
- package/src/kit/build-identity.ts +16 -0
- package/src/kit/bytes-codec.ts +62 -0
- package/src/kit/cancellation-reason.ts +58 -0
- package/src/kit/canvas-frames.ts +88 -0
- package/src/kit/capture-camera-pose.ts +77 -0
- package/src/kit/capture-size.ts +88 -0
- package/src/kit/chrome-registry.ts +159 -0
- package/src/kit/chrome-slot-registry.ts +91 -0
- package/src/kit/collaboration-client.ts +264 -0
- package/src/kit/collaboration-presence.ts +41 -0
- package/src/kit/command-dispatch.ts +19 -0
- package/src/kit/command-listener.ts +2182 -0
- package/src/kit/command-registry.ts +71 -0
- package/src/kit/component-board-registry.ts +205 -0
- package/src/kit/component-states-registry.ts +199 -0
- package/src/kit/components/AlignToolbar.tsx +204 -0
- package/src/kit/components/ApplicationMenus.tsx +372 -0
- package/src/kit/components/AssetEditorShell.tsx +216 -0
- package/src/kit/components/AssetInspectorToolSection.tsx +124 -0
- package/src/kit/components/BoardRulers.tsx +354 -0
- package/src/kit/components/CanvasAddNodeDialogs.tsx +529 -0
- package/src/kit/components/CanvasSceneViewport.tsx +1195 -0
- package/src/kit/components/ChromeSlot.tsx +20 -0
- package/src/kit/components/CodeView.tsx +470 -0
- package/src/kit/components/CompactInspectorShell.tsx +39 -0
- package/src/kit/components/ConsolePanel.tsx +273 -0
- package/src/kit/components/GameplaySessionTimeline.tsx +295 -0
- package/src/kit/components/InspectionProjection.tsx +932 -0
- package/src/kit/components/Inspector.tsx +270 -0
- package/src/kit/components/InspectorCanvasPreview.tsx +35 -0
- package/src/kit/components/InspectorFieldsSection.tsx +290 -0
- package/src/kit/components/InspectorStoriesSection.tsx +92 -0
- package/src/kit/components/InspectorToolSection.tsx +96 -0
- package/src/kit/components/InspectorTransformSection.tsx +245 -0
- package/src/kit/components/LightExplorerPanel.tsx +433 -0
- package/src/kit/components/MediaProperties.tsx +145 -0
- package/src/kit/components/ProjectHeader.tsx +328 -0
- package/src/kit/components/ReactCanvasControls.tsx +358 -0
- package/src/kit/components/RootSelectionOverlay.tsx +3688 -0
- package/src/kit/components/RootTextEditor.tsx +79 -0
- package/src/kit/components/SaveStatus.tsx +70 -0
- package/src/kit/components/SurfaceCrashBoundary.tsx +105 -0
- package/src/kit/components/SurfaceStateOverlay.tsx +24 -0
- package/src/kit/components/ToolContributionSurfaces.tsx +49 -0
- package/src/kit/components/ToolHost.tsx +380 -0
- package/src/kit/components/Toolbar.tsx +811 -0
- package/src/kit/components/TransientHint.tsx +44 -0
- package/src/kit/components/VersionControlSection.tsx +470 -0
- package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
- package/src/kit/components/VolterLogo.tsx +18 -0
- package/src/kit/components/WorktreeSwitcher.tsx +712 -0
- package/src/kit/components/account-documents.tsx +1162 -0
- package/src/kit/components/asset-documents.tsx +794 -0
- package/src/kit/components/asset-editor-persistence.ts +216 -0
- package/src/kit/components/asset-selection-section.tsx +545 -0
- package/src/kit/components/asset-thumbnails.tsx +307 -0
- package/src/kit/components/asset-viewers/AudioViewer.tsx +201 -0
- package/src/kit/components/asset-viewers/GenericJsonViewer.tsx +102 -0
- package/src/kit/components/asset-viewers/ImageViewer.tsx +300 -0
- package/src/kit/components/asset-viewers/JsonAssetDocument.tsx +98 -0
- package/src/kit/components/asset-viewers/OnlineAssetDetail.tsx +426 -0
- package/src/kit/components/asset-viewers/SourceAssetViewer.tsx +356 -0
- package/src/kit/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
- package/src/kit/components/asset-viewers/VideoViewer.tsx +101 -0
- package/src/kit/components/asset-viewers/shader-source.ts +144 -0
- package/src/kit/components/board-guides.ts +150 -0
- package/src/kit/components/canvas-scene-hotkeys.ts +37 -0
- package/src/kit/components/canvas-temporary-pivot.ts +34 -0
- package/src/kit/components/core-utilities.tsx +94 -0
- package/src/kit/components/inspector-preview-section.tsx +223 -0
- package/src/kit/components/inspector-revert-label.ts +20 -0
- package/src/kit/components/inspector-selection.ts +42 -0
- package/src/kit/components/inspector-stories-gating.ts +171 -0
- package/src/kit/components/inspector-transform-subject.ts +11 -0
- package/src/kit/components/inspector-transform.ts +88 -0
- package/src/kit/components/kind-documents.tsx +544 -0
- package/src/kit/components/primitives/DraftColorInput.tsx +74 -0
- package/src/kit/components/project-tool-documents.tsx +402 -0
- package/src/kit/components/scene-documents.tsx +221 -0
- package/src/kit/components/status-contributions.tsx +407 -0
- package/src/kit/components/tool-documents.tsx +302 -0
- package/src/kit/components/tool-schema-form.tsx +262 -0
- package/src/kit/components/use-after-paint.ts +41 -0
- package/src/kit/components/use-project-image-assets.ts +86 -0
- package/src/kit/components/workspace-history.ts +32 -0
- package/src/kit/components/world-documents.tsx +570 -0
- package/src/kit/components/world-overlay-gestures.ts +1939 -0
- package/src/kit/composite-screenshot.ts +2238 -0
- package/src/kit/content-entry-source-registry.ts +184 -0
- package/src/kit/contribution-surfaces.ts +48 -0
- package/src/kit/coverage/canvas-reveal.ts +192 -0
- package/src/kit/coverage/design-time-surfaces.ts +101 -0
- package/src/kit/coverage/ontology-invariants.ts +466 -0
- package/src/kit/coverage/session-vitals.ts +503 -0
- package/src/kit/crash-null-boundary.ts +36 -0
- package/src/kit/creation-site-edit.ts +1491 -0
- package/src/kit/creation-site-registry.ts +160 -0
- package/src/kit/delegate-harness-registry.ts +134 -0
- package/src/kit/document-areas.ts +70 -0
- package/src/kit/document-context-registry.ts +193 -0
- package/src/kit/document-open-registry.ts +200 -0
- package/src/kit/document-play-extension.ts +221 -0
- package/src/kit/document-preview-source.ts +20 -0
- package/src/kit/document-renderer-session.ts +138 -0
- package/src/kit/document-stage-sessions.ts +26 -0
- package/src/kit/document-viewports.ts +120 -0
- package/src/kit/editor-api.ts +46 -0
- package/src/kit/editor-chrome-capture.ts +136 -0
- package/src/kit/editor-commands.ts +176 -0
- package/src/kit/editor-console.ts +580 -0
- package/src/kit/editor-current-view.ts +56 -0
- package/src/kit/editor-document-probe.ts +1166 -0
- package/src/kit/editor-git-client.ts +115 -0
- package/src/kit/editor-hotkeys.ts +728 -0
- package/src/kit/editor-lease-view.ts +39 -0
- package/src/kit/editor-lease.ts +415 -0
- package/src/kit/editor-mode.ts +19 -0
- package/src/kit/editor-notifications.ts +140 -0
- package/src/kit/editor-presence.ts +563 -0
- package/src/kit/editor-presentation-activity.ts +58 -0
- package/src/kit/editor-presentation-notice.ts +42 -0
- package/src/kit/editor-runtime.tsx +147 -0
- package/src/kit/editor-server-response.ts +86 -0
- package/src/kit/editor-session-attribution.ts +85 -0
- package/src/kit/editor-session-mode.ts +54 -0
- package/src/kit/editor-state-facets.ts +74 -0
- package/src/kit/editor-view-presentation.ts +777 -0
- package/src/kit/environment-images.ts +58 -0
- package/src/kit/eyedropper-session.ts +60 -0
- package/src/kit/files/file-provider.ts +62 -0
- package/src/kit/files/project-files.ts +270 -0
- package/src/kit/finders/index.ts +137 -0
- package/src/kit/finders/scenes-from-entrypoint-selection.ts +387 -0
- package/src/kit/frame/frame-parts.ts +30 -0
- package/src/kit/framed-document-capture.ts +34 -0
- package/src/kit/game-globals-prelude.ts +143 -0
- package/src/kit/game-surface-defaults.ts +33 -0
- package/src/kit/gameplay-dom-recording.ts +318 -0
- package/src/kit/gameplay-export-state.ts +14 -0
- package/src/kit/gameplay-replay.ts +417 -0
- package/src/kit/gameplay-session-time.ts +9 -0
- package/src/kit/gameplay-sessions.ts +204 -0
- package/src/kit/hierarchy-component-marks.ts +298 -0
- package/src/kit/hierarchy-internals.ts +197 -0
- package/src/kit/hierarchy-kind-icon.ts +217 -0
- package/src/kit/hierarchy-menu-registry.ts +67 -0
- package/src/kit/hierarchy-node-rows.ts +307 -0
- package/src/kit/hierarchy-panel-view.ts +280 -0
- package/src/kit/hierarchy-projection.ts +76 -0
- package/src/kit/hierarchy-row-media.ts +45 -0
- package/src/kit/hierarchy-row-model.ts +308 -0
- package/src/kit/hierarchy-rows.ts +11 -0
- package/src/kit/hierarchy-walk.ts +86 -0
- package/src/kit/history/editor-session.ts +25 -0
- package/src/kit/history/history-commands.ts +147 -0
- package/src/kit/history/history-delegate.ts +187 -0
- package/src/kit/history/history-limit-notices.ts +43 -0
- package/src/kit/history/history-service.ts +1189 -0
- package/src/kit/history/persistence-coordinator.ts +35 -0
- package/src/kit/history/project-file-history.ts +386 -0
- package/src/kit/history/project-root-history-backends.ts +139 -0
- package/src/kit/history/resource-registry.ts +209 -0
- package/src/kit/history/snapshot-store.ts +103 -0
- package/src/kit/history/source-history-backend.ts +546 -0
- package/src/kit/history-types.ts +124 -0
- package/src/kit/hmr-registration-group.ts +67 -0
- package/src/kit/hmr-stable-react-context.ts +23 -0
- package/src/kit/hotkeys.ts +190 -0
- package/src/kit/inference-diagnostics.ts +69 -0
- package/src/kit/initial-project.ts +80 -0
- package/src/kit/inspection/active-subject.ts +571 -0
- package/src/kit/inspection/active-surface.ts +142 -0
- package/src/kit/inspection/compose-subject.ts +1055 -0
- package/src/kit/inspection/compose.ts +7 -0
- package/src/kit/inspection/display.ts +171 -0
- package/src/kit/inspection/document-subject.ts +109 -0
- package/src/kit/inspection/game-subject.ts +85 -0
- package/src/kit/inspection/null-subject.ts +119 -0
- package/src/kit/inspection/serialize.ts +357 -0
- package/src/kit/inspection/use-active-inspection.ts +180 -0
- package/src/kit/inspection-model.ts +542 -0
- package/src/kit/inspection-node-media.ts +58 -0
- package/src/kit/inspector-presentation.ts +203 -0
- package/src/kit/inspector-property-grouping.ts +64 -0
- package/src/kit/inspector-section-registry.ts +221 -0
- package/src/kit/instance-source-actions.ts +163 -0
- package/src/kit/js-heap.ts +71 -0
- package/src/kit/key-actions.ts +91 -0
- package/src/kit/keymap-presets.ts +428 -0
- package/src/kit/layout-policy.ts +31 -0
- package/src/kit/light-explorer-model.ts +134 -0
- package/src/kit/live-canvas-frame.ts +55 -0
- package/src/kit/live-document.ts +296 -0
- package/src/kit/live-gesture-lock.ts +50 -0
- package/src/kit/live-seam-evidence.ts +11 -0
- package/src/kit/live-session-registry.ts +220 -0
- package/src/kit/live-transition.ts +391 -0
- package/src/kit/manifest-project.ts +107 -0
- package/src/kit/module-fetch-diagnosis.ts +192 -0
- package/src/kit/mount-failure-report.ts +154 -0
- package/src/kit/native-selection-style.ts +497 -0
- package/src/kit/object3d-document-write-policy.ts +137 -0
- package/src/kit/packaged-runtime.ts +108 -0
- package/src/kit/palettes/maya.palette.json +57 -0
- package/src/kit/palettes/substance.palette.json +57 -0
- package/src/kit/performance-profiler.ts +367 -0
- package/src/kit/performance-sources.ts +69 -0
- package/src/kit/photograph-notice.ts +141 -0
- package/src/kit/play-boot-phase.ts +166 -0
- package/src/kit/play-camera-flight.ts +35 -0
- package/src/kit/png-encode.worker.ts +26 -0
- package/src/kit/presentation-surface.ts +248 -0
- package/src/kit/product-command.ts +90 -0
- package/src/kit/project-adapter.ts +1140 -0
- package/src/kit/project-asset-refresh.ts +23 -0
- package/src/kit/project-asset-roots.ts +68 -0
- package/src/kit/project-local-state.ts +151 -0
- package/src/kit/project-manager.ts +243 -0
- package/src/kit/project-module-changes.ts +201 -0
- package/src/kit/project-module-split.ts +270 -0
- package/src/kit/project-play-layers.ts +25 -0
- package/src/kit/project-provenance.ts +115 -0
- package/src/kit/project-ready.ts +42 -0
- package/src/kit/project-shape.ts +68 -0
- package/src/kit/project-tools.ts +107 -0
- package/src/kit/projection-types.ts +44 -0
- package/src/kit/readiness.ts +113 -0
- package/src/kit/renderer-resource-counts.ts +27 -0
- package/src/kit/reported-play-state.ts +90 -0
- package/src/kit/resolve-contributed-command.ts +14 -0
- package/src/kit/resolve-relative-specifier.ts +33 -0
- package/src/kit/retained-document-states.ts +91 -0
- package/src/kit/scene-document-plan.ts +320 -0
- package/src/kit/scene-live-open.ts +210 -0
- package/src/kit/scoped-game-css.ts +152 -0
- package/src/kit/served-url.ts +5 -0
- package/src/kit/session-close.ts +17 -0
- package/src/kit/session-tombstone.ts +127 -0
- package/src/kit/settings/settings-provider.ts +82 -0
- package/src/kit/settings-store.ts +348 -0
- package/src/kit/shell-document-state.ts +27 -0
- package/src/kit/shell-store-door.ts +45 -0
- package/src/kit/shell-store.ts +722 -0
- package/src/kit/source-conflict.ts +122 -0
- package/src/kit/stage-context.ts +377 -0
- package/src/kit/stage-invalidation.ts +25 -0
- package/src/kit/stage-store-registry.ts +69 -0
- package/src/kit/startup-failure.ts +80 -0
- package/src/kit/state-report-deferral.ts +73 -0
- package/src/kit/storage/host-files-storage.ts +97 -0
- package/src/kit/storage/http-storage.ts +174 -0
- package/src/kit/storage/index.ts +75 -0
- package/src/kit/storage/mem-storage.ts +158 -0
- package/src/kit/storage/path-lock.ts +44 -0
- package/src/kit/storage/paths.ts +26 -0
- package/src/kit/storage-types.ts +127 -0
- package/src/kit/stories/StoryPreviewMount.tsx +306 -0
- package/src/kit/stories/compose-project-stories.ts +255 -0
- package/src/kit/stories/prefabs-finder.ts +54 -0
- package/src/kit/stories/prefabs-from-stories.ts +182 -0
- package/src/kit/stories/project-story-regions.ts +24 -0
- package/src/kit/stories/story-capture.ts +579 -0
- package/src/kit/stories/story-declared-medium.ts +126 -0
- package/src/kit/stories/story-discovery.ts +176 -0
- package/src/kit/stories/story-dom-runtime.ts +78 -0
- package/src/kit/stories/story-grouping.ts +111 -0
- package/src/kit/stories/story-mount-turn.ts +27 -0
- package/src/kit/stories/story-presentation.ts +215 -0
- package/src/kit/stories/story-preview-component.ts +7 -0
- package/src/kit/stories/story-registry.ts +530 -0
- package/src/kit/stories-scope.ts +35 -0
- package/src/kit/story-document-openers.ts +36 -0
- package/src/kit/story-thumbnails.ts +47 -0
- package/src/kit/surface-keyboard.ts +101 -0
- package/src/kit/surface-state.ts +135 -0
- package/src/kit/system-seam-evidence.ts +72 -0
- package/src/kit/tab-census.ts +202 -0
- package/src/kit/tab-lifecycle-client.ts +227 -0
- package/src/kit/theme-library.ts +897 -0
- package/src/kit/theme-preference.ts +429 -0
- package/src/kit/three-viewport-presentation.ts +23 -0
- package/src/kit/tool-contribution-play.ts +74 -0
- package/src/kit/tool-loader.ts +1918 -0
- package/src/kit/transform-mode-request.ts +66 -0
- package/src/kit/transient-hint.ts +78 -0
- package/src/kit/transport-strip.tsx +174 -0
- package/src/kit/ui-source/adapter-region-includes.ts +238 -0
- package/src/kit/ui-source/file-region-resolver.ts +302 -0
- package/src/kit/ui-source/inspect.ts +775 -0
- package/src/kit/ui-source/source-write-backend.ts +605 -0
- package/src/kit/ui-source/tier-source-write-backend.ts +279 -0
- package/src/kit/user-local-state.ts +105 -0
- package/src/kit/viewport-activation-timings.ts +840 -0
- package/src/kit/viewport-editor-controls.ts +22 -0
- package/src/kit/viewport-presentation.ts +668 -0
- package/src/kit/viewport-surface-status.tsx +55 -0
- package/src/kit/wait-until.ts +37 -0
- package/src/kit/worker-call-metrics.ts +166 -0
- package/src/kit/workspace-areas.ts +191 -0
- package/src/kit/workspace-aux-commands.ts +11 -0
- package/src/kit/workspace-available-documents.ts +142 -0
- package/src/kit/workspace-core-utilities.ts +31 -0
- package/src/kit/workspace-document-ids.ts +59 -0
- package/src/kit/workspace-document-registry.ts +624 -0
- package/src/kit/workspace-document-restore.ts +146 -0
- package/src/kit/workspace-host-commands.ts +141 -0
- package/src/kit/workspace-persistence-gate.ts +40 -0
- package/src/kit/workspace-play-utilities.ts +44 -0
- package/src/kit/workspace-presets.ts +446 -0
- package/src/kit/workspace-regions.ts +276 -0
- package/src/kit/workspace-static-panels.ts +73 -0
- package/src/kit/workspace-status-registry.ts +121 -0
- package/src/kit/workspace-storage.ts +35 -0
- package/src/kit/workspace-style.ts +226 -0
- package/src/kit/workspace-utility-commands.ts +74 -0
- package/src/kit/workspace-utility-registry.ts +263 -0
- package/src/kit/world-adoption-event.ts +23 -0
- package/src/kit/world-adoption.ts +115 -0
- package/src/kit/world-canvas-viewport-state.ts +35 -0
- package/src/kit/world-document-routing.ts +104 -0
- package/src/kit/world-pan-state.ts +198 -0
- package/src/kit/write-pipe.ts +173 -0
- package/src/layout-arrangements.ts +5 -0
- package/src/layouts.tsx +108 -0
- package/src/looks.ts +16 -0
- package/src/project/output-roots.ts +73 -0
- package/src/project/tab-census.ts +155 -0
- package/src/project-tool-catalog.ts +104 -0
- package/src/selection.tsx +107 -0
- package/src/services.ts +18 -0
- package/src/session/build-report.ts +22 -0
- package/src/session/collaboration-types.ts +262 -0
- package/src/session/command-table.ts +327 -0
- package/src/session/discovery.ts +100 -0
- package/src/session/editor-brand.ts +48 -0
- package/src/session/editor-compatibility.ts +317 -0
- package/src/session/editor-control-lifecycle.ts +68 -0
- package/src/session/editor-control-protocol.ts +5 -0
- package/src/session/entrypoint-selection-readers.ts +66 -0
- package/src/session/entrypoint-selection-source.ts +120 -0
- package/src/session/game-css-scope.ts +30 -0
- package/src/session/hosted-attachment.ts +225 -0
- package/src/session/limited-view.ts +82 -0
- package/src/session/product-create.ts +24 -0
- package/src/session/product-locator.ts +478 -0
- package/src/session/project-module-url.ts +242 -0
- package/src/session/project-serving.ts +164 -0
- package/src/session/project-upgrade.ts +403 -0
- package/src/session/registry-format.ts +210 -0
- package/src/session/relative-path-guard.ts +56 -0
- package/src/session/scoped-game-css.ts +461 -0
- package/src/session/source-glob.ts +15 -0
- package/src/session/tool-contribution-convention.ts +123 -0
- package/src/session/workbench-locator.ts +712 -0
- package/src/session.ts +41 -0
- package/src/share.ts +160 -0
- package/src/source-analysis.ts +28 -0
- package/src/source-authoring.ts +439 -0
- package/src/tools/errors.ts +91 -0
- package/src/tools/provider-execution.ts +70 -0
- package/src/tools/registry.ts +341 -0
- package/src/tools/types.ts +159 -0
- package/src/transport.ts +100 -0
- package/src/types.ts +1693 -0
- package/src/views.ts +164 -0
- package/src/widgets/design-system.ts +93 -0
- package/src/widgets/editor-appearance.ts +151 -0
- package/src/widgets/editor-material.ts +83 -0
- package/src/widgets/icon-set-registry.ts +105 -0
- package/src/widgets/index.ts +71 -0
- package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
- package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
- package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
- package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
- package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
- package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
- package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
- package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
- package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
- package/src/widgets/inspector-widgets/color-utils.ts +201 -0
- package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
- package/src/widgets/inspector-widgets/index.ts +25 -0
- package/src/widgets/inspector-widgets/shared.tsx +140 -0
- package/src/widgets/interactive-edit-scope.ts +33 -0
- package/src/widgets/patterns/Dialog.tsx +140 -0
- package/src/widgets/patterns/Fields.tsx +44 -0
- package/src/widgets/patterns/List.tsx +25 -0
- package/src/widgets/patterns/StateSurface.tsx +40 -0
- package/src/widgets/patterns/Surfaces.tsx +122 -0
- package/src/widgets/patterns/Tabs.tsx +80 -0
- package/src/widgets/patterns/Toolbar.tsx +72 -0
- package/src/widgets/patterns/Tree.tsx +72 -0
- package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
- package/src/widgets/primitives/Button.tsx +62 -0
- package/src/widgets/primitives/ColorInput.tsx +78 -0
- package/src/widgets/primitives/DraftTextInput.tsx +63 -0
- package/src/widgets/primitives/EditorIcon.tsx +157 -0
- package/src/widgets/primitives/FormControls.tsx +88 -0
- package/src/widgets/primitives/HoverPreview.tsx +96 -0
- package/src/widgets/primitives/JsonInput.tsx +113 -0
- package/src/widgets/primitives/Layout.tsx +100 -0
- package/src/widgets/primitives/Menu.tsx +161 -0
- package/src/widgets/primitives/NumberInput.tsx +169 -0
- package/src/widgets/primitives/Panel.tsx +80 -0
- package/src/widgets/primitives/SectionHeader.tsx +77 -0
- package/src/widgets/primitives/Text.tsx +54 -0
- package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
- package/src/widgets/primitives/Tooltip.tsx +204 -0
- package/src/widgets/primitives/Vec3Input.tsx +70 -0
- package/src/widgets/primitives/banner-tones.ts +32 -0
- package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
- package/src/widgets/primitives/editor-icons.ts +254 -0
- package/src/widgets/primitives/panel-header-styles.ts +42 -0
- package/src/widgets/theme.ts +2841 -0
- package/src/widgets/z-index.ts +25 -0
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
faBullseye,
|
|
3
|
+
faCheck,
|
|
4
|
+
faCrosshairs,
|
|
5
|
+
faHand,
|
|
6
|
+
faLayerGroup,
|
|
7
|
+
faListUl,
|
|
8
|
+
faLock,
|
|
9
|
+
faLockOpen,
|
|
10
|
+
faRulerCombined,
|
|
11
|
+
faMagnifyingGlassMinus,
|
|
12
|
+
faMagnifyingGlassPlus,
|
|
13
|
+
} from '@fortawesome/free-solid-svg-icons';
|
|
14
|
+
import {
|
|
15
|
+
AnchoredMenu,
|
|
16
|
+
Button,
|
|
17
|
+
EditorIcon,
|
|
18
|
+
FloatingToolbar,
|
|
19
|
+
IconButton,
|
|
20
|
+
MenuItem,
|
|
21
|
+
MenuSeparator,
|
|
22
|
+
MenuSubmenu,
|
|
23
|
+
Tooltip,
|
|
24
|
+
} from '@volter/sdk/widgets';
|
|
25
|
+
import type { AuthoringAdapter, DOMRectLike } from '@volter/project/adapter';
|
|
26
|
+
import {
|
|
27
|
+
type PointerEvent as ReactPointerEvent,
|
|
28
|
+
type RefObject,
|
|
29
|
+
useCallback,
|
|
30
|
+
useEffect,
|
|
31
|
+
useRef,
|
|
32
|
+
useState,
|
|
33
|
+
useSyncExternalStore,
|
|
34
|
+
} from 'react';
|
|
35
|
+
import { collectAllNodeIds } from '@volter/sdk/kit/authoring/active-adapter';
|
|
36
|
+
import { SCENE_MAX_ZOOM, SCENE_MIN_ZOOM } from '@volter/sdk/kit/authoring/react-canvas-navigation';
|
|
37
|
+
import {
|
|
38
|
+
addCanvasSceneGuide,
|
|
39
|
+
canvasSceneGuideRevision,
|
|
40
|
+
canvasSceneGuides,
|
|
41
|
+
clearCanvasSceneGuides,
|
|
42
|
+
moveCanvasSceneGuide,
|
|
43
|
+
removeCanvasSceneGuide,
|
|
44
|
+
subscribeCanvasSceneGuides,
|
|
45
|
+
} from '../authoring/canvas-scene-guides';
|
|
46
|
+
import type { RootViewController } from '@volter/sdk/kit/world-pan-state';
|
|
47
|
+
import { useEditorStore } from '@volter/sdk/kit/editor-runtime';
|
|
48
|
+
import {
|
|
49
|
+
bindViewPresentation,
|
|
50
|
+
setViewDrafting,
|
|
51
|
+
setViewGridVisible,
|
|
52
|
+
viewDrafting,
|
|
53
|
+
subscribeViewportPresentation,
|
|
54
|
+
viewGridVisible,
|
|
55
|
+
viewportPresentationVersion,
|
|
56
|
+
} from '@volter/sdk/kit/viewport-presentation';
|
|
57
|
+
import { getCurrentProject } from '@volter/sdk/kit/project-manager';
|
|
58
|
+
import { ToolStrip } from '@volter/sdk/kit/components/Toolbar';
|
|
59
|
+
import { bindCanvasSceneKeys } from './canvas-scene-hotkeys';
|
|
60
|
+
import { setTemporaryPivot } from './canvas-temporary-pivot';
|
|
61
|
+
import { markCanvasStageDocument } from '@volter/sdk/kit/stage-context';
|
|
62
|
+
import { TransientHintOverlay } from '@volter/sdk/kit/components/TransientHint';
|
|
63
|
+
import { ViewportPickMenu } from '@volter/sdk/kit/components/RootSelectionOverlay';
|
|
64
|
+
import {
|
|
65
|
+
EDGE_SNAP_THRESHOLD_PX,
|
|
66
|
+
frameForId,
|
|
67
|
+
rectFrame,
|
|
68
|
+
rectForId,
|
|
69
|
+
snapPivotPoint,
|
|
70
|
+
spatialHandlesForId,
|
|
71
|
+
} from '@volter/sdk/kit/components/world-overlay-gestures';
|
|
72
|
+
import { pickCandidates } from '@volter/sdk/kit/authoring/layered-pick';
|
|
73
|
+
|
|
74
|
+
interface Bounds {
|
|
75
|
+
x: number;
|
|
76
|
+
y: number;
|
|
77
|
+
width: number;
|
|
78
|
+
height: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Neutral spatial-workspace ground shared in spirit with the Three Scene.
|
|
82
|
+
* Unlike the component boards' media background, this is editor viewport
|
|
83
|
+
* dressing: native Pixi content renders transparently above it. */
|
|
84
|
+
export const CANVAS_SCENE_BACKGROUND = '#111214';
|
|
85
|
+
|
|
86
|
+
/** Keep the drafting grid legible without divorcing it from world units.
|
|
87
|
+
* Every visible interval is a power-of-two multiple/division of the base
|
|
88
|
+
* cell, so grid intersections never swim relative to authored points. */
|
|
89
|
+
function adaptiveGridWorldSpacing(zoom: number, base = 32): number {
|
|
90
|
+
let worldUnits = base > 0 ? base : 32;
|
|
91
|
+
while (worldUnits * zoom < 12) worldUnits *= 2;
|
|
92
|
+
while (worldUnits * zoom > 48) worldUnits /= 2;
|
|
93
|
+
return worldUnits;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function unionRects(rects: readonly DOMRectLike[]): Bounds | null {
|
|
97
|
+
if (rects.length === 0) return null;
|
|
98
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
99
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
100
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
101
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
102
|
+
for (const rect of rects) {
|
|
103
|
+
minX = Math.min(minX, rect.x);
|
|
104
|
+
minY = Math.min(minY, rect.y);
|
|
105
|
+
maxX = Math.max(maxX, rect.x + rect.width);
|
|
106
|
+
maxY = Math.max(maxY, rect.y + rect.height);
|
|
107
|
+
}
|
|
108
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function frameBounds(
|
|
112
|
+
container: HTMLElement,
|
|
113
|
+
view: RootViewController,
|
|
114
|
+
bounds: Bounds | null,
|
|
115
|
+
): void {
|
|
116
|
+
if (!bounds || !(bounds.width > 0) || !(bounds.height > 0)) return;
|
|
117
|
+
const availableWidth = Math.max(1, container.clientWidth - 96);
|
|
118
|
+
const availableHeight = Math.max(1, container.clientHeight - 96);
|
|
119
|
+
const zoom = Math.min(
|
|
120
|
+
4,
|
|
121
|
+
Math.max(SCENE_MIN_ZOOM, Math.min(availableWidth / bounds.width, availableHeight / bounds.height)),
|
|
122
|
+
);
|
|
123
|
+
view.setView(
|
|
124
|
+
container.clientWidth / 2 - (bounds.x + bounds.width / 2) * zoom,
|
|
125
|
+
container.clientHeight / 2 - (bounds.y + bounds.height / 2) * zoom,
|
|
126
|
+
zoom,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Infinite XY drafting overlay for a native Pixi stage. The axes and grid
|
|
131
|
+
* follow the editor camera; neither exists in the game's source or runtime.
|
|
132
|
+
* It sits above source-owned backgrounds (which otherwise erase any useful
|
|
133
|
+
* drafting reference) and below selection/transform chrome. */
|
|
134
|
+
export function CanvasSceneBackdrop({ view, documentId }: { view: RootViewController; documentId: string }) {
|
|
135
|
+
const store = useEditorStore();
|
|
136
|
+
useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
137
|
+
// The 2D scene's grid switch is its VIEW's, like every stage's (`kit/viewport-presentation`):
|
|
138
|
+
// bound under the host document's own id, so a door that toggles the active view's grid
|
|
139
|
+
// reaches this one, and two canvases never share a switch.
|
|
140
|
+
useEffect(() => bindViewPresentation(documentId, 'canvas'), [documentId]);
|
|
141
|
+
useSyncExternalStore(subscribeViewportPresentation, viewportPresentationVersion);
|
|
142
|
+
const showGrid = viewGridVisible(documentId);
|
|
143
|
+
const drafting = viewDrafting(documentId);
|
|
144
|
+
const resolution = getCurrentProject()?.config.resolution;
|
|
145
|
+
const pose = useSyncExternalStore(view.subscribe, view.get, view.get);
|
|
146
|
+
useSyncExternalStore(
|
|
147
|
+
useCallback((listener) => subscribeCanvasSceneGuides(view, listener), [view]),
|
|
148
|
+
useCallback(() => canvasSceneGuideRevision(view), [view]),
|
|
149
|
+
);
|
|
150
|
+
const guides = canvasSceneGuides(view);
|
|
151
|
+
// The grid is drawn at the snap step from its offset, as Godot's is: a snapped move lands on a
|
|
152
|
+
// drawn line (zoomed out far enough, every other line thins away by powers of two).
|
|
153
|
+
const grid = store.snap2D;
|
|
154
|
+
const minor = adaptiveGridWorldSpacing(pose.zoom, grid.step) * pose.zoom;
|
|
155
|
+
const major = minor * Math.max(1, Math.round(grid.primaryEvery));
|
|
156
|
+
const gridX = pose.x + grid.offsetX * pose.zoom;
|
|
157
|
+
const gridY = pose.y + grid.offsetY * pose.zoom;
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
data-testid="canvas-scene-backdrop"
|
|
161
|
+
style={{
|
|
162
|
+
position: 'absolute',
|
|
163
|
+
inset: 0,
|
|
164
|
+
zIndex: 2,
|
|
165
|
+
overflow: 'hidden',
|
|
166
|
+
pointerEvents: 'none',
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
{showGrid && (!drafting.gridWhenSnapping || store.snapEnabled) && (
|
|
170
|
+
<div
|
|
171
|
+
data-testid="canvas-scene-grid"
|
|
172
|
+
style={{
|
|
173
|
+
position: 'absolute',
|
|
174
|
+
inset: 0,
|
|
175
|
+
backgroundImage:
|
|
176
|
+
'linear-gradient(rgba(255,255,255,.075) 1px, transparent 1px),' +
|
|
177
|
+
'linear-gradient(90deg, rgba(255,255,255,.075) 1px, transparent 1px),' +
|
|
178
|
+
'linear-gradient(rgba(255,255,255,.14) 1px, transparent 1px),' +
|
|
179
|
+
'linear-gradient(90deg, rgba(255,255,255,.14) 1px, transparent 1px)',
|
|
180
|
+
backgroundPosition: `${gridX}px ${gridY}px`,
|
|
181
|
+
backgroundSize: `${minor}px ${minor}px, ${minor}px ${minor}px, ${major}px ${major}px, ${major}px ${major}px`,
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
{drafting.viewport && resolution && resolution.width > 0 && resolution.height > 0 && (
|
|
186
|
+
// The game's viewport: the manifest's resolution from the origin, as Godot draws its
|
|
187
|
+
// project window size (View › Show Viewport).
|
|
188
|
+
<div
|
|
189
|
+
data-testid="canvas-scene-viewport-rect"
|
|
190
|
+
style={{
|
|
191
|
+
position: 'absolute',
|
|
192
|
+
left: pose.x,
|
|
193
|
+
top: pose.y,
|
|
194
|
+
width: resolution.width * pose.zoom,
|
|
195
|
+
height: resolution.height * pose.zoom,
|
|
196
|
+
boxSizing: 'border-box',
|
|
197
|
+
borderStyle: 'solid',
|
|
198
|
+
borderWidth: 1,
|
|
199
|
+
borderColor: 'rgba(160, 120, 255, .75)',
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
{drafting.origin && (
|
|
204
|
+
<>
|
|
205
|
+
<div
|
|
206
|
+
data-testid="canvas-scene-origin-y"
|
|
207
|
+
style={{
|
|
208
|
+
position: 'absolute',
|
|
209
|
+
left: 0,
|
|
210
|
+
right: 0,
|
|
211
|
+
top: pose.y,
|
|
212
|
+
height: 1,
|
|
213
|
+
background: 'rgba(238, 91, 91, .8)',
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
|
+
<div
|
|
217
|
+
data-testid="canvas-scene-origin-x"
|
|
218
|
+
style={{
|
|
219
|
+
position: 'absolute',
|
|
220
|
+
top: 0,
|
|
221
|
+
bottom: 0,
|
|
222
|
+
left: pose.x,
|
|
223
|
+
width: 1,
|
|
224
|
+
background: 'rgba(87, 199, 126, .8)',
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
<div
|
|
228
|
+
style={{
|
|
229
|
+
position: 'absolute',
|
|
230
|
+
left: pose.x + 6,
|
|
231
|
+
top: pose.y + 6,
|
|
232
|
+
color: 'rgba(255,255,255,.68)',
|
|
233
|
+
font: '10px ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
0, 0
|
|
237
|
+
</div>
|
|
238
|
+
</>
|
|
239
|
+
)}
|
|
240
|
+
{drafting.guides &&
|
|
241
|
+
guides.map((guide) => (
|
|
242
|
+
<div
|
|
243
|
+
key={guide.id}
|
|
244
|
+
data-testid={`canvas-scene-guide-${guide.axis}`}
|
|
245
|
+
title={`${guide.axis.toUpperCase()} ${Math.round(guide.value * 100) / 100} · drag to move · right-click to remove`}
|
|
246
|
+
onContextMenu={(event) => {
|
|
247
|
+
event.preventDefault();
|
|
248
|
+
removeCanvasSceneGuide(view, guide.id);
|
|
249
|
+
}}
|
|
250
|
+
onPointerDown={(event) => {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
event.stopPropagation();
|
|
253
|
+
const host = event.currentTarget.parentElement?.getBoundingClientRect();
|
|
254
|
+
if (!host) return;
|
|
255
|
+
const move = (next: PointerEvent) => {
|
|
256
|
+
const screen =
|
|
257
|
+
guide.axis === 'x' ? next.clientX - host.left : next.clientY - host.top;
|
|
258
|
+
const pan = view.get();
|
|
259
|
+
moveCanvasSceneGuide(
|
|
260
|
+
view,
|
|
261
|
+
guide.id,
|
|
262
|
+
(screen - (guide.axis === 'x' ? pan.x : pan.y)) / pan.zoom,
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
const end = () => {
|
|
266
|
+
window.removeEventListener('pointermove', move);
|
|
267
|
+
window.removeEventListener('pointerup', end);
|
|
268
|
+
};
|
|
269
|
+
window.addEventListener('pointermove', move);
|
|
270
|
+
window.addEventListener('pointerup', end, { once: true });
|
|
271
|
+
}}
|
|
272
|
+
style={{
|
|
273
|
+
position: 'absolute',
|
|
274
|
+
pointerEvents: 'auto',
|
|
275
|
+
cursor: guide.axis === 'x' ? 'ew-resize' : 'ns-resize',
|
|
276
|
+
...(guide.axis === 'x'
|
|
277
|
+
? {
|
|
278
|
+
left: pose.x + guide.value * pose.zoom - 4,
|
|
279
|
+
top: 20,
|
|
280
|
+
bottom: 0,
|
|
281
|
+
width: 9,
|
|
282
|
+
borderLeft: '1px solid rgba(74, 180, 255, .95)',
|
|
283
|
+
}
|
|
284
|
+
: {
|
|
285
|
+
top: pose.y + guide.value * pose.zoom - 4,
|
|
286
|
+
left: 20,
|
|
287
|
+
right: 0,
|
|
288
|
+
height: 9,
|
|
289
|
+
borderTop: '1px solid rgba(74, 180, 255, .95)',
|
|
290
|
+
}),
|
|
291
|
+
}}
|
|
292
|
+
/>
|
|
293
|
+
))}
|
|
294
|
+
{drafting.rulers && <CanvasSceneRulers view={view} />}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function CanvasSceneRulers({ view }: { view: RootViewController }) {
|
|
300
|
+
const pose = useSyncExternalStore(view.subscribe, view.get, view.get);
|
|
301
|
+
const horizontalRef = useRef<HTMLButtonElement>(null);
|
|
302
|
+
const verticalRef = useRef<HTMLButtonElement>(null);
|
|
303
|
+
const [extent, setExtent] = useState({ width: 0, height: 0 });
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
const update = () => {
|
|
307
|
+
const width = horizontalRef.current?.clientWidth ?? 0;
|
|
308
|
+
const height = verticalRef.current?.clientHeight ?? 0;
|
|
309
|
+
setExtent((current) =>
|
|
310
|
+
current.width === width && current.height === height ? current : { width, height },
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
update();
|
|
314
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
315
|
+
const observer = new ResizeObserver(update);
|
|
316
|
+
if (horizontalRef.current) observer.observe(horizontalRef.current);
|
|
317
|
+
if (verticalRef.current) observer.observe(verticalRef.current);
|
|
318
|
+
return () => observer.disconnect();
|
|
319
|
+
}, []);
|
|
320
|
+
|
|
321
|
+
const ticksFor = (axis: 'x' | 'y', length: number) => {
|
|
322
|
+
if (!(length > 0)) return [];
|
|
323
|
+
const pan = axis === 'x' ? pose.x : pose.y;
|
|
324
|
+
const worldStep = adaptiveGridWorldSpacing(pose.zoom);
|
|
325
|
+
const screenStep = worldStep * pose.zoom;
|
|
326
|
+
const rulerStart = 20;
|
|
327
|
+
const first = Math.ceil((rulerStart - pan) / screenStep);
|
|
328
|
+
const last = Math.floor((rulerStart + length - pan) / screenStep);
|
|
329
|
+
return Array.from({ length: Math.max(0, last - first + 1) }, (_, offset) => {
|
|
330
|
+
const index = first + offset;
|
|
331
|
+
return {
|
|
332
|
+
major: index % 4 === 0,
|
|
333
|
+
screen: pan + index * screenStep - rulerStart,
|
|
334
|
+
value: index * worldStep,
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
const horizontalTicks = ticksFor('x', extent.width);
|
|
339
|
+
const verticalTicks = ticksFor('y', extent.height);
|
|
340
|
+
|
|
341
|
+
const beginGuide = (axis: 'x' | 'y', event: ReactPointerEvent<HTMLButtonElement>): void => {
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
event.stopPropagation();
|
|
344
|
+
const host = event.currentTarget.parentElement?.getBoundingClientRect();
|
|
345
|
+
if (!host) return;
|
|
346
|
+
const valueAt = (clientX: number, clientY: number): number => {
|
|
347
|
+
const pose = view.get();
|
|
348
|
+
const screen = axis === 'x' ? clientX - host.left : clientY - host.top;
|
|
349
|
+
return (screen - (axis === 'x' ? pose.x : pose.y)) / pose.zoom;
|
|
350
|
+
};
|
|
351
|
+
const id = addCanvasSceneGuide(view, axis, valueAt(event.clientX, event.clientY));
|
|
352
|
+
const move = (next: PointerEvent) =>
|
|
353
|
+
moveCanvasSceneGuide(view, id, valueAt(next.clientX, next.clientY));
|
|
354
|
+
const end = () => {
|
|
355
|
+
window.removeEventListener('pointermove', move);
|
|
356
|
+
window.removeEventListener('pointerup', end);
|
|
357
|
+
};
|
|
358
|
+
window.addEventListener('pointermove', move);
|
|
359
|
+
window.addEventListener('pointerup', end, { once: true });
|
|
360
|
+
};
|
|
361
|
+
const rulerBase = {
|
|
362
|
+
position: 'absolute' as const,
|
|
363
|
+
pointerEvents: 'auto' as const,
|
|
364
|
+
backgroundColor: 'rgba(24, 26, 30, .96)',
|
|
365
|
+
border: 0,
|
|
366
|
+
borderColor: 'rgba(255,255,255,.14)',
|
|
367
|
+
boxSizing: 'border-box' as const,
|
|
368
|
+
};
|
|
369
|
+
return (
|
|
370
|
+
<>
|
|
371
|
+
<button
|
|
372
|
+
ref={horizontalRef}
|
|
373
|
+
type="button"
|
|
374
|
+
data-testid="canvas-scene-ruler-x"
|
|
375
|
+
aria-label="Horizontal ruler; drag to create a horizontal guide"
|
|
376
|
+
onPointerDown={(event) => beginGuide('y', event)}
|
|
377
|
+
style={{
|
|
378
|
+
...rulerBase,
|
|
379
|
+
left: 20,
|
|
380
|
+
right: 0,
|
|
381
|
+
top: 0,
|
|
382
|
+
height: 20,
|
|
383
|
+
borderBottomStyle: 'solid',
|
|
384
|
+
borderBottomWidth: 1,
|
|
385
|
+
cursor: 'row-resize',
|
|
386
|
+
padding: 0,
|
|
387
|
+
overflow: 'hidden',
|
|
388
|
+
}}
|
|
389
|
+
>
|
|
390
|
+
{horizontalTicks.map((tick) => (
|
|
391
|
+
<span
|
|
392
|
+
key={tick.value}
|
|
393
|
+
aria-hidden="true"
|
|
394
|
+
data-ruler-axis="x"
|
|
395
|
+
data-ruler-value={tick.value}
|
|
396
|
+
style={{
|
|
397
|
+
position: 'absolute',
|
|
398
|
+
left: tick.screen,
|
|
399
|
+
bottom: 0,
|
|
400
|
+
width: 1,
|
|
401
|
+
height: tick.major ? 8 : 4,
|
|
402
|
+
background: tick.major ? 'rgba(255,255,255,.5)' : 'rgba(255,255,255,.28)',
|
|
403
|
+
pointerEvents: 'none',
|
|
404
|
+
}}
|
|
405
|
+
>
|
|
406
|
+
{tick.major && (
|
|
407
|
+
<span
|
|
408
|
+
style={{
|
|
409
|
+
position: 'absolute',
|
|
410
|
+
left: 3,
|
|
411
|
+
bottom: 5,
|
|
412
|
+
color: 'rgba(255,255,255,.62)',
|
|
413
|
+
font: '9px ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
414
|
+
lineHeight: 1,
|
|
415
|
+
whiteSpace: 'nowrap',
|
|
416
|
+
}}
|
|
417
|
+
>
|
|
418
|
+
{tick.value}
|
|
419
|
+
</span>
|
|
420
|
+
)}
|
|
421
|
+
</span>
|
|
422
|
+
))}
|
|
423
|
+
</button>
|
|
424
|
+
<button
|
|
425
|
+
ref={verticalRef}
|
|
426
|
+
type="button"
|
|
427
|
+
data-testid="canvas-scene-ruler-y"
|
|
428
|
+
aria-label="Vertical ruler; drag to create a vertical guide"
|
|
429
|
+
onPointerDown={(event) => beginGuide('x', event)}
|
|
430
|
+
style={{
|
|
431
|
+
...rulerBase,
|
|
432
|
+
top: 20,
|
|
433
|
+
bottom: 0,
|
|
434
|
+
left: 0,
|
|
435
|
+
width: 20,
|
|
436
|
+
borderRightStyle: 'solid',
|
|
437
|
+
borderRightWidth: 1,
|
|
438
|
+
cursor: 'col-resize',
|
|
439
|
+
padding: 0,
|
|
440
|
+
overflow: 'hidden',
|
|
441
|
+
}}
|
|
442
|
+
>
|
|
443
|
+
{verticalTicks.map((tick) => (
|
|
444
|
+
<span
|
|
445
|
+
key={tick.value}
|
|
446
|
+
aria-hidden="true"
|
|
447
|
+
data-ruler-axis="y"
|
|
448
|
+
data-ruler-value={tick.value}
|
|
449
|
+
style={{
|
|
450
|
+
position: 'absolute',
|
|
451
|
+
top: tick.screen,
|
|
452
|
+
right: 0,
|
|
453
|
+
width: tick.major ? 8 : 4,
|
|
454
|
+
height: 1,
|
|
455
|
+
background: tick.major ? 'rgba(255,255,255,.5)' : 'rgba(255,255,255,.28)',
|
|
456
|
+
pointerEvents: 'none',
|
|
457
|
+
}}
|
|
458
|
+
>
|
|
459
|
+
{tick.major && (
|
|
460
|
+
<span
|
|
461
|
+
style={{
|
|
462
|
+
position: 'absolute',
|
|
463
|
+
right: 4,
|
|
464
|
+
top: 3,
|
|
465
|
+
color: 'rgba(255,255,255,.62)',
|
|
466
|
+
font: '9px ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
467
|
+
lineHeight: 1,
|
|
468
|
+
whiteSpace: 'nowrap',
|
|
469
|
+
writingMode: 'vertical-rl',
|
|
470
|
+
transform: 'rotate(180deg)',
|
|
471
|
+
}}
|
|
472
|
+
>
|
|
473
|
+
{tick.value}
|
|
474
|
+
</span>
|
|
475
|
+
)}
|
|
476
|
+
</span>
|
|
477
|
+
))}
|
|
478
|
+
</button>
|
|
479
|
+
<div style={{ ...rulerBase, left: 0, top: 0, width: 20, height: 20 }} />
|
|
480
|
+
</>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Standard 2D scene controls: transform tools on the left, display/framing
|
|
485
|
+
* on the right, and zoom at the lower-right. All operate on the independent
|
|
486
|
+
* editor camera; none changes the game's runtime camera or resolution. */
|
|
487
|
+
export function CanvasSceneControls({
|
|
488
|
+
active,
|
|
489
|
+
adapter,
|
|
490
|
+
containerRef,
|
|
491
|
+
view,
|
|
492
|
+
documentId,
|
|
493
|
+
}: {
|
|
494
|
+
active: boolean;
|
|
495
|
+
adapter?: AuthoringAdapter;
|
|
496
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
497
|
+
view: RootViewController;
|
|
498
|
+
documentId: string;
|
|
499
|
+
}) {
|
|
500
|
+
const store = useEditorStore();
|
|
501
|
+
useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
502
|
+
useSyncExternalStore(subscribeViewportPresentation, viewportPresentationVersion);
|
|
503
|
+
const pose = useSyncExternalStore(view.subscribe, view.get, view.get);
|
|
504
|
+
// The mode is the view's, so it survives the remount a source write causes (Godot's stays on).
|
|
505
|
+
const [mode, setModeState] = useState<CanvasSceneMode | null>(() => sceneModes.get(view) ?? null);
|
|
506
|
+
const setMode = useCallback(
|
|
507
|
+
(next: CanvasSceneMode | null) => {
|
|
508
|
+
sceneModes.set(view, next);
|
|
509
|
+
setModeState(next);
|
|
510
|
+
},
|
|
511
|
+
[view],
|
|
512
|
+
);
|
|
513
|
+
// This document paints a canvas stage: the stage context says so, and the canvas keys key on it.
|
|
514
|
+
useEffect(() => markCanvasStageDocument(documentId), [documentId]);
|
|
515
|
+
// The tools' keys and Godot's Pan and Ruler keys, on this 2D view while it is the active one.
|
|
516
|
+
useEffect(() => {
|
|
517
|
+
if (!active) return;
|
|
518
|
+
return bindCanvasSceneKeys(
|
|
519
|
+
store,
|
|
520
|
+
() => setMode(null),
|
|
521
|
+
(mode) => setMode(sceneModes.get(view) === mode ? null : mode),
|
|
522
|
+
() => toggleCanvasGrid(documentId),
|
|
523
|
+
);
|
|
524
|
+
}, [active, store, setMode, view, documentId]);
|
|
525
|
+
// One radio group however a tool is picked: a transform tool chosen by its key (W, E, R, T)
|
|
526
|
+
// leaves List Select, Pivot, Pan and Ruler as its button does.
|
|
527
|
+
const transformMode = store.transformMode;
|
|
528
|
+
const lastTransformMode = useRef(transformMode);
|
|
529
|
+
useEffect(() => {
|
|
530
|
+
if (lastTransformMode.current === transformMode) return;
|
|
531
|
+
lastTransformMode.current = transformMode;
|
|
532
|
+
setMode(null);
|
|
533
|
+
}, [transformMode, setMode]);
|
|
534
|
+
|
|
535
|
+
const zoomAroundCenter = useCallback(
|
|
536
|
+
(requestedZoom: number) => {
|
|
537
|
+
const container = containerRef.current;
|
|
538
|
+
if (!container) return;
|
|
539
|
+
const zoom = Math.min(SCENE_MAX_ZOOM, Math.max(SCENE_MIN_ZOOM, requestedZoom));
|
|
540
|
+
const cx = container.clientWidth / 2;
|
|
541
|
+
const cy = container.clientHeight / 2;
|
|
542
|
+
const worldX = (cx - pose.x) / pose.zoom;
|
|
543
|
+
const worldY = (cy - pose.y) / pose.zoom;
|
|
544
|
+
view.setView(cx - worldX * zoom, cy - worldY * zoom, zoom);
|
|
545
|
+
},
|
|
546
|
+
[containerRef, pose, view],
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
const rectsFor = useCallback(
|
|
550
|
+
(ids: Iterable<string>): DOMRectLike[] => {
|
|
551
|
+
if (!adapter?.rects) return [];
|
|
552
|
+
const rects: DOMRectLike[] = [];
|
|
553
|
+
for (const id of ids) {
|
|
554
|
+
const rect = adapter.rects.rect(id);
|
|
555
|
+
if (rect && rect.width > 0 && rect.height > 0) rects.push(rect);
|
|
556
|
+
}
|
|
557
|
+
return rects;
|
|
558
|
+
},
|
|
559
|
+
[adapter],
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
const frameSelection = useCallback(() => {
|
|
563
|
+
const container = containerRef.current;
|
|
564
|
+
if (!container) return;
|
|
565
|
+
frameBounds(container, view, unionRects(rectsFor(store.selectedEntityIds)));
|
|
566
|
+
}, [containerRef, rectsFor, store, view]);
|
|
567
|
+
|
|
568
|
+
// The zoom widget's first button (Godot's Center View): the game's viewport rectangle, or the
|
|
569
|
+
// origin when the project declares no resolution, in the middle of the pane at this zoom.
|
|
570
|
+
const centerView = useCallback(() => {
|
|
571
|
+
const container = containerRef.current;
|
|
572
|
+
if (!container) return;
|
|
573
|
+
const now = view.get();
|
|
574
|
+
const resolution = getCurrentProject()?.config.resolution;
|
|
575
|
+
const cx = resolution ? resolution.width / 2 : 0;
|
|
576
|
+
const cy = resolution ? resolution.height / 2 : 0;
|
|
577
|
+
view.setView(container.clientWidth / 2 - cx * now.zoom, container.clientHeight / 2 - cy * now.zoom, now.zoom);
|
|
578
|
+
}, [containerRef, view]);
|
|
579
|
+
|
|
580
|
+
const centerSelection = useCallback(() => {
|
|
581
|
+
const container = containerRef.current;
|
|
582
|
+
const bounds = unionRects(rectsFor(store.selectedEntityIds));
|
|
583
|
+
if (!container || !bounds) return;
|
|
584
|
+
const now = view.get();
|
|
585
|
+
view.setView(
|
|
586
|
+
container.clientWidth / 2 - (bounds.x + bounds.width / 2) * now.zoom,
|
|
587
|
+
container.clientHeight / 2 - (bounds.y + bounds.height / 2) * now.zoom,
|
|
588
|
+
now.zoom,
|
|
589
|
+
);
|
|
590
|
+
}, [containerRef, rectsFor, store, view]);
|
|
591
|
+
|
|
592
|
+
const frameScene = useCallback(() => {
|
|
593
|
+
const container = containerRef.current;
|
|
594
|
+
if (!container || !adapter) return;
|
|
595
|
+
frameBounds(container, view, unionRects(rectsFor(collectAllNodeIds(adapter))));
|
|
596
|
+
}, [adapter, containerRef, rectsFor, view]);
|
|
597
|
+
|
|
598
|
+
// Focus is an editor action, not a Three-camera action. The native Canvas
|
|
599
|
+
// Scene consumes the same hierarchy/hotkey commands as the Three Scene,
|
|
600
|
+
// then resolves them through Pixi's own bounds and its independent 2D
|
|
601
|
+
// editor camera. An inactive document stays MOUNTED, so only the active
|
|
602
|
+
// Scene may respond.
|
|
603
|
+
useEffect(() => {
|
|
604
|
+
if (!active) return;
|
|
605
|
+
return store.onViewportAction((action) => {
|
|
606
|
+
if (action.type === 'focus-entity') {
|
|
607
|
+
const container = containerRef.current;
|
|
608
|
+
if (!container) return;
|
|
609
|
+
frameBounds(container, view, unionRects(rectsFor([action.id])));
|
|
610
|
+
} else if (action.type === 'focus-selection') {
|
|
611
|
+
frameSelection();
|
|
612
|
+
} else if (action.type === 'focus-scene') {
|
|
613
|
+
frameScene();
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}, [active, containerRef, frameScene, frameSelection, rectsFor, store, view]);
|
|
617
|
+
|
|
618
|
+
return (
|
|
619
|
+
<>
|
|
620
|
+
{/* The editor's one transient-hint channel renders per SCENE surface
|
|
621
|
+
* (the world root's stage mounts it for the three viewport). Without it here a
|
|
622
|
+
* canvas world had no hint surface at all, so a refused write — the
|
|
623
|
+
* declared-prop transform refusal — could only speak in the Console,
|
|
624
|
+
* which a user watching the board does not read (pass 65). */}
|
|
625
|
+
<TransientHintOverlay />
|
|
626
|
+
{/* Godot's 2D modes are ONE radio group: a transform tool leaves List Select, Pivot, Pan and
|
|
627
|
+
Ruler, and while one of those is on no transform tool is lit. */}
|
|
628
|
+
<ToolStrip
|
|
629
|
+
dimensions="2d"
|
|
630
|
+
otherToolActive={mode !== null}
|
|
631
|
+
onToolArmed={() => setMode(null)}
|
|
632
|
+
// Godot's one row: Select, Move, Rotate, Scale, then List Select, Pivot, Pan and Ruler.
|
|
633
|
+
viewTools={
|
|
634
|
+
<>
|
|
635
|
+
<Tooltip text="List Select: click to list the selectable nodes there">
|
|
636
|
+
<IconButton
|
|
637
|
+
aria-label="List Select mode"
|
|
638
|
+
aria-pressed={mode === 'list'}
|
|
639
|
+
size="comfortable"
|
|
640
|
+
onClick={() => setMode(mode === 'list' ? null : 'list')}
|
|
641
|
+
>
|
|
642
|
+
<EditorIcon icon={faListUl} size="md" />
|
|
643
|
+
</IconButton>
|
|
644
|
+
</Tooltip>
|
|
645
|
+
<Tooltip text="Pivot: click or drag to put the selected node's pivot there">
|
|
646
|
+
<IconButton
|
|
647
|
+
aria-label="Pivot mode"
|
|
648
|
+
aria-pressed={mode === 'pivot'}
|
|
649
|
+
size="comfortable"
|
|
650
|
+
onClick={(event) => {
|
|
651
|
+
// Godot: Shift on this button puts the temporary pivot at the selection's centre.
|
|
652
|
+
if (event.shiftKey) {
|
|
653
|
+
const rects = adapter
|
|
654
|
+
? [...store.selectedEntityIds]
|
|
655
|
+
.map((id) => adapter.rects?.rect(id))
|
|
656
|
+
.filter((rect): rect is DOMRectLike => rect != null)
|
|
657
|
+
: [];
|
|
658
|
+
const bounds = unionRects(rects);
|
|
659
|
+
if (bounds) {
|
|
660
|
+
setTemporaryPivot(view, {
|
|
661
|
+
x: bounds.x + bounds.width / 2,
|
|
662
|
+
y: bounds.y + bounds.height / 2,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
setMode('pivot');
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
setMode(mode === 'pivot' ? null : 'pivot');
|
|
669
|
+
}}
|
|
670
|
+
>
|
|
671
|
+
<EditorIcon icon={faBullseye} size="md" />
|
|
672
|
+
</IconButton>
|
|
673
|
+
</Tooltip>
|
|
674
|
+
<Tooltip text="Pan (Space-drag pans in any mode)">
|
|
675
|
+
<IconButton
|
|
676
|
+
aria-label="Pan mode"
|
|
677
|
+
aria-pressed={mode === 'pan'}
|
|
678
|
+
size="comfortable"
|
|
679
|
+
onClick={() => setMode(mode === 'pan' ? null : 'pan')}
|
|
680
|
+
>
|
|
681
|
+
<EditorIcon icon={faHand} size="md" />
|
|
682
|
+
</IconButton>
|
|
683
|
+
</Tooltip>
|
|
684
|
+
<Tooltip text="Ruler: drag to measure distance and angle">
|
|
685
|
+
<IconButton
|
|
686
|
+
aria-label="Ruler mode"
|
|
687
|
+
aria-pressed={mode === 'ruler'}
|
|
688
|
+
size="comfortable"
|
|
689
|
+
onClick={() => setMode(mode === 'ruler' ? null : 'ruler')}
|
|
690
|
+
>
|
|
691
|
+
<EditorIcon icon={faRulerCombined} size="md" />
|
|
692
|
+
</IconButton>
|
|
693
|
+
</Tooltip>
|
|
694
|
+
</>
|
|
695
|
+
}
|
|
696
|
+
// …and after snapping, Godot's Lock, Group and View menu, in the same row.
|
|
697
|
+
trailingTools={
|
|
698
|
+
<>
|
|
699
|
+
<CanvasSceneLockButton adapter={adapter} selected={[...store.selectedEntityIds]} />
|
|
700
|
+
<CanvasSceneGroupButton adapter={adapter} selected={[...store.selectedEntityIds]} />
|
|
701
|
+
<CanvasSceneViewMenu
|
|
702
|
+
documentId={documentId}
|
|
703
|
+
view={view}
|
|
704
|
+
hasSelection={store.selectedEntityIds.size > 0}
|
|
705
|
+
onCenterSelection={centerSelection}
|
|
706
|
+
onFrameSelection={frameSelection}
|
|
707
|
+
/>
|
|
708
|
+
</>
|
|
709
|
+
}
|
|
710
|
+
/>
|
|
711
|
+
<CanvasSceneEditGizmos adapter={adapter} view={view} documentId={documentId} />
|
|
712
|
+
{mode ? (
|
|
713
|
+
<CanvasSceneModeLayer
|
|
714
|
+
mode={mode}
|
|
715
|
+
view={view}
|
|
716
|
+
adapter={adapter}
|
|
717
|
+
containerRef={containerRef}
|
|
718
|
+
onExit={() => setMode(null)}
|
|
719
|
+
/>
|
|
720
|
+
) : null}
|
|
721
|
+
<FloatingToolbar
|
|
722
|
+
label="2D scene display"
|
|
723
|
+
className="volter-viewport-toolbar volter-viewport-toolbar-right"
|
|
724
|
+
data-volter-canvas-navigation-ignore="true"
|
|
725
|
+
>
|
|
726
|
+
<Button aria-label="Frame all" variant="ghost" size="comfortable" onClick={frameScene}>
|
|
727
|
+
Frame all
|
|
728
|
+
</Button>
|
|
729
|
+
</FloatingToolbar>
|
|
730
|
+
<FloatingToolbar
|
|
731
|
+
label="2D scene navigation"
|
|
732
|
+
className="volter-canvas-scene-navigation"
|
|
733
|
+
data-volter-canvas-navigation-ignore="true"
|
|
734
|
+
>
|
|
735
|
+
<IconButton
|
|
736
|
+
aria-label="Center view"
|
|
737
|
+
title="Center view: the game's viewport in the middle of the pane, at this zoom"
|
|
738
|
+
size="comfortable"
|
|
739
|
+
onClick={centerView}
|
|
740
|
+
>
|
|
741
|
+
<EditorIcon icon={faCrosshairs} size="md" />
|
|
742
|
+
</IconButton>
|
|
743
|
+
<IconButton
|
|
744
|
+
aria-label="Zoom out"
|
|
745
|
+
title="Zoom out"
|
|
746
|
+
size="comfortable"
|
|
747
|
+
onClick={() => zoomAroundCenter(pose.zoom / 1.2)}
|
|
748
|
+
>
|
|
749
|
+
<EditorIcon icon={faMagnifyingGlassMinus} size="md" />
|
|
750
|
+
</IconButton>
|
|
751
|
+
<Button
|
|
752
|
+
variant="ghost"
|
|
753
|
+
aria-label="Reset zoom to 100%"
|
|
754
|
+
title="Reset zoom to 100%"
|
|
755
|
+
size="comfortable"
|
|
756
|
+
onClick={() => zoomAroundCenter(1)}
|
|
757
|
+
>
|
|
758
|
+
{Math.round(pose.zoom * 100)}%
|
|
759
|
+
</Button>
|
|
760
|
+
<IconButton
|
|
761
|
+
aria-label="Zoom in"
|
|
762
|
+
title="Zoom in"
|
|
763
|
+
size="comfortable"
|
|
764
|
+
onClick={() => zoomAroundCenter(pose.zoom * 1.2)}
|
|
765
|
+
>
|
|
766
|
+
<EditorIcon icon={faMagnifyingGlassPlus} size="md" />
|
|
767
|
+
</IconButton>
|
|
768
|
+
</FloatingToolbar>
|
|
769
|
+
</>
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* THE 2D VIEW MENU — Godot's 2D View menu, the home of what the view draws and where it looks:
|
|
775
|
+
* the grid, rulers, guides, origin and the game's viewport rectangle (each the view's own switch,
|
|
776
|
+
* `overlays` in its presentation), then Center Selection, Frame Selection and Clear Guides.
|
|
777
|
+
*/
|
|
778
|
+
/** Godot's lock and group gizmos: a mark at each locked or grouped node's corner, drawn over the
|
|
779
|
+
* scene and never in the way of a click. */
|
|
780
|
+
function CanvasSceneEditGizmos({
|
|
781
|
+
adapter,
|
|
782
|
+
view,
|
|
783
|
+
documentId,
|
|
784
|
+
}: {
|
|
785
|
+
adapter: AuthoringAdapter | undefined;
|
|
786
|
+
view: RootViewController;
|
|
787
|
+
documentId: string;
|
|
788
|
+
}) {
|
|
789
|
+
const pose = useSyncExternalStore(view.subscribe, view.get, view.get);
|
|
790
|
+
const subscribe = useCallback(
|
|
791
|
+
(listener: () => void) => adapter?.subscribe?.(listener) ?? (() => undefined),
|
|
792
|
+
[adapter],
|
|
793
|
+
);
|
|
794
|
+
const [revision, bump] = useState(0);
|
|
795
|
+
useEffect(() => subscribe(() => bump((n) => n + 1)), [subscribe]);
|
|
796
|
+
useSyncExternalStore(subscribeViewportPresentation, viewportPresentationVersion);
|
|
797
|
+
const drafting = viewDrafting(documentId);
|
|
798
|
+
if (!adapter?.inspector || !adapter.rects || (!drafting.lock && !drafting.group)) return null;
|
|
799
|
+
const marks = collectAllNodeIds(adapter).flatMap((id) => {
|
|
800
|
+
const locked = drafting.lock && adapter.inspector?.get(id, 'locked') === true;
|
|
801
|
+
const grouped = drafting.group && adapter.inspector?.get(id, 'grouped') === true;
|
|
802
|
+
const rect = locked || grouped ? adapter.rects?.rect(id) : null;
|
|
803
|
+
return rect ? [{ id, locked, grouped, x: pose.x + rect.x * pose.zoom, y: pose.y + rect.y * pose.zoom }] : [];
|
|
804
|
+
});
|
|
805
|
+
return (
|
|
806
|
+
<div
|
|
807
|
+
data-testid="canvas-scene-edit-gizmos"
|
|
808
|
+
data-revision={revision}
|
|
809
|
+
style={{ position: 'absolute', inset: 0, zIndex: 55, pointerEvents: 'none', overflow: 'hidden' }}
|
|
810
|
+
>
|
|
811
|
+
{marks.map((mark) => (
|
|
812
|
+
<div
|
|
813
|
+
key={mark.id}
|
|
814
|
+
data-testid="canvas-scene-edit-gizmo"
|
|
815
|
+
data-locked={mark.locked}
|
|
816
|
+
data-grouped={mark.grouped}
|
|
817
|
+
style={{ position: 'absolute', left: mark.x + 2, top: mark.y + 2, display: 'flex', gap: 2, color: '#fff' }}
|
|
818
|
+
>
|
|
819
|
+
{mark.locked ? <EditorIcon icon={faLock} size="xs" /> : null}
|
|
820
|
+
{mark.grouped ? <EditorIcon icon={faLayerGroup} size="xs" /> : null}
|
|
821
|
+
</div>
|
|
822
|
+
))}
|
|
823
|
+
</div>
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function CanvasSceneViewMenu({
|
|
828
|
+
documentId,
|
|
829
|
+
view,
|
|
830
|
+
hasSelection,
|
|
831
|
+
onCenterSelection,
|
|
832
|
+
onFrameSelection,
|
|
833
|
+
}: {
|
|
834
|
+
documentId: string;
|
|
835
|
+
view: RootViewController;
|
|
836
|
+
hasSelection: boolean;
|
|
837
|
+
onCenterSelection: () => void;
|
|
838
|
+
onFrameSelection: () => void;
|
|
839
|
+
}) {
|
|
840
|
+
const [open, setOpen] = useState(false);
|
|
841
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
842
|
+
useSyncExternalStore(subscribeViewportPresentation, viewportPresentationVersion);
|
|
843
|
+
useSyncExternalStore(
|
|
844
|
+
useCallback((listener) => subscribeCanvasSceneGuides(view, listener), [view]),
|
|
845
|
+
useCallback(() => canvasSceneGuideRevision(view), [view]),
|
|
846
|
+
);
|
|
847
|
+
const store = useEditorStore();
|
|
848
|
+
useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
849
|
+
const drafting = viewDrafting(documentId);
|
|
850
|
+
const gridShown = viewGridVisible(documentId);
|
|
851
|
+
// Godot's Grid submenu: Show Grid, Show When Snapping, Hide Grid.
|
|
852
|
+
const gridStates: readonly { label: string; on: boolean; pick: () => void }[] = [
|
|
853
|
+
{
|
|
854
|
+
label: 'Show Grid',
|
|
855
|
+
on: gridShown && !drafting.gridWhenSnapping,
|
|
856
|
+
pick: () => {
|
|
857
|
+
setViewGridVisible(documentId, true);
|
|
858
|
+
setViewDrafting(documentId, { gridWhenSnapping: false });
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
label: 'Show Grid When Snapping',
|
|
863
|
+
on: gridShown && drafting.gridWhenSnapping,
|
|
864
|
+
pick: () => {
|
|
865
|
+
setViewGridVisible(documentId, true);
|
|
866
|
+
setViewDrafting(documentId, { gridWhenSnapping: true });
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
{ label: 'Hide Grid', on: !gridShown, pick: () => setViewGridVisible(documentId, false) },
|
|
870
|
+
];
|
|
871
|
+
const toggleGrid = (): void => toggleCanvasGrid(documentId);
|
|
872
|
+
const switches: readonly { label: string; on: boolean; toggle: () => void }[] = [
|
|
873
|
+
{ label: 'Rulers', on: drafting.rulers, toggle: () => setViewDrafting(documentId, { rulers: !drafting.rulers }) },
|
|
874
|
+
{ label: 'Guides', on: drafting.guides, toggle: () => setViewDrafting(documentId, { guides: !drafting.guides }) },
|
|
875
|
+
{ label: 'Origin', on: drafting.origin, toggle: () => setViewDrafting(documentId, { origin: !drafting.origin }) },
|
|
876
|
+
{ label: 'Viewport', on: drafting.viewport, toggle: () => setViewDrafting(documentId, { viewport: !drafting.viewport }) },
|
|
877
|
+
];
|
|
878
|
+
const act = (run: () => void) => () => {
|
|
879
|
+
run();
|
|
880
|
+
setOpen(false);
|
|
881
|
+
};
|
|
882
|
+
return (
|
|
883
|
+
<>
|
|
884
|
+
<Button
|
|
885
|
+
ref={triggerRef}
|
|
886
|
+
aria-label="View"
|
|
887
|
+
aria-haspopup="menu"
|
|
888
|
+
aria-expanded={open}
|
|
889
|
+
variant="ghost"
|
|
890
|
+
size="comfortable"
|
|
891
|
+
onClick={() => setOpen(!open)}
|
|
892
|
+
>
|
|
893
|
+
View
|
|
894
|
+
</Button>
|
|
895
|
+
{open && (
|
|
896
|
+
<AnchoredMenu anchorRef={triggerRef} align="end" clamp aria-label="2D view" onDismiss={() => setOpen(false)}>
|
|
897
|
+
{/* Godot's View menu: a Grid submenu, the Show switches, a Gizmos submenu, then the view
|
|
898
|
+
verbs. */}
|
|
899
|
+
<MenuSubmenu label="Grid" data-testid="canvas-scene-grid-submenu">
|
|
900
|
+
{gridStates.map((entry) => (
|
|
901
|
+
<MenuItem key={entry.label} role="menuitemradio" aria-checked={entry.on} onSelect={act(entry.pick)}>
|
|
902
|
+
<span className="volter-menu-check">{entry.on && <EditorIcon icon={faCheck} size="xs" />}</span>
|
|
903
|
+
{entry.label}
|
|
904
|
+
</MenuItem>
|
|
905
|
+
))}
|
|
906
|
+
<MenuSeparator />
|
|
907
|
+
<MenuItem data-testid="canvas-scene-toggle-grid" onSelect={act(toggleGrid)}>
|
|
908
|
+
<span className="volter-menu-check" />
|
|
909
|
+
Toggle Grid
|
|
910
|
+
</MenuItem>
|
|
911
|
+
</MenuSubmenu>
|
|
912
|
+
{switches.map((entry) => (
|
|
913
|
+
<MenuItem
|
|
914
|
+
key={entry.label}
|
|
915
|
+
role="menuitemcheckbox"
|
|
916
|
+
aria-checked={entry.on}
|
|
917
|
+
// Godot's menu closes on a checked item too (`hide_on_checkable_item_selection`).
|
|
918
|
+
onSelect={act(entry.toggle)}
|
|
919
|
+
>
|
|
920
|
+
<span className="volter-menu-check">{entry.on && <EditorIcon icon={faCheck} size="xs" />}</span>
|
|
921
|
+
{entry.label.startsWith('Show ') ? entry.label : `Show ${entry.label}`}
|
|
922
|
+
</MenuItem>
|
|
923
|
+
))}
|
|
924
|
+
{/* Godot's Gizmos submenu, in its order: Position, Lock, Group, Transformation. */}
|
|
925
|
+
<MenuSubmenu label="Gizmos" data-testid="canvas-scene-gizmos-submenu">
|
|
926
|
+
{(
|
|
927
|
+
[
|
|
928
|
+
['position', 'Position'],
|
|
929
|
+
['lock', 'Lock'],
|
|
930
|
+
['group', 'Group'],
|
|
931
|
+
['transformation', 'Transformation'],
|
|
932
|
+
] as const
|
|
933
|
+
).map(([key, label]) => (
|
|
934
|
+
<MenuItem
|
|
935
|
+
key={key}
|
|
936
|
+
role="menuitemcheckbox"
|
|
937
|
+
aria-checked={drafting[key]}
|
|
938
|
+
onSelect={act(() => setViewDrafting(documentId, { [key]: !drafting[key] }))}
|
|
939
|
+
>
|
|
940
|
+
<span className="volter-menu-check">{drafting[key] && <EditorIcon icon={faCheck} size="xs" />}</span>
|
|
941
|
+
{label}
|
|
942
|
+
</MenuItem>
|
|
943
|
+
))}
|
|
944
|
+
</MenuSubmenu>
|
|
945
|
+
<MenuSeparator />
|
|
946
|
+
<MenuItem disabled={!hasSelection} onSelect={act(onCenterSelection)}>
|
|
947
|
+
<span className="volter-menu-check" />
|
|
948
|
+
Center Selection
|
|
949
|
+
</MenuItem>
|
|
950
|
+
<MenuItem disabled={!hasSelection} onSelect={act(onFrameSelection)}>
|
|
951
|
+
<span className="volter-menu-check" />
|
|
952
|
+
Frame Selection
|
|
953
|
+
</MenuItem>
|
|
954
|
+
<MenuItem disabled={canvasSceneGuides(view).length === 0} onSelect={act(() => clearCanvasSceneGuides(view))}>
|
|
955
|
+
<span className="volter-menu-check" />
|
|
956
|
+
Clear Guides
|
|
957
|
+
</MenuItem>
|
|
958
|
+
</AnchoredMenu>
|
|
959
|
+
)}
|
|
960
|
+
</>
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Lock the selected node against selection and movement — Godot's 2D toolbar Lock, a shortcut to
|
|
966
|
+
* the lock the hierarchy owns (`locked`, the same session-local flag its row toggles).
|
|
967
|
+
*/
|
|
968
|
+
function CanvasSceneLockButton({ adapter, selected }: { adapter: AuthoringAdapter | undefined; selected: readonly string[] }) {
|
|
969
|
+
// Godot's Lock acts on every selected node: all locked, it unlocks them; else it locks them all.
|
|
970
|
+
const offered = selected.filter((id) => adapter?.inspector?.properties(id).some((p) => p.path === 'locked'));
|
|
971
|
+
const locked = offered.length > 0 && offered.every((id) => adapter?.inspector?.get(id, 'locked') === true);
|
|
972
|
+
const plural = offered.length > 1 ? 'nodes' : 'node';
|
|
973
|
+
const label = locked ? `Unlock selected ${plural}` : `Lock selected ${plural}`;
|
|
974
|
+
return (
|
|
975
|
+
<Tooltip text={offered.length > 0 ? label : 'Select a node to lock it'}>
|
|
976
|
+
<IconButton
|
|
977
|
+
aria-label={label}
|
|
978
|
+
aria-pressed={locked}
|
|
979
|
+
size="comfortable"
|
|
980
|
+
disabled={offered.length === 0 || !adapter?.inspector?.set}
|
|
981
|
+
onClick={() => {
|
|
982
|
+
for (const id of offered) adapter?.inspector?.set?.(id, 'locked', !locked);
|
|
983
|
+
}}
|
|
984
|
+
>
|
|
985
|
+
<EditorIcon icon={locked ? faLock : faLockOpen} size="md" />
|
|
986
|
+
</IconButton>
|
|
987
|
+
</Tooltip>
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Godot's Pan and Ruler modes: while one is on, this layer over the scene takes the pointer. Pan
|
|
993
|
+
* drags the view with the primary button; Ruler draws the drag as a line and reads its length in
|
|
994
|
+
* world units and its angle. Escape leaves the mode.
|
|
995
|
+
*/
|
|
996
|
+
/** Godot's Toggle Grid (measured on 4.7.1): from Show it goes to Show When Snapping, and from any
|
|
997
|
+
* other state (When Snapping, Hide) to Show — Hide → Show → When Snapping → Show. */
|
|
998
|
+
function toggleCanvasGrid(documentId: string): void {
|
|
999
|
+
const showing = viewGridVisible(documentId) && !viewDrafting(documentId).gridWhenSnapping;
|
|
1000
|
+
setViewGridVisible(documentId, true);
|
|
1001
|
+
setViewDrafting(documentId, { gridWhenSnapping: showing });
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/** Godot's 2D modes beside the transform tools: Pan, Ruler, List Select and Pivot. */
|
|
1005
|
+
type CanvasSceneMode = 'pan' | 'ruler' | 'list' | 'pivot';
|
|
1006
|
+
const sceneModes = new WeakMap<RootViewController, CanvasSceneMode | null>();
|
|
1007
|
+
|
|
1008
|
+
function CanvasSceneModeLayer({
|
|
1009
|
+
mode,
|
|
1010
|
+
view,
|
|
1011
|
+
adapter,
|
|
1012
|
+
containerRef,
|
|
1013
|
+
onExit,
|
|
1014
|
+
}: {
|
|
1015
|
+
mode: CanvasSceneMode;
|
|
1016
|
+
view: RootViewController;
|
|
1017
|
+
adapter: AuthoringAdapter | undefined;
|
|
1018
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
1019
|
+
onExit: () => void;
|
|
1020
|
+
}) {
|
|
1021
|
+
const store = useEditorStore();
|
|
1022
|
+
const pose = useSyncExternalStore(view.subscribe, view.get, view.get);
|
|
1023
|
+
const [measure, setMeasure] = useState<{ from: { x: number; y: number }; to: { x: number; y: number } } | null>(null);
|
|
1024
|
+
const [pick, setPick] = useState<{ x: number; y: number; ids: readonly string[] } | null>(null);
|
|
1025
|
+
useEffect(() => {
|
|
1026
|
+
const onKey = (event: KeyboardEvent) => {
|
|
1027
|
+
if (event.key === 'Escape') {
|
|
1028
|
+
// Escape in Pivot mode lets go of the temporary pivot too.
|
|
1029
|
+
if (mode === 'pivot') setTemporaryPivot(view, null);
|
|
1030
|
+
onExit();
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
window.addEventListener('keydown', onKey);
|
|
1034
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
1035
|
+
}, [onExit]);
|
|
1036
|
+
const worldAt = (clientX: number, clientY: number) => {
|
|
1037
|
+
const box = containerRef.current?.getBoundingClientRect();
|
|
1038
|
+
const now = view.get();
|
|
1039
|
+
const x = clientX - (box?.left ?? 0);
|
|
1040
|
+
const y = clientY - (box?.top ?? 0);
|
|
1041
|
+
return { x: (x - now.x) / now.zoom, y: (y - now.y) / now.zoom };
|
|
1042
|
+
};
|
|
1043
|
+
/** A ruler point snaps as Godot's does (measured on 4.7.1: with grid snap on the point read
|
|
1044
|
+
* (1136, 552) on the 8 px grid where it read (1136, 549) off): the grid when the magnet is on,
|
|
1045
|
+
* then whole pixels under Use Pixel Snap. */
|
|
1046
|
+
const rulerPoint = (point: { x: number; y: number }): { x: number; y: number } => {
|
|
1047
|
+
const grid = store.snap2D;
|
|
1048
|
+
const onGrid = (value: number, offset: number): number =>
|
|
1049
|
+
store.snapEnabled && grid.step > 0 ? Math.round((value - offset) / grid.step) * grid.step + offset : value;
|
|
1050
|
+
const snapped = { x: onGrid(point.x, grid.offsetX), y: onGrid(point.y, grid.offsetY) };
|
|
1051
|
+
return grid.pixel ? { x: Math.round(snapped.x), y: Math.round(snapped.y) } : snapped;
|
|
1052
|
+
};
|
|
1053
|
+
/** Where the pivot goes for a point: snapped to its node's sides and centre as a dragged pivot is. */
|
|
1054
|
+
const pivotPoint = (id: string, point: { x: number; y: number }, invert: boolean) => {
|
|
1055
|
+
if (!adapter) return point;
|
|
1056
|
+
const rect = rectForId(adapter, id);
|
|
1057
|
+
return snapPivotPoint(point, frameForId(adapter, id) ?? (rect ? rectFrame(rect) : null), {
|
|
1058
|
+
invert,
|
|
1059
|
+
smart: store.smartSnap,
|
|
1060
|
+
gridOn: store.snapEnabled,
|
|
1061
|
+
grid: store.snap2D,
|
|
1062
|
+
threshold: EDGE_SNAP_THRESHOLD_PX / Math.max(view.get().zoom, 0.01),
|
|
1063
|
+
});
|
|
1064
|
+
};
|
|
1065
|
+
const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
|
|
1066
|
+
if (event.button !== 0) return;
|
|
1067
|
+
event.preventDefault();
|
|
1068
|
+
if (mode === 'list') {
|
|
1069
|
+
// Godot's List Select: every selectable node under the click, to choose from.
|
|
1070
|
+
const ids = pickCandidates(store, event.clientX, event.clientY, adapter ? { adapter } : {});
|
|
1071
|
+
setPick(ids.length > 0 ? { x: event.clientX, y: event.clientY, ids } : null);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (mode === 'pivot' && event.shiftKey) {
|
|
1075
|
+
// Godot's Shift in Pivot mode: a temporary pivot rotation turns around, no node's own pivot
|
|
1076
|
+
// changed.
|
|
1077
|
+
setTemporaryPivot(view, rulerPoint(worldAt(event.clientX, event.clientY)));
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (mode === 'pivot') {
|
|
1081
|
+
const selected = store.selectedEntityIds;
|
|
1082
|
+
const id = selected.size === 1 && adapter ? [...selected][0]! : null;
|
|
1083
|
+
const provider = id && adapter ? spatialHandlesForId(adapter, id) : null;
|
|
1084
|
+
const handle = id
|
|
1085
|
+
? provider
|
|
1086
|
+
?.layers(id)
|
|
1087
|
+
.find((layer) => layer.category === 'origin')
|
|
1088
|
+
?.handles.find((h) => h.writable)
|
|
1089
|
+
: undefined;
|
|
1090
|
+
if (!id || !provider || !handle) return;
|
|
1091
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1092
|
+
const at = (next: { clientX: number; clientY: number; metaKey: boolean; ctrlKey: boolean }) => {
|
|
1093
|
+
const point = pivotPoint(id, worldAt(next.clientX, next.clientY), next.metaKey || next.ctrlKey);
|
|
1094
|
+
return [point.x, point.y, 0] as const;
|
|
1095
|
+
};
|
|
1096
|
+
provider.preview(id, handle.id, at(event));
|
|
1097
|
+
const move = (next: PointerEvent) => provider.preview(id, handle.id, at(next));
|
|
1098
|
+
const end = (last: PointerEvent) => {
|
|
1099
|
+
window.removeEventListener('pointermove', move);
|
|
1100
|
+
void provider.commit(id, handle.id, at(last));
|
|
1101
|
+
};
|
|
1102
|
+
window.addEventListener('pointermove', move);
|
|
1103
|
+
window.addEventListener('pointerup', end, { once: true });
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1107
|
+
const start = { clientX: event.clientX, clientY: event.clientY, pose: view.get() };
|
|
1108
|
+
const from = worldAt(event.clientX, event.clientY);
|
|
1109
|
+
if (mode === 'ruler') setMeasure({ from: rulerPoint(from), to: rulerPoint(from) });
|
|
1110
|
+
const move = (next: PointerEvent) => {
|
|
1111
|
+
if (mode === 'pan') {
|
|
1112
|
+
view.setView(
|
|
1113
|
+
start.pose.x + next.clientX - start.clientX,
|
|
1114
|
+
start.pose.y + next.clientY - start.clientY,
|
|
1115
|
+
start.pose.zoom,
|
|
1116
|
+
);
|
|
1117
|
+
} else {
|
|
1118
|
+
setMeasure({ from: rulerPoint(from), to: rulerPoint(worldAt(next.clientX, next.clientY)) });
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
const end = () => {
|
|
1122
|
+
window.removeEventListener('pointermove', move);
|
|
1123
|
+
window.removeEventListener('pointerup', end);
|
|
1124
|
+
};
|
|
1125
|
+
window.addEventListener('pointermove', move);
|
|
1126
|
+
window.addEventListener('pointerup', end, { once: true });
|
|
1127
|
+
};
|
|
1128
|
+
const screen = (point: { x: number; y: number }) => ({ x: pose.x + point.x * pose.zoom, y: pose.y + point.y * pose.zoom });
|
|
1129
|
+
const length = measure ? Math.hypot(measure.to.x - measure.from.x, measure.to.y - measure.from.y) : 0;
|
|
1130
|
+
const angle = measure ? (Math.atan2(measure.to.y - measure.from.y, measure.to.x - measure.from.x) * 180) / Math.PI : 0;
|
|
1131
|
+
const a = measure ? screen(measure.from) : null;
|
|
1132
|
+
const b = measure ? screen(measure.to) : null;
|
|
1133
|
+
return (
|
|
1134
|
+
<div
|
|
1135
|
+
data-testid={`canvas-scene-${mode}-layer`}
|
|
1136
|
+
data-volter-canvas-navigation-ignore="true"
|
|
1137
|
+
onPointerDown={onPointerDown}
|
|
1138
|
+
// Above the selection hit layer (z50), below the viewport toolbars.
|
|
1139
|
+
style={{ position: 'absolute', inset: 0, zIndex: 60, cursor: mode === 'pan' ? 'grab' : mode === 'list' ? 'context-menu' : 'crosshair' }}
|
|
1140
|
+
>
|
|
1141
|
+
{pick && adapter ? <ViewportPickMenu state={pick} adapter={adapter} onClose={() => setPick(null)} /> : null}
|
|
1142
|
+
{a && b ? (
|
|
1143
|
+
<>
|
|
1144
|
+
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
|
|
1145
|
+
<line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke="#ff5fa2" strokeWidth={1.5} />
|
|
1146
|
+
</svg>
|
|
1147
|
+
<div
|
|
1148
|
+
data-testid="canvas-scene-ruler-reading"
|
|
1149
|
+
style={{
|
|
1150
|
+
position: 'absolute',
|
|
1151
|
+
left: (a.x + b.x) / 2 + 8,
|
|
1152
|
+
top: (a.y + b.y) / 2 + 8,
|
|
1153
|
+
padding: '2px 6px',
|
|
1154
|
+
borderRadius: 4,
|
|
1155
|
+
background: 'rgba(24, 26, 30, .92)',
|
|
1156
|
+
color: '#fff',
|
|
1157
|
+
font: '11px ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
1158
|
+
pointerEvents: 'none',
|
|
1159
|
+
whiteSpace: 'nowrap',
|
|
1160
|
+
}}
|
|
1161
|
+
>
|
|
1162
|
+
{`${length.toFixed(1)} px · ${angle.toFixed(1)}° · Δ ${(measure!.to.x - measure!.from.x).toFixed(1)}, ${(measure!.to.y - measure!.from.y).toFixed(1)}`}
|
|
1163
|
+
</div>
|
|
1164
|
+
</>
|
|
1165
|
+
) : null}
|
|
1166
|
+
</div>
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Group the selected node with its children — Godot's 2D toolbar Group: a click on any node inside
|
|
1172
|
+
* it then selects the group (`grouped`, beside `locked`; the hierarchy still reaches every child).
|
|
1173
|
+
*/
|
|
1174
|
+
function CanvasSceneGroupButton({ adapter, selected }: { adapter: AuthoringAdapter | undefined; selected: readonly string[] }) {
|
|
1175
|
+
// Godot's Group acts on every selected node, as Lock does.
|
|
1176
|
+
const offered = selected.filter((id) => adapter?.inspector?.properties(id).some((p) => p.path === 'grouped'));
|
|
1177
|
+
const grouped = offered.length > 0 && offered.every((id) => adapter?.inspector?.get(id, 'grouped') === true);
|
|
1178
|
+
const plural = offered.length > 1 ? 'nodes' : 'node';
|
|
1179
|
+
const label = grouped ? `Ungroup selected ${plural}` : `Group selected ${plural} with their children`;
|
|
1180
|
+
return (
|
|
1181
|
+
<Tooltip text={offered.length > 0 ? label : 'Select a node to group it'}>
|
|
1182
|
+
<IconButton
|
|
1183
|
+
aria-label={label}
|
|
1184
|
+
aria-pressed={grouped}
|
|
1185
|
+
size="comfortable"
|
|
1186
|
+
disabled={offered.length === 0 || !adapter?.inspector?.set}
|
|
1187
|
+
onClick={() => {
|
|
1188
|
+
for (const id of offered) adapter?.inspector?.set?.(id, 'grouped', !grouped);
|
|
1189
|
+
}}
|
|
1190
|
+
>
|
|
1191
|
+
<EditorIcon icon={faLayerGroup} size="md" />
|
|
1192
|
+
</IconButton>
|
|
1193
|
+
</Tooltip>
|
|
1194
|
+
);
|
|
1195
|
+
}
|