@volter/editor-game 0.5.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- package/src/xstate/xstate-layout.ts +76 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PERSISTENCE BACKEND seam for a LIVE authoring adapter, on ANY surface:
|
|
3
|
+
* where a closed gesture's value goes, and what a surface says about that
|
|
4
|
+
* before the gesture happens.
|
|
5
|
+
*
|
|
6
|
+
* IT IS KEYED ON NEITHER SURFACE NOR PROVENANCE — a per-substrate copy is
|
|
7
|
+
* the fork ARCHITECTURE-CORE §Rules names, wearing a filename: an ingested Pixi
|
|
8
|
+
* game needs the SAME ownership gate, the SAME per-edit refusals and the SAME
|
|
9
|
+
* one-transaction-carries-both-halves rule as an ingested three game, and a seam
|
|
10
|
+
* named after one substrate is how a second copy gets written instead of reused.
|
|
11
|
+
* `ThreeAuthoringAdapter` and the canvas write target
|
|
12
|
+
* (`pixi-live-write-target.ts`) are both its consumers; nothing in this file
|
|
13
|
+
* mentions `Object3D` or `Container`, because the currency is an entity id, a
|
|
14
|
+
* property path, an anchor and two values.
|
|
15
|
+
*
|
|
16
|
+
* The adapter owns the gesture and the session journal; it never decides the
|
|
17
|
+
* destination. It hands a backend the creation-site anchor for the property
|
|
18
|
+
* being edited plus the before/after values, and journals the edit live-only
|
|
19
|
+
* whenever the backend declines — which is the DEFAULT, not the exception.
|
|
20
|
+
*
|
|
21
|
+
* ABSENCE IS A BACKEND. An adapter constructed with no backend at all persists
|
|
22
|
+
* nothing and journals nothing: its edits live on the running object for the
|
|
23
|
+
* session and the host injects `createEphemeralPersistence` over them. That is
|
|
24
|
+
* what a play-mode adoption wants — pushing an undo entry there would write
|
|
25
|
+
* play state into the edit-mode history.
|
|
26
|
+
*
|
|
27
|
+
* {@link createCreationSitePersistence} is the other shipped backend, and it
|
|
28
|
+
* covers two of the three destinations the ownership doctrine names:
|
|
29
|
+
* - the CREATION-SITE LITERAL WRITER, when this tier can reach a dev server to
|
|
30
|
+
* write the world's own source through (`/__ingest-source/*`);
|
|
31
|
+
* - an HONEST LIVE-ONLY-WITH-REASON, when it cannot — inventing a fallback
|
|
32
|
+
* that writes somewhere else is exactly the sidecar the doctrine forbids.
|
|
33
|
+
* Every refusal still names its gate.
|
|
34
|
+
*
|
|
35
|
+
* The third shipped backend is `oid-source-persistence.ts`, for a world whose
|
|
36
|
+
* own source the serve-time OID transform DID reach — a vendored R3F game is
|
|
37
|
+
* the case that forces it to exist. There, no `new` expression in the game's
|
|
38
|
+
* source constructs anything (fiber does, inside `node_modules`), so
|
|
39
|
+
* creation-site anchoring is structurally empty and its writer can never fire;
|
|
40
|
+
* the JSX callsite the OID stamp names is the object's real source address.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import type { NodeCreationSite, WriteAnchorKind } from '@volter/editor-project/adapter';
|
|
44
|
+
import type {
|
|
45
|
+
ChannelValue,
|
|
46
|
+
CreationSiteLiteralReport,
|
|
47
|
+
CreationSiteSurface,
|
|
48
|
+
CreationSiteWriteScope,
|
|
49
|
+
} from '@volter/editor-core/creation-site-edit';
|
|
50
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
51
|
+
import { editorIsAuthoring } from '@volter/editor-core/editor-session-mode';
|
|
52
|
+
import type { HistoryService } from '@volter/editor-core/history/history-service';
|
|
53
|
+
import { ingestSourceWritesRecordedIfPrimed } from '@volter/editor-core/ui-source/tier-source-write-backend';
|
|
54
|
+
import type { IngestInspectRequest } from './ingest-source-persistence';
|
|
55
|
+
import {
|
|
56
|
+
IngestSourcePersistence,
|
|
57
|
+
ingestOwnershipNow,
|
|
58
|
+
reportIngestSourceRefusal,
|
|
59
|
+
} from './ingest-source-persistence';
|
|
60
|
+
import { LIVE_ONLY_DESTINATION } from '@volter/editor-core/authoring/write-pipe';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The adapter's own channel read/write, handed to a backend so the live half of
|
|
64
|
+
* a persisted edit can be restored by undo through the SAME writers an ordinary
|
|
65
|
+
* edit uses.
|
|
66
|
+
*/
|
|
67
|
+
export interface LiveChannelAccess {
|
|
68
|
+
/** The value in force for one property, or `undefined` when the node is gone. */
|
|
69
|
+
read(entityId: string, property: string): ChannelValue | undefined;
|
|
70
|
+
/** Put a value back on the live object (undo/redo). */
|
|
71
|
+
apply(entityId: string, property: string, value: ChannelValue): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* WHAT is being edited — everything a backend needs to decide, before any value
|
|
76
|
+
* exists. One object rather than a positional list because the two shipped
|
|
77
|
+
* backends read DIFFERENT members of it: the creation-site backend answers from
|
|
78
|
+
* `anchor`/`instances`, the OID backend from `sourceOid`, and a positional
|
|
79
|
+
* signature would have grown a fourth parameter nobody reading the call could
|
|
80
|
+
* name.
|
|
81
|
+
*/
|
|
82
|
+
export interface SourceWriteSubject {
|
|
83
|
+
readonly entityId: string;
|
|
84
|
+
/** An editor property path (a key of the surface's channel table). */
|
|
85
|
+
readonly property: string;
|
|
86
|
+
/**
|
|
87
|
+
* Which surface's vocabulary {@link SourceWriteSubject.property} is in.
|
|
88
|
+
* Absent ⇒ `three`. It travels with the subject rather than being fixed at
|
|
89
|
+
* backend construction because it is a fact about the PROPERTY NAME, and the
|
|
90
|
+
* server — which owns the channel tables — is the only thing that resolves it.
|
|
91
|
+
*/
|
|
92
|
+
readonly surface?: CreationSiteSurface | undefined;
|
|
93
|
+
/** Where the value being edited LIVES: the creation site of the object that
|
|
94
|
+
* owns the property (for a material colour, the material — not the mesh), or
|
|
95
|
+
* the record in the game's own data file that addresses this object. */
|
|
96
|
+
readonly anchor: NodeCreationSite;
|
|
97
|
+
/**
|
|
98
|
+
* WHICH LANE this subject's write would travel, in the pinned vocabulary
|
|
99
|
+
* (`WriteAnchorKind`) — planned by whoever built the subject, since that is
|
|
100
|
+
* the code that chose the branch, and carried here so a caller does not have
|
|
101
|
+
* to re-derive it from the anchor's shape (which cannot distinguish a JSX
|
|
102
|
+
* prop from a construction literal, nor either from a body-placed spawn).
|
|
103
|
+
*/
|
|
104
|
+
readonly anchorKind: WriteAnchorKind;
|
|
105
|
+
/** How many objects that anchor has constructed. A SOURCE-anchor fact only —
|
|
106
|
+
* a data record addresses exactly one object by construction, so the count
|
|
107
|
+
* is 1 there and nothing reads it. */
|
|
108
|
+
readonly instances: number;
|
|
109
|
+
/** Ordinary gestures omit this and address one live object. The explicit
|
|
110
|
+
* component-default gesture carries `creation-site`, authorizing a rewrite
|
|
111
|
+
* whose declared effect is all `instances` objects. */
|
|
112
|
+
readonly writeScope?: CreationSiteWriteScope | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* The serve-time OID stamp of the object that OWNS the property (the same
|
|
115
|
+
* owner hop `anchor` follows), when the served source carried one. Absent for
|
|
116
|
+
* every unstamped world, which is what makes the two backends' domains
|
|
117
|
+
* disjoint rather than overlapping.
|
|
118
|
+
*/
|
|
119
|
+
readonly sourceOid?: string | undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** One property edit, fully planned, offered to the backend. */
|
|
123
|
+
export interface SourceWriteRequest extends SourceWriteSubject {
|
|
124
|
+
readonly baseline: ChannelValue;
|
|
125
|
+
readonly next: ChannelValue;
|
|
126
|
+
/** History label for the transaction ("Transform Sun"). */
|
|
127
|
+
readonly label: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* One REMOVAL, fully planned, offered to the backend — the same subject a write
|
|
132
|
+
* carries, minus the values. The missing `baseline`/`next` IS the difference:
|
|
133
|
+
* absence is not a value, and a door that took one would be a value write
|
|
134
|
+
* wearing another name.
|
|
135
|
+
*/
|
|
136
|
+
export interface SourceRemoveRequest extends SourceWriteSubject {
|
|
137
|
+
/** History label for the transaction ("Remove Sun position"). */
|
|
138
|
+
readonly label: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* THE REMOVAL DOOR — drop the authored value at a subject's address entirely,
|
|
143
|
+
* rather than writing a value into it.
|
|
144
|
+
*
|
|
145
|
+
* WHY A BACKEND NEEDS ONE AT ALL: every dialect writer here appends
|
|
146
|
+
* (`addIfMissing`), so a gesture on a callsite that authored no `position`
|
|
147
|
+
* ADDS one, and writing the old numbers back leaves that attribute standing —
|
|
148
|
+
* the file ends one attribute heavier than it started and no byte-level
|
|
149
|
+
* edit/revert round trip can close. Only removal expresses that absence.
|
|
150
|
+
*
|
|
151
|
+
* ABSENT ⇒ this backend cannot express byte-absence at all, and the adapter
|
|
152
|
+
* above it declares no `TransformProvider.remove`, so the protocol's removal
|
|
153
|
+
* door refuses BY NAME (`REMOVAL_UNAVAILABLE`) instead of a value write
|
|
154
|
+
* pretending to be a revert. The two members are ONE optional group rather
|
|
155
|
+
* than two optional methods precisely so "there is a door" and "here is who
|
|
156
|
+
* may walk through it" cannot come apart.
|
|
157
|
+
*/
|
|
158
|
+
export interface SourceRemovalDoor {
|
|
159
|
+
/**
|
|
160
|
+
* Is there a door for THIS subject's property, right now?
|
|
161
|
+
*
|
|
162
|
+
* SYNCHRONOUS, because its caller is `TransformProvider.editability`, which
|
|
163
|
+
* has to answer before the gesture. So it may only read what the backend
|
|
164
|
+
* already holds — the same rule {@link SourcePersistenceBackend.gate} states
|
|
165
|
+
* for the write side, and for the same reason.
|
|
166
|
+
*/
|
|
167
|
+
available(subject: SourceWriteSubject): boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Attempt the removal. `true` ⇒ the authored value is gone and this
|
|
170
|
+
* backend's own transaction carries the undo. `false` ⇒ NOTHING was dropped
|
|
171
|
+
* — the callsite carried no such attribute, or the writer refused it — and
|
|
172
|
+
* the backend has already reported why, so the caller acks the live-only
|
|
173
|
+
* floor rather than a revert nobody performed.
|
|
174
|
+
*/
|
|
175
|
+
perform(request: SourceRemoveRequest): Promise<boolean>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export type SourceWriteVerdict = { ok: true } | { ok: false; reason: string };
|
|
179
|
+
|
|
180
|
+
export interface SourcePersistenceBackend {
|
|
181
|
+
/** Called once by the adapter, before any gesture. */
|
|
182
|
+
attach(live: LiveChannelAccess): void;
|
|
183
|
+
/**
|
|
184
|
+
* The source address this backend would write `sourceOid` at, when it knows
|
|
185
|
+
* one — the adapter prefers it over the creation-site registry's answer, so a
|
|
186
|
+
* stamped object's "created at" line names its real JSX callsite instead of
|
|
187
|
+
* the registry's honest-but-useless "constructed outside project source".
|
|
188
|
+
*
|
|
189
|
+
* ABSENT on a backend with no OID index (the creation-site one), and `null`
|
|
190
|
+
* for an oid this backend's index has not resolved. Never a guess.
|
|
191
|
+
*/
|
|
192
|
+
anchor?(sourceOid: string): NodeCreationSite | null;
|
|
193
|
+
/**
|
|
194
|
+
* Is the value at `sourceOid`'s anchor read by a PHYSICS BODY BINDING — i.e.
|
|
195
|
+
* would a write here land at a spawn a simulation re-poses from, rather than
|
|
196
|
+
* at a prop the element itself keeps?
|
|
197
|
+
*
|
|
198
|
+
* Only a backend holding the game's own source index can answer, which is why
|
|
199
|
+
* it lives here rather than on the adapter: the fact is `physicsBinding` /
|
|
200
|
+
* `r3fAuthoring.bodyForwarded` on the served `OidEntry`
|
|
201
|
+
* (`ui-source/r3f-physics-binding.ts`). ABSENT ⇒ this backend indexes no
|
|
202
|
+
* source and every anchor it plans is classified by its own branch alone.
|
|
203
|
+
*/
|
|
204
|
+
physicsPlaced?(sourceOid: string, property: string): boolean;
|
|
205
|
+
/** The sentence `transforms.editability` cites for this node + property. */
|
|
206
|
+
describe(subject: SourceWriteSubject): string;
|
|
207
|
+
/** Synchronous "is a write even on the table?", with the reason when not.
|
|
208
|
+
* Must not await: the live-only path stays as synchronous as it was before
|
|
209
|
+
* any of this existed, so an `endEdit(); undo()` sequence still works. */
|
|
210
|
+
gate(subject: SourceWriteSubject): SourceWriteVerdict;
|
|
211
|
+
/**
|
|
212
|
+
* Could this backend write at all right now — a reachable writer, and a
|
|
213
|
+
* session that is AUTHORING rather than playing? Decides only whether a
|
|
214
|
+
* CHEAP-gate refusal is worth telling the user about: "that property has no
|
|
215
|
+
* source anchor" is honesty during an authoring gesture and noise on every
|
|
216
|
+
* frame of a game somebody is playing.
|
|
217
|
+
*/
|
|
218
|
+
armed(): boolean;
|
|
219
|
+
/** Report a refusal where the user can see it. */
|
|
220
|
+
report(label: string, reason: string): void;
|
|
221
|
+
/**
|
|
222
|
+
* READ what the construction statement says these properties are — the
|
|
223
|
+
* component-instance diff's other half ("the site's literal is the default,
|
|
224
|
+
* the live object is the instance").
|
|
225
|
+
*
|
|
226
|
+
* ABSENT ⇒ this backend has no readable source behind its subjects, and the
|
|
227
|
+
* adapter reports no component-instance description at all rather than one
|
|
228
|
+
* whose defaults it made up. Present ⇒ every property gets an answer or is
|
|
229
|
+
* simply missing from the map, which is the honest "the site names none".
|
|
230
|
+
*/
|
|
231
|
+
readSiteLiterals?(
|
|
232
|
+
request: IngestInspectRequest,
|
|
233
|
+
): Promise<Record<string, CreationSiteLiteralReport>>;
|
|
234
|
+
/** Attempt the write. `true` ⇒ persisted, and the adapter skips its journal
|
|
235
|
+
* because the backend's own transaction already carries both halves. */
|
|
236
|
+
write(request: SourceWriteRequest): Promise<boolean>;
|
|
237
|
+
/**
|
|
238
|
+
* The other half of `write` for a backend that can express ABSENCE — see
|
|
239
|
+
* {@link SourceRemovalDoor}. Absent on a backend that cannot, which is how a
|
|
240
|
+
* lane honestly reports that byte-absence is unreachable through it rather
|
|
241
|
+
* than reverting with a value write.
|
|
242
|
+
*/
|
|
243
|
+
readonly removal?: SourceRemovalDoor;
|
|
244
|
+
/** What `AuthoringAdapter.persistence.destination` reports. */
|
|
245
|
+
destination(): string;
|
|
246
|
+
dispose(): void;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface CreationSitePersistenceOptions {
|
|
250
|
+
readonly history: HistoryService | null;
|
|
251
|
+
/**
|
|
252
|
+
* Injectable so a unit test can drive the write path without a dev server.
|
|
253
|
+
* Absent in production: the backend builds its own when the tier can reach
|
|
254
|
+
* one, and stays at the honest live-only floor when it cannot.
|
|
255
|
+
*/
|
|
256
|
+
readonly writer?: IngestSourcePersistence | undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function createCreationSitePersistence(
|
|
260
|
+
options: CreationSitePersistenceOptions,
|
|
261
|
+
): SourcePersistenceBackend {
|
|
262
|
+
let writer: IngestSourcePersistence | null = null;
|
|
263
|
+
return {
|
|
264
|
+
attach(live) {
|
|
265
|
+
// Whether an ingest edit is RECORDED is what this session's host serves
|
|
266
|
+
// (`/__ingest-source/*`, from `creationSiteWritePlugin`), never how the
|
|
267
|
+
// editor shell was built — the packaged editor runs a production bundle
|
|
268
|
+
// and serves that route, so `import.meta.env.DEV` refused the write on the
|
|
269
|
+
// one tier a registry install has. See
|
|
270
|
+
// `../ui-source/tier-source-write-backend.ts`.
|
|
271
|
+
writer =
|
|
272
|
+
options.writer ??
|
|
273
|
+
(ingestSourceWritesRecordedIfPrimed('An ingest root’s source persistence')
|
|
274
|
+
? new IngestSourcePersistence({ history: options.history, live })
|
|
275
|
+
: null);
|
|
276
|
+
},
|
|
277
|
+
describe({ anchor, instances, property, writeScope }) {
|
|
278
|
+
if (writer) return writer.describe(anchor, instances, property, writeScope);
|
|
279
|
+
if (!anchor.anchored) return `Live-only edit — ${anchor.reason}.`;
|
|
280
|
+
return anchor.kind === 'data'
|
|
281
|
+
? `Live-only edit — this object is ${anchor.display}.`
|
|
282
|
+
: `Live-only edit — this object is created at ${anchor.display}.`;
|
|
283
|
+
},
|
|
284
|
+
gate({ anchor, instances, property, writeScope }) {
|
|
285
|
+
if (!writer) return { ok: false, reason: 'this editor tier cannot write project source' };
|
|
286
|
+
return writer.gate(anchor, instances, property, writeScope);
|
|
287
|
+
},
|
|
288
|
+
armed: () => writer !== null && editorIsAuthoring(),
|
|
289
|
+
readSiteLiterals: (request) => (writer ? writer.inspect(request) : Promise.resolve({})),
|
|
290
|
+
report: (label, reason) => reportIngestSourceRefusal(label, reason),
|
|
291
|
+
async write(request) {
|
|
292
|
+
if (!writer) return false;
|
|
293
|
+
try {
|
|
294
|
+
const outcome = await writer.persist(request);
|
|
295
|
+
if (outcome.persisted) {
|
|
296
|
+
editorConsole.log(
|
|
297
|
+
`[ingest] ${request.label} written to the game's own source — ${outcome.detail}`,
|
|
298
|
+
);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
reportIngestSourceRefusal(request.label, outcome.reason);
|
|
302
|
+
return false;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
reportIngestSourceRefusal(
|
|
305
|
+
request.label,
|
|
306
|
+
`the write failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
307
|
+
);
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
/**
|
|
312
|
+
* THE REMOVAL DOOR on the creation-site lane — the byte-absence half the
|
|
313
|
+
* insertion arm makes necessary: an authored transform can ADD a
|
|
314
|
+
* `receiver.member.axis = value;` statement for an axis the source never
|
|
315
|
+
* named, and no value write can revert an added property. The plan is
|
|
316
|
+
* `planCreationSiteRemoval` (server-side, same `prepare`/`apply` route and
|
|
317
|
+
* checksum guard as a value write, so a vendored game's lock rides along
|
|
318
|
+
* in the same gesture): the ONE deletable shape is the whole own-line
|
|
319
|
+
* assignment statement insertion writes; everything else — constructor
|
|
320
|
+
* literals, `.set(…)` arguments, alias writes, computed expressions —
|
|
321
|
+
* refuses in the planner's own words.
|
|
322
|
+
*
|
|
323
|
+
* `available` answers for the DOOR (a writer exists and the gate is open
|
|
324
|
+
* for this subject), the same honesty split the OID lane records: whether
|
|
325
|
+
* the site currently AUTHORS such a statement is the file's fact, and
|
|
326
|
+
* `perform` — which returns the server plan's own verdict — answers it.
|
|
327
|
+
*/
|
|
328
|
+
removal: {
|
|
329
|
+
available(subject) {
|
|
330
|
+
if (!writer) return false;
|
|
331
|
+
return writer.gate(subject.anchor, subject.instances, subject.property, subject.writeScope)
|
|
332
|
+
.ok;
|
|
333
|
+
},
|
|
334
|
+
async perform(request) {
|
|
335
|
+
if (!writer) return false;
|
|
336
|
+
try {
|
|
337
|
+
const outcome = await writer.remove({
|
|
338
|
+
property: request.property,
|
|
339
|
+
surface: request.surface,
|
|
340
|
+
anchor: request.anchor,
|
|
341
|
+
instances: request.instances,
|
|
342
|
+
writeScope: request.writeScope,
|
|
343
|
+
label: request.label,
|
|
344
|
+
});
|
|
345
|
+
if (outcome.persisted) {
|
|
346
|
+
editorConsole.log(
|
|
347
|
+
`[ingest] ${request.label} — dropped from the game's own source (${outcome.detail})`,
|
|
348
|
+
);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
reportIngestSourceRefusal(request.label, outcome.reason);
|
|
352
|
+
return false;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
reportIngestSourceRefusal(
|
|
355
|
+
request.label,
|
|
356
|
+
`the removal failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
357
|
+
);
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
destination() {
|
|
363
|
+
const owned = ingestOwnershipNow();
|
|
364
|
+
return editorIsAuthoring() && owned?.writable && writer
|
|
365
|
+
? "the game's own source (creation-site write-back)"
|
|
366
|
+
: LIVE_ONLY_DESTINATION;
|
|
367
|
+
},
|
|
368
|
+
dispose() {
|
|
369
|
+
writer?.dispose();
|
|
370
|
+
writer = null;
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Match source revisions to completed Vite updates, irrespective of channel
|
|
2
|
+
* order. A websocket message alone is not evidence that an update applied.
|
|
3
|
+
* Entries are bounded by current file paths, never by edit count.
|
|
4
|
+
*/
|
|
5
|
+
export interface RefreshSource {
|
|
6
|
+
file: string;
|
|
7
|
+
path: string;
|
|
8
|
+
sha: string;
|
|
9
|
+
timestamp?: number;
|
|
10
|
+
/** Accepted importer URLs for a changed non-component dependency. */
|
|
11
|
+
boundaries?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function updateFile(path: string): string {
|
|
15
|
+
return decodeURIComponent(path.split('?')[0]!)
|
|
16
|
+
.replace(/^\/@fs\/(?=[A-Za-z]:\/)/, '')
|
|
17
|
+
.replace(/^\/@fs/, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SourceRefreshRevisions {
|
|
21
|
+
private readonly sources = new Map<string, RefreshSource>();
|
|
22
|
+
private readonly applied = new Map<string, string>();
|
|
23
|
+
private readonly expected = new Map<string, string | null>();
|
|
24
|
+
private missingRevision = false;
|
|
25
|
+
|
|
26
|
+
source(source: RefreshSource): void {
|
|
27
|
+
this.sources.set(source.file, source);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
complete(updates: readonly { acceptedPath: string; timestamp: number }[]): void {
|
|
31
|
+
for (const [file, source] of this.sources) {
|
|
32
|
+
const current = updates.filter((update) => source.timestamp === update.timestamp);
|
|
33
|
+
const complete = source.boundaries
|
|
34
|
+
? source.boundaries.every((boundary) =>
|
|
35
|
+
current.some((update) => update.acceptedPath === boundary),
|
|
36
|
+
)
|
|
37
|
+
: current.some((update) => updateFile(update.acceptedPath) === file);
|
|
38
|
+
if (!complete) continue;
|
|
39
|
+
this.applied.set(source.path, source.sha);
|
|
40
|
+
this.sources.delete(file);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
matches(update: { acceptedPath: string; timestamp: number }): boolean {
|
|
45
|
+
const file = updateFile(update.acceptedPath);
|
|
46
|
+
return [...this.sources.values()].some(
|
|
47
|
+
(source) =>
|
|
48
|
+
source.timestamp === update.timestamp &&
|
|
49
|
+
(source.boundaries
|
|
50
|
+
? source.boundaries.includes(update.acceptedPath)
|
|
51
|
+
: source.file === file),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
revision(resources: readonly { path: string; sha: string | null }[]): void {
|
|
56
|
+
for (const resource of resources) {
|
|
57
|
+
this.expected.set(resource.path, resource.sha);
|
|
58
|
+
if (resource.sha === null) this.applied.delete(resource.path);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
needsRemount(): boolean {
|
|
63
|
+
if (this.missingRevision) return true;
|
|
64
|
+
for (const [path, sha] of this.expected) {
|
|
65
|
+
if (sha === null || this.applied.get(path) !== sha) return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
missing(): void {
|
|
71
|
+
this.missingRevision = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A completed cold mount consumed these revisions through ordinary source. */
|
|
75
|
+
mounted(): void {
|
|
76
|
+
this.expected.clear();
|
|
77
|
+
this.applied.clear();
|
|
78
|
+
this.sources.clear();
|
|
79
|
+
this.missingRevision = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE STRUCTURAL WRITE PIPE — the ONE producer of the `source-structure`
|
|
3
|
+
* resolution and its live-only floor, shared by every source-document lane.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from three near-verbatim copies (owner-directed overlap audit,
|
|
6
|
+
* 2026-08-22): `R3fSourceAuthoringAdapter` (`structOp`/`removeMany`/
|
|
7
|
+
* `groupMany`/`pipedStructWrite`/`structRefusal`), `ReactRootAuthoringAdapter`
|
|
8
|
+
* (`structOp`/`removeManyElements`/`pipedStructWrite`/`structRefusal`) and
|
|
9
|
+
* `pixi-source-write-target` (`structOp`/`structMany`/`pipedStruct`/
|
|
10
|
+
* `structRefusal`). The copies had already drifted in the small ways their own
|
|
11
|
+
* doc comments warn about — refusal spellings, destination defaults — and one
|
|
12
|
+
* module makes the drift impossible rather than policed.
|
|
13
|
+
*
|
|
14
|
+
* WHY THE STRUCT DIALECT RESOLVES ON ITS OWN VERB (kept from every copy's
|
|
15
|
+
* header): `writeStruct`/`writeStructMany` is a DIFFERENT DOOR from the
|
|
16
|
+
* attribute writer, so it acks `source-structure`, never `source-prop` —
|
|
17
|
+
* resolving a delete on `writeProp`'s presence is how a lane comes to name a
|
|
18
|
+
* value lane that never carried the bytes. `record` is a no-op because the
|
|
19
|
+
* backend is wrapped in `withProjectSourceHistory`: the sha-guarded whole-file
|
|
20
|
+
* transaction that carries the bytes IS the undo entry, and journaling here
|
|
21
|
+
* would make one gesture two undos.
|
|
22
|
+
*
|
|
23
|
+
* WHAT STAYS PER-LANE, deliberately: OID RESOLUTION (identity is the lane's
|
|
24
|
+
* own scheme — a caller hands in the resolved oid), the DESTINATION sentence,
|
|
25
|
+
* and the ON-CHANGED hook (react marks dirty and queues a source reconcile;
|
|
26
|
+
* the canvas target refreshes its index; r3f notifies the store). A lane hands
|
|
27
|
+
* those in; this module never guesses them.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { showTransientHint } from '@volter/editor-core/transient-hint';
|
|
31
|
+
import type { SourceWriteBackend } from '@volter/editor-core/ui-source/source-write-backend';
|
|
32
|
+
import { resolvesLiveOnly, runWritePipe, type WriteAck, type WriteResolution } from '@volter/editor-core/authoring/write-pipe';
|
|
33
|
+
|
|
34
|
+
/** The `writeStruct` options bag, spelled once (the backend's own shape). */
|
|
35
|
+
export type StructOpOptions = Parameters<SourceWriteBackend['writeStruct']>[2];
|
|
36
|
+
|
|
37
|
+
export interface StructWritePipeConfig {
|
|
38
|
+
/** The lane's own refusal/no-op reporter — prefix included, e.g.
|
|
39
|
+
* `` (m) => console.warn(`[R3fSourceAuthoringAdapter] ${m}`) ``. */
|
|
40
|
+
report(message: string): void;
|
|
41
|
+
/** The live-only floor's reason when no struct door is bound. */
|
|
42
|
+
readonly noWriterReason: string;
|
|
43
|
+
/** The lane's backend, read at call time (a session can gain or lose one). */
|
|
44
|
+
backend(): SourceWriteBackend | null | undefined;
|
|
45
|
+
/** Ran after a LANDED structural write, before the ack resolves — the
|
|
46
|
+
* lane's own reconcile/notify choreography. */
|
|
47
|
+
onChanged(): void | Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface StructWritePipe {
|
|
51
|
+
/** Every structural source write, through the pipe on the struct verb. */
|
|
52
|
+
pipedStruct(
|
|
53
|
+
write: () => Promise<boolean>,
|
|
54
|
+
op: string,
|
|
55
|
+
bound: boolean,
|
|
56
|
+
destination: string,
|
|
57
|
+
): Promise<WriteAck>;
|
|
58
|
+
/** A structural verb that never reaches a writer, answered THROUGH the pipe
|
|
59
|
+
* so the lane has exactly ONE producer of the live-only floor. `hint`, when
|
|
60
|
+
* given, is the sentence the author who made the gesture also sees. */
|
|
61
|
+
structRefusal(reason: string, hint?: string): Promise<WriteAck>;
|
|
62
|
+
/** One `writeStruct` op against a resolved oid; `null` oid refuses by name. */
|
|
63
|
+
structOp(
|
|
64
|
+
oid: string | null | undefined,
|
|
65
|
+
subjectLabel: string,
|
|
66
|
+
op: string,
|
|
67
|
+
opts: StructOpOptions,
|
|
68
|
+
destination: string,
|
|
69
|
+
): Promise<WriteAck>;
|
|
70
|
+
/** One `writeStructMany` batch. The CALLER owns oid collection and any
|
|
71
|
+
* every-node-addressable refusal — batching policy is lane policy. */
|
|
72
|
+
structMany(
|
|
73
|
+
oids: readonly string[],
|
|
74
|
+
op: string,
|
|
75
|
+
opts: { wrapperTag?: string } | undefined,
|
|
76
|
+
destination: string,
|
|
77
|
+
): Promise<WriteAck>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createStructWritePipe(config: StructWritePipeConfig): StructWritePipe {
|
|
81
|
+
const pipedStruct: StructWritePipe['pipedStruct'] = (write, op, bound, destination) =>
|
|
82
|
+
runWritePipe({
|
|
83
|
+
resolve: (): WriteResolution =>
|
|
84
|
+
bound
|
|
85
|
+
? { reaches: 'writer', anchorKind: 'source-structure', destination, write }
|
|
86
|
+
: resolvesLiveOnly(config.noWriterReason),
|
|
87
|
+
record: () => undefined,
|
|
88
|
+
report: (reason) => config.report(`"${op}" stays live-only — ${reason}.`),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const structRefusal: StructWritePipe['structRefusal'] = (reason, hint) =>
|
|
92
|
+
runWritePipe({
|
|
93
|
+
resolve: () => resolvesLiveOnly(reason),
|
|
94
|
+
record: () => undefined,
|
|
95
|
+
report: (said) => {
|
|
96
|
+
config.report(said);
|
|
97
|
+
if (hint) showTransientHint(hint);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
pipedStruct,
|
|
103
|
+
structRefusal,
|
|
104
|
+
structOp: (oid, subjectLabel, op, opts, destination) => {
|
|
105
|
+
if (!oid) {
|
|
106
|
+
return structRefusal(`"${op}" refused: "${subjectLabel}" has no source stamp.`);
|
|
107
|
+
}
|
|
108
|
+
return pipedStruct(
|
|
109
|
+
async () => {
|
|
110
|
+
const backend = config.backend()!;
|
|
111
|
+
const res = await backend.writeStruct(oid, op, opts);
|
|
112
|
+
if (!res.changed) {
|
|
113
|
+
config.report(
|
|
114
|
+
`struct "${op}" refused/no-op for oid "${oid}": ${res.error ?? 'no change'}`,
|
|
115
|
+
);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
await config.onChanged();
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
op,
|
|
122
|
+
config.backend()?.writeStruct !== undefined,
|
|
123
|
+
destination,
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
structMany: (oids, op, opts, destination) =>
|
|
127
|
+
pipedStruct(
|
|
128
|
+
async () => {
|
|
129
|
+
const backend = config.backend()!;
|
|
130
|
+
const res = await backend.writeStructMany!(oids, op, opts);
|
|
131
|
+
if (!res.changed) {
|
|
132
|
+
config.report(`batch "${op}" refused/no-op: ${res.error ?? 'no change'}`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
await config.onChanged();
|
|
136
|
+
return true;
|
|
137
|
+
},
|
|
138
|
+
`${op} (batch)`,
|
|
139
|
+
config.backend()?.writeStructMany !== undefined,
|
|
140
|
+
destination,
|
|
141
|
+
),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What is still allowed to move the editor camera after a world is adopted,
|
|
3
|
+
* and when that stops.
|
|
4
|
+
*
|
|
5
|
+
* Opening the Scene tab on an adopted world is not one action, because the
|
|
6
|
+
* world is not one event: a game captures on its first drawn frame and keeps
|
|
7
|
+
* building itself for seconds afterwards (a level streams in; an R3F game's
|
|
8
|
+
* own camera appears only when its `<Suspense>` content resolves). So there is
|
|
9
|
+
* a WINDOW, and two things may fire inside it — the game's own camera, which
|
|
10
|
+
* wins outright, and a measured framing, which lands once as the interim
|
|
11
|
+
* answer. `scene-framing.ts` is what each of those means.
|
|
12
|
+
*
|
|
13
|
+
* The rule that outranks both: **the reader's first camera gesture ends the
|
|
14
|
+
* window** (#1706). Yanking the view out from under someone already looking is
|
|
15
|
+
* worse than never framing at all, so `cancel()` is total — after it, nothing
|
|
16
|
+
* in here touches the camera again, whatever is still loading.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** How long a requested auto-frame keeps waiting for a world that is still
|
|
20
|
+
* building itself, and how often it re-checks. */
|
|
21
|
+
export const AUTO_FRAME_WINDOW_MS = 20_000;
|
|
22
|
+
export const AUTO_FRAME_RETRY_MS = 250;
|
|
23
|
+
/** How long an adopted game gets to produce its OWN camera before the editor
|
|
24
|
+
* settles for its measured framing. Shorter than the framing window on
|
|
25
|
+
* purpose: replacing the view is welcome while the reader is still watching
|
|
26
|
+
* the world appear, and an intrusion long after that. */
|
|
27
|
+
export const AUTO_SEED_WINDOW_MS = 5_000;
|
|
28
|
+
|
|
29
|
+
/** The two things that may open the reader on the world. Each returns whether
|
|
30
|
+
* it actually moved the camera. */
|
|
31
|
+
export interface AutoFrameSteps {
|
|
32
|
+
/** Adopt the adopted game's own camera; false when it has none yet, or none
|
|
33
|
+
* whose view is usable. */
|
|
34
|
+
seedFromGameCamera(): boolean;
|
|
35
|
+
/** Frame the content that exists right now; false when there is none yet. */
|
|
36
|
+
frameContent(): boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class AutoFrameWindow {
|
|
40
|
+
#framingDeadline = 0;
|
|
41
|
+
#seedDeadline = 0;
|
|
42
|
+
#nextTry = 0;
|
|
43
|
+
readonly #steps: AutoFrameSteps;
|
|
44
|
+
|
|
45
|
+
constructor(steps: AutoFrameSteps) {
|
|
46
|
+
this.#steps = steps;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** True while something in here may still move the camera. */
|
|
50
|
+
get pending(): boolean {
|
|
51
|
+
return this.#framingDeadline > 0 || this.#seedDeadline > 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Open the window and take the first attempt immediately.
|
|
56
|
+
* `watchForGameCamera` is false for a world that has no game camera to wait
|
|
57
|
+
* for — a first-party scene never gets one.
|
|
58
|
+
*/
|
|
59
|
+
begin(now: number, watchForGameCamera: boolean): void {
|
|
60
|
+
this.#framingDeadline = now + AUTO_FRAME_WINDOW_MS;
|
|
61
|
+
this.#seedDeadline = watchForGameCamera ? now + AUTO_SEED_WINDOW_MS : 0;
|
|
62
|
+
this.#nextTry = 0;
|
|
63
|
+
this.tick(now);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Ride the caller's frame loop. Cheap and self-rate-limiting: a no-op
|
|
67
|
+
* unless the window is open and the retry cadence has come round. */
|
|
68
|
+
tick(now: number): void {
|
|
69
|
+
if (!this.pending || now < this.#nextTry) return;
|
|
70
|
+
this.#nextTry = now + AUTO_FRAME_RETRY_MS;
|
|
71
|
+
// The game's own camera outranks any measurement, and ends the window: it
|
|
72
|
+
// is the answer both other paths were approximating.
|
|
73
|
+
if (this.#seedDeadline > 0 && this.#steps.seedFromGameCamera()) {
|
|
74
|
+
this.cancel();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// The framing lands ONCE — re-framing every retry would drag the view
|
|
78
|
+
// around for the whole window while a level streams in.
|
|
79
|
+
if (this.#framingDeadline > 0 && this.#steps.frameContent()) this.#framingDeadline = 0;
|
|
80
|
+
if (now >= this.#framingDeadline) this.#framingDeadline = 0;
|
|
81
|
+
if (now >= this.#seedDeadline) this.#seedDeadline = 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The reader took the camera. Nothing here moves it again. */
|
|
85
|
+
cancel(): void {
|
|
86
|
+
this.#framingDeadline = 0;
|
|
87
|
+
this.#seedDeadline = 0;
|
|
88
|
+
}
|
|
89
|
+
}
|