@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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognizing WHICH canvas runtime a mounted game is actually running, from
|
|
3
|
+
* that realm's own public handles.
|
|
4
|
+
*
|
|
5
|
+
* The host never infers a library from the manifest: a canvas root says
|
|
6
|
+
* `canvas`, and which of Pixi/Phaser/Babylon draws on it is a fact about the
|
|
7
|
+
* live realm. These two probes read the runtimes' OWN documented registries, so
|
|
8
|
+
* a game that ships neither is answered `null` — and the mount fails by name
|
|
9
|
+
* rather than being presented as a library it is not, or as an editable tree
|
|
10
|
+
* the editor fabricated.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { BabylonEngineLike } from './babylon-authoring-adapter';
|
|
14
|
+
import type { PhaserGameLike } from './phaser-live-authoring-adapter';
|
|
15
|
+
|
|
16
|
+
export interface PhaserRealmLike {
|
|
17
|
+
Phaser?: { GAMES?: PhaserGameLike[] };
|
|
18
|
+
/** Phaser 3.16 predates `Phaser.GAMES`; this is its standard public handle. */
|
|
19
|
+
game?: PhaserGameLike;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BabylonRealmLike {
|
|
23
|
+
BABYLON?: { Engine?: { Instances?: BabylonEngineLike[] } };
|
|
24
|
+
/** Host build seam for module-bundled Babylon, where no BABYLON global exists. */
|
|
25
|
+
__vgaiBabylon?: {
|
|
26
|
+
engines?: BabylonEngineLike[];
|
|
27
|
+
/** Optional boot promise published by a host-built classic bundle. */
|
|
28
|
+
ready?: PromiseLike<unknown>;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Recognize Phaser through public realm handles across old and new Phaser 3. */
|
|
33
|
+
export function findPhaserGame(realm: PhaserRealmLike | null): PhaserGameLike | null {
|
|
34
|
+
if (typeof realm?.game?.scene?.getScenes === 'function') return realm.game;
|
|
35
|
+
const games = realm?.Phaser?.GAMES;
|
|
36
|
+
if (!Array.isArray(games)) return null;
|
|
37
|
+
return games.find((game) => typeof game?.scene?.getScenes === 'function') ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Recognize Babylon from its public engine registry or the module-build host seam. */
|
|
41
|
+
export function findBabylonEngine(realm: BabylonRealmLike | null): BabylonEngineLike | null {
|
|
42
|
+
const bridged = realm?.__vgaiBabylon?.engines;
|
|
43
|
+
const engines = Array.isArray(bridged) ? bridged : realm?.BABYLON?.Engine?.Instances;
|
|
44
|
+
if (!Array.isArray(engines)) return null;
|
|
45
|
+
return (
|
|
46
|
+
[...engines]
|
|
47
|
+
.reverse()
|
|
48
|
+
.find((engine) => Array.isArray(engine?.scenes) && engine.scenes.length > 0) ?? null
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects an ingested game's declared semantic hierarchy groups onto the
|
|
3
|
+
* ordinary AuthoringAdapter hierarchy. The shell remains content-blind: the
|
|
4
|
+
* game supplies ids, labels, and live counts; this decorator only translates
|
|
5
|
+
* them into EditorNode currency and keeps the captured render tree available
|
|
6
|
+
* under one explicitly named folder.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AuthoringAdapter, EditorNode, HierarchyProvider } from '@volter/editor-project/adapter';
|
|
10
|
+
import type {
|
|
11
|
+
VgaiGameHierarchyGroup,
|
|
12
|
+
VgaiGameHierarchyProvider,
|
|
13
|
+
} from '@volter/editor-project/adapter/ingest/game-contract';
|
|
14
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
15
|
+
|
|
16
|
+
const GROUP_PREFIX = 'contract-hierarchy:group:';
|
|
17
|
+
const RENDER_TREE_ID = 'contract-hierarchy:render-tree';
|
|
18
|
+
|
|
19
|
+
function groupNodeId(id: string): string {
|
|
20
|
+
return `${GROUP_PREFIX}${encodeURIComponent(id)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function groupNode(group: VgaiGameHierarchyGroup): EditorNode {
|
|
24
|
+
return {
|
|
25
|
+
id: groupNodeId(group.id),
|
|
26
|
+
label: group.label,
|
|
27
|
+
secondaryLabel: `${group.count} ${group.count === 1 ? 'item' : 'items'}`,
|
|
28
|
+
role: 'folder',
|
|
29
|
+
kind: 'group',
|
|
30
|
+
parentId: null,
|
|
31
|
+
childIds: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertGroups(value: unknown): VgaiGameHierarchyGroup[] {
|
|
36
|
+
if (!Array.isArray(value)) throw new Error('hierarchy() did not return an array');
|
|
37
|
+
const ids = new Set<string>();
|
|
38
|
+
for (const group of value) {
|
|
39
|
+
if (
|
|
40
|
+
typeof group !== 'object' ||
|
|
41
|
+
group === null ||
|
|
42
|
+
typeof group.id !== 'string' ||
|
|
43
|
+
group.id.trim().length === 0 ||
|
|
44
|
+
typeof group.label !== 'string' ||
|
|
45
|
+
group.label.trim().length === 0 ||
|
|
46
|
+
typeof group.count !== 'number' ||
|
|
47
|
+
!Number.isInteger(group.count) ||
|
|
48
|
+
group.count < 0
|
|
49
|
+
) {
|
|
50
|
+
throw new Error('hierarchy() returned an invalid { id, label, count } group');
|
|
51
|
+
}
|
|
52
|
+
if (ids.has(group.id)) throw new Error(`hierarchy() returned duplicate id "${group.id}"`);
|
|
53
|
+
ids.add(group.id);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Identity-preserving when the game declares no hierarchy surface. */
|
|
59
|
+
export function withContractHierarchy(
|
|
60
|
+
base: AuthoringAdapter,
|
|
61
|
+
provider: VgaiGameHierarchyProvider | undefined,
|
|
62
|
+
): AuthoringAdapter {
|
|
63
|
+
if (!provider) return base;
|
|
64
|
+
|
|
65
|
+
let warned = false;
|
|
66
|
+
const groups = (): VgaiGameHierarchyGroup[] => {
|
|
67
|
+
try {
|
|
68
|
+
return assertGroups(provider());
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (!warned) {
|
|
71
|
+
warned = true;
|
|
72
|
+
editorConsole.warn(
|
|
73
|
+
`Ignored invalid game-contract hierarchy: ${error instanceof Error ? error.message : String(error)}`,
|
|
74
|
+
'ingest',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const roots = () => {
|
|
82
|
+
const semantic = groups();
|
|
83
|
+
const rendered = base.hierarchy.roots();
|
|
84
|
+
if (semantic.length === 0) return rendered;
|
|
85
|
+
return [
|
|
86
|
+
...semantic.map(groupNode),
|
|
87
|
+
{
|
|
88
|
+
id: RENDER_TREE_ID,
|
|
89
|
+
label: 'Rendered objects',
|
|
90
|
+
secondaryLabel: 'live tree',
|
|
91
|
+
role: 'folder',
|
|
92
|
+
kind: 'group',
|
|
93
|
+
parentId: null,
|
|
94
|
+
childIds: rendered.map((node) => node.id),
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
};
|
|
98
|
+
const node = (id: string) => {
|
|
99
|
+
const semantic = groups();
|
|
100
|
+
const group = semantic.find((candidate) => groupNodeId(candidate.id) === id);
|
|
101
|
+
if (group) return groupNode(group);
|
|
102
|
+
if (semantic.length > 0 && id === RENDER_TREE_ID) {
|
|
103
|
+
const rendered = base.hierarchy.roots();
|
|
104
|
+
return {
|
|
105
|
+
id: RENDER_TREE_ID,
|
|
106
|
+
label: 'Rendered objects',
|
|
107
|
+
secondaryLabel: 'live tree',
|
|
108
|
+
role: 'folder',
|
|
109
|
+
kind: 'group',
|
|
110
|
+
parentId: null,
|
|
111
|
+
childIds: rendered.map((node) => node.id),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const rendered = base.hierarchy.node(id);
|
|
115
|
+
return semantic.length > 0 && rendered?.parentId === null
|
|
116
|
+
? { ...rendered, parentId: RENDER_TREE_ID }
|
|
117
|
+
: rendered;
|
|
118
|
+
};
|
|
119
|
+
const hierarchy = new Proxy(base.hierarchy, {
|
|
120
|
+
get(target, property) {
|
|
121
|
+
if (property === 'roots') return roots;
|
|
122
|
+
if (property === 'node') return node;
|
|
123
|
+
const value = Reflect.get(target, property, target);
|
|
124
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
125
|
+
},
|
|
126
|
+
}) satisfies HierarchyProvider;
|
|
127
|
+
const subscribe = (listener: () => void): (() => void) => {
|
|
128
|
+
const unsubscribeBase = base.subscribe?.(listener);
|
|
129
|
+
const unsubscribeContract = provider.subscribe?.(listener);
|
|
130
|
+
return () => {
|
|
131
|
+
unsubscribeBase?.();
|
|
132
|
+
unsubscribeContract?.();
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return new Proxy(base, {
|
|
137
|
+
get(target, property) {
|
|
138
|
+
if (property === 'hierarchy') return hierarchy;
|
|
139
|
+
if (property === 'subscribe') return subscribe;
|
|
140
|
+
const value = Reflect.get(target, property, target);
|
|
141
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects an ingested game's declared SCENES (`window.vgaiGame.scenes`, see
|
|
3
|
+
* `@volter/editor-project/adapter/ingest/game-contract`) onto the ordinary
|
|
4
|
+
* {@link StoriesProvider} the editor's story picker already reads — the sibling
|
|
5
|
+
* of `contract-hierarchy-authoring.ts`, which does the same job for the
|
|
6
|
+
* contract's `hierarchy` member.
|
|
7
|
+
*
|
|
8
|
+
* Why stories: a multi-screen game's screens ARE the design-time states of its
|
|
9
|
+
* world, and the editor's stories vocabulary is exactly "the states a world can
|
|
10
|
+
* be put into". Projecting rather than adding a second door means the picker,
|
|
11
|
+
* the inspector's serialized subject and `adapter.stories` all work on an
|
|
12
|
+
* ingested game unchanged and unaware of the provenance.
|
|
13
|
+
*
|
|
14
|
+
* SCOPE IS WORLD-LEVEL — `storiesFor`/`active` ignore `nodeId` entirely, the
|
|
15
|
+
* same scope `ReactRootAuthoringAdapter.stories` has (B2). A game's screen is a
|
|
16
|
+
* property of the whole world, not of the node the author happens to have
|
|
17
|
+
* selected, and pretending otherwise would need a per-node scene model no game
|
|
18
|
+
* declares.
|
|
19
|
+
*
|
|
20
|
+
* `isolate` is deliberately ABSENT: an ingested game cannot render ONE of its
|
|
21
|
+
* display objects against a scene — the scene IS the game's whole screen. An
|
|
22
|
+
* absent optional member is the honest answer; a no-op one would advertise a
|
|
23
|
+
* capability that does nothing.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
LiveSceneSwitch,
|
|
28
|
+
LiveSceneSwitchSettled,
|
|
29
|
+
LiveSceneTable,
|
|
30
|
+
} from '@volter/editor-sdk/host';
|
|
31
|
+
import type { StoriesProvider, StoryRef } from '@volter/editor-project/adapter';
|
|
32
|
+
import type { VgaiGameContract, VgaiGameScene } from '@volter/editor-project/adapter/ingest/game-contract';
|
|
33
|
+
import { readGameScenes } from '@volter/editor-project/adapter/ingest/game-contract';
|
|
34
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
35
|
+
|
|
36
|
+
function assertScenes(value: unknown): VgaiGameScene[] {
|
|
37
|
+
if (!Array.isArray(value)) throw new Error('scenes.list() did not return an array');
|
|
38
|
+
const ids = new Set<string>();
|
|
39
|
+
for (const scene of value) {
|
|
40
|
+
if (
|
|
41
|
+
typeof scene !== 'object' ||
|
|
42
|
+
scene === null ||
|
|
43
|
+
typeof scene.id !== 'string' ||
|
|
44
|
+
scene.id.trim().length === 0 ||
|
|
45
|
+
typeof scene.label !== 'string' ||
|
|
46
|
+
scene.label.trim().length === 0
|
|
47
|
+
) {
|
|
48
|
+
throw new Error('scenes.list() returned an invalid { id, label } scene');
|
|
49
|
+
}
|
|
50
|
+
if (ids.has(scene.id)) throw new Error(`scenes.list() returned duplicate id "${scene.id}"`);
|
|
51
|
+
ids.add(scene.id);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The switch vocabulary is the SDK's (`LiveSceneSwitch`), not this lane's.
|
|
58
|
+
* It was declared here and the host's live registry imported it, which is how
|
|
59
|
+
* the ingest game contract reached a core host module; the contract now states
|
|
60
|
+
* its own result and this lane implements it (phase 1 of the open-source
|
|
61
|
+
* launch, 2026-09-18). The aliases stay because this module's own readers
|
|
62
|
+
* speak scenes, not "live scene tables".
|
|
63
|
+
*/
|
|
64
|
+
export type SceneSwitchSettled = LiveSceneSwitchSettled;
|
|
65
|
+
export type SceneSwitch = LiveSceneSwitch;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The scenes projection: the ordinary {@link StoriesProvider} the picker reads,
|
|
69
|
+
* plus `goToScene` — the SAME switch with its answer kept instead of dropped.
|
|
70
|
+
*
|
|
71
|
+
* One path, two callers. `apply` is `StoriesProvider`'s synchronous,
|
|
72
|
+
* answer-less shape (the picker has `active()` to read afterwards); the `open`
|
|
73
|
+
* protocol verb needs the game's answer and the refusal as VALUES, and getting
|
|
74
|
+
* them by calling the contract's `goTo` directly would route around the
|
|
75
|
+
* membership check this module owns.
|
|
76
|
+
*/
|
|
77
|
+
export type ContractScenesStories = LiveSceneTable;
|
|
78
|
+
|
|
79
|
+
/** True when `provider` is a scenes projection — i.e. carries the awaited,
|
|
80
|
+
* membership-checked switch above. A plain `StoriesProvider` (a react world's
|
|
81
|
+
* CSF states) does not, and must not be navigated as though it were a game's
|
|
82
|
+
* own screens. */
|
|
83
|
+
export function isContractScenesStories(
|
|
84
|
+
provider: StoriesProvider | null | undefined,
|
|
85
|
+
): provider is ContractScenesStories {
|
|
86
|
+
return typeof (provider as ContractScenesStories | null | undefined)?.goToScene === 'function';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** What the MOUNT contributes to a switch this projection accepted. */
|
|
90
|
+
export interface ContractScenesSeams {
|
|
91
|
+
/**
|
|
92
|
+
* Called synchronously the moment a switch is ACCEPTED — before the game's
|
|
93
|
+
* own `goTo` settles, because the host's held-mount repaint has to draw
|
|
94
|
+
* frames DURING the transition rather than after it lands
|
|
95
|
+
* (`ingest/held-scene-repaint.ts`). Never called for a refused switch.
|
|
96
|
+
*
|
|
97
|
+
* A seam rather than a wrapper around the returned object: `apply` and
|
|
98
|
+
* `goToScene` are ONE path through this module, and a mount that wrapped only
|
|
99
|
+
* the outer member would leave the picker's own switches unpainted.
|
|
100
|
+
*/
|
|
101
|
+
readonly onSwitchAccepted?: (sceneId: string) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The stories collaborator for a game that declared a usable scene surface, or
|
|
106
|
+
* `null` for one that declared none — the ABSENCE the mount then passes on, so
|
|
107
|
+
* the coverage report names the missing provider instead of the picker showing
|
|
108
|
+
* an empty list.
|
|
109
|
+
*
|
|
110
|
+
* A declared-but-unusable surface is refused loudly (by name, once) rather than
|
|
111
|
+
* bound: `readGameScenes` owns that shape check.
|
|
112
|
+
*/
|
|
113
|
+
export function createContractScenesStories(
|
|
114
|
+
contract: VgaiGameContract | null | undefined,
|
|
115
|
+
seams: ContractScenesSeams = {},
|
|
116
|
+
): ContractScenesStories | null {
|
|
117
|
+
const { scenes, malformed } = readGameScenes(contract);
|
|
118
|
+
if (malformed !== null) {
|
|
119
|
+
editorConsole.warn(`Ignored the game contract's \`scenes\` surface: ${malformed}`, 'ingest');
|
|
120
|
+
}
|
|
121
|
+
if (!scenes) return null;
|
|
122
|
+
|
|
123
|
+
let warned = false;
|
|
124
|
+
const list = (): VgaiGameScene[] => {
|
|
125
|
+
try {
|
|
126
|
+
return assertScenes(scenes.list());
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (!warned) {
|
|
129
|
+
warned = true;
|
|
130
|
+
editorConsole.warn(
|
|
131
|
+
`Ignored invalid game-contract scenes: ${error instanceof Error ? error.message : String(error)}`,
|
|
132
|
+
'ingest',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const goToScene = (sceneId: string): SceneSwitch => {
|
|
140
|
+
const known = list().map((scene) => scene.id);
|
|
141
|
+
if (!known.includes(sceneId)) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
error: `scenes.goTo: "${sceneId}" is not one of this game's scenes (${
|
|
145
|
+
known.length > 0 ? known.join(', ') : 'it lists none'
|
|
146
|
+
}).`,
|
|
147
|
+
known,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
seams.onSwitchAccepted?.(sceneId);
|
|
151
|
+
// The game's own navigation call. It commonly loads the scene's assets
|
|
152
|
+
// first, so a rejection is REPORTED rather than swallowed — and reported as
|
|
153
|
+
// a value, beside the scene the game is actually in, which is read off the
|
|
154
|
+
// game's own model either way.
|
|
155
|
+
const settled = (async (): Promise<SceneSwitchSettled> => {
|
|
156
|
+
try {
|
|
157
|
+
await scenes.goTo(sceneId);
|
|
158
|
+
return { requested: sceneId, current: scenes.current() };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
requested: sceneId,
|
|
162
|
+
current: scenes.current(),
|
|
163
|
+
error: error instanceof Error ? error.message : String(error),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
})();
|
|
167
|
+
return { ok: true, settled };
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
// The game calls these SCENES, so every surface that presents them does
|
|
172
|
+
// too — the projection borrows the stories seam, never its vocabulary.
|
|
173
|
+
title: 'Scenes',
|
|
174
|
+
storiesFor: (_nodeId): StoryRef[] => list().map(({ id, label }) => ({ id, label })),
|
|
175
|
+
active: (_nodeId) => scenes.current(),
|
|
176
|
+
goToScene,
|
|
177
|
+
apply: (_nodeId, storyId) => {
|
|
178
|
+
if (storyId === null) {
|
|
179
|
+
// The picker's "(none)" clears a story back to the document's default.
|
|
180
|
+
// A running game has no such state — it is always in SOME scene — so
|
|
181
|
+
// this refuses and says why rather than pretending to clear one.
|
|
182
|
+
editorConsole.warn(
|
|
183
|
+
'This game’s scenes cannot be cleared: it is always in one of its own scenes. ' +
|
|
184
|
+
'Pick the scene to switch to instead.',
|
|
185
|
+
'ingest',
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// THE SAME SWITCH the `open` verb takes — `apply` differs only in what it
|
|
190
|
+
// does with the answer: nothing waits on it here, because
|
|
191
|
+
// `StoriesProvider.apply` is synchronous and the picker's authoritative
|
|
192
|
+
// reading is `active()`.
|
|
193
|
+
const switched = goToScene(storyId);
|
|
194
|
+
if (!switched.ok) {
|
|
195
|
+
editorConsole.warn(`${switched.error} — ignoring.`, 'ingest');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
void switched.settled.then((result) => {
|
|
199
|
+
if (result.error === undefined) return;
|
|
200
|
+
editorConsole.error(`scenes.goTo("${storyId}") failed: ${result.error}`, 'ingest');
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CREATION-SITE RELATED LINKS — a kit piece of the live-adapter toolbox
|
|
3
|
+
* (owner-directed program, 2026-08-22: everything substrate-neutral is
|
|
4
|
+
* shared, so a new substrate adapter costs only its projection).
|
|
5
|
+
*
|
|
6
|
+
* The ONE jump a live subject honestly has is to the source line that
|
|
7
|
+
* constructs it — the serve-time OID stamp for a fiber world, the `new` site
|
|
8
|
+
* for a structural one. This existed twice, self-admittedly ("the canvas
|
|
9
|
+
* lane's method transcribed"): `PixiAuthoringAdapter.creationSiteLinks` and
|
|
10
|
+
* `ThreeAuthoringAdapter.related`. One producer now; an adapter hands in only
|
|
11
|
+
* its own site resolution.
|
|
12
|
+
*
|
|
13
|
+
* Only a SOURCE anchor names a `file:line` the editor's source-locator door
|
|
14
|
+
* speaks; an unanchored or level-data subject reports no link at all (the
|
|
15
|
+
* inspector's creation-site note already says why, in its own words) — a link
|
|
16
|
+
* titled "unknown" would be a jump to nowhere. Opening reuses
|
|
17
|
+
* `runInstanceSourceAction` verbatim, so no lane can drift into a second
|
|
18
|
+
* sentence for the same act. The locator is a standalone object and NOT a
|
|
19
|
+
* `sourceLocation` method on any adapter, for the reason the Pixi lane
|
|
20
|
+
* records: that name is duck-typed by `instanceSourceLocatorFor` to decide
|
|
21
|
+
* which rows get hierarchy source items.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { NodeCreationSite, RelatedSubjectsProvider } from '@volter/editor-project/adapter';
|
|
25
|
+
import { type InstanceSourceLocator, runInstanceSourceAction } from '@volter/editor-core/instance-source-actions';
|
|
26
|
+
import { showTransientHint } from '@volter/editor-core/transient-hint';
|
|
27
|
+
|
|
28
|
+
/** The related provider for an adapter whose truth can place a subject at a
|
|
29
|
+
* creation site. `resolveSite` is the adapter's own id→site hop. */
|
|
30
|
+
export function creationSiteRelated(
|
|
31
|
+
resolveSite: (id: string) => NodeCreationSite,
|
|
32
|
+
): RelatedSubjectsProvider {
|
|
33
|
+
const locator: InstanceSourceLocator = {
|
|
34
|
+
sourceLocation: (id) => {
|
|
35
|
+
const site = resolveSite(id);
|
|
36
|
+
if (!site.anchored || site.kind !== 'source') return undefined;
|
|
37
|
+
return { file: site.file, line: site.line };
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
links: (id) => {
|
|
42
|
+
const site = resolveSite(id);
|
|
43
|
+
if (!site.anchored || site.kind !== 'source') return [];
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
id: `creation-site:${site.file}:${site.line}:${site.col}`,
|
|
47
|
+
title: `Constructed at ${site.display}`,
|
|
48
|
+
open: () => {
|
|
49
|
+
void runInstanceSourceAction(locator, id, 'callsite').then(showTransientHint);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ephemeral persistence provider — for sessions whose edits must NOT survive past
|
|
3
|
+
* the session (play-mode's adoption of the live game scene; design).
|
|
4
|
+
*
|
|
5
|
+
* `isDirty()` is hard-coded `false`: nothing here is ever "unsaved" because there
|
|
6
|
+
* is nowhere for it to go — a no-op `save()` that pretended otherwise would lie in
|
|
7
|
+
* the save-status UI. Per the contract, the host never calls `save()` when
|
|
8
|
+
* `isDirty()` is false, so `save()` below is a documented no-op, not a silent
|
|
9
|
+
* swallow of a real write.
|
|
10
|
+
*
|
|
11
|
+
* EPHEMERAL IS A PIPE DESTINATION, not a parallel stack: an edit made here
|
|
12
|
+
* resolves `live-only` at {@link EPHEMERAL_DESTINATION} through
|
|
13
|
+
* `write-pipe.ts`, which is why the destination string is owned there beside
|
|
14
|
+
* the other two rather than here.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { PersistenceProvider } from '@volter/editor-project/adapter';
|
|
18
|
+
import { EPHEMERAL_DESTINATION } from '@volter/editor-core/authoring/write-pipe';
|
|
19
|
+
|
|
20
|
+
export function createEphemeralPersistence(): PersistenceProvider {
|
|
21
|
+
return {
|
|
22
|
+
isDirty: () => false,
|
|
23
|
+
save: async () => {
|
|
24
|
+
// Never called by the host (isDirty() is always false — see doc comment
|
|
25
|
+
// above). No-op: there is nothing to write, not a swallowed failure.
|
|
26
|
+
},
|
|
27
|
+
destination: EPHEMERAL_DESTINATION,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PERSIST ONE CHANNEL WRITE — a kit piece of the live-adapter toolbox
|
|
3
|
+
* (ARCHITECTURE-CORE §Roots, the four kit rules).
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the cited twins `ThreeAuthoringAdapter.pipedWrite` and
|
|
6
|
+
* `pixi-live-write-target`'s `persistOrJournal` resolution (whose own header
|
|
7
|
+
* declares the transcription). The core is the ONE decision every live lane
|
|
8
|
+
* must make identically when a gesture closes on a single property: resolve
|
|
9
|
+
* the subject, ask the backend's GATE first — because it names the precise
|
|
10
|
+
* reason (no source stamp, an expression-bound prop, a game that is playing
|
|
11
|
+
* rather than held) — then honor the planned LANE second, as the structural
|
|
12
|
+
* guarantee that a subject classified `live-only` can never reach a dialect
|
|
13
|
+
* writer even if a gate waved it through. The two copies had drifted on
|
|
14
|
+
* exactly this order (the canvas copy checked the lane first), so live-only
|
|
15
|
+
* subjects earned different refusal sentences per lane; the documented order
|
|
16
|
+
* wins and is now the only spelling.
|
|
17
|
+
*
|
|
18
|
+
* The LIVE-ONLY path stays fully SYNCHRONOUS through the pipe: `runWritePipe`
|
|
19
|
+
* awaits nothing before `record` on that arm, so an `endEdit(); undo()`
|
|
20
|
+
* sequence still finds its history entry. A cheap-gate refusal is reported on
|
|
21
|
+
* the same surface as the deep ones, and only while the backend is armed — a
|
|
22
|
+
* refusal on every frame of a game somebody is playing is noise.
|
|
23
|
+
*
|
|
24
|
+
* What stays per-lane, by the kit rules: the gesture-close POLICY (which
|
|
25
|
+
* channels count as changed — the canvas lane's origin-move rule is
|
|
26
|
+
* load-bearing and different), the history journaling itself (`journal` is
|
|
27
|
+
* the lane's own push), and every identity/anchor hop (`subject` is resolved
|
|
28
|
+
* lazily, inside the pipe, by the lane's own planner).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { WriteAck, WriteAnchorKind } from '@volter/editor-project/adapter';
|
|
32
|
+
import type { ChannelValue } from '@volter/editor-core/creation-site-edit';
|
|
33
|
+
import type { SourcePersistenceBackend, SourceWriteSubject } from './source-persistence-backend';
|
|
34
|
+
import { resolvesLiveOnly, runWritePipe, type WriteResolution } from '@volter/editor-core/authoring/write-pipe';
|
|
35
|
+
|
|
36
|
+
/** The one-property-per-gesture refusal, spelled once — it is the persistence
|
|
37
|
+
* pipeline's own rule, not any substrate's voice. */
|
|
38
|
+
export function multiChannelRefusal(changed: readonly string[]): string {
|
|
39
|
+
return (
|
|
40
|
+
`this gesture changed ${changed.length} properties at once (${changed.join(', ')}), and ` +
|
|
41
|
+
'creation-site write-back plans one property per gesture'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ChannelWriteRequest {
|
|
46
|
+
readonly backend: SourcePersistenceBackend;
|
|
47
|
+
/** The lane's own id+property → subject hop, resolved INSIDE the pipe. */
|
|
48
|
+
readonly subject: () => SourceWriteSubject;
|
|
49
|
+
readonly baseline: ChannelValue;
|
|
50
|
+
readonly next: ChannelValue;
|
|
51
|
+
/** History label for the transaction ("Transform Sun"). */
|
|
52
|
+
readonly label: string;
|
|
53
|
+
/** The lane's own live-only journal push (captures its `before` snapshot). */
|
|
54
|
+
readonly journal: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One channel write through the pipe: `resolve → write → record`. The
|
|
58
|
+
* persisted path's own transaction carries BOTH halves (the file and the
|
|
59
|
+
* live value), so the journal fires only when nothing persisted. */
|
|
60
|
+
export function persistChannelWrite(request: ChannelWriteRequest): Promise<WriteAck> {
|
|
61
|
+
const { backend, label } = request;
|
|
62
|
+
return runWritePipe({
|
|
63
|
+
resolve: (): WriteResolution => {
|
|
64
|
+
const subject = request.subject();
|
|
65
|
+
const verdict = backend.gate(subject);
|
|
66
|
+
if (!verdict.ok) return resolvesLiveOnly(verdict.reason);
|
|
67
|
+
const kind: WriteAnchorKind = subject.anchorKind;
|
|
68
|
+
if (kind === 'live-only') return resolvesLiveOnly(backend.describe(subject));
|
|
69
|
+
return {
|
|
70
|
+
reaches: 'writer',
|
|
71
|
+
anchorKind: kind,
|
|
72
|
+
destination: backend.destination(),
|
|
73
|
+
write: () =>
|
|
74
|
+
backend.write({ ...subject, baseline: request.baseline, next: request.next, label }),
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
record: (persisted) => {
|
|
78
|
+
if (!persisted) request.journal();
|
|
79
|
+
},
|
|
80
|
+
report: (reason) => {
|
|
81
|
+
if (backend.armed()) backend.report(label, reason);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|