@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,1479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `planCreationSiteEdit` — the PURE plan for "write this property edit
|
|
3
|
+
* into the game's own source at the line that constructed the object".
|
|
4
|
+
*
|
|
5
|
+
* The read half: a serve-time transform stamps every direct
|
|
6
|
+
* `new` in a served project module, and a host-side `WeakMap` answers "which
|
|
7
|
+
* `file:line` built this live object?" (`creation-site-registry.ts`). This
|
|
8
|
+
* module is the write half's decision-maker. It takes the anchor, the source
|
|
9
|
+
* bytes, the value in force BEFORE the edit and the value the user produced,
|
|
10
|
+
* and returns either exact new bytes or a NAMED refusal — never a guess.
|
|
11
|
+
*
|
|
12
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
* WHY THIS IS NOT A THREE.JS SIGNATURE TABLE, AND MUST NEVER BECOME ONE
|
|
14
|
+
*
|
|
15
|
+
* The tempting way to write `new THREE.DirectionalLight(0xffffff, 2)` back is a
|
|
16
|
+
* table saying "argument 1 of DirectionalLight is `intensity`". That table is
|
|
17
|
+
* fabrication with extra steps: it is unverifiable at the edit, it is wrong for
|
|
18
|
+
* every constructor nobody wrote a row for, and when it is wrong it silently
|
|
19
|
+
* corrupts someone else's game. This module knows NOTHING about three.js.
|
|
20
|
+
*
|
|
21
|
+
* Two rules replace that knowledge, and they do different jobs. The first
|
|
22
|
+
* decides WHICH literal may be written; the second decides WHETHER writing it
|
|
23
|
+
* is still true. Both must hold.
|
|
24
|
+
*
|
|
25
|
+
* 1. NAME ADDRESSING — the only way a literal ever becomes a candidate. The
|
|
26
|
+
* SOURCE ITSELF must name the property: `sun.position.set(…)` contains the
|
|
27
|
+
* word `position`; `new THREE.MeshBasicMaterial({ opacity: 0.2 })` contains
|
|
28
|
+
* the word `opacity`. A BARE POSITIONAL ARGUMENT NAMES NOTHING AND IS NEVER
|
|
29
|
+
* A CANDIDATE, no matter what its value happens to be. `scanConstructor`'s
|
|
30
|
+
* comment carries the three real corruptions that rule was bought with.
|
|
31
|
+
*
|
|
32
|
+
* 2. THE BASELINE-EQUALITY BACKSTOP. A candidate may be rewritten only when
|
|
33
|
+
* its CURRENT value equals the value in force on the live object. That
|
|
34
|
+
* subsumes an unbounded amount of static analysis this module deliberately
|
|
35
|
+
* does not do:
|
|
36
|
+
* - a later `sun.position.copy(target)` we never modelled? then the
|
|
37
|
+
* literal is not what is in force, so we refuse;
|
|
38
|
+
* - the object escaped into a function that mutated it? same;
|
|
39
|
+
* - the simulation animates the value every frame? same.
|
|
40
|
+
* Its contrapositive is the honest claim this feature makes: when we DO
|
|
41
|
+
* write, the literal we replace is demonstrably the value the user was
|
|
42
|
+
* looking at, so replacing it produces what they dragged to.
|
|
43
|
+
*
|
|
44
|
+
* NOTE WHAT RULE 2 CANNOT DO, because an earlier revision of this file got it
|
|
45
|
+
* wrong and shipped a corruption: it cannot tell a right slot from a wrong one.
|
|
46
|
+
* Equality is evidence about the VALUE, never about which slot means what — and
|
|
47
|
+
* when a coincidence is what put the two in agreement, the coincidence IS the
|
|
48
|
+
* equality, so the backstop nods it through. That is precisely why rule 1 is a
|
|
49
|
+
* gate and not a preference: rule 2 defends the write's TRUTH, and only rule 1
|
|
50
|
+
* defends its TARGET.
|
|
51
|
+
*
|
|
52
|
+
* Everything else here — receiver resolution, the enclosing-function walk,
|
|
53
|
+
* multiplicity, per-component homes — exists to FIND candidates under rule 1.
|
|
54
|
+
*
|
|
55
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
* BYTE-IDENTITY ON REFUSAL. Every refusal returns `newSource === prevSource`,
|
|
57
|
+
* identity-equal, and `changed: false`. A caller that writes on `changed` can
|
|
58
|
+
* never be tricked into a no-op write, and the file a refused gesture touched is
|
|
59
|
+
* bit-for-bit the file it started as. (`creation-site-edit.test.ts` asserts this
|
|
60
|
+
* over every refusal branch — the reparent-guard precedent.)
|
|
61
|
+
*
|
|
62
|
+
* ONE TRANSACTION. Every rule is decided before the first byte moves, and an
|
|
63
|
+
* accepted plan is ONE new whole-file string covering every component the
|
|
64
|
+
* gesture touched. The caller turns that into one history transaction pairing
|
|
65
|
+
* the live mutation with the source write, so undo restores both.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
import ts from 'typescript';
|
|
69
|
+
import type { CreationSite } from './creation-site-registry';
|
|
70
|
+
import { lineColToOffset } from './ui-source/oid-transform';
|
|
71
|
+
|
|
72
|
+
/** Float slack for "is this literal the value in force". Gizmo drags and the
|
|
73
|
+
* quaternion→Euler round trip both land a few ULPs off an authored decimal. */
|
|
74
|
+
const EPS = 1e-6;
|
|
75
|
+
|
|
76
|
+
/** What a channel's value looks like crossing this seam. Vector channels arrive
|
|
77
|
+
* as three numbers; everything else is one scalar. */
|
|
78
|
+
export type ChannelValue = number | boolean | string | readonly number[];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The live sub-object that actually OWNS a property, and therefore whose
|
|
82
|
+
* creation site the edit must be anchored to.
|
|
83
|
+
*
|
|
84
|
+
* `material.color` is the case that forces this to exist: the mesh was built by
|
|
85
|
+
* `new THREE.Mesh(geometry, gridMaterial)`, but the colour lives on the
|
|
86
|
+
* MATERIAL, built by its own `new THREE.MeshBasicMaterial({ color: … })`
|
|
87
|
+
* elsewhere. Anchoring a colour edit to the mesh's line would look for a `color`
|
|
88
|
+
* the mesh's constructor never mentions — and the only ways out of that are
|
|
89
|
+
* refusing a perfectly writable edit or inventing a link. Asking the caller for
|
|
90
|
+
* the material's OWN anchor is neither.
|
|
91
|
+
*/
|
|
92
|
+
export type ChannelOwner = 'self' | 'material';
|
|
93
|
+
|
|
94
|
+
export type ChannelKind =
|
|
95
|
+
| 'vector3'
|
|
96
|
+
| 'vector2'
|
|
97
|
+
| 'euler'
|
|
98
|
+
| 'color'
|
|
99
|
+
| 'number'
|
|
100
|
+
| 'boolean'
|
|
101
|
+
| 'string';
|
|
102
|
+
|
|
103
|
+
export interface PropertyChannel {
|
|
104
|
+
/** Which live object's creation site this property is anchored to. */
|
|
105
|
+
readonly owner: ChannelOwner;
|
|
106
|
+
/** The RECEIVER-relative member path as it is spelled in source
|
|
107
|
+
* (`position`, `intensity`, `castShadow`). */
|
|
108
|
+
readonly member: string;
|
|
109
|
+
readonly kind: ChannelKind;
|
|
110
|
+
/**
|
|
111
|
+
* Receiver-relative member paths that each hold ONE component of this channel
|
|
112
|
+
* DIRECTLY, in source order — a SECOND spelling of the same storage, not a
|
|
113
|
+
* second property.
|
|
114
|
+
*
|
|
115
|
+
* Pixi is why this exists: `Container.x` is a getter/setter over
|
|
116
|
+
* `this.position.x`, so `hud.x = 16` and `hud.position.set(16, 16)` are the
|
|
117
|
+
* same value written two ways, and both are ordinary idiom in an unmodified
|
|
118
|
+
* game. Modelling them as two channels would be the two-parallel-lists defect
|
|
119
|
+
* `IngestSourcePersistence.gate`'s comment records; modelling the alias as a
|
|
120
|
+
* spelling keeps ONE channel, one baseline, and one last-writer-wins ordering
|
|
121
|
+
* over every literal found either way.
|
|
122
|
+
*
|
|
123
|
+
* Absent for every three channel: `Object3D` has no `.x`.
|
|
124
|
+
*/
|
|
125
|
+
readonly componentMembers?: readonly string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The editor's inspector/transform property paths → what they are in source.
|
|
130
|
+
*
|
|
131
|
+
* This is a DATA table over paths the ingest adapter already reads and writes
|
|
132
|
+
* (`ThreeAuthoringAdapter.inspector.properties`), not a vocabulary invented
|
|
133
|
+
* here: every key is a path some existing editor surface already produces, and
|
|
134
|
+
* a path absent from this table is refused by name rather than guessed at.
|
|
135
|
+
*/
|
|
136
|
+
export const CREATION_SITE_CHANNELS: Readonly<Record<string, PropertyChannel>> = {
|
|
137
|
+
position: { owner: 'self', member: 'position', kind: 'vector3' },
|
|
138
|
+
rotation: { owner: 'self', member: 'rotation', kind: 'euler' },
|
|
139
|
+
scale: { owner: 'self', member: 'scale', kind: 'vector3' },
|
|
140
|
+
visible: { owner: 'self', member: 'visible', kind: 'boolean' },
|
|
141
|
+
name: { owner: 'self', member: 'name', kind: 'string' },
|
|
142
|
+
'material.color': { owner: 'material', member: 'color', kind: 'color' },
|
|
143
|
+
'light.color': { owner: 'self', member: 'color', kind: 'color' },
|
|
144
|
+
'light.intensity': { owner: 'self', member: 'intensity', kind: 'number' },
|
|
145
|
+
'light.distance': { owner: 'self', member: 'distance', kind: 'number' },
|
|
146
|
+
'camera.fov': { owner: 'self', member: 'fov', kind: 'number' },
|
|
147
|
+
'camera.near': { owner: 'self', member: 'near', kind: 'number' },
|
|
148
|
+
'camera.far': { owner: 'self', member: 'far', kind: 'number' },
|
|
149
|
+
'shadow.cast': { owner: 'self', member: 'castShadow', kind: 'boolean' },
|
|
150
|
+
'shadow.receive': { owner: 'self', member: 'receiveShadow', kind: 'boolean' },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The CANVAS surface's table — the same contract, a different vocabulary.
|
|
155
|
+
*
|
|
156
|
+
* It is a SEPARATE table rather than extra rows because the same editor path
|
|
157
|
+
* means a different thing on each surface: `position` is a three-component
|
|
158
|
+
* vector on a `THREE.Object3D` and a two-component one on a `PIXI.Container`,
|
|
159
|
+
* and `rotation` is an Euler triple there and a scalar in radians here. Merging
|
|
160
|
+
* them would need a per-row surface tag on every row, which is the same fork
|
|
161
|
+
* wearing a field.
|
|
162
|
+
*
|
|
163
|
+
* Every key is a path the canvas adapter already produces — the three
|
|
164
|
+
* `TransformChannel`s plus the reflected inspector rows
|
|
165
|
+
* (`AuthoringAdapter2D.properties`) — so nothing here is a vocabulary invented
|
|
166
|
+
* at the write path.
|
|
167
|
+
*
|
|
168
|
+
* `scale` has no `componentMembers` on purpose: Pixi has `sprite.x` but no
|
|
169
|
+
* `sprite.scaleX`, so listing one would be a spelling no game can contain.
|
|
170
|
+
*/
|
|
171
|
+
export const PIXI_CREATION_SITE_CHANNELS: Readonly<Record<string, PropertyChannel>> = {
|
|
172
|
+
position: { owner: 'self', member: 'position', kind: 'vector2', componentMembers: ['x', 'y'] },
|
|
173
|
+
rotation: { owner: 'self', member: 'rotation', kind: 'number' },
|
|
174
|
+
scale: { owner: 'self', member: 'scale', kind: 'vector2' },
|
|
175
|
+
visible: { owner: 'self', member: 'visible', kind: 'boolean' },
|
|
176
|
+
// The editor's `name` row is Pixi's `label` (`AuthoringAdapter2D.set` maps it).
|
|
177
|
+
name: { owner: 'self', member: 'label', kind: 'string' },
|
|
178
|
+
alpha: { owner: 'self', member: 'alpha', kind: 'number' },
|
|
179
|
+
tint: { owner: 'self', member: 'tint', kind: 'color' },
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** Babylon's native imperative transform/property spellings. The editor paths
|
|
183
|
+
* stay universal; only this substrate table says how those paths are written
|
|
184
|
+
* in Babylon source. */
|
|
185
|
+
export const BABYLON_CREATION_SITE_CHANNELS: Readonly<Record<string, PropertyChannel>> = {
|
|
186
|
+
position: { owner: 'self', member: 'position', kind: 'vector3' },
|
|
187
|
+
rotation: { owner: 'self', member: 'rotation', kind: 'euler' },
|
|
188
|
+
scale: { owner: 'self', member: 'scaling', kind: 'vector3' },
|
|
189
|
+
name: { owner: 'self', member: 'name', kind: 'string' },
|
|
190
|
+
visible: { owner: 'self', member: 'isVisible', kind: 'boolean' },
|
|
191
|
+
visibility: { owner: 'self', member: 'visibility', kind: 'number' },
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** Which RENDER SURFACE's vocabulary a request speaks. Not provenance: an
|
|
195
|
+
* ingested Pixi game and a first-party canvas world answer identically here,
|
|
196
|
+
* and a three world differs because `Object3D` is a different shape. */
|
|
197
|
+
export type CreationSiteSurface = 'three' | 'pixi' | 'babylon';
|
|
198
|
+
|
|
199
|
+
/** Authority carried by a creation-site write. Ordinary gestures address one
|
|
200
|
+
* live object; the component-default gesture deliberately addresses the source
|
|
201
|
+
* site itself and therefore every object constructed there. */
|
|
202
|
+
export type CreationSiteWriteScope = 'instance' | 'creation-site';
|
|
203
|
+
|
|
204
|
+
/** The channel for an editor property path on one surface, or `null` when this
|
|
205
|
+
* seam has no source expression for it. */
|
|
206
|
+
export function channelFor(
|
|
207
|
+
property: string,
|
|
208
|
+
surface: CreationSiteSurface = 'three',
|
|
209
|
+
): PropertyChannel | null {
|
|
210
|
+
const table =
|
|
211
|
+
surface === 'pixi'
|
|
212
|
+
? PIXI_CREATION_SITE_CHANNELS
|
|
213
|
+
: surface === 'babylon'
|
|
214
|
+
? BABYLON_CREATION_SITE_CHANNELS
|
|
215
|
+
: CREATION_SITE_CHANNELS;
|
|
216
|
+
return table[property] ?? null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface CreationSiteEditRequest {
|
|
220
|
+
/** The anchor the registry reported for the property's OWNING object. */
|
|
221
|
+
readonly site: CreationSite;
|
|
222
|
+
/**
|
|
223
|
+
* How many live objects have been recorded at this anchor. `1` is the only
|
|
224
|
+
* count a single-object edit may be written at — see {@link multiplicityRefusal}.
|
|
225
|
+
*/
|
|
226
|
+
readonly instances: number;
|
|
227
|
+
/** Absent means the ordinary single-object gesture. `creation-site` is used
|
|
228
|
+
* only by the explicit Apply-to-component confirmation, whose UI names the
|
|
229
|
+
* full affected instance count before this request exists. */
|
|
230
|
+
readonly writeScope?: CreationSiteWriteScope;
|
|
231
|
+
/** An editor property path — a key of the surface's channel table. */
|
|
232
|
+
readonly property: string;
|
|
233
|
+
/** Which surface's vocabulary {@link CreationSiteEditRequest.property} is in.
|
|
234
|
+
* Absent ⇒ `three`, which is what every caller predating the canvas lane
|
|
235
|
+
* meant. */
|
|
236
|
+
readonly surface?: CreationSiteSurface;
|
|
237
|
+
/** The value in force on the live object BEFORE this gesture. */
|
|
238
|
+
readonly baseline: ChannelValue;
|
|
239
|
+
/** The value the gesture produced. */
|
|
240
|
+
readonly next: ChannelValue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface CreationSiteEditPlan {
|
|
244
|
+
readonly changed: boolean;
|
|
245
|
+
readonly prevSource: string;
|
|
246
|
+
/** Identity-equal to `prevSource` whenever `changed` is false. */
|
|
247
|
+
readonly newSource: string;
|
|
248
|
+
/** Present iff `!changed` — the named reason, per the never-fabricate rule. */
|
|
249
|
+
readonly reason?: string;
|
|
250
|
+
/** Present iff `changed` — a one-line description for the history label. */
|
|
251
|
+
readonly wrote?: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function refuse(source: string, reason: string): CreationSiteEditPlan {
|
|
255
|
+
return { changed: false, prevSource: source, newSource: source, reason };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The refusal for a shared creation site, stated with the count that makes it
|
|
260
|
+
* true. SimCity's `const tile = new Tile(x, y)` builds all 256 tiles from one
|
|
261
|
+
* line, so a per-tile edit has nowhere honest to go: writing that line would
|
|
262
|
+
* move every tile, and writing it "just for this one" is a lie about what the
|
|
263
|
+
* source says. The count comes from the registry, which is the only thing that
|
|
264
|
+
* knows it.
|
|
265
|
+
*/
|
|
266
|
+
export function multiplicityRefusal(instances: number): string {
|
|
267
|
+
return (
|
|
268
|
+
`this creation site constructs ${instances} objects; ` +
|
|
269
|
+
'a per-object edit cannot be written there'
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ───────────────────────────────────────────────────────── value normalization
|
|
274
|
+
|
|
275
|
+
type Scalar = number | boolean | string;
|
|
276
|
+
|
|
277
|
+
interface Components {
|
|
278
|
+
readonly values: readonly Scalar[];
|
|
279
|
+
/** `.x`/`.y`/`.z` for vector channels; `''` for a scalar channel. */
|
|
280
|
+
readonly labels: readonly string[];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function componentsOf(kind: ChannelKind, value: ChannelValue): Components | null {
|
|
284
|
+
if (kind === 'vector3' || kind === 'euler' || kind === 'vector2') {
|
|
285
|
+
const arity = kind === 'vector2' ? 2 : 3;
|
|
286
|
+
if (!Array.isArray(value) || value.length !== arity) return null;
|
|
287
|
+
const values = value as readonly number[];
|
|
288
|
+
if (values.some((n) => typeof n !== 'number' || !Number.isFinite(n))) return null;
|
|
289
|
+
return { values, labels: arity === 2 ? ['x', 'y'] : ['x', 'y', 'z'] };
|
|
290
|
+
}
|
|
291
|
+
if (kind === 'number') {
|
|
292
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
293
|
+
? { values: [value], labels: [''] }
|
|
294
|
+
: null;
|
|
295
|
+
}
|
|
296
|
+
if (kind === 'boolean') {
|
|
297
|
+
return typeof value === 'boolean' ? { values: [value], labels: [''] } : null;
|
|
298
|
+
}
|
|
299
|
+
// 'color' and 'string' both cross the seam as strings.
|
|
300
|
+
return typeof value === 'string' ? { values: [value], labels: [''] } : null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** A colour as a 24-bit int, whatever notation it arrived in — the one form two
|
|
304
|
+
* colour literals can be compared in. `null` when it is not a colour at all
|
|
305
|
+
* (a named CSS colour, `new THREE.Color(…)`): unrecognized, therefore refused. */
|
|
306
|
+
function colorInt(value: Scalar): number | null {
|
|
307
|
+
if (typeof value === 'number') return Number.isInteger(value) && value >= 0 ? value : null;
|
|
308
|
+
if (typeof value !== 'string') return null;
|
|
309
|
+
const hex = value.trim().replace(/^#/, '');
|
|
310
|
+
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
311
|
+
return Number.parseInt(hex, 16);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Is the literal currently in source the value currently in force? THE rule —
|
|
315
|
+
* see the module header. */
|
|
316
|
+
function sameValue(kind: ChannelKind, literal: Scalar, live: Scalar): boolean {
|
|
317
|
+
if (kind === 'color') {
|
|
318
|
+
const a = colorInt(literal);
|
|
319
|
+
const b = colorInt(live);
|
|
320
|
+
return a !== null && b !== null && a === b;
|
|
321
|
+
}
|
|
322
|
+
if (typeof literal === 'number' && typeof live === 'number') {
|
|
323
|
+
return Math.abs(literal - live) <= EPS;
|
|
324
|
+
}
|
|
325
|
+
return literal === live;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function formatNumber(n: number): string {
|
|
329
|
+
// Trim the float noise a gizmo drag carries without pretending to a precision
|
|
330
|
+
// the source never had; `-0` is normalized away because `Object.is` distinguishes
|
|
331
|
+
// it and no author wrote it.
|
|
332
|
+
const rounded = Number(n.toFixed(6));
|
|
333
|
+
return Object.is(rounded, -0) ? '0' : String(rounded);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Render the new value in the SAME notation the literal being replaced used —
|
|
337
|
+
* a game written in `0x` hex stays written in `0x` hex. */
|
|
338
|
+
function formatReplacement(kind: ChannelKind, value: Scalar, existingText: string): string | null {
|
|
339
|
+
if (kind === 'color') {
|
|
340
|
+
const int = colorInt(value);
|
|
341
|
+
if (int === null) return null;
|
|
342
|
+
if (/^0[xX]/.test(existingText)) return `0x${int.toString(16).padStart(6, '0')}`;
|
|
343
|
+
const quote = existingText[0];
|
|
344
|
+
if (quote === '"' || quote === "'" || quote === '`') {
|
|
345
|
+
return `${quote}#${int.toString(16).padStart(6, '0')}${quote}`;
|
|
346
|
+
}
|
|
347
|
+
return String(int);
|
|
348
|
+
}
|
|
349
|
+
if (kind === 'string') {
|
|
350
|
+
const quote = existingText[0] === '"' ? '"' : "'";
|
|
351
|
+
return `${quote}${String(value).replace(/\\/g, '\\\\').replace(new RegExp(quote, 'g'), `\\${quote}`)}${quote}`;
|
|
352
|
+
}
|
|
353
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
354
|
+
if (typeof value === 'number') return formatNumber(value);
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ───────────────────────────────────────────────────────────── literal reading
|
|
359
|
+
|
|
360
|
+
interface LiteralRead {
|
|
361
|
+
readonly value: Scalar;
|
|
362
|
+
readonly start: number;
|
|
363
|
+
readonly end: number;
|
|
364
|
+
readonly text: string;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The literal an expression IS, or `null` when it is an expression that merely
|
|
369
|
+
* has a value. A leading `-` counts (authors write `-0.04`, not `0 - 0.04`);
|
|
370
|
+
* anything else — an identifier, a call, arithmetic — does not, and that `null`
|
|
371
|
+
* is what becomes "the editor never overwrites an expression".
|
|
372
|
+
*/
|
|
373
|
+
function readLiteral(node: ts.Expression, sf: ts.SourceFile): LiteralRead | null {
|
|
374
|
+
const start = node.getStart(sf);
|
|
375
|
+
const end = node.getEnd();
|
|
376
|
+
const text = node.getText(sf);
|
|
377
|
+
const at = (value: Scalar): LiteralRead => ({ value, start, end, text });
|
|
378
|
+
|
|
379
|
+
if (ts.isNumericLiteral(node)) return at(Number(node.text));
|
|
380
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return at(node.text);
|
|
381
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return at(true);
|
|
382
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return at(false);
|
|
383
|
+
if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
|
|
384
|
+
if (node.operator === ts.SyntaxKind.MinusToken) return at(-Number(node.operand.text));
|
|
385
|
+
if (node.operator === ts.SyntaxKind.PlusToken) return at(Number(node.operand.text));
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─────────────────────────────────────────────────────────── source navigation
|
|
391
|
+
|
|
392
|
+
/** A direct constructor or factory initializer stamped by the serve transform. */
|
|
393
|
+
type ConstructionExpression = ts.NewExpression | ts.CallExpression;
|
|
394
|
+
|
|
395
|
+
/** The construction expression this anchor points at, or `null` when the file
|
|
396
|
+
* moved under us. */
|
|
397
|
+
function constructionExpressionAt(
|
|
398
|
+
sf: ts.SourceFile,
|
|
399
|
+
offset: number,
|
|
400
|
+
): ConstructionExpression | null {
|
|
401
|
+
let found: ConstructionExpression | null = null;
|
|
402
|
+
const visit = (node: ts.Node): void => {
|
|
403
|
+
if (found) return;
|
|
404
|
+
if (node.getEnd() <= offset || node.getStart(sf) > offset) return;
|
|
405
|
+
if ((ts.isNewExpression(node) || ts.isCallExpression(node)) && node.getStart(sf) === offset) {
|
|
406
|
+
found = node;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
ts.forEachChild(node, visit);
|
|
410
|
+
};
|
|
411
|
+
visit(sf);
|
|
412
|
+
return found;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The name the constructed object is bound to, as it must be SPELLED to address
|
|
417
|
+
* it again (`sun`, `this.camera`), or `null` when the construction is never
|
|
418
|
+
* bound at all (`scene.add(new THREE.AmbientLight(…))`) — in which case only
|
|
419
|
+
* the constructor's own arguments can hold this property.
|
|
420
|
+
*/
|
|
421
|
+
function receiverOf(node: ConstructionExpression, sf: ts.SourceFile): string | null {
|
|
422
|
+
let current: ts.Node = node;
|
|
423
|
+
// Step out through parentheses/`as`/`!` so `const a = (new Foo())` still binds.
|
|
424
|
+
while (
|
|
425
|
+
current.parent &&
|
|
426
|
+
(ts.isParenthesizedExpression(current.parent) ||
|
|
427
|
+
ts.isAsExpression(current.parent) ||
|
|
428
|
+
ts.isNonNullExpression(current.parent))
|
|
429
|
+
) {
|
|
430
|
+
current = current.parent;
|
|
431
|
+
}
|
|
432
|
+
const parent = current.parent;
|
|
433
|
+
if (!parent) return null;
|
|
434
|
+
if (ts.isVariableDeclaration(parent) && parent.initializer === current) {
|
|
435
|
+
return ts.isIdentifier(parent.name) ? parent.name.text : null;
|
|
436
|
+
}
|
|
437
|
+
if (ts.isPropertyDeclaration(parent) && parent.initializer === current) {
|
|
438
|
+
// A class field: `camera = new THREE.OrthographicCamera(…)` is addressed as
|
|
439
|
+
// `this.camera` by every statement that follows it.
|
|
440
|
+
return ts.isIdentifier(parent.name) || ts.isStringLiteral(parent.name)
|
|
441
|
+
? `this.${parent.name.text}`
|
|
442
|
+
: null;
|
|
443
|
+
}
|
|
444
|
+
if (
|
|
445
|
+
ts.isBinaryExpression(parent) &&
|
|
446
|
+
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
447
|
+
parent.right === current
|
|
448
|
+
) {
|
|
449
|
+
const left = parent.left;
|
|
450
|
+
if (ts.isIdentifier(left) || ts.isPropertyAccessExpression(left)) return normalized(left, sf);
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Source text with insignificant whitespace squeezed out, so `this . camera`
|
|
456
|
+
* and `this.camera` are the same receiver. */
|
|
457
|
+
function normalized(node: ts.Node, sf: ts.SourceFile): string {
|
|
458
|
+
return node.getText(sf).replace(/\s+/g, '');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
type BodyNode = ts.Block | ts.SourceFile | ts.ModuleBlock;
|
|
462
|
+
|
|
463
|
+
/** The function body (or module top level) the creation statement lives in —
|
|
464
|
+
* the scope the spec bounds the literal search to. */
|
|
465
|
+
function enclosingBody(node: ts.Node): BodyNode | null {
|
|
466
|
+
let current: ts.Node | undefined = node;
|
|
467
|
+
while (current) {
|
|
468
|
+
if (ts.isSourceFile(current)) return current;
|
|
469
|
+
if (
|
|
470
|
+
ts.isFunctionDeclaration(current) ||
|
|
471
|
+
ts.isFunctionExpression(current) ||
|
|
472
|
+
ts.isArrowFunction(current) ||
|
|
473
|
+
ts.isMethodDeclaration(current) ||
|
|
474
|
+
ts.isConstructorDeclaration(current) ||
|
|
475
|
+
ts.isGetAccessorDeclaration(current) ||
|
|
476
|
+
ts.isSetAccessorDeclaration(current)
|
|
477
|
+
) {
|
|
478
|
+
const body = current.body;
|
|
479
|
+
return body && ts.isBlock(body) ? body : null;
|
|
480
|
+
}
|
|
481
|
+
// A class field initializer's scope is the constructor's, which we cannot
|
|
482
|
+
// see from here; the class body is the honest boundary.
|
|
483
|
+
if (ts.isClassDeclaration(current) || ts.isClassExpression(current)) return null;
|
|
484
|
+
current = current.parent;
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* True when `node`'s own statement sits DIRECTLY in `body` — not nested inside
|
|
491
|
+
* a loop, a branch, or a callback, where source order says nothing about which
|
|
492
|
+
* write is in force.
|
|
493
|
+
*
|
|
494
|
+
* The check is on the NEAREST enclosing statement, not on any ancestor that
|
|
495
|
+
* happens to be one. A first cut climbed until an ancestor's parent was `body`
|
|
496
|
+
* and accepted it if that ancestor was a statement — which is true of the `for`
|
|
497
|
+
* a write is buried inside, so a per-iteration write read as top-level. That
|
|
498
|
+
* bug is what the `for`-loop case in `creation-site-edit.test.ts` pins.
|
|
499
|
+
*/
|
|
500
|
+
function isTopLevelIn(node: ts.Node, body: BodyNode): boolean {
|
|
501
|
+
let current: ts.Node | undefined = node;
|
|
502
|
+
while (current && !ts.isStatement(current)) current = current.parent;
|
|
503
|
+
return current !== undefined && current.parent === body;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** A human name for the construct a nested write is buried in, for the refusal. */
|
|
507
|
+
function nestingKindOf(node: ts.Node, body: BodyNode): string {
|
|
508
|
+
let current: ts.Node | undefined = node.parent;
|
|
509
|
+
while (current && current !== body) {
|
|
510
|
+
if (
|
|
511
|
+
ts.isForStatement(current) ||
|
|
512
|
+
ts.isForOfStatement(current) ||
|
|
513
|
+
ts.isForInStatement(current)
|
|
514
|
+
) {
|
|
515
|
+
return 'loop';
|
|
516
|
+
}
|
|
517
|
+
if (ts.isWhileStatement(current) || ts.isDoStatement(current)) return 'loop';
|
|
518
|
+
if (ts.isIfStatement(current)) return 'conditional';
|
|
519
|
+
if (ts.isTryStatement(current) || ts.isCatchClause(current)) return 'try block';
|
|
520
|
+
if (
|
|
521
|
+
ts.isArrowFunction(current) ||
|
|
522
|
+
ts.isFunctionExpression(current) ||
|
|
523
|
+
ts.isFunctionDeclaration(current)
|
|
524
|
+
) {
|
|
525
|
+
return 'nested function';
|
|
526
|
+
}
|
|
527
|
+
current = current.parent;
|
|
528
|
+
}
|
|
529
|
+
return 'nested block';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ───────────────────────────────────────────────────────────── home collection
|
|
533
|
+
|
|
534
|
+
/** One literal in source that holds one component of the edited channel. */
|
|
535
|
+
interface Home {
|
|
536
|
+
readonly component: number;
|
|
537
|
+
readonly literal: LiteralRead;
|
|
538
|
+
readonly line: number;
|
|
539
|
+
/**
|
|
540
|
+
* The span of the whole OWN statement holding this home, present exactly
|
|
541
|
+
* when the home is a top-level `receiver.member[.axis] = literal;`
|
|
542
|
+
* expression statement — the one shape the insertion arm writes, and
|
|
543
|
+
* therefore the one shape {@link planCreationSiteRemoval} may delete to
|
|
544
|
+
* restore byte-absence. Constructor literals, `.set(…)` arguments and alias
|
|
545
|
+
* writes carry no statement: dropping those would rewrite the game's own
|
|
546
|
+
* code, not revert an authored override.
|
|
547
|
+
*/
|
|
548
|
+
readonly statement?: { readonly start: number; readonly end: number };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* A write to the channel we found but cannot rewrite — kept so the refusal can
|
|
553
|
+
* name it instead of silently rewriting an earlier, dead literal.
|
|
554
|
+
*
|
|
555
|
+
* `component` is what keeps this from over-refusing. SimCity's
|
|
556
|
+
* `grid.position.set(city.size / 2 - 0.5, -0.04, city.size / 2 - 0.5)` blocks
|
|
557
|
+
* `x` and `z` and holds a perfectly good literal for `y`; a blocker without a
|
|
558
|
+
* component would make the x/z expressions veto a y-only drag, which is the
|
|
559
|
+
* opposite of property-by-property honesty. `null` means the blocker is about
|
|
560
|
+
* the whole channel (an ambiguous constructor, a whole-channel assignment, a
|
|
561
|
+
* write in a nested scope) and does veto every component.
|
|
562
|
+
*/
|
|
563
|
+
interface Blocker {
|
|
564
|
+
readonly reason: string;
|
|
565
|
+
readonly offset: number;
|
|
566
|
+
readonly component: number | null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Does this blocker stand in the way of `component`? */
|
|
570
|
+
function blocks(blocker: Blocker, component: number): boolean {
|
|
571
|
+
return blocker.component === null || blocker.component === component;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
interface Scan {
|
|
575
|
+
readonly homes: readonly Home[];
|
|
576
|
+
readonly blockers: readonly Blocker[];
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function lineOf(sf: ts.SourceFile, pos: number): number {
|
|
580
|
+
return ts.getLineAndCharacterOfPosition(sf, pos).line + 1;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Every literal in the constructor's arguments that could hold this channel —
|
|
585
|
+
* which means every NAME-ADDRESSED one, and nothing else: a key in an
|
|
586
|
+
* object-literal argument matching the member
|
|
587
|
+
* (`new THREE.MeshBasicMaterial({ opacity: 0.2 })`).
|
|
588
|
+
*
|
|
589
|
+
* NO POSITIONAL MATCHING. A bare argument is never a home, however well its
|
|
590
|
+
* literal happens to agree with the value in force.
|
|
591
|
+
*
|
|
592
|
+
* Matching positionally — "the one argument whose literal equals the live
|
|
593
|
+
* value", on the theory that agreement is evidence — corrupts source. Three
|
|
594
|
+
* cases driven through this planner:
|
|
595
|
+
*
|
|
596
|
+
* const player = new Player(scene, true); // unchecking Visible wrote `false`
|
|
597
|
+
* const lamp = new Lamp(1); // an Intensity drag wrote the type code
|
|
598
|
+
* const npc = new NPC('goblin'); // a rename overwrote the model key
|
|
599
|
+
*
|
|
600
|
+
* In each the matched argument means something else entirely and merely holds a
|
|
601
|
+
* value equal to a three.js DEFAULT the source never set. The two guards this
|
|
602
|
+
* module leans on are both blind to it: the ambiguity check only fires on TWO
|
|
603
|
+
* matches, and the baseline-equality backstop cannot fire because THE
|
|
604
|
+
* COINCIDENCE *IS* BASELINE EQUALITY. Nor is the exposure exotic — the evidence
|
|
605
|
+
* a value carries is its entropy, and the values in play are `true`, `false`,
|
|
606
|
+
* `0` and `1`; `visible` alone puts every object with one boolean argument at
|
|
607
|
+
* risk, and it is in the inspector for every object.
|
|
608
|
+
*
|
|
609
|
+
* So the rule is the doctrine's: REFUSE RATHER THAN GUESS. For
|
|
610
|
+
* `new THREE.DirectionalLight(0xffffff, 2)` the honest answer to an intensity
|
|
611
|
+
* drag is "nothing at game.js:105 names `intensity`" — a live-only edit with a
|
|
612
|
+
* named reason, which is precisely the presentation the architecture asks for.
|
|
613
|
+
* Matching a slot by value is fabricating signature knowledge with extra steps:
|
|
614
|
+
* sourced from a coincidence instead of from a table, but fabricated either way.
|
|
615
|
+
*/
|
|
616
|
+
function scanConstructor(
|
|
617
|
+
node: ConstructionExpression,
|
|
618
|
+
sf: ts.SourceFile,
|
|
619
|
+
channel: PropertyChannel,
|
|
620
|
+
baseline: Components,
|
|
621
|
+
): Scan {
|
|
622
|
+
const args = node.arguments ? [...node.arguments] : [];
|
|
623
|
+
const homes: Home[] = [];
|
|
624
|
+
const blockers: Blocker[] = [];
|
|
625
|
+
|
|
626
|
+
for (const arg of args) {
|
|
627
|
+
if (!ts.isObjectLiteralExpression(arg)) continue;
|
|
628
|
+
for (const prop of arg.properties) {
|
|
629
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
630
|
+
const key = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : '';
|
|
631
|
+
if (key !== channel.member) continue;
|
|
632
|
+
const init = prop.initializer;
|
|
633
|
+
if (ts.isArrayLiteralExpression(init) && baseline.values.length === init.elements.length) {
|
|
634
|
+
init.elements.forEach((element, index) => {
|
|
635
|
+
const literal = readLiteral(element, sf);
|
|
636
|
+
if (literal) homes.push({ component: index, literal, line: lineOf(sf, literal.start) });
|
|
637
|
+
else {
|
|
638
|
+
blockers.push({
|
|
639
|
+
reason: expressionRefusal(channel, baseline.labels[index]!, element, sf),
|
|
640
|
+
offset: element.getStart(sf),
|
|
641
|
+
component: index,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const literal = readLiteral(init, sf);
|
|
648
|
+
if (literal && baseline.values.length === 1) {
|
|
649
|
+
homes.push({ component: 0, literal, line: lineOf(sf, literal.start) });
|
|
650
|
+
} else {
|
|
651
|
+
blockers.push({
|
|
652
|
+
reason: expressionRefusal(channel, baseline.labels[0]!, init, sf),
|
|
653
|
+
offset: init.getStart(sf),
|
|
654
|
+
component: baseline.values.length === 1 ? 0 : null,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Nothing else. A BARE POSITIONAL ARGUMENT IS NEVER A HOME — see the
|
|
660
|
+
// "no positional matching" note above `scanConstructor`. When the constructor
|
|
661
|
+
// names nothing, this scan is empty and the caller's honest answer is
|
|
662
|
+
// "nothing at <file>:<line> names <property>".
|
|
663
|
+
return { homes, blockers };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function expressionRefusal(
|
|
667
|
+
channel: PropertyChannel,
|
|
668
|
+
label: string,
|
|
669
|
+
node: ts.Node,
|
|
670
|
+
sf: ts.SourceFile,
|
|
671
|
+
): string {
|
|
672
|
+
const path = label ? `${channel.member}.${label}` : channel.member;
|
|
673
|
+
const text = node.getText(sf).replace(/\s+/g, ' ').trim();
|
|
674
|
+
return (
|
|
675
|
+
`${path} is set from an expression at line ${lineOf(sf, node.getStart(sf))} ` +
|
|
676
|
+
`(\`${text}\`), not a literal — the editor never overwrites an expression`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Every write to `<receiver>.<member>` in the creation statement's enclosing
|
|
682
|
+
* function.
|
|
683
|
+
*
|
|
684
|
+
* Recognized as REWRITABLE: `R.m.set(a, b, c)` with the right arity, a per-axis
|
|
685
|
+
* assignment `R.m.x = a`, and a whole-channel assignment `R.m = a`. Recognized
|
|
686
|
+
* as a BLOCKER: any of those whose value is an expression, a compound assignment
|
|
687
|
+
* (`+=`), and any write that is not at the enclosing function's top statement
|
|
688
|
+
* level.
|
|
689
|
+
*
|
|
690
|
+
* Deliberately NOT modelled: `R.m.copy(v)`, `R.m.applyMatrix4(…)`, and every
|
|
691
|
+
* other mutating three.js method. Enumerating them would be the signature table
|
|
692
|
+
* this file exists to avoid, and it is unnecessary: a write we did not see moves
|
|
693
|
+
* the live value away from the literal we would have rewritten, and the
|
|
694
|
+
* baseline-equality check then refuses. Missing a mutator costs a refusal, never
|
|
695
|
+
* a corruption.
|
|
696
|
+
*/
|
|
697
|
+
function scanReceiverWrites(
|
|
698
|
+
body: BodyNode,
|
|
699
|
+
sf: ts.SourceFile,
|
|
700
|
+
receiver: string,
|
|
701
|
+
channel: PropertyChannel,
|
|
702
|
+
baseline: Components,
|
|
703
|
+
afterOffset: number,
|
|
704
|
+
): Scan {
|
|
705
|
+
const homes: Home[] = [];
|
|
706
|
+
const blockers: Blocker[] = [];
|
|
707
|
+
const channelPath = `${receiver}.${channel.member}`;
|
|
708
|
+
|
|
709
|
+
const note = (node: ts.Node, reason: string, component: number | null): void => {
|
|
710
|
+
blockers.push({ reason, offset: node.getStart(sf), component });
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const visit = (node: ts.Node): void => {
|
|
714
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
715
|
+
const target = node.expression;
|
|
716
|
+
if (target.name.text === 'set' && normalized(target.expression, sf) === channelPath) {
|
|
717
|
+
if (node.getStart(sf) < afterOffset) return;
|
|
718
|
+
if (!isTopLevelIn(node, body)) {
|
|
719
|
+
note(node, nestedRefusal(channelPath, sf, node, body), null);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (node.arguments.length !== baseline.values.length) {
|
|
723
|
+
note(
|
|
724
|
+
node,
|
|
725
|
+
`${channelPath}.set(…) at line ${lineOf(sf, node.getStart(sf))} passes ` +
|
|
726
|
+
`${node.arguments.length} arguments for a ${baseline.values.length}-component ` +
|
|
727
|
+
"value — the editor will not change a call's arity",
|
|
728
|
+
null,
|
|
729
|
+
);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
node.arguments.forEach((arg, index) => {
|
|
733
|
+
const literal = readLiteral(arg, sf);
|
|
734
|
+
if (literal) homes.push({ component: index, literal, line: lineOf(sf, literal.start) });
|
|
735
|
+
else note(arg, expressionRefusal(channel, baseline.labels[index]!, arg, sf), index);
|
|
736
|
+
});
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (
|
|
742
|
+
ts.isBinaryExpression(node) &&
|
|
743
|
+
ts.isPropertyAccessExpression(node.left) &&
|
|
744
|
+
isAssignmentOperator(node.operatorToken.kind)
|
|
745
|
+
) {
|
|
746
|
+
const left = normalized(node.left, sf);
|
|
747
|
+
const isWhole = left === channelPath;
|
|
748
|
+
const axis = baseline.labels.indexOf(node.left.name.text);
|
|
749
|
+
const isAxis =
|
|
750
|
+
axis >= 0 &&
|
|
751
|
+
baseline.labels[0] !== '' &&
|
|
752
|
+
normalized(node.left.expression, sf) === channelPath;
|
|
753
|
+
if ((isWhole || isAxis) && node.getStart(sf) >= afterOffset) {
|
|
754
|
+
const component = isWhole ? 0 : axis;
|
|
755
|
+
if (node.operatorToken.kind !== ts.SyntaxKind.EqualsToken) {
|
|
756
|
+
note(
|
|
757
|
+
node,
|
|
758
|
+
`${left} is updated in place at line ${lineOf(sf, node.getStart(sf))} ` +
|
|
759
|
+
`(\`${node.getText(sf).replace(/\s+/g, ' ').trim()}\`) — the editor never rewrites a ` +
|
|
760
|
+
'compound assignment',
|
|
761
|
+
isWhole ? null : component,
|
|
762
|
+
);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (isWhole && baseline.values.length !== 1) {
|
|
766
|
+
note(
|
|
767
|
+
node,
|
|
768
|
+
`${left} is assigned whole at line ${lineOf(sf, node.getStart(sf))} ` +
|
|
769
|
+
`(\`${node.right.getText(sf).replace(/\s+/g, ' ').trim()}\`) — a ` +
|
|
770
|
+
`${baseline.values.length}-component value has no literal to rewrite there`,
|
|
771
|
+
null,
|
|
772
|
+
);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (!isTopLevelIn(node, body)) {
|
|
776
|
+
note(node, nestedRefusal(left, sf, node, body), isWhole ? null : component);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const literal = readLiteral(node.right, sf);
|
|
780
|
+
if (literal) {
|
|
781
|
+
const statement =
|
|
782
|
+
ts.isExpressionStatement(node.parent) && node.parent.expression === node
|
|
783
|
+
? { start: node.parent.getStart(sf), end: node.parent.getEnd() }
|
|
784
|
+
: undefined;
|
|
785
|
+
homes.push({
|
|
786
|
+
component,
|
|
787
|
+
literal,
|
|
788
|
+
line: lineOf(sf, literal.start),
|
|
789
|
+
...(statement ? { statement } : {}),
|
|
790
|
+
});
|
|
791
|
+
} else {
|
|
792
|
+
note(
|
|
793
|
+
node.right,
|
|
794
|
+
expressionRefusal(channel, baseline.labels[component]!, node.right, sf),
|
|
795
|
+
component,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
ts.forEachChild(node, visit);
|
|
803
|
+
};
|
|
804
|
+
ts.forEachChild(body, visit);
|
|
805
|
+
return { homes, blockers };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Is this binary operator a WRITE (`=`, `+=`, `??=`, …) rather than a read?
|
|
810
|
+
*
|
|
811
|
+
* `scanReceiverWrites` looks for `<receiver>.<member> <op> <value>`, and without
|
|
812
|
+
* this guard it read ANY binary expression whose left side is that member path
|
|
813
|
+
* as a write — so `const bx = bird.x - 14` was reported as "bird.x is updated in
|
|
814
|
+
* place … the editor never rewrites a compound assignment", refusing a
|
|
815
|
+
* perfectly writable edit with a sentence that is not true of the line it names.
|
|
816
|
+
*
|
|
817
|
+
* It only became reachable with `componentMembers`: with the three channels the
|
|
818
|
+
* left side is `sun.position`, and no real game does arithmetic on a `Vector3`
|
|
819
|
+
* member path, so the bug sat unfired. Pixi's `bird.x` is a NUMBER, and reading
|
|
820
|
+
* it in arithmetic is ordinary in every frame of every game. Found live on the
|
|
821
|
+
* flappy ingest fixture, not by a test.
|
|
822
|
+
*/
|
|
823
|
+
function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
|
|
824
|
+
return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function nestedRefusal(path: string, sf: ts.SourceFile, node: ts.Node, body: BodyNode): string {
|
|
828
|
+
return (
|
|
829
|
+
`${path} is also written at line ${lineOf(sf, node.getStart(sf))} inside a ` +
|
|
830
|
+
`${nestingKindOf(node, body)} — the editor cannot know which write is in force`
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* The channel's ALIAS spellings — each `componentMembers` entry scanned as if it
|
|
836
|
+
* were its own one-component channel, with the resulting homes and blockers
|
|
837
|
+
* relabelled onto the component they actually hold.
|
|
838
|
+
*
|
|
839
|
+
* Nothing new is decided here. It re-runs {@link scanConstructor} and
|
|
840
|
+
* {@link scanReceiverWrites} verbatim over a synthetic scalar channel, so
|
|
841
|
+
* `hud.x = 16` is accepted, refused and worded by exactly the rules that govern
|
|
842
|
+
* `hud.intensity = 16` — including the nested-scope, compound-assignment and
|
|
843
|
+
* expression refusals. That reuse is the point: an alias must not become a
|
|
844
|
+
* second, laxer path to the same bytes.
|
|
845
|
+
*/
|
|
846
|
+
function scanComponentAliases(
|
|
847
|
+
construction: ConstructionExpression,
|
|
848
|
+
sf: ts.SourceFile,
|
|
849
|
+
channel: PropertyChannel,
|
|
850
|
+
baseline: Components,
|
|
851
|
+
receiver: string | null,
|
|
852
|
+
body: BodyNode | null,
|
|
853
|
+
): Scan {
|
|
854
|
+
const homes: Home[] = [];
|
|
855
|
+
const blockers: Blocker[] = [];
|
|
856
|
+
const aliases = channel.componentMembers;
|
|
857
|
+
if (!aliases) return { homes, blockers };
|
|
858
|
+
aliases.forEach((member, component) => {
|
|
859
|
+
const value = baseline.values[component];
|
|
860
|
+
if (value === undefined) return;
|
|
861
|
+
const scalar: PropertyChannel = { owner: channel.owner, member, kind: 'number' };
|
|
862
|
+
const one: Components = { values: [value], labels: [''] };
|
|
863
|
+
const scans: Scan[] = [scanConstructor(construction, sf, scalar, one)];
|
|
864
|
+
if (receiver && body) {
|
|
865
|
+
scans.push(scanReceiverWrites(body, sf, receiver, scalar, one, construction.getStart(sf)));
|
|
866
|
+
}
|
|
867
|
+
for (const scan of scans) {
|
|
868
|
+
for (const home of scan.homes) homes.push({ ...home, component });
|
|
869
|
+
for (const blocker of scan.blockers) blockers.push({ ...blocker, component });
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
return { homes, blockers };
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ─────────────────────────────────────────────────────────────────── the plan
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* WHICH LITERAL HOLDS EACH COMPONENT of one channel at one creation site — the
|
|
879
|
+
* whole of "read the source" with none of "decide the write".
|
|
880
|
+
*
|
|
881
|
+
* Extracted so the READ surface ({@link readCreationSiteLiteral}, which shows an
|
|
882
|
+
* instance's overrides against the component default the site names) and the
|
|
883
|
+
* WRITE decision ({@link planCreationSiteEdit}) cannot drift into two scanning
|
|
884
|
+
* rules. Every judgement about what a literal IS stays here; every judgement
|
|
885
|
+
* about whether replacing it is TRUE stays in the caller.
|
|
886
|
+
*/
|
|
887
|
+
interface SiteScan {
|
|
888
|
+
readonly sf: ts.SourceFile;
|
|
889
|
+
/** Component index → the literal in force for it (LAST WRITER WINS). */
|
|
890
|
+
readonly chosen: ReadonlyMap<number, Home>;
|
|
891
|
+
/** EVERY home found, chosen or superseded — the removal planner needs the
|
|
892
|
+
* losers too: deleting the last writer while an earlier author stands
|
|
893
|
+
* would fall back to that author's value, not to absence. */
|
|
894
|
+
readonly homes: readonly Home[];
|
|
895
|
+
readonly blockers: readonly Blocker[];
|
|
896
|
+
/**
|
|
897
|
+
* Where a per-axis assignment could be INSERTED when a component has no
|
|
898
|
+
* home and no blocker: right after the statement that binds the
|
|
899
|
+
* construction to `receiver`. `null` when the construction is not a
|
|
900
|
+
* body-level `const x = new …` / `x = new …` — a class-field initializer
|
|
901
|
+
* or an expression-position construction has no honest statement slot.
|
|
902
|
+
*/
|
|
903
|
+
readonly insertAt: {
|
|
904
|
+
readonly offset: number;
|
|
905
|
+
readonly indent: string;
|
|
906
|
+
readonly receiver: string;
|
|
907
|
+
} | null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function scanSite(
|
|
911
|
+
source: string,
|
|
912
|
+
site: CreationSite,
|
|
913
|
+
channel: PropertyChannel,
|
|
914
|
+
baseline: Components,
|
|
915
|
+
): SiteScan | { readonly refusal: string } {
|
|
916
|
+
const sf = ts.createSourceFile(
|
|
917
|
+
site.file,
|
|
918
|
+
source,
|
|
919
|
+
ts.ScriptTarget.Latest,
|
|
920
|
+
/* setParentNodes */ true,
|
|
921
|
+
scriptKindFor(site.file),
|
|
922
|
+
);
|
|
923
|
+
const offset = lineColToOffset(source, site.line, site.col);
|
|
924
|
+
const construction = constructionExpressionAt(sf, offset);
|
|
925
|
+
if (!construction) {
|
|
926
|
+
return {
|
|
927
|
+
refusal:
|
|
928
|
+
`${site.file}:${site.line} no longer holds a constructor or factory call — ` +
|
|
929
|
+
'the file changed since the game was loaded',
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const scans: Scan[] = [scanConstructor(construction, sf, channel, baseline)];
|
|
934
|
+
const receiver = receiverOf(construction, sf);
|
|
935
|
+
const body = receiver ? enclosingBody(construction) : null;
|
|
936
|
+
if (receiver && body) {
|
|
937
|
+
scans.push(
|
|
938
|
+
scanReceiverWrites(body, sf, receiver, channel, baseline, construction.getStart(sf)),
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
scans.push(scanComponentAliases(construction, sf, channel, baseline, receiver, body));
|
|
942
|
+
|
|
943
|
+
const homes = scans.flatMap((scan) => scan.homes);
|
|
944
|
+
const blockers = scans.flatMap((scan) => scan.blockers);
|
|
945
|
+
|
|
946
|
+
// LAST WRITER WINS, by source position: everything we look at is either inside
|
|
947
|
+
// the `new` (which runs first) or a top-level statement of the same function
|
|
948
|
+
// after it, so textual order IS execution order here — which is exactly why
|
|
949
|
+
// anything nested became a blocker above rather than a candidate.
|
|
950
|
+
const chosen = new Map<number, Home>();
|
|
951
|
+
for (const home of [...homes].sort((a, b) => a.literal.start - b.literal.start)) {
|
|
952
|
+
chosen.set(home.component, home);
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
sf,
|
|
956
|
+
chosen,
|
|
957
|
+
homes,
|
|
958
|
+
blockers,
|
|
959
|
+
insertAt: insertionAnchor(construction, sf, source, receiver),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** The insertion slot for {@link SiteScan.insertAt} — see its doc. */
|
|
964
|
+
function insertionAnchor(
|
|
965
|
+
construction: ConstructionExpression,
|
|
966
|
+
sf: ts.SourceFile,
|
|
967
|
+
source: string,
|
|
968
|
+
receiver: string | null,
|
|
969
|
+
): SiteScan['insertAt'] {
|
|
970
|
+
if (!receiver) return null;
|
|
971
|
+
// Walk up to the statement holding the construction — and the walk may not
|
|
972
|
+
// CROSS a class, function, or property boundary on the way, because the
|
|
973
|
+
// first `isStatement` hit above such a boundary is some ENCLOSING
|
|
974
|
+
// declaration, not the construction's own statement. The shipped corruption
|
|
975
|
+
// this refuses (measured on bubbo-bubbo): a class-property initializer
|
|
976
|
+
// (`private readonly _hitContainer = new Container()`) walked up to the
|
|
977
|
+
// CLASS declaration — a statement — and the insertion landed after the
|
|
978
|
+
// class's closing brace as `this._hitContainer.position.y = 1.5;`, where
|
|
979
|
+
// `this` is dead: the module then throws at load and the whole game stops
|
|
980
|
+
// mounting. Only the construction's OWN statement — a variable statement or
|
|
981
|
+
// an assignment expression statement — puts the inserted assignment in the
|
|
982
|
+
// scope where the receiver is genuinely in force.
|
|
983
|
+
let node: ts.Node = construction;
|
|
984
|
+
while (node.parent && !ts.isStatement(node)) {
|
|
985
|
+
if (ts.isClassLike(node) || ts.isFunctionLike(node) || ts.isPropertyDeclaration(node)) {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
node = node.parent;
|
|
989
|
+
}
|
|
990
|
+
if (!ts.isVariableStatement(node) && !ts.isExpressionStatement(node)) return null;
|
|
991
|
+
const holder = node.parent;
|
|
992
|
+
if (!holder || !(ts.isBlock(holder) || ts.isSourceFile(holder))) return null;
|
|
993
|
+
const statementStart = node.getStart(sf);
|
|
994
|
+
const lineStart = source.lastIndexOf('\n', statementStart - 1) + 1;
|
|
995
|
+
const indentText = source.slice(lineStart, statementStart);
|
|
996
|
+
if (!/^[ \t]*$/.test(indentText)) return null; // shares a line with other code
|
|
997
|
+
return { offset: node.getEnd(), indent: indentText, receiver };
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/** The refusal for a component with no rewritable literal, in the planner's own
|
|
1001
|
+
* words: the blocker that stands in its way, or the plain absence. */
|
|
1002
|
+
function homeRefusal(
|
|
1003
|
+
site: CreationSite,
|
|
1004
|
+
channel: PropertyChannel,
|
|
1005
|
+
label: string,
|
|
1006
|
+
blockers: readonly Blocker[],
|
|
1007
|
+
): string {
|
|
1008
|
+
const path = label ? `${channel.member}.${label}` : channel.member;
|
|
1009
|
+
const blocker = [...blockers].sort((a, b) => b.offset - a.offset)[0];
|
|
1010
|
+
return (
|
|
1011
|
+
blocker?.reason ??
|
|
1012
|
+
`nothing at ${site.file}:${site.line} names ${path} — the value has no literal to rewrite`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** The refusal when a component with no home could only be written by INSERTION
|
|
1017
|
+
* but the construction is not bound to a name at statement level. ONE spelling,
|
|
1018
|
+
* shared by {@link planCreationSiteEdit} and {@link readCreationSiteLiteral},
|
|
1019
|
+
* because the read's `writable` is defined as "would the planner accept?" and a
|
|
1020
|
+
* paraphrase here would let the two answers drift apart. */
|
|
1021
|
+
function insertionRefusal(site: CreationSite, channel: PropertyChannel): string {
|
|
1022
|
+
return (
|
|
1023
|
+
`nothing at ${site.file}:${site.line} names ${channel.member}, and the ` +
|
|
1024
|
+
'constructed object is not bound to a name at statement level — the editor has no ' +
|
|
1025
|
+
'honest place to write the value into this source'
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Plan one property edit against the game's own source.
|
|
1031
|
+
*
|
|
1032
|
+
* `source` is the file's bytes as they sit on disk — the caller reads them; this
|
|
1033
|
+
* function does no I/O so that both the dev server and a test can drive the
|
|
1034
|
+
* same decision.
|
|
1035
|
+
*/
|
|
1036
|
+
export function planCreationSiteEdit(
|
|
1037
|
+
source: string,
|
|
1038
|
+
request: CreationSiteEditRequest,
|
|
1039
|
+
): CreationSiteEditPlan {
|
|
1040
|
+
if (request.instances !== 1 && request.writeScope !== 'creation-site') {
|
|
1041
|
+
return refuse(source, multiplicityRefusal(request.instances));
|
|
1042
|
+
}
|
|
1043
|
+
const channel = channelFor(request.property, request.surface);
|
|
1044
|
+
if (!channel) {
|
|
1045
|
+
return refuse(source, `the editor has no source expression for "${request.property}"`);
|
|
1046
|
+
}
|
|
1047
|
+
const baseline = componentsOf(channel.kind, request.baseline);
|
|
1048
|
+
const next = componentsOf(channel.kind, request.next);
|
|
1049
|
+
if (!baseline || !next) {
|
|
1050
|
+
return refuse(source, `"${request.property}" did not arrive as a ${channel.kind} value`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const changedComponents = baseline.values
|
|
1054
|
+
.map((value, index) => (sameValue(channel.kind, value, next.values[index]!) ? -1 : index))
|
|
1055
|
+
.filter((index) => index >= 0);
|
|
1056
|
+
if (changedComponents.length === 0) {
|
|
1057
|
+
return { changed: false, prevSource: source, newSource: source, reason: 'nothing changed' };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const scan = scanSite(source, request.site, channel, baseline);
|
|
1061
|
+
if ('refusal' in scan) return refuse(source, scan.refusal);
|
|
1062
|
+
const { sf, chosen, blockers, insertAt } = scan;
|
|
1063
|
+
|
|
1064
|
+
const edits: Array<{ start: number; end: number; text: string }> = [];
|
|
1065
|
+
const insertions: Array<{ path: string; value: Scalar }> = [];
|
|
1066
|
+
for (const component of changedComponents) {
|
|
1067
|
+
const label = baseline.labels[component]!;
|
|
1068
|
+
const path = label ? `${channel.member}.${label}` : channel.member;
|
|
1069
|
+
const relevant = blockers.filter((blocker) => blocks(blocker, component));
|
|
1070
|
+
const home = chosen.get(component);
|
|
1071
|
+
if (!home) {
|
|
1072
|
+
// INSERTION — the one case where a write can be honest without a
|
|
1073
|
+
// literal to rewrite: the site names NOTHING for this component (no
|
|
1074
|
+
// home, and no blocker either — a blocker would mean something else
|
|
1075
|
+
// already computes or writes it, and inserting a second writer would
|
|
1076
|
+
// fight it), and the construction binds a named receiver at statement
|
|
1077
|
+
// level. Then a per-axis named assignment right after the construction
|
|
1078
|
+
// statement IS the smallest source that produces the dragged value:
|
|
1079
|
+
// it names the property (rule 1 by construction — we write the name),
|
|
1080
|
+
// it needs no library knowledge (the channel path demonstrably exists
|
|
1081
|
+
// on the live object the baseline was read from), and it is exactly
|
|
1082
|
+
// the shape `scanReceiverWrites` already recognizes — so the NEXT drag
|
|
1083
|
+
// rewrites this literal through the ordinary path instead of
|
|
1084
|
+
// inserting again. An options-bag key is deliberately NOT the target:
|
|
1085
|
+
// whether a constructor reads a given key is the signature table this
|
|
1086
|
+
// module refuses to own.
|
|
1087
|
+
if (relevant.length === 0) {
|
|
1088
|
+
insertions.push({ path, value: next.values[component]! });
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
return refuse(source, homeRefusal(request.site, channel, label, relevant));
|
|
1092
|
+
}
|
|
1093
|
+
// A blocker for THIS component that runs after the home we picked means
|
|
1094
|
+
// something we cannot rewrite has the last word; rewriting the earlier
|
|
1095
|
+
// literal would be a dead edit that silently does nothing at runtime.
|
|
1096
|
+
const later = relevant.find((blocker) => blocker.offset > home.literal.start);
|
|
1097
|
+
if (later) return refuse(source, later.reason);
|
|
1098
|
+
|
|
1099
|
+
// THE BACKSTOP (module header, rule 2).
|
|
1100
|
+
if (!sameValue(channel.kind, home.literal.value, baseline.values[component]!)) {
|
|
1101
|
+
return refuse(
|
|
1102
|
+
source,
|
|
1103
|
+
`the literal at ${request.site.file}:${home.line} is \`${home.literal.text}\`, but ` +
|
|
1104
|
+
`${path} is ${describe(baseline.values[component]!)} in the running game — ` +
|
|
1105
|
+
'the value is computed at runtime, so writing that literal would not produce it',
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
const replacement = formatReplacement(channel.kind, next.values[component]!, home.literal.text);
|
|
1109
|
+
if (replacement === null) {
|
|
1110
|
+
return refuse(source, `the editor cannot express this ${channel.kind} value in source`);
|
|
1111
|
+
}
|
|
1112
|
+
edits.push({ start: home.literal.start, end: home.literal.end, text: replacement });
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (insertions.length > 0) {
|
|
1116
|
+
if (!insertAt) {
|
|
1117
|
+
return refuse(source, insertionRefusal(request.site, channel));
|
|
1118
|
+
}
|
|
1119
|
+
const lines = insertions
|
|
1120
|
+
.map(({ path, value }) => {
|
|
1121
|
+
const text =
|
|
1122
|
+
typeof value === 'number'
|
|
1123
|
+
? formatNumber(value)
|
|
1124
|
+
: typeof value === 'boolean'
|
|
1125
|
+
? String(value)
|
|
1126
|
+
: typeof value === 'string'
|
|
1127
|
+
? `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
1128
|
+
: null;
|
|
1129
|
+
return text === null ? null : `\n${insertAt.indent}${insertAt.receiver}.${path} = ${text};`;
|
|
1130
|
+
})
|
|
1131
|
+
.filter((line): line is string => line !== null);
|
|
1132
|
+
if (lines.length !== insertions.length) {
|
|
1133
|
+
return refuse(source, `the editor cannot express this ${channel.kind} value in source`);
|
|
1134
|
+
}
|
|
1135
|
+
edits.push({ start: insertAt.offset, end: insertAt.offset, text: lines.join('') });
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
edits.sort((a, b) => a.start - b.start);
|
|
1139
|
+
let out = '';
|
|
1140
|
+
let cursor = 0;
|
|
1141
|
+
for (const edit of edits) {
|
|
1142
|
+
out += source.slice(cursor, edit.start) + edit.text;
|
|
1143
|
+
cursor = edit.end;
|
|
1144
|
+
}
|
|
1145
|
+
out += source.slice(cursor);
|
|
1146
|
+
if (out === source) {
|
|
1147
|
+
return { changed: false, prevSource: source, newSource: source, reason: 'nothing changed' };
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
changed: true,
|
|
1151
|
+
prevSource: source,
|
|
1152
|
+
newSource: out,
|
|
1153
|
+
wrote: `${request.property} at ${request.site.file}:${edits.length === 1 ? lineOf(sf, edits[0]!.start) : request.site.line}`,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** A removal is a write request minus BOTH values — absence is not a value,
|
|
1158
|
+
* and a shape that carried one would be a value write wearing another name
|
|
1159
|
+
* (the engine's `SourceRemoveRequest` states the same rule). The channel's
|
|
1160
|
+
* arity and labels are static facts of its kind, so no live reading is
|
|
1161
|
+
* needed to scan for the authored statements. */
|
|
1162
|
+
export interface CreationSiteRemovalRequest {
|
|
1163
|
+
readonly site: CreationSite;
|
|
1164
|
+
readonly instances: number;
|
|
1165
|
+
readonly writeScope: CreationSiteWriteScope;
|
|
1166
|
+
readonly property: string;
|
|
1167
|
+
readonly surface: CreationSiteSurface;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/** The channel's shape with no live value behind it — labels and arity only,
|
|
1171
|
+
* which is all scanning reads (the write path's equality backstop is the one
|
|
1172
|
+
* consumer of real component VALUES, and a removal never compares). */
|
|
1173
|
+
function componentsShapeOf(kind: ChannelKind): Components {
|
|
1174
|
+
if (kind === 'vector3' || kind === 'euler') {
|
|
1175
|
+
return { values: [0, 0, 0], labels: ['x', 'y', 'z'] };
|
|
1176
|
+
}
|
|
1177
|
+
if (kind === 'vector2') return { values: [0, 0], labels: ['x', 'y'] };
|
|
1178
|
+
if (kind === 'number') return { values: [0], labels: [''] };
|
|
1179
|
+
if (kind === 'boolean') return { values: [false], labels: [''] };
|
|
1180
|
+
return { values: [''], labels: [''] };
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Plan the REMOVAL of a property's authored value from the game's own source —
|
|
1185
|
+
* the byte-absence door the insertion arm makes necessary: an authored
|
|
1186
|
+
* transform can ADD a `receiver.member.axis = value;` statement for an axis
|
|
1187
|
+
* the source never named, and no value write can revert an added property.
|
|
1188
|
+
*
|
|
1189
|
+
* THE ONE DELETABLE SHAPE is exactly the shape insertion writes: a whole
|
|
1190
|
+
* own-line, top-level `receiver.member[.axis] = <literal>;` expression
|
|
1191
|
+
* statement ({@link Home.statement}). Everything else refuses in its own
|
|
1192
|
+
* words — a constructor literal or `.set(…)` argument is the game's own code
|
|
1193
|
+
* (deleting it would change the construction, not drop an override), a
|
|
1194
|
+
* blocker's expression is something the game computes, and a statement
|
|
1195
|
+
* sharing its line with other code cannot be deleted without taking that code
|
|
1196
|
+
* with it. A component the source never names is simply already absent.
|
|
1197
|
+
*
|
|
1198
|
+
* Deletion removes the statement's line whole — the preceding newline through
|
|
1199
|
+
* the statement's trailing semicolon and any same-line whitespace — which is
|
|
1200
|
+
* the byte-exact inverse of what insertion appended.
|
|
1201
|
+
*/
|
|
1202
|
+
export function planCreationSiteRemoval(
|
|
1203
|
+
source: string,
|
|
1204
|
+
request: CreationSiteRemovalRequest,
|
|
1205
|
+
): CreationSiteEditPlan {
|
|
1206
|
+
if (request.instances !== 1 && request.writeScope !== 'creation-site') {
|
|
1207
|
+
return refuse(source, multiplicityRefusal(request.instances));
|
|
1208
|
+
}
|
|
1209
|
+
const channel = channelFor(request.property, request.surface);
|
|
1210
|
+
if (!channel) {
|
|
1211
|
+
return refuse(source, `the editor has no source expression for "${request.property}"`);
|
|
1212
|
+
}
|
|
1213
|
+
const baseline = componentsShapeOf(channel.kind);
|
|
1214
|
+
const scan = scanSite(source, request.site, channel, baseline);
|
|
1215
|
+
if ('refusal' in scan) return refuse(source, scan.refusal);
|
|
1216
|
+
const { sf, chosen, homes, blockers } = scan;
|
|
1217
|
+
|
|
1218
|
+
const statements: Array<{ start: number; end: number }> = [];
|
|
1219
|
+
for (let component = 0; component < baseline.values.length; component++) {
|
|
1220
|
+
const home = chosen.get(component);
|
|
1221
|
+
const relevant = blockers.filter((blocker) => blocks(blocker, component));
|
|
1222
|
+
if (!home) {
|
|
1223
|
+
// No rewritable home, but something still MENTIONS this component — an
|
|
1224
|
+
// expression, a compound assignment, a constructor shape the scanner
|
|
1225
|
+
// cannot read. That is not absence, and deleting cannot make it absence,
|
|
1226
|
+
// so the refusal is the blocker's own description. With no blocker
|
|
1227
|
+
// either, nothing names this component and there is nothing to do.
|
|
1228
|
+
const blocker = [...relevant].sort((a, b) => b.offset - a.offset)[0];
|
|
1229
|
+
if (blocker) return refuse(source, blocker.reason);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
const label = baseline.labels[component]!;
|
|
1233
|
+
const path = label ? `${channel.member}.${label}` : channel.member;
|
|
1234
|
+
if (!home.statement) {
|
|
1235
|
+
return refuse(
|
|
1236
|
+
source,
|
|
1237
|
+
`${path} is authored at ${request.site.file}:${home.line} inside the construction ` +
|
|
1238
|
+
'itself (a constructor literal, a `.set(…)` argument, or an alias write) — that is ' +
|
|
1239
|
+
"the game's own code, not a removable own-line assignment, so the editor will not " +
|
|
1240
|
+
'delete it',
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
// A blocker AFTER the chosen home means something unremovable has the last
|
|
1244
|
+
// word; deleting the earlier statement would not produce absence.
|
|
1245
|
+
const later = relevant.find((blocker) => blocker.offset > home.statement!.start);
|
|
1246
|
+
if (later) return refuse(source, later.reason);
|
|
1247
|
+
// A second author for the same component: deleting the last writer would
|
|
1248
|
+
// fall back to the earlier author's value, not to absence.
|
|
1249
|
+
const earlier = homes.find((other) => other.component === component && other !== home);
|
|
1250
|
+
if (earlier) {
|
|
1251
|
+
return refuse(
|
|
1252
|
+
source,
|
|
1253
|
+
`${path} is authored more than once (line ${earlier.line} and line ${home.line}) — ` +
|
|
1254
|
+
`deleting the last writer would fall back to line ${earlier.line}'s value, ` +
|
|
1255
|
+
'not to absence',
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
statements.push(home.statement);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (statements.length === 0) {
|
|
1262
|
+
return {
|
|
1263
|
+
changed: false,
|
|
1264
|
+
prevSource: source,
|
|
1265
|
+
newSource: source,
|
|
1266
|
+
reason:
|
|
1267
|
+
`nothing at ${request.site.file}:${request.site.line} authors ${channel.member} as an ` +
|
|
1268
|
+
'own-line assignment — the value is already absent from the source',
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Deduplicate (a whole-channel statement can be chosen by several
|
|
1273
|
+
// components) and delete, verifying each statement owns its line.
|
|
1274
|
+
const spans = [...new Map(statements.map((s) => [s.start, s])).values()].sort(
|
|
1275
|
+
(a, b) => a.start - b.start,
|
|
1276
|
+
);
|
|
1277
|
+
let out = '';
|
|
1278
|
+
let cursor = 0;
|
|
1279
|
+
for (const span of spans) {
|
|
1280
|
+
const lineStart = source.lastIndexOf('\n', span.start - 1) + 1;
|
|
1281
|
+
if (!/^[ \t]*$/.test(source.slice(lineStart, span.start))) {
|
|
1282
|
+
return refuse(
|
|
1283
|
+
source,
|
|
1284
|
+
`the assignment at ${request.site.file}:${lineOf(sf, span.start)} shares its line with ` +
|
|
1285
|
+
"other code — deleting the line would take the game's own code with it",
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
let lineEnd = source.indexOf('\n', span.end);
|
|
1289
|
+
if (lineEnd === -1) lineEnd = source.length;
|
|
1290
|
+
if (!/^[ \t]*$/.test(source.slice(span.end, lineEnd))) {
|
|
1291
|
+
return refuse(
|
|
1292
|
+
source,
|
|
1293
|
+
`the assignment at ${request.site.file}:${lineOf(sf, span.start)} shares its line with ` +
|
|
1294
|
+
"other code — deleting the line would take the game's own code with it",
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
// Delete the preceding newline through the line's end (the byte-exact
|
|
1298
|
+
// inverse of insertion's `\n<indent><stmt>;`); at the file's first line
|
|
1299
|
+
// delete through the trailing newline instead.
|
|
1300
|
+
const deleteStart = lineStart === 0 ? 0 : lineStart - 1;
|
|
1301
|
+
const deleteEnd = lineStart === 0 ? Math.min(lineEnd + 1, source.length) : lineEnd;
|
|
1302
|
+
out += source.slice(cursor, deleteStart);
|
|
1303
|
+
cursor = deleteEnd;
|
|
1304
|
+
}
|
|
1305
|
+
out += source.slice(cursor);
|
|
1306
|
+
if (out === source) {
|
|
1307
|
+
return { changed: false, prevSource: source, newSource: source, reason: 'nothing changed' };
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
changed: true,
|
|
1311
|
+
prevSource: source,
|
|
1312
|
+
newSource: out,
|
|
1313
|
+
wrote: `${request.property} removed at ${request.site.file}:${lineOf(sf, spans[0]!.start)}`,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function describe(value: Scalar): string {
|
|
1318
|
+
return typeof value === 'number' ? formatNumber(value) : String(value);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// ───────────────────────────────────────────────── the read: what the site says
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* ONE property, at one creation site, READ rather than written — what the
|
|
1325
|
+
* construction statement says the value is, so a surface can show the live
|
|
1326
|
+
* object's value as an OVERRIDE of it.
|
|
1327
|
+
*
|
|
1328
|
+
* `live` is the value in force. Its shape decides the channel's arity, exactly
|
|
1329
|
+
* as {@link CreationSiteEditRequest.baseline} does, so a caller cannot ask this
|
|
1330
|
+
* question in a vocabulary the write path would reject.
|
|
1331
|
+
*/
|
|
1332
|
+
export interface CreationSiteLiteralRequest {
|
|
1333
|
+
readonly site: CreationSite;
|
|
1334
|
+
/** How many objects the anchor constructed — a shared site is unwritable for
|
|
1335
|
+
* an ordinary instance edit and writable only for the explicit
|
|
1336
|
+
* creation-site-default gesture that names the full count. */
|
|
1337
|
+
readonly instances: number;
|
|
1338
|
+
/** Which write the `writable` verdict is answering for. */
|
|
1339
|
+
readonly writeScope?: CreationSiteWriteScope;
|
|
1340
|
+
readonly property: string;
|
|
1341
|
+
readonly surface?: CreationSiteSurface;
|
|
1342
|
+
readonly live: ChannelValue;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* What the site says, and whether it could be made to say something else.
|
|
1347
|
+
*
|
|
1348
|
+
* `literal` is normalized into the CALLER's own notation (a `0xff00ff` colour
|
|
1349
|
+
* literal comes back as `#ff00ff`, because that is the form the editor's colour
|
|
1350
|
+
* rows read and write), so a caller can put it straight back on the live object
|
|
1351
|
+
* without knowing how the game happened to spell it.
|
|
1352
|
+
*/
|
|
1353
|
+
export interface CreationSiteLiteralReport {
|
|
1354
|
+
/** The value the construction site names, or `null` when it names none. */
|
|
1355
|
+
readonly literal: ChannelValue | null;
|
|
1356
|
+
/** The literal's own source text (per component, comma-joined) — the string a
|
|
1357
|
+
* surface shows as "the component default". */
|
|
1358
|
+
readonly text?: string;
|
|
1359
|
+
/** Would {@link planCreationSiteEdit} accept a write of a new value here? */
|
|
1360
|
+
readonly writable: boolean;
|
|
1361
|
+
/** Present iff `!writable` — the planner's own words, never a paraphrase. */
|
|
1362
|
+
readonly reason?: string;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/** Put one literal back in the caller's own notation (see {@link
|
|
1366
|
+
* CreationSiteLiteralReport.literal}). */
|
|
1367
|
+
function normalizeLiteral(kind: ChannelKind, value: Scalar): Scalar | null {
|
|
1368
|
+
if (kind !== 'color') return value;
|
|
1369
|
+
const int = colorInt(value);
|
|
1370
|
+
return int === null ? null : `#${int.toString(16).padStart(6, '0')}`;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
export function readCreationSiteLiteral(
|
|
1374
|
+
source: string,
|
|
1375
|
+
request: CreationSiteLiteralRequest,
|
|
1376
|
+
): CreationSiteLiteralReport {
|
|
1377
|
+
const channel = channelFor(request.property, request.surface);
|
|
1378
|
+
if (!channel) {
|
|
1379
|
+
return {
|
|
1380
|
+
literal: null,
|
|
1381
|
+
writable: false,
|
|
1382
|
+
reason: `the editor has no source expression for "${request.property}"`,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
const live = componentsOf(channel.kind, request.live);
|
|
1386
|
+
if (!live) {
|
|
1387
|
+
return {
|
|
1388
|
+
literal: null,
|
|
1389
|
+
writable: false,
|
|
1390
|
+
reason: `"${request.property}" did not arrive as a ${channel.kind} value`,
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
const scan = scanSite(source, request.site, channel, live);
|
|
1394
|
+
if ('refusal' in scan) {
|
|
1395
|
+
return { literal: null, writable: false, reason: scan.refusal };
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const values: Scalar[] = [];
|
|
1399
|
+
const texts: string[] = [];
|
|
1400
|
+
// A component with NO literal ends the read literal-less (there is no default
|
|
1401
|
+
// to show) — unless the planner would refuse it outright, ending the read
|
|
1402
|
+
// with that refusal. One that merely cannot be REWRITTEN does neither: the
|
|
1403
|
+
// first such reason is kept and the values are still reported. That is the
|
|
1404
|
+
// difference between "we cannot show you the default" and "we can show it
|
|
1405
|
+
// but cannot change it".
|
|
1406
|
+
let refusal: string | null = null;
|
|
1407
|
+
let insertable = false;
|
|
1408
|
+
for (let component = 0; component < live.values.length; component++) {
|
|
1409
|
+
const relevant = scan.blockers.filter((blocker) => blocks(blocker, component));
|
|
1410
|
+
const home = scan.chosen.get(component);
|
|
1411
|
+
if (!home) {
|
|
1412
|
+
// MIRROR THE PLANNER's homeless arm exactly (`writable` is defined as
|
|
1413
|
+
// "would planCreationSiteEdit accept a write here?"): with a blocker the
|
|
1414
|
+
// write refuses; unblocked and unbound it refuses in the insertion arm's
|
|
1415
|
+
// own words; unblocked with an insertion anchor it WRITES — by inserting
|
|
1416
|
+
// a named assignment — so this report must say writable even though the
|
|
1417
|
+
// site names no default to show (`literal` stays null for the property,
|
|
1418
|
+
// which is the truth: an insertable value has no source default). The
|
|
1419
|
+
// scan continues, because a LATER component's blocker still refuses the
|
|
1420
|
+
// whole write and this report must say so.
|
|
1421
|
+
if (relevant.length > 0) {
|
|
1422
|
+
return {
|
|
1423
|
+
literal: null,
|
|
1424
|
+
writable: false,
|
|
1425
|
+
reason: homeRefusal(request.site, channel, live.labels[component]!, relevant),
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
if (!scan.insertAt) {
|
|
1429
|
+
return { literal: null, writable: false, reason: insertionRefusal(request.site, channel) };
|
|
1430
|
+
}
|
|
1431
|
+
insertable = true;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
const normalized = normalizeLiteral(channel.kind, home.literal.value);
|
|
1435
|
+
if (normalized === null) {
|
|
1436
|
+
return {
|
|
1437
|
+
literal: null,
|
|
1438
|
+
writable: false,
|
|
1439
|
+
reason:
|
|
1440
|
+
`the literal at ${request.site.file}:${home.line} is \`${home.literal.text}\`, which ` +
|
|
1441
|
+
`the editor cannot read as a ${channel.kind}`,
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
values.push(normalized);
|
|
1445
|
+
texts.push(home.literal.text);
|
|
1446
|
+
const later = relevant.find((blocker) => blocker.offset > home.literal.start);
|
|
1447
|
+
if (later) refusal ??= later.reason;
|
|
1448
|
+
}
|
|
1449
|
+
if (request.instances !== 1 && request.writeScope !== 'creation-site') {
|
|
1450
|
+
refusal ??= multiplicityRefusal(request.instances);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (insertable) {
|
|
1454
|
+
// At least one component would be written by insertion, so the site names
|
|
1455
|
+
// no complete default — `literal` is null (nothing honest to show), while
|
|
1456
|
+
// `writable` still answers for the whole write.
|
|
1457
|
+
return {
|
|
1458
|
+
literal: null,
|
|
1459
|
+
writable: refusal === null,
|
|
1460
|
+
...(refusal === null ? {} : { reason: refusal }),
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const literal: ChannelValue =
|
|
1465
|
+
live.values.length > 1 ? (values as readonly number[]) : (values[0] as Scalar);
|
|
1466
|
+
return {
|
|
1467
|
+
literal,
|
|
1468
|
+
text: texts.join(', '),
|
|
1469
|
+
writable: refusal === null,
|
|
1470
|
+
...(refusal === null ? {} : { reason: refusal }),
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function scriptKindFor(file: string): ts.ScriptKind {
|
|
1475
|
+
if (file.endsWith('.tsx')) return ts.ScriptKind.TSX;
|
|
1476
|
+
if (file.endsWith('.jsx')) return ts.ScriptKind.JSX;
|
|
1477
|
+
if (file.endsWith('.ts')) return ts.ScriptKind.TS;
|
|
1478
|
+
return ts.ScriptKind.JS;
|
|
1479
|
+
}
|