@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,764 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DomAuthoringAdapter — the editor's {@link AuthoringAdapter} for an
|
|
3
|
+
* INGESTED (unmodified) native-React world.
|
|
4
|
+
*
|
|
5
|
+
* Unlike {@link ReactRootAuthoringAdapter} (the first-party react-WORLD
|
|
6
|
+
* adapter, T6.2 slice 2), an ingested react game's JSX is NEVER `data-oid`-
|
|
7
|
+
* stamped (three independent guards — see the design doc's §0) and is never
|
|
8
|
+
* written back to (the never-modify-game-source rule, `CLAUDE.md`). So this
|
|
9
|
+
* adapter cannot reuse `walkOidTree` (it would see an EMPTY tree — nothing
|
|
10
|
+
* is stamped) and cannot reuse the JSX write-back seam at all. It is
|
|
11
|
+
* structurally independent:
|
|
12
|
+
*
|
|
13
|
+
* - Hierarchy: a derived view over the shared DOM projector
|
|
14
|
+
* (`../projection/dom.ts`) under the structural-path identity. The
|
|
15
|
+
* projector walks every ELEMENT under a live DOM root (snapshot per
|
|
16
|
+
* call — the DOM is live and may re-render, exactly like the OID
|
|
17
|
+
* scheme's "no cached/mirrored state" contract). Identity is
|
|
18
|
+
* `'rdom:' + '/'-joined <tag>[k]` segments — the DOM analog of the
|
|
19
|
+
* Object3D structural-path scheme. The root itself is not a node —
|
|
20
|
+
* its element children are the hierarchy roots.
|
|
21
|
+
* - Labels derive from real DOM only — tag, plus `#id` or the first CSS
|
|
22
|
+
* class when present — never a fabricated/authored name (anti-shim).
|
|
23
|
+
* - Persistence is LIVE-ONLY: edits write the live element's inline `style`
|
|
24
|
+
* DIRECTLY and record the same write into an in-memory
|
|
25
|
+
* {@link DomStyleEdits} document that is never saved anywhere. (Until
|
|
26
|
+
* 2026-08-02 that document was flushed to a per-world JSON sidecar under
|
|
27
|
+
* the project's `.vgai/`; the owner deleted that system outright — no
|
|
28
|
+
* compat read, no replacement.) There is NO `SourceWriteBackend` import
|
|
29
|
+
* anywhere in this file and NO `/__ui-source` traffic of any kind, ever —
|
|
30
|
+
* this is the one hard invariant the design doc's §4 NOTs pin (AC-K7).
|
|
31
|
+
* - `object3D()`/`idForObject3D()` are ABSENT — these entities are DOM, not
|
|
32
|
+
* `Object3D` (no 3D gizmo binding, mirroring `ReactRootAuthoringAdapter`).
|
|
33
|
+
*
|
|
34
|
+
* Inspector reads/writes reuse `ReactRootAuthoringAdapter`'s pure style
|
|
35
|
+
* helpers (`styleProp`/`numericStyleValue`/`cssColorToHex` — exported for
|
|
36
|
+
* this purpose, D-K5) but declare their OWN `STYLE_PROPERTIES` list: this
|
|
37
|
+
* adapter's writes are not routed through the JSX writer's supported-property
|
|
38
|
+
* ceiling, so its list is free to diverge (kept identical today for
|
|
39
|
+
* predictability — nothing stops a later slice from growing it).
|
|
40
|
+
*
|
|
41
|
+
* The edit document is one session-scoped JSON history resource. Exact
|
|
42
|
+
* before/after documents are journaled by HistoryService and expire when this
|
|
43
|
+
* mount ends — which is also when the edits themselves expire.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { numericStyleValue } from '@volter/editor-core/authoring/css-numeric-style';
|
|
47
|
+
import { createEphemeralPersistence } from '../host/authoring/ephemeral-persistence';
|
|
48
|
+
import { LIVE_ONLY_DESTINATION } from '@volter/editor-core/authoring/write-pipe';
|
|
49
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
50
|
+
import { type JournalSubject, JsonHistoryResource } from '../host/history/json-history-resource';
|
|
51
|
+
import { DomProjector, structuralDomIdentity } from '../host/projection/dom';
|
|
52
|
+
import { browserOrInlineResolver, getComputedStyleValue } from '@volter/editor-core/ui-source/inspect';
|
|
53
|
+
import type {
|
|
54
|
+
AssetSubjectProvider,
|
|
55
|
+
AuthoringAdapter,
|
|
56
|
+
AuthoringAssetSubject,
|
|
57
|
+
AuthoringCapabilities,
|
|
58
|
+
AuthoringProvenance,
|
|
59
|
+
BoxEditProvider,
|
|
60
|
+
ColorSampleProvider,
|
|
61
|
+
DOMRectLike,
|
|
62
|
+
EditorNode,
|
|
63
|
+
HierarchyProvider,
|
|
64
|
+
InspectorProvider,
|
|
65
|
+
PersistenceProvider,
|
|
66
|
+
PickProvider,
|
|
67
|
+
PropertyDescriptor,
|
|
68
|
+
RectProvider,
|
|
69
|
+
SelectionProvider,
|
|
70
|
+
} from '@volter/editor-project/adapter';
|
|
71
|
+
import { cssColorToHex, mapBoxEditPatchKey, styleProp } from './react-world-authoring-adapter';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The minimal structural shape this adapter needs from a live DOM element —
|
|
75
|
+
* deliberately NOT `HTMLElement` (same testability reasoning as
|
|
76
|
+
* `OidElementLike` in `react-world-authoring-adapter.ts`: this repo's vitest
|
|
77
|
+
* environment for THIS adapter's own unit test is `node`, no real DOM). A
|
|
78
|
+
* real `Element`/`HTMLElement` satisfies this structurally (`style` is
|
|
79
|
+
* optional here for the same reason it is on `OidElementLike` — a plain
|
|
80
|
+
* `Element` has no `style` getter, only `HTMLElement`/`SVGElement` do, but
|
|
81
|
+
* TS treats an optional field as satisfied by its absence).
|
|
82
|
+
*/
|
|
83
|
+
export interface DomElementLike {
|
|
84
|
+
readonly tagName: string;
|
|
85
|
+
readonly children: ArrayLike<DomElementLike>;
|
|
86
|
+
readonly id: string;
|
|
87
|
+
/** HTML exposes a string; SVG exposes SVGAnimatedString. */
|
|
88
|
+
readonly className: string | { readonly baseVal: string };
|
|
89
|
+
readonly style?: unknown;
|
|
90
|
+
readonly outerHTML?: string;
|
|
91
|
+
/**
|
|
92
|
+
* T0 (spec 27 §2) — the element's live viewport rect, for `rects.rect`'s
|
|
93
|
+
* host-relative geometry below (see `OidElementLike.getBoundingClientRect`'s
|
|
94
|
+
* identical doc comment in `react-world-authoring-adapter.ts`). Optional so
|
|
95
|
+
* existing plain-object test fixtures keep type-checking unchanged — a node
|
|
96
|
+
* with no `getBoundingClientRect` simply has no rect (never a fabricated 0×0).
|
|
97
|
+
*/
|
|
98
|
+
getBoundingClientRect?(): {
|
|
99
|
+
left: number;
|
|
100
|
+
top: number;
|
|
101
|
+
right: number;
|
|
102
|
+
bottom: number;
|
|
103
|
+
width: number;
|
|
104
|
+
height: number;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One structural-path node resolved from a live-DOM walk. */
|
|
109
|
+
interface DomNode {
|
|
110
|
+
/** `'rdom:' + '/'-joined <tag>[k] segments` — see the class doc comment. */
|
|
111
|
+
id: string;
|
|
112
|
+
tag: string;
|
|
113
|
+
el: DomElementLike;
|
|
114
|
+
parentId: string | null;
|
|
115
|
+
childIds: string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface DomTree {
|
|
119
|
+
nodes: Map<string, DomNode>;
|
|
120
|
+
rootIds: string[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** First whitespace-separated CSS class across HTML and SVG DOM elements. */
|
|
124
|
+
function firstClassName(
|
|
125
|
+
className: DomElementLike['className'] | null | undefined,
|
|
126
|
+
): string | undefined {
|
|
127
|
+
const value = typeof className === 'string' ? className : className?.baseVal;
|
|
128
|
+
const trimmed = value?.trim() ?? '';
|
|
129
|
+
if (!trimmed) return undefined;
|
|
130
|
+
return trimmed.split(/\s+/)[0];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Real-DOM-only label (anti-shim): tag, `#id` when present, else `.firstClass` when present. */
|
|
134
|
+
function labelFor(el: DomElementLike, tag: string): string {
|
|
135
|
+
if (el.id) return `${tag}#${el.id}`;
|
|
136
|
+
const cls = firstClassName(el.className ?? '');
|
|
137
|
+
if (cls) return `${tag}.${cls}`;
|
|
138
|
+
return tag;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function viewFromProjection(projector: DomProjector<DomElementLike>): DomTree {
|
|
142
|
+
const nodes = new Map<string, DomNode>();
|
|
143
|
+
for (const [id, node] of projector.nodes) {
|
|
144
|
+
nodes.set(id, {
|
|
145
|
+
id,
|
|
146
|
+
tag: node.object.tagName.toLowerCase(),
|
|
147
|
+
el: node.object,
|
|
148
|
+
parentId: node.parentId,
|
|
149
|
+
childIds: [...node.childIds],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return { nodes, rootIds: [...projector.rootIds] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A reasonably useful, curated inline-style property list — this adapter's
|
|
156
|
+
* OWN list (D-K2: not shared with `ReactRootAuthoringAdapter`'s, though
|
|
157
|
+
* kept identical in shape today — this one is not bounded by a JSX writer's
|
|
158
|
+
* supported-property ceiling). */
|
|
159
|
+
const STYLE_PROPERTIES: ReadonlyArray<{
|
|
160
|
+
prop: string;
|
|
161
|
+
label: string;
|
|
162
|
+
type: PropertyDescriptor['type'];
|
|
163
|
+
options?: string[];
|
|
164
|
+
}> = [
|
|
165
|
+
{ prop: 'color', label: 'Color', type: 'color' },
|
|
166
|
+
{ prop: 'backgroundColor', label: 'Background', type: 'color' },
|
|
167
|
+
{ prop: 'borderColor', label: 'Border Color', type: 'color' },
|
|
168
|
+
{ prop: 'fontSize', label: 'Font Size', type: 'number' },
|
|
169
|
+
{ prop: 'width', label: 'Width', type: 'number' },
|
|
170
|
+
{ prop: 'height', label: 'Height', type: 'number' },
|
|
171
|
+
{ prop: 'padding', label: 'Padding', type: 'number' },
|
|
172
|
+
{ prop: 'margin', label: 'Margin', type: 'number' },
|
|
173
|
+
{ prop: 'borderRadius', label: 'Border Radius', type: 'number' },
|
|
174
|
+
{ prop: 'gap', label: 'Gap', type: 'number' },
|
|
175
|
+
{ prop: 'display', label: 'Display', type: 'enum', options: ['flex', 'block', 'none', 'grid'] },
|
|
176
|
+
{ prop: 'textAlign', label: 'Text Align', type: 'enum', options: ['left', 'center', 'right'] },
|
|
177
|
+
{ prop: 'flexDirection', label: 'Flex Direction', type: 'enum', options: ['row', 'column'] },
|
|
178
|
+
];
|
|
179
|
+
const STYLE_PATH_PREFIX = 'style.';
|
|
180
|
+
|
|
181
|
+
/** `prop -> declared type` — see `ReactRootAuthoringAdapter`'s identical map
|
|
182
|
+
* for why `inspector.get` needs this (a real CSSOM value is always a
|
|
183
|
+
* unit-suffixed string / an `rgb(...)` string, never a bare number/hex). */
|
|
184
|
+
const STYLE_PROPERTY_TYPE: ReadonlyMap<string, PropertyDescriptor['type']> = new Map(
|
|
185
|
+
STYLE_PROPERTIES.map(({ prop, type }) => [prop, type]),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
/** structural id -> cssProp -> value (D-K3). Values are always strings — the
|
|
189
|
+
* same literal an inline style write uses. In-memory for this session only. */
|
|
190
|
+
export type DomStyleEdits = Record<string, Record<string, string>>;
|
|
191
|
+
|
|
192
|
+
export interface DomAuthoringOptions {
|
|
193
|
+
/**
|
|
194
|
+
* The UNDO axis — whose journal this mount's live edits belong to. See the
|
|
195
|
+
* ownership block in `history/json-history-resource.ts`: a held DOM surface's
|
|
196
|
+
* subject is its world, which outlives the mount.
|
|
197
|
+
*/
|
|
198
|
+
readonly journal: JournalSubject;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface DomHistoryState {
|
|
202
|
+
edits: DomStyleEdits;
|
|
203
|
+
styles: Record<string, Record<string, string>>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export class DomAuthoringAdapter implements AuthoringAdapter {
|
|
207
|
+
// THE ABSENT COLUMN, stated so a reader never has to guess whether a missing
|
|
208
|
+
// provider is a decision or an oversight. Facts about THIS ADAPTER CLASS —
|
|
209
|
+
// true for every live DOM it is pointed at, never per-game:
|
|
210
|
+
//
|
|
211
|
+
// - `transforms` — a DOM node has no 3D
|
|
212
|
+
// pose for the gizmo to drive; its spatial seam is `boxEdit`/`rects` below.
|
|
213
|
+
// - `structure` / `text` / `assetDrop` — this adapter's edit document is
|
|
214
|
+
// `DomStyleEdits`: structural id -> cssProp -> value, and nothing else.
|
|
215
|
+
// It has no vocabulary for creating, removing or re-texting an element, so
|
|
216
|
+
// every one of these would write into a live document it cannot record, undo
|
|
217
|
+
// or reconcile — and its ids are structural paths, so an insert or a removal
|
|
218
|
+
// re-indexes the siblings every existing edit is keyed by. A live-insert /
|
|
219
|
+
// live-write seam for a foreign DOM is a HOST work order, not a per-mount
|
|
220
|
+
// shim; until it exists, absence is the honest report.
|
|
221
|
+
// - `stories` — a story is declared in a PROJECT's own portable CSF source
|
|
222
|
+
// ("Association is derived from portable CSF source, never an editor-private
|
|
223
|
+
// label or registry", ARCHITECTURE-CORE §Roots). This adapter projects a
|
|
224
|
+
// game's live DOM, which is not owned source.
|
|
225
|
+
// - `creationSite` — no index is built here. Unlike the four above this is not
|
|
226
|
+
// an impossibility: a same-realm game's elements ARE appended by readable
|
|
227
|
+
// source, so an index is buildable and its absence is a standing host gap.
|
|
228
|
+
readonly capabilities: AuthoringCapabilities = {
|
|
229
|
+
transform: false, // no 3D gizmo — DOM has no Object3D pose
|
|
230
|
+
inspectorFields: true,
|
|
231
|
+
persist: false, // live-only: this game has no source path the editor may write
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
readonly provenance: AuthoringProvenance = {
|
|
235
|
+
source: 'foreign',
|
|
236
|
+
label: 'live-only',
|
|
237
|
+
detail:
|
|
238
|
+
'Unmodified ingested React game — style edits apply for this session only and are never saved.',
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
private readonly doc: DomStyleEdits = {};
|
|
242
|
+
private readonly historyResource: JsonHistoryResource<DomHistoryState> | null;
|
|
243
|
+
/** The shared DOM projector under the structural-path identity. */
|
|
244
|
+
private readonly projector = new DomProjector(structuralDomIdentity<DomElementLike>());
|
|
245
|
+
/** Synchronous-burst cache for `snapshot()` — see that method's doc. */
|
|
246
|
+
private snapCache: DomTree | null = null;
|
|
247
|
+
private snapInvalidationScheduled = false;
|
|
248
|
+
/**
|
|
249
|
+
* T0 (spec 27 §4, B1) — the currently-open `boxEdit` begin/apply×N/end gesture, or
|
|
250
|
+
* `null` between gestures. Mirrors `ReactRootAuthoringAdapter.boxEditSession` (see
|
|
251
|
+
* its doc comment for why `priorInline` exists) — `touched` here maps prop → the
|
|
252
|
+
* FINAL CSS-STRING value to write (this adapter always stores CSS strings,
|
|
253
|
+
* never bare numbers, unlike the react-world source-write route).
|
|
254
|
+
*/
|
|
255
|
+
private boxEditSession: {
|
|
256
|
+
id: string;
|
|
257
|
+
touched: Map<string, string>;
|
|
258
|
+
priorInline: Map<string, string>;
|
|
259
|
+
} | null = null;
|
|
260
|
+
/** D4 (spec27 §6 D4, layer-tree "lock" toggle) — kept out of the edit
|
|
261
|
+
* document entirely: an ingested game's DOM has no "locked" concept (the
|
|
262
|
+
* document only ever records style overrides). See
|
|
263
|
+
* `ReactRootAuthoringAdapter.lockedIds`'s doc comment for the same
|
|
264
|
+
* reasoning, INCLUDING D4.R1 (now read by `pickable.pick` below and by
|
|
265
|
+
* the selection overlay's marquee candidate pool; NOT by `hierarchy`/`selection` —
|
|
266
|
+
* a locked node stays selectable/unlockable from the layer tree). */
|
|
267
|
+
private readonly lockedIds = new Set<string>();
|
|
268
|
+
|
|
269
|
+
constructor(
|
|
270
|
+
private readonly root: DomElementLike,
|
|
271
|
+
private readonly store: EditorShellStore,
|
|
272
|
+
opts: DomAuthoringOptions,
|
|
273
|
+
) {
|
|
274
|
+
const history = store.projectHistory;
|
|
275
|
+
this.historyResource = history
|
|
276
|
+
? new JsonHistoryResource({
|
|
277
|
+
history,
|
|
278
|
+
kind: 'session-state',
|
|
279
|
+
scope: 'session',
|
|
280
|
+
// The SUBJECT's session, not this mount's — see the ownership block
|
|
281
|
+
// in `history/json-history-resource.ts`.
|
|
282
|
+
subject: { ...opts.journal, id: `${opts.journal.id}/react-dom-edits` },
|
|
283
|
+
displayName: 'React DOM edits',
|
|
284
|
+
capture: () => this.captureHistoryState(),
|
|
285
|
+
restore: async (state) => {
|
|
286
|
+
this.restoreHistoryState(state);
|
|
287
|
+
},
|
|
288
|
+
})
|
|
289
|
+
: null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* A live-DOM walk, memoized for the current SYNCHRONOUS burst only.
|
|
294
|
+
*
|
|
295
|
+
* `hierarchy.node(id)` is called once per node by the shell's row flattener
|
|
296
|
+
* (`flattenHierarchyRows`), and a large live react game (react-rpg's dungeon
|
|
297
|
+
* is ~1900 walked rows over a several-thousand-element DOM) turned that into
|
|
298
|
+
* an O(N²) re-walk-per-node — a ~9s synchronous block that hung the editor
|
|
299
|
+
* on selection (the hierarchy re-renders synchronously inside the click's
|
|
300
|
+
* discrete-event flush). JS is single-threaded, so the DOM cannot change
|
|
301
|
+
* mid-render: caching the walk for the current synchronous stack and
|
|
302
|
+
* invalidating it on the next microtask keeps the "the DOM is live, never
|
|
303
|
+
* stale across renders" contract (a subsequent render, after the game may
|
|
304
|
+
* have re-rendered, re-walks) while collapsing one render's N node() calls
|
|
305
|
+
* to ONE walk. Style READS stay live regardless — `inspector.get` reads
|
|
306
|
+
* `n.el.style` off the cached element ref, not a cached value. Structural
|
|
307
|
+
* mutations (none in v1 — this adapter has no `structure` provider) would
|
|
308
|
+
* land between ticks (the game's own re-render), which the microtask
|
|
309
|
+
* invalidation already covers.
|
|
310
|
+
*/
|
|
311
|
+
private snapshot(): DomTree {
|
|
312
|
+
if (this.snapCache) return this.snapCache;
|
|
313
|
+
this.projector.project(this.root);
|
|
314
|
+
const tree = viewFromProjection(this.projector);
|
|
315
|
+
this.snapCache = tree;
|
|
316
|
+
if (!this.snapInvalidationScheduled) {
|
|
317
|
+
this.snapInvalidationScheduled = true;
|
|
318
|
+
queueMicrotask(() => {
|
|
319
|
+
this.snapCache = null;
|
|
320
|
+
this.snapInvalidationScheduled = false;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return tree;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* D-L1 — total ELEMENT nodes currently walked under the
|
|
328
|
+
* root: a fresh snapshot every call (same "the DOM is live and may
|
|
329
|
+
* re-render, never cached" discipline as
|
|
330
|
+
* `hierarchy.roots()`/`hierarchy.node()` above), not a count taken once at
|
|
331
|
+
* construction. Backs `__vgaiIngestReact.domEvidence().hierarchyNodeCount`
|
|
332
|
+
* — one of the four legs `vgai doctor`'s react-ingest MOUNTED bar requires
|
|
333
|
+
* (`clearsReactIngestMountedBar`, `packages/editor/src/doctor/report.ts`).
|
|
334
|
+
*/
|
|
335
|
+
nodeCount(): number {
|
|
336
|
+
return this.snapshot().nodes.size;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private toEditorNode(n: DomNode): EditorNode {
|
|
340
|
+
return {
|
|
341
|
+
id: n.id,
|
|
342
|
+
label: labelFor(n.el, n.tag),
|
|
343
|
+
role: 'element',
|
|
344
|
+
kind: n.tag,
|
|
345
|
+
parentId: n.parentId,
|
|
346
|
+
childIds: n.childIds,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private applyStyleToElement(el: DomElementLike, prop: string, value: string): void {
|
|
351
|
+
if (!el.style) return;
|
|
352
|
+
(el.style as Record<string, unknown>)[prop] = value;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private recordStyleEdit(id: string, prop: string, value: string): void {
|
|
356
|
+
const entry = this.doc[id] ?? {};
|
|
357
|
+
entry[prop] = value;
|
|
358
|
+
this.doc[id] = entry;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
readonly hierarchy: HierarchyProvider = {
|
|
362
|
+
roots: () => {
|
|
363
|
+
const { nodes, rootIds } = this.snapshot();
|
|
364
|
+
return rootIds.map((id) => this.toEditorNode(nodes.get(id)!));
|
|
365
|
+
},
|
|
366
|
+
node: (id) => {
|
|
367
|
+
const n = this.snapshot().nodes.get(id);
|
|
368
|
+
return n ? this.toEditorNode(n) : null;
|
|
369
|
+
},
|
|
370
|
+
// No `object3D`/`idForObject3D`: DOM, not Object3D — the concept does not
|
|
371
|
+
// apply here (no 3D gizmo binding).
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
readonly selection: SelectionProvider = {
|
|
375
|
+
get: () => [...this.store.selectedEntityIds],
|
|
376
|
+
set: (ids) => this.store.selectMultiple(ids),
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* A2 gap close-out (spec 27 §2, spec:237-254/spec:251-254) — GEOMETRIC rect
|
|
381
|
+
* hit-test over the live DOM tree, symmetric with
|
|
382
|
+
* `ReactRootAuthoringAdapter.pickable` (see that provider's doc comment
|
|
383
|
+
* for the full rationale, identical here): NOT `document.elementFromPoint`
|
|
384
|
+
* (which skips a `pointer-events:none` design-time wrapper), smallest-area
|
|
385
|
+
* element containing the point wins (the deepest/most-specific node), ties
|
|
386
|
+
* broken by later tree-walk order (the topmost). Without this, an ingested
|
|
387
|
+
* react world's `rdom:`-id nodes are never `pickable`, so the shell's
|
|
388
|
+
* `pickTopmost` (`layered-pick.ts`) always falls through to `null` for this
|
|
389
|
+
* adapter — click-to-select through the viewport never resolves any node,
|
|
390
|
+
* even though `hierarchy`/`selection` already work from the panel.
|
|
391
|
+
*
|
|
392
|
+
* The winning id is the SAME structural `rdom:<tag>[k]` id
|
|
393
|
+
* `hierarchy`/`selection` already use — a hit routes straight into
|
|
394
|
+
* `composite.selection.set([id])` with no translation.
|
|
395
|
+
*/
|
|
396
|
+
readonly pickable: PickProvider = {
|
|
397
|
+
pick: (clientX, clientY) => {
|
|
398
|
+
const { nodes } = this.snapshot();
|
|
399
|
+
let bestId: string | null = null;
|
|
400
|
+
let bestArea = Number.POSITIVE_INFINITY;
|
|
401
|
+
let bestOrder = -1;
|
|
402
|
+
let order = -1;
|
|
403
|
+
for (const node of nodes.values()) {
|
|
404
|
+
order++;
|
|
405
|
+
// D4.R1 — a locked node is SKIPPED, not returned (falls through to
|
|
406
|
+
// whatever unlocked node is behind/around it) — mirrors
|
|
407
|
+
// `ReactRootAuthoringAdapter.pickable`'s identical fix and the
|
|
408
|
+
// first-party `viewport-raycast.ts` locked-skip.
|
|
409
|
+
if (this.lockedIds.has(node.id)) continue;
|
|
410
|
+
// The host layer is pointer-events:none at rest, so computed style
|
|
411
|
+
// would make every descendant appear non-interactive. Honor only the
|
|
412
|
+
// node's own explicit declaration: a transparent full-size wrapper
|
|
413
|
+
// must not swallow picks intended for a world below it.
|
|
414
|
+
if ((node.el.style as { pointerEvents?: unknown } | undefined)?.pointerEvents === 'none') {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const rect = node.el.getBoundingClientRect?.();
|
|
418
|
+
if (!rect) continue; // no live rect (test fixture, or unmounted) — never a candidate
|
|
419
|
+
if (
|
|
420
|
+
clientX < rect.left ||
|
|
421
|
+
clientX > rect.right ||
|
|
422
|
+
clientY < rect.top ||
|
|
423
|
+
clientY > rect.bottom
|
|
424
|
+
) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const area = rect.width * rect.height;
|
|
428
|
+
// smallest-area (deepest) wins; ties broken by later tree-order (topmost)
|
|
429
|
+
if (area < bestArea || (area === bestArea && order > bestOrder)) {
|
|
430
|
+
bestId = node.id;
|
|
431
|
+
bestArea = area;
|
|
432
|
+
bestOrder = order;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return bestId;
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/** Host rect to subtract for {@link rects}' HOST-RELATIVE geometry — same
|
|
440
|
+
* reasoning as `ReactRootAuthoringAdapter.hostRect` (this adapter's own
|
|
441
|
+
* `this.root` is symmetrically the ingested world's mounted DOM layer). */
|
|
442
|
+
private hostRect(): { left: number; top: number } {
|
|
443
|
+
const r = this.root.getBoundingClientRect?.();
|
|
444
|
+
return { left: r?.left ?? 0, top: r?.top ?? 0 };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private toHostRelative(r: {
|
|
448
|
+
left: number;
|
|
449
|
+
top: number;
|
|
450
|
+
width: number;
|
|
451
|
+
height: number;
|
|
452
|
+
}): DOMRectLike {
|
|
453
|
+
const host = this.hostRect();
|
|
454
|
+
return { x: r.left - host.left, y: r.top - host.top, width: r.width, height: r.height };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* T0 (spec 27 §2) — per-node screen geometry, HOST-RELATIVE (see
|
|
459
|
+
* {@link hostRect}'s doc comment), symmetric with
|
|
460
|
+
* `ReactRootAuthoringAdapter.rects` so the overlay this feeds (Phase A3)
|
|
461
|
+
* treats both react adapters identically.
|
|
462
|
+
*/
|
|
463
|
+
readonly rects: RectProvider = {
|
|
464
|
+
rect: (id) => {
|
|
465
|
+
const n = this.snapshot().nodes.get(id);
|
|
466
|
+
const r = n?.el.getBoundingClientRect?.();
|
|
467
|
+
return r ? this.toHostRelative(r) : null;
|
|
468
|
+
},
|
|
469
|
+
contextRects: (id) => {
|
|
470
|
+
const { nodes } = this.snapshot();
|
|
471
|
+
const n = nodes.get(id);
|
|
472
|
+
if (!n) return {};
|
|
473
|
+
const parentNode = n.parentId ? nodes.get(n.parentId) : undefined;
|
|
474
|
+
const parentRect = parentNode?.el.getBoundingClientRect?.();
|
|
475
|
+
const siblingIds = (parentNode ? parentNode.childIds : this.snapshot().rootIds).filter(
|
|
476
|
+
(sid) => sid !== id,
|
|
477
|
+
);
|
|
478
|
+
const siblings = siblingIds
|
|
479
|
+
.map((sid) => nodes.get(sid)?.el.getBoundingClientRect?.())
|
|
480
|
+
.filter((r): r is NonNullable<typeof r> => r != null)
|
|
481
|
+
.map((r) => this.toHostRelative(r));
|
|
482
|
+
const el = n.el;
|
|
483
|
+
const rect = el.getBoundingClientRect?.();
|
|
484
|
+
let paddingBox: DOMRectLike | undefined;
|
|
485
|
+
if (rect) {
|
|
486
|
+
const bt =
|
|
487
|
+
numericStyleValue(getComputedStyleValue(el, 'borderTopWidth', browserOrInlineResolver)) ??
|
|
488
|
+
0;
|
|
489
|
+
const br =
|
|
490
|
+
numericStyleValue(
|
|
491
|
+
getComputedStyleValue(el, 'borderRightWidth', browserOrInlineResolver),
|
|
492
|
+
) ?? 0;
|
|
493
|
+
const bb =
|
|
494
|
+
numericStyleValue(
|
|
495
|
+
getComputedStyleValue(el, 'borderBottomWidth', browserOrInlineResolver),
|
|
496
|
+
) ?? 0;
|
|
497
|
+
const bl =
|
|
498
|
+
numericStyleValue(
|
|
499
|
+
getComputedStyleValue(el, 'borderLeftWidth', browserOrInlineResolver),
|
|
500
|
+
) ?? 0;
|
|
501
|
+
paddingBox = this.toHostRelative({
|
|
502
|
+
left: rect.left + bl,
|
|
503
|
+
top: rect.top + bt,
|
|
504
|
+
width: Math.max(0, rect.width - bl - br),
|
|
505
|
+
height: Math.max(0, rect.height - bt - bb),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
...(parentRect ? { parent: this.toHostRelative(parentRect) } : {}),
|
|
510
|
+
...(siblings.length ? { siblings } : {}),
|
|
511
|
+
...(paddingBox ? { paddingBox } : {}),
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* D3.d (spec 27 §6) — eyedropper FALLBACK color-sample path, symmetric with
|
|
518
|
+
* `ReactRootAuthoringAdapter.colorSample` (see that provider's doc comment
|
|
519
|
+
* for the full rationale — raw/un-normalized values, hit-element first).
|
|
520
|
+
*/
|
|
521
|
+
readonly colorSample: ColorSampleProvider = {
|
|
522
|
+
backgroundChainAt: (clientX, clientY) => {
|
|
523
|
+
const hitId = this.pickable.pick(clientX, clientY);
|
|
524
|
+
if (!hitId) return null;
|
|
525
|
+
const { nodes } = this.snapshot();
|
|
526
|
+
const chain: string[] = [];
|
|
527
|
+
let cur: DomNode | undefined = nodes.get(hitId);
|
|
528
|
+
while (cur) {
|
|
529
|
+
chain.push(getComputedStyleValue(cur.el, 'backgroundColor', browserOrInlineResolver));
|
|
530
|
+
cur = cur.parentId ? nodes.get(cur.parentId) : undefined;
|
|
531
|
+
}
|
|
532
|
+
return chain;
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* T0 (spec 27 §4, B1) — same interface as `ReactRootAuthoringAdapter.boxEdit`,
|
|
538
|
+
* routed to the live element + this session's in-memory edit document instead of
|
|
539
|
+
* source (spec:305 — "Ingested adapter routes ... instead of source, same
|
|
540
|
+
* interface"). `apply` is LIVE PREVIEW ONLY — it mutates the live element's inline
|
|
541
|
+
* style via `applyStyleToElement` WITHOUT touching `this.doc`. `end` commits
|
|
542
|
+
* every touched prop ONCE through {@link writeStyleEntry} (preserving the
|
|
543
|
+
* restore-BOTH-halves undo invariant, D-K2), composed into exactly ONE undo entry
|
|
544
|
+
* for the whole gesture (acceptance:310) even across multiple touched props.
|
|
545
|
+
*/
|
|
546
|
+
readonly boxEdit: BoxEditProvider = {
|
|
547
|
+
begin: (id) => {
|
|
548
|
+
this.historyResource?.assertCanMutate();
|
|
549
|
+
this.boxEditSession = { id, touched: new Map(), priorInline: new Map() };
|
|
550
|
+
},
|
|
551
|
+
apply: (id, patch) => {
|
|
552
|
+
this.historyResource?.assertCanMutate();
|
|
553
|
+
const session = this.boxEditSession;
|
|
554
|
+
if (!session || session.id !== id) return; // no open gesture for this id
|
|
555
|
+
const n = this.snapshot().nodes.get(id);
|
|
556
|
+
if (!n) return; // unresolved id — no-op (per contract)
|
|
557
|
+
const pos = getComputedStyleValue(n.el, 'position', browserOrInlineResolver);
|
|
558
|
+
const isPositioned = pos === 'absolute' || pos === 'fixed';
|
|
559
|
+
for (const [key, v] of Object.entries(patch)) {
|
|
560
|
+
const mapped = mapBoxEditPatchKey(key, isPositioned);
|
|
561
|
+
if (!mapped) {
|
|
562
|
+
// biome-ignore lint/suspicious/noConsole: a dropped edit must not be silent
|
|
563
|
+
console.warn(
|
|
564
|
+
`[DomAuthoringAdapter] boxEdit: dropping patch key "${key}" for "${id}" — ` +
|
|
565
|
+
(key === 'x' || key === 'y'
|
|
566
|
+
? `node is not absolutely/fixed positioned (computed position: "${pos}"), ` +
|
|
567
|
+
'no left/top to move'
|
|
568
|
+
: 'unrecognized box-edit patch key'),
|
|
569
|
+
);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
// Lazily snapshot the TRUE pre-gesture inline value the first time THIS
|
|
573
|
+
// prop is touched in this gesture — before mutating it (see
|
|
574
|
+
// `boxEditSession`'s doc comment, mirroring the react-world adapter).
|
|
575
|
+
if (!session.priorInline.has(mapped.prop)) {
|
|
576
|
+
const prevRaw = styleProp(n.el.style, mapped.prop);
|
|
577
|
+
session.priorInline.set(mapped.prop, prevRaw == null ? '' : String(prevRaw));
|
|
578
|
+
}
|
|
579
|
+
const cssValue = mapped.cssValue(v);
|
|
580
|
+
this.applyStyleToElement(n.el, mapped.prop, cssValue);
|
|
581
|
+
session.touched.set(mapped.prop, cssValue); // this route always commits CSS strings
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
end: (id) => {
|
|
585
|
+
const session = this.boxEditSession;
|
|
586
|
+
this.boxEditSession = null;
|
|
587
|
+
if (!session || session.id !== id || session.touched.size === 0) return;
|
|
588
|
+
this.commitBoxEdit(id, session.touched, session.priorInline);
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* T0 (spec 27 §4, B1) — commit every prop touched by one `boxEdit` gesture, each
|
|
594
|
+
* through {@link writeStyleEntry} (preserving the restore-BOTH-halves undo
|
|
595
|
+
* invariant per prop), composed into exactly ONE undo entry (acceptance:310) whose
|
|
596
|
+
* inverse runs in REVERSE order and whose redo runs in gesture order.
|
|
597
|
+
*/
|
|
598
|
+
private commitBoxEdit(
|
|
599
|
+
id: string,
|
|
600
|
+
touched: Map<string, string>,
|
|
601
|
+
priorInline: Map<string, string>,
|
|
602
|
+
): void {
|
|
603
|
+
this.historyResource?.assertCanMutate();
|
|
604
|
+
const before = this.captureHistoryState();
|
|
605
|
+
const beforeStyles = before.styles[id] ?? {};
|
|
606
|
+
before.styles[id] = beforeStyles;
|
|
607
|
+
for (const [prop, value] of priorInline) beforeStyles[prop] = value;
|
|
608
|
+
let changed = false;
|
|
609
|
+
for (const [prop, value] of touched) {
|
|
610
|
+
changed = this.writeStyleEntry(id, prop, value) || changed;
|
|
611
|
+
}
|
|
612
|
+
if (!changed) return;
|
|
613
|
+
if (this.historyResource) {
|
|
614
|
+
void this.historyResource
|
|
615
|
+
.record(`Edit ${touched.size} Styles`, before, this.captureHistoryState())
|
|
616
|
+
.catch(() => {});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
readonly inspector: InspectorProvider = {
|
|
621
|
+
properties: (): PropertyDescriptor[] =>
|
|
622
|
+
STYLE_PROPERTIES.map(({ prop, label, type, options }) => ({
|
|
623
|
+
path: `${STYLE_PATH_PREFIX}${prop}`,
|
|
624
|
+
label,
|
|
625
|
+
type,
|
|
626
|
+
...(options ? { options } : {}),
|
|
627
|
+
})),
|
|
628
|
+
get: (id, path) => {
|
|
629
|
+
// D4 (spec27 §6 D4, layer-tree visibility/lock) — see
|
|
630
|
+
// `ReactRootAuthoringAdapter`'s matching branch for the reasoning
|
|
631
|
+
// (`locked` session-local; `visible` sugar for the `visibility` inline
|
|
632
|
+
// style — this adapter writes synchronously, so no echo is needed).
|
|
633
|
+
if (path === 'locked') return this.lockedIds.has(id);
|
|
634
|
+
if (path === 'visible') {
|
|
635
|
+
const n = this.snapshot().nodes.get(id);
|
|
636
|
+
if (!n) return undefined;
|
|
637
|
+
return styleProp(n.el.style, 'visibility') !== 'hidden';
|
|
638
|
+
}
|
|
639
|
+
if (!path.startsWith(STYLE_PATH_PREFIX)) return undefined;
|
|
640
|
+
const prop = path.slice(STYLE_PATH_PREFIX.length);
|
|
641
|
+
const n = this.snapshot().nodes.get(id);
|
|
642
|
+
if (!n) return undefined;
|
|
643
|
+
const raw = styleProp(n.el.style, prop);
|
|
644
|
+
const declaredType = STYLE_PROPERTY_TYPE.get(prop);
|
|
645
|
+
if (declaredType === 'number') return numericStyleValue(raw);
|
|
646
|
+
if (declaredType === 'color') return cssColorToHex(raw);
|
|
647
|
+
return raw;
|
|
648
|
+
},
|
|
649
|
+
set: (id, path, value) => {
|
|
650
|
+
if (path === 'locked') {
|
|
651
|
+
if (value) this.lockedIds.add(id);
|
|
652
|
+
else this.lockedIds.delete(id);
|
|
653
|
+
this.store.notifyIngestEdit();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (path === 'visible') {
|
|
657
|
+
this.writeStyle(id, 'visibility', value ? 'visible' : 'hidden');
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (!path.startsWith(STYLE_PATH_PREFIX)) return;
|
|
661
|
+
const prop = path.slice(STYLE_PATH_PREFIX.length);
|
|
662
|
+
this.writeStyle(id, prop, String(value));
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
readonly assetSubject: AssetSubjectProvider = {
|
|
667
|
+
get: (id) => this.assetSubjectForNode(this.snapshot(), id),
|
|
668
|
+
entries: () => {
|
|
669
|
+
const tree = this.snapshot();
|
|
670
|
+
return [...tree.nodes.keys()].flatMap((id) => {
|
|
671
|
+
const subject = this.assetSubjectForNode(tree, id);
|
|
672
|
+
return subject ? [{ id, subject }] : [];
|
|
673
|
+
});
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
private assetSubjectForNode(tree: DomTree, id: string): AuthoringAssetSubject | null {
|
|
678
|
+
const node = tree.nodes.get(id);
|
|
679
|
+
if (!node || node.tag !== 'svg' || !node.el.outerHTML) return null;
|
|
680
|
+
return {
|
|
681
|
+
kind: 'image',
|
|
682
|
+
name: labelFor(node.el, node.tag),
|
|
683
|
+
mediaType: 'image/svg+xml',
|
|
684
|
+
text: node.el.outerHTML,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* T0 (spec 27 §4, B1) — the write body extracted from {@link writeStyle}, returning
|
|
690
|
+
* whether the edit document changed so a multi-property box gesture can record once.
|
|
691
|
+
* `priorInlineOverride`, when given, is used as the captured PRE-gesture inline value
|
|
692
|
+
* instead of re-reading `n.el.style` — needed because `boxEdit.apply` already mutated
|
|
693
|
+
* the live inline style for preview before `end` gets here (see `boxEditSession`'s
|
|
694
|
+
* doc comment). D-K2's restore-BOTH-halves invariant (inline AND document) is
|
|
695
|
+
* unchanged — only WHERE `prevInline` comes from differs when an override is given.
|
|
696
|
+
*/
|
|
697
|
+
private writeStyleEntry(id: string, prop: string, value: string): boolean {
|
|
698
|
+
const n = this.snapshot().nodes.get(id);
|
|
699
|
+
if (!n) return false;
|
|
700
|
+
|
|
701
|
+
this.applyStyleToElement(n.el, prop, value);
|
|
702
|
+
this.recordStyleEdit(id, prop, value);
|
|
703
|
+
this.store.notifyIngestEdit();
|
|
704
|
+
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* D-K2: mutate the live element's inline style AND the in-memory edit
|
|
710
|
+
* document, then journal the complete state as one session transaction.
|
|
711
|
+
*/
|
|
712
|
+
private writeStyle(id: string, prop: string, value: string): void {
|
|
713
|
+
this.historyResource?.assertCanMutate();
|
|
714
|
+
const before = this.captureHistoryState();
|
|
715
|
+
if (!this.writeStyleEntry(id, prop, value)) return;
|
|
716
|
+
if (this.historyResource) {
|
|
717
|
+
void this.historyResource
|
|
718
|
+
.record(`Set ${prop}`, before, this.captureHistoryState())
|
|
719
|
+
.catch(() => {});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
dispose(): void {
|
|
724
|
+
this.historyResource?.dispose();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private captureHistoryState(): DomHistoryState {
|
|
728
|
+
const styles: DomHistoryState['styles'] = {};
|
|
729
|
+
for (const [id, node] of this.snapshot().nodes) {
|
|
730
|
+
const values: Record<string, string> = {};
|
|
731
|
+
for (const { prop } of STYLE_PROPERTIES) {
|
|
732
|
+
const value = styleProp(node.el.style, prop);
|
|
733
|
+
if (value !== '') values[prop] = String(value);
|
|
734
|
+
}
|
|
735
|
+
styles[id] = values;
|
|
736
|
+
}
|
|
737
|
+
return { edits: structuredClone(this.doc), styles };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private restoreHistoryState(state: DomHistoryState): void {
|
|
741
|
+
for (const key of Object.keys(this.doc)) delete this.doc[key];
|
|
742
|
+
Object.assign(this.doc, structuredClone(state.edits));
|
|
743
|
+
const { nodes } = this.snapshot();
|
|
744
|
+
for (const [id, node] of nodes) {
|
|
745
|
+
const values = state.styles[id] ?? {};
|
|
746
|
+
for (const { prop } of STYLE_PROPERTIES) {
|
|
747
|
+
this.applyStyleToElement(node.el, prop, values[prop] ?? '');
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
this.store.notifyIngestEdit();
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
subscribe(listener: () => void): () => void {
|
|
754
|
+
return this.store.subscribe(listener);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Live-only: an unmodified React game has no source path this editor may
|
|
759
|
+
* write, so this reports the ephemeral provider honestly rather than a file.
|
|
760
|
+
*/
|
|
761
|
+
get persistence(): PersistenceProvider {
|
|
762
|
+
return { ...createEphemeralPersistence(), destination: LIVE_ONLY_DESTINATION };
|
|
763
|
+
}
|
|
764
|
+
}
|