@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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PROJECTOR FOR THE DOM SURFACE — one walk, one index, one set of derived
|
|
3
|
+
* views, for every authoring adapter that drives a live element tree
|
|
4
|
+
* (ARCHITECTURE-CORE §The editor protocol, projection family).
|
|
5
|
+
*
|
|
6
|
+
* The projection unit is the RUNTIME ELEMENT. Nothing here reads source, an AST
|
|
7
|
+
* or a document; a walk answers from the tree that is actually mounted, so an
|
|
8
|
+
* empty projection over a mounted world is a defect rather than a fact.
|
|
9
|
+
*
|
|
10
|
+
* ## What this owns, and what it deliberately does not
|
|
11
|
+
*
|
|
12
|
+
* OWNS — the two things both DOM authoring classes used to spell separately:
|
|
13
|
+
*
|
|
14
|
+
* - the walk itself ({@link walkDomGraph}), including the "root is not a node"
|
|
15
|
+
* rule and the fold-into-the-nearest-admitted-ancestor rule,
|
|
16
|
+
* - IDENTITY MINTING, parameterized by scheme ({@link structuralDomIdentity},
|
|
17
|
+
* {@link oidDomIdentity}) — the id FORMATS are the schemes', the walk that
|
|
18
|
+
* applies them is one.
|
|
19
|
+
*
|
|
20
|
+
* DOES NOT own the LANE-SPECIFIC hierarchy VIEW built on top. The first-party
|
|
21
|
+
* React world adapter overlays portable-CSF / catalog rows on the OID
|
|
22
|
+
* projection; the ingested React adapter does not. Both read this projection
|
|
23
|
+
* and derive their rows from it.
|
|
24
|
+
*
|
|
25
|
+
* PICKING is not here either. DOM hit-testing is a geometric rect walk over
|
|
26
|
+
* `getBoundingClientRect` (and must honor design-time `pointer-events`), which
|
|
27
|
+
* is a derived view the adapters already share by transcription, not a
|
|
28
|
+
* raycast the projector can own the way {@link ThreeProjector.pick} does.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { ProjectedNode, Projection } from '@volter/editor-core/projection/types';
|
|
32
|
+
|
|
33
|
+
export type { ProjectedNode, Projection } from '@volter/editor-core/projection/types';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The minimum shape a live DOM walk needs. Deliberately not `Element` so a
|
|
37
|
+
* headless fixture (plain object, no jsdom) can project the same way a real
|
|
38
|
+
* element does. A real `Element` satisfies this structurally.
|
|
39
|
+
*/
|
|
40
|
+
export interface DomWalkable {
|
|
41
|
+
readonly tagName: string;
|
|
42
|
+
readonly children: ArrayLike<DomWalkable>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Extra surface a scheme needs when identity is an authored attribute. */
|
|
46
|
+
export interface OidWalkable extends DomWalkable {
|
|
47
|
+
getAttribute(name: string): string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One projected authoring node over a live DOM tree. */
|
|
51
|
+
export type DomNode<TEl extends DomWalkable = DomWalkable> = ProjectedNode<TEl>;
|
|
52
|
+
|
|
53
|
+
export interface DomWalkStats {
|
|
54
|
+
readonly count: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type DomProjection<TEl extends DomWalkable = DomWalkable> = Projection<TEl, DomWalkStats>;
|
|
58
|
+
|
|
59
|
+
export const EMPTY_DOM_WALK_STATS: DomWalkStats = { count: 0 };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The IDENTITY seam — how a live element tree is addressed: which elements
|
|
63
|
+
* are authoring nodes, what their ids are, and how they nest.
|
|
64
|
+
*
|
|
65
|
+
* An adapter never mints an id itself. It asks a scheme for a
|
|
66
|
+
* {@link DomProjection} and drives every provider (hierarchy, selection,
|
|
67
|
+
* inspector, picking) off that one walk.
|
|
68
|
+
*/
|
|
69
|
+
export interface DomIdentity<TEl extends DomWalkable = DomWalkable> {
|
|
70
|
+
project(root: TEl): DomProjection<TEl>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Where the walk currently is, for a scheme that mints from position. */
|
|
74
|
+
export interface DomWalkContext<TEl extends DomWalkable = DomWalkable> {
|
|
75
|
+
/** The nearest ADMITTED ancestor's id — `null` ⇒ this element hangs under
|
|
76
|
+
* transparent wrappers only, so a node admitted here is a projection root. */
|
|
77
|
+
readonly ownerId: string | null;
|
|
78
|
+
/** Whether anything above already claims this subtree as its parts. */
|
|
79
|
+
readonly ownedByAncestor: boolean;
|
|
80
|
+
/** The element whose `.children` this visit came from — the walk root on
|
|
81
|
+
* the first level, an already-visited descendant after that. */
|
|
82
|
+
readonly parent: TEl;
|
|
83
|
+
/** Index in `siblings`. */
|
|
84
|
+
readonly siblingIndex: number;
|
|
85
|
+
/** The parent's element children, in tree order. */
|
|
86
|
+
readonly siblings: readonly TEl[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The per-lane half of a walk: what to skip, what to admit, what to call it. */
|
|
90
|
+
export interface DomIdentityScheme<TEl extends DomWalkable = DomWalkable> {
|
|
91
|
+
begin?(): void;
|
|
92
|
+
/** This element never enters the projection, WITH its descendants. */
|
|
93
|
+
skip?(el: TEl): boolean;
|
|
94
|
+
/** This element's id, or `null` when it is not an authoring node — in which
|
|
95
|
+
* case its children fold into the nearest admitted ancestor. */
|
|
96
|
+
identify(el: TEl, context: DomWalkContext<TEl>): string | null;
|
|
97
|
+
/** Visit this element's children? Defaults to yes. `false` is how an inline
|
|
98
|
+
* SVG stays one atomic node instead of exposing its `g`/`path` internals. */
|
|
99
|
+
descend?(el: TEl): boolean;
|
|
100
|
+
leave?(el: TEl, id: string | null): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const OID_ATTR = 'data-oid';
|
|
104
|
+
|
|
105
|
+
function tally<TEl extends DomWalkable>(nodes: ReadonlyMap<string, DomNode<TEl>>): DomWalkStats {
|
|
106
|
+
return { count: nodes.size };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isSvg(el: DomWalkable): boolean {
|
|
110
|
+
return el.tagName.toLowerCase() === 'svg';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* THE WALK. One DFS over a live DOM root's ELEMENT children. The root itself
|
|
115
|
+
* is never a node — its element children are the projection roots — matching
|
|
116
|
+
* both DOM adapters' pre-extraction contract.
|
|
117
|
+
*
|
|
118
|
+
* Idempotent for every scheme that mints from the tree's own facts: re-walking
|
|
119
|
+
* the same tree re-derives the same ids for the same elements.
|
|
120
|
+
*/
|
|
121
|
+
export function walkDomGraph<TEl extends DomWalkable>(
|
|
122
|
+
root: TEl,
|
|
123
|
+
scheme: DomIdentityScheme<TEl>,
|
|
124
|
+
): DomProjection<TEl> {
|
|
125
|
+
const nodes = new Map<string, DomNode<TEl>>();
|
|
126
|
+
const rootIds: string[] = [];
|
|
127
|
+
scheme.begin?.();
|
|
128
|
+
|
|
129
|
+
const visitChildren = (parent: TEl, ownerId: string | null, ownedByAncestor: boolean): void => {
|
|
130
|
+
const siblings = Array.from(parent.children) as TEl[];
|
|
131
|
+
siblings.forEach((el, siblingIndex) => {
|
|
132
|
+
if (scheme.skip?.(el)) return;
|
|
133
|
+
const id = scheme.identify(el, {
|
|
134
|
+
ownerId,
|
|
135
|
+
ownedByAncestor,
|
|
136
|
+
parent,
|
|
137
|
+
siblingIndex,
|
|
138
|
+
siblings,
|
|
139
|
+
});
|
|
140
|
+
if (id !== null) {
|
|
141
|
+
nodes.set(id, { object: el, id, parentId: ownerId, childIds: [] });
|
|
142
|
+
if (ownerId === null) rootIds.push(id);
|
|
143
|
+
else nodes.get(ownerId)?.childIds.push(id);
|
|
144
|
+
}
|
|
145
|
+
const nextOwner = id ?? ownerId;
|
|
146
|
+
const nextOwned = ownedByAncestor || id !== null;
|
|
147
|
+
if (scheme.descend?.(el) !== false) visitChildren(el, nextOwner, nextOwned);
|
|
148
|
+
scheme.leave?.(el, id);
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
visitChildren(root, null, false);
|
|
153
|
+
return { nodes, rootIds, stats: tally(nodes) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The STRUCTURAL-PATH scheme. Deterministic from the tree's shape: each
|
|
158
|
+
* segment is `<tag>[k]` where `k` is the element's index among preceding
|
|
159
|
+
* SAME-TAG element siblings. Prefixed `rdom:`. This is the scheme for a
|
|
160
|
+
* graph whose source was never stamped.
|
|
161
|
+
*/
|
|
162
|
+
export function structuralDomIdentity<TEl extends DomWalkable = DomWalkable>(): DomIdentity<TEl> {
|
|
163
|
+
return {
|
|
164
|
+
project: (root) => {
|
|
165
|
+
const pathById = new Map<string, string>();
|
|
166
|
+
return walkDomGraph(root, {
|
|
167
|
+
begin: () => pathById.clear(),
|
|
168
|
+
identify: (el, context) => {
|
|
169
|
+
const tag = el.tagName.toLowerCase();
|
|
170
|
+
let k = 0;
|
|
171
|
+
for (let i = 0; i < context.siblingIndex; i++) {
|
|
172
|
+
if (context.siblings[i]!.tagName.toLowerCase() === tag) k++;
|
|
173
|
+
}
|
|
174
|
+
const segment = `${tag}[${k}]`;
|
|
175
|
+
const parentPath = context.ownerId ? (pathById.get(context.ownerId) ?? '') : '';
|
|
176
|
+
const path = parentPath ? `${parentPath}/${segment}` : segment;
|
|
177
|
+
const id = `rdom:${path}`;
|
|
178
|
+
pathById.set(id, path);
|
|
179
|
+
return id;
|
|
180
|
+
},
|
|
181
|
+
descend: (el) => !isSvg(el),
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The OID scheme. A `data-oid`-stamped element is a node; an unstamped wrapper
|
|
189
|
+
* is transparent and its children fold into the nearest stamped ancestor.
|
|
190
|
+
* Repeats of the same oid (one JSX line rendered N times) keep the bare oid
|
|
191
|
+
* on the first occurrence and suffix `#n` after that.
|
|
192
|
+
*/
|
|
193
|
+
export function oidDomIdentity<TEl extends OidWalkable = OidWalkable>(): DomIdentity<TEl> {
|
|
194
|
+
return {
|
|
195
|
+
project: (root) => {
|
|
196
|
+
const occurrence = new Map<string, number>();
|
|
197
|
+
return walkDomGraph(root, {
|
|
198
|
+
begin: () => occurrence.clear(),
|
|
199
|
+
identify: (el) => {
|
|
200
|
+
const oid = el.getAttribute(OID_ATTR);
|
|
201
|
+
if (!oid) return null;
|
|
202
|
+
const n = occurrence.get(oid) ?? 0;
|
|
203
|
+
occurrence.set(oid, n + 1);
|
|
204
|
+
return n === 0 ? oid : `${oid}#${n}`;
|
|
205
|
+
},
|
|
206
|
+
descend: (el) => !isSvg(el),
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Walk a live DOM root under the structural-path scheme. */
|
|
213
|
+
export function projectStructuralDom<TEl extends DomWalkable>(root: TEl): DomProjection<TEl> {
|
|
214
|
+
return structuralDomIdentity<TEl>().project(root);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Walk a live DOM root under the OID scheme. */
|
|
218
|
+
export function projectOidDom<TEl extends OidWalkable>(root: TEl): DomProjection<TEl> {
|
|
219
|
+
return oidDomIdentity<TEl>().project(root);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The projector: one identity scheme, the index its walk produces, and the
|
|
224
|
+
* derived views every DOM authoring adapter reads instead of re-walking.
|
|
225
|
+
*/
|
|
226
|
+
export class DomProjector<TEl extends DomWalkable = DomWalkable> {
|
|
227
|
+
private nodesById = new Map<string, DomNode<TEl>>();
|
|
228
|
+
private objectsById = new Map<string, TEl>();
|
|
229
|
+
private idsByObjectIndex = new Map<TEl, string>();
|
|
230
|
+
private roots: string[] = [];
|
|
231
|
+
private walkStats: DomWalkStats = EMPTY_DOM_WALK_STATS;
|
|
232
|
+
|
|
233
|
+
constructor(private readonly identity: DomIdentity<TEl>) {}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* (Re)walk. `null` is an honest "nothing is mounted" — the projection empties
|
|
237
|
+
* rather than keeping a stale index, because a hierarchy over a torn-down
|
|
238
|
+
* world is worse than no hierarchy.
|
|
239
|
+
*/
|
|
240
|
+
project(root: TEl | null): DomWalkStats {
|
|
241
|
+
if (!root) {
|
|
242
|
+
this.nodesById = new Map();
|
|
243
|
+
this.objectsById = new Map();
|
|
244
|
+
this.idsByObjectIndex = new Map();
|
|
245
|
+
this.roots = [];
|
|
246
|
+
this.walkStats = EMPTY_DOM_WALK_STATS;
|
|
247
|
+
return this.walkStats;
|
|
248
|
+
}
|
|
249
|
+
const { nodes, rootIds, stats } = this.identity.project(root);
|
|
250
|
+
this.nodesById = nodes;
|
|
251
|
+
this.objectsById = new Map([...nodes].map(([id, node]) => [id, node.object] as const));
|
|
252
|
+
this.idsByObjectIndex = new Map([...nodes].map(([id, node]) => [node.object, id] as const));
|
|
253
|
+
this.roots = rootIds;
|
|
254
|
+
this.walkStats = stats;
|
|
255
|
+
return stats;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
get nodes(): ReadonlyMap<string, DomNode<TEl>> {
|
|
259
|
+
return this.nodesById;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
get objects(): ReadonlyMap<string, TEl> {
|
|
263
|
+
return this.objectsById;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
get idsByObject(): ReadonlyMap<TEl, string> {
|
|
267
|
+
return this.idsByObjectIndex;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
get rootIds(): readonly string[] {
|
|
271
|
+
return this.roots;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
get stats(): DomWalkStats {
|
|
275
|
+
return this.walkStats;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
node(id: string): DomNode<TEl> | null {
|
|
279
|
+
return this.nodesById.get(id) ?? null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
rootNodes(): DomNode<TEl>[] {
|
|
283
|
+
return this.roots
|
|
284
|
+
.map((id) => this.nodesById.get(id))
|
|
285
|
+
.filter((node): node is DomNode<TEl> => node !== undefined);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
objectOf(id: string): TEl | null {
|
|
289
|
+
return this.objectsById.get(id) ?? null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
idOf(el: TEl): string | null {
|
|
293
|
+
return this.idsByObjectIndex.get(el) ?? null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PROJECTOR FOR THE CANVAS SURFACE — one live display-tree walk, one
|
|
3
|
+
* identity index, and the picking/bounds views derived from it.
|
|
4
|
+
*
|
|
5
|
+
* `AuthoringAdapter2D` remains the Pixi truth backend: it applies transforms,
|
|
6
|
+
* reflected properties, and session overlays. This class owns the PROJECTION
|
|
7
|
+
* currency that every canvas authoring view reads. The split is deliberate:
|
|
8
|
+
* the projector answers what exists and where it is; a write target answers
|
|
9
|
+
* where an edit lands.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { AuthoringAdapter2D, EditorNode2D } from '@volter/game-runtime/pixi/authoring';
|
|
13
|
+
import type { DOMRectLike } from '@volter/editor-project/adapter';
|
|
14
|
+
import type { Container, PointData } from 'pixi.js';
|
|
15
|
+
import type { ProjectedNode, Projection } from '@volter/editor-core/projection/types';
|
|
16
|
+
|
|
17
|
+
export type PixiNode = ProjectedNode<Container>;
|
|
18
|
+
|
|
19
|
+
export interface PixiWalkStats {
|
|
20
|
+
readonly count: number;
|
|
21
|
+
readonly sprites: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type PixiProjection = Projection<Container, PixiWalkStats>;
|
|
25
|
+
|
|
26
|
+
export const EMPTY_PIXI_WALK_STATS: PixiWalkStats = { count: 0, sprites: 0 };
|
|
27
|
+
|
|
28
|
+
export interface PixiSurfaceMapping {
|
|
29
|
+
readonly surface: () => HTMLElement | null;
|
|
30
|
+
readonly pointFromClient?: (clientX: number, clientY: number, rect: DOMRect) => PointData | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Project a live Pixi stage through the identity scheme held by
|
|
35
|
+
* {@link AuthoringAdapter2D}. The backend is injected so projection and truth
|
|
36
|
+
* share the exact same object index; neither side can mint a second id.
|
|
37
|
+
*/
|
|
38
|
+
export class PixiProjector {
|
|
39
|
+
private current: PixiProjection = {
|
|
40
|
+
nodes: new Map(),
|
|
41
|
+
rootIds: [],
|
|
42
|
+
stats: EMPTY_PIXI_WALK_STATS,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** True while {@link current} may be reused instead of re-walked. Closed at
|
|
46
|
+
* the next microtask checkpoint by {@link scheduleWindowClose}. */
|
|
47
|
+
private projectionFresh = false;
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
private readonly root: Container,
|
|
51
|
+
private readonly index: AuthoringAdapter2D,
|
|
52
|
+
private readonly mapping?: PixiSurfaceMapping,
|
|
53
|
+
/** Injected by the unit test that drives the coalescing window by hand;
|
|
54
|
+
* the editor always uses the microtask checkpoint. */
|
|
55
|
+
private readonly scheduleWindowClose: (close: () => void) => void = (close) =>
|
|
56
|
+
queueMicrotask(close),
|
|
57
|
+
) {
|
|
58
|
+
this.refresh();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Rewalk identity and reapply the truth backend's saved overlay. */
|
|
62
|
+
refresh(): PixiWalkStats {
|
|
63
|
+
const stats = this.index.refresh();
|
|
64
|
+
this.current = this.readProjection(stats);
|
|
65
|
+
this.openProjectionWindow();
|
|
66
|
+
return stats;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Rewalk identity only, then publish one immutable projection snapshot —
|
|
71
|
+
* AT MOST ONCE PER SYNCHRONOUS TURN.
|
|
72
|
+
*
|
|
73
|
+
* The read is still the refresh point: a world that commits nodes after the
|
|
74
|
+
* adapter was handed its root is still picked up on the next read, because
|
|
75
|
+
* the window this coalesces over closes at the next microtask checkpoint.
|
|
76
|
+
* What it removes is the RE-walk inside one derivation — measured
|
|
77
|
+
* 2026-08-20 on a 1822-node canvas world, one projection is 70ms and a
|
|
78
|
+
* single coverage/status pass asked for eight of them, because every
|
|
79
|
+
* `hierarchy.roots()`, every `pickable.candidates()` and every composite
|
|
80
|
+
* group-node read projects again.
|
|
81
|
+
*
|
|
82
|
+
* The window is only sound while nothing moves the display list behind it.
|
|
83
|
+
* Two kinds of mover exist and BOTH have a door: this adapter's own write
|
|
84
|
+
* paths call {@link reproject}, and the GAME — a world that commits its own
|
|
85
|
+
* nodes when an atlas or a resource load resolves — is caught by the owner's
|
|
86
|
+
* `childAdded`/`childRemoved` watcher, which calls {@link invalidate} the
|
|
87
|
+
* synchronous instant pixi fires. Without that second door a read landing in
|
|
88
|
+
* the same turn as the game's own commit answers from the stale walk, which
|
|
89
|
+
* is exactly the late-commit defect the watcher exists to prevent.
|
|
90
|
+
*/
|
|
91
|
+
project(): PixiProjection {
|
|
92
|
+
if (this.projectionFresh) return this.current;
|
|
93
|
+
return this.reproject();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Project unconditionally — the write paths' door, for a caller that has
|
|
97
|
+
* just mutated the display list and must not read its own stale window. */
|
|
98
|
+
reproject(): PixiProjection {
|
|
99
|
+
const stats = this.index.reindex();
|
|
100
|
+
this.current = this.readProjection(stats);
|
|
101
|
+
this.openProjectionWindow();
|
|
102
|
+
return this.current;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Close the coalescing window WITHOUT walking — the door for a mutation this
|
|
107
|
+
* projector cannot see coming (the game's own commit, reported by its
|
|
108
|
+
* owner's display-list watcher). The next read re-walks; a burst of commits
|
|
109
|
+
* with no read between them still costs one walk, so the measured win of the
|
|
110
|
+
* window survives intact.
|
|
111
|
+
*/
|
|
112
|
+
invalidate(): void {
|
|
113
|
+
this.projectionFresh = false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Hold this projection for the rest of the current synchronous turn. */
|
|
117
|
+
private openProjectionWindow(): void {
|
|
118
|
+
if (this.projectionFresh) return;
|
|
119
|
+
this.projectionFresh = true;
|
|
120
|
+
this.scheduleWindowClose(() => {
|
|
121
|
+
this.projectionFresh = false;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
projection(): PixiProjection {
|
|
126
|
+
return this.current;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The live container for `id`, or null.
|
|
131
|
+
*
|
|
132
|
+
* A DESTROYED container is not a live object. A survivor-style game destroys
|
|
133
|
+
* entities every frame and this map is only refreshed by the next walk, so
|
|
134
|
+
* between a destroy and that walk every consumer here would be handed a
|
|
135
|
+
* husk whose `children`, `effects`, `pivot` and `anchor` are nulled — and
|
|
136
|
+
* Pixi's own recursive bounds walk plus the editor's origin read both crash
|
|
137
|
+
* on it mid-React-render (`Cannot read properties of null` reading 'length'
|
|
138
|
+
* then 'x'; runhuman passes 115 and 119, two call sites of the same husk).
|
|
139
|
+
* Answering null here is what stops the next reader from being the third.
|
|
140
|
+
*/
|
|
141
|
+
object(id: string): Container | null {
|
|
142
|
+
const object = this.current.nodes.get(id)?.object ?? null;
|
|
143
|
+
return object === null || object.destroyed ? null : object;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
node(id: string): EditorNode2D | null {
|
|
147
|
+
const projected = this.current.nodes.get(id);
|
|
148
|
+
if (!projected) return null;
|
|
149
|
+
const native = this.index.node(id);
|
|
150
|
+
if (!native) return null;
|
|
151
|
+
return {
|
|
152
|
+
...native,
|
|
153
|
+
parentId: projected.parentId,
|
|
154
|
+
childIds: [...projected.childIds],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
roots(): EditorNode2D[] {
|
|
159
|
+
return this.current.rootIds
|
|
160
|
+
.map((id) => this.node(id))
|
|
161
|
+
.filter((node): node is EditorNode2D => node !== null);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pick(clientX: number, clientY: number, excluded: (id: string) => boolean): string | null {
|
|
165
|
+
return this.candidates(clientX, clientY, excluded)[0] ?? null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
candidates(clientX: number, clientY: number, excluded: (id: string) => boolean): string[] {
|
|
169
|
+
const point = this.pointFromClient(clientX, clientY);
|
|
170
|
+
if (!point) return [];
|
|
171
|
+
this.project();
|
|
172
|
+
return this.hitIds(point, excluded);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private pointFromClient(clientX: number, clientY: number): PointData | null {
|
|
176
|
+
const surface = this.mapping?.surface() ?? null;
|
|
177
|
+
if (!surface) return null;
|
|
178
|
+
const rect = surface.getBoundingClientRect();
|
|
179
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
180
|
+
const mapped = this.mapping?.pointFromClient?.(clientX, clientY, rect) ?? null;
|
|
181
|
+
if (this.mapping?.pointFromClient) return mapped;
|
|
182
|
+
const logicalWidth = Number(surface.dataset?.['vgaiStageWidth'] ?? rect.width);
|
|
183
|
+
const logicalHeight = Number(surface.dataset?.['vgaiStageHeight'] ?? rect.height);
|
|
184
|
+
if (!(logicalWidth > 0) || !(logicalHeight > 0)) return null;
|
|
185
|
+
return {
|
|
186
|
+
x: (clientX - rect.left) * (logicalWidth / rect.width),
|
|
187
|
+
y: (clientY - rect.top) * (logicalHeight / rect.height),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private hitIds(point: PointData, excluded: (id: string) => boolean): string[] {
|
|
192
|
+
const visual: Container[] = [];
|
|
193
|
+
const hitAreas: Container[] = [];
|
|
194
|
+
this.collectHits(this.root, point, 'visual', visual);
|
|
195
|
+
this.collectHits(this.root, point, 'hitArea', hitAreas);
|
|
196
|
+
|
|
197
|
+
const ids: string[] = [];
|
|
198
|
+
const seen = new Set<string>();
|
|
199
|
+
for (const object of [...visual, ...hitAreas]) {
|
|
200
|
+
const id = (object as Container & { __authId?: string }).__authId;
|
|
201
|
+
if (id && !excluded(id) && !seen.has(id)) {
|
|
202
|
+
seen.add(id);
|
|
203
|
+
ids.push(id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return ids;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
rect(id: string): DOMRectLike | null {
|
|
210
|
+
const object = this.object(id);
|
|
211
|
+
if (!object || typeof object.getBounds !== 'function') return null;
|
|
212
|
+
const bounds = object.getBounds();
|
|
213
|
+
if (!(bounds.width > 0) || !(bounds.height > 0)) return null;
|
|
214
|
+
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
contextRects(id: string): { parent?: DOMRectLike; siblings: DOMRectLike[] } {
|
|
218
|
+
const node = this.node(id);
|
|
219
|
+
if (!node) return { siblings: [] };
|
|
220
|
+
const siblingIds = node.parentId
|
|
221
|
+
? (this.node(node.parentId)?.childIds ?? [])
|
|
222
|
+
: this.current.rootIds;
|
|
223
|
+
const parent = node.parentId ? this.rect(node.parentId) : null;
|
|
224
|
+
return {
|
|
225
|
+
...(parent ? { parent } : {}),
|
|
226
|
+
siblings: siblingIds
|
|
227
|
+
.filter((siblingId) => siblingId !== id)
|
|
228
|
+
.map((siblingId) => this.rect(siblingId))
|
|
229
|
+
.filter((rect): rect is DOMRectLike => rect !== null),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private readProjection(stats: PixiWalkStats): PixiProjection {
|
|
234
|
+
const nodes = new Map<string, PixiNode>();
|
|
235
|
+
const rootIds: string[] = [];
|
|
236
|
+
const visit = (native: EditorNode2D): void => {
|
|
237
|
+
const object = this.index.displayObject(native.id);
|
|
238
|
+
if (!object) return;
|
|
239
|
+
const node: PixiNode = {
|
|
240
|
+
object,
|
|
241
|
+
id: native.id,
|
|
242
|
+
parentId: native.parentId,
|
|
243
|
+
childIds: [...native.childIds],
|
|
244
|
+
};
|
|
245
|
+
nodes.set(native.id, node);
|
|
246
|
+
for (const childId of native.childIds) {
|
|
247
|
+
const child = this.index.node(childId);
|
|
248
|
+
if (child) visit(child);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
for (const root of this.index.roots()) {
|
|
252
|
+
rootIds.push(root.id);
|
|
253
|
+
visit(root);
|
|
254
|
+
}
|
|
255
|
+
return { nodes, rootIds, stats };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Pixi paint order, frontmost first; rendered geometry precedes hit areas. */
|
|
259
|
+
private collectHits(
|
|
260
|
+
parent: Container,
|
|
261
|
+
point: PointData,
|
|
262
|
+
mode: 'visual' | 'hitArea',
|
|
263
|
+
hits: Container[],
|
|
264
|
+
): void {
|
|
265
|
+
const children = parent.children ?? [];
|
|
266
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
267
|
+
const child = children[index] as Container;
|
|
268
|
+
if (!child.visible || !child.renderable) continue;
|
|
269
|
+
child.updateLocalTransform();
|
|
270
|
+
const local: PointData = { x: 0, y: 0 };
|
|
271
|
+
child.localTransform.applyInverse(point, local);
|
|
272
|
+
this.collectHits(child, local, mode, hits);
|
|
273
|
+
if (this.contains(child, local, mode)) hits.push(child);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private contains(object: Container, local: PointData, mode: 'visual' | 'hitArea'): boolean {
|
|
278
|
+
if (!(object as Container & { __authId?: string }).__authId) return false;
|
|
279
|
+
const shaped = object as Container & {
|
|
280
|
+
hitArea?: { contains(x: number, y: number): boolean } | null;
|
|
281
|
+
containsPoint?: (point: PointData) => boolean;
|
|
282
|
+
};
|
|
283
|
+
if (mode === 'hitArea') {
|
|
284
|
+
return shaped.hitArea ? shaped.hitArea.contains(local.x, local.y) : false;
|
|
285
|
+
}
|
|
286
|
+
return typeof shaped.containsPoint === 'function' ? shaped.containsPoint(local) : false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the R3F entry factory from the same module graph as project worlds.
|
|
3
|
+
* The packaged editor's UI is prebuilt with its own React/Fiber instances,
|
|
4
|
+
* while project modules are transformed by a separate project-rooted Vite
|
|
5
|
+
* server. Building a project component's adapter with the UI bundle's factory
|
|
6
|
+
* crosses those graphs and crashes Fiber hooks on a null React dispatcher.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resolveR3FEntryAdapter } from '@volter/game-runtime/world3d-react';
|
|
10
|
+
import type { RootAdapter, ThreeHostContext } from '@volter/editor-project/adapter';
|
|
11
|
+
import type * as THREE from 'three';
|
|
12
|
+
import {
|
|
13
|
+
R3F_ENTRY_RUNTIME_PATH,
|
|
14
|
+
R3F_RUNTIME_PATH,
|
|
15
|
+
} from '@volter/editor-sdk/host';
|
|
16
|
+
import { isPackagedRuntime } from '@volter/editor-core/packaged-runtime';
|
|
17
|
+
|
|
18
|
+
type R3FEntryResolver = typeof resolveR3FEntryAdapter;
|
|
19
|
+
|
|
20
|
+
interface PackagedR3FRuntime {
|
|
21
|
+
readonly resolve: R3FEntryResolver;
|
|
22
|
+
readonly projectThree: typeof THREE;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cachedPackagedRuntime: Promise<PackagedR3FRuntime> | null = null;
|
|
26
|
+
|
|
27
|
+
async function packagedRuntime(): Promise<PackagedR3FRuntime> {
|
|
28
|
+
const [entry, mod] = await Promise.all([
|
|
29
|
+
import(/* @vite-ignore */ R3F_ENTRY_RUNTIME_PATH) as Promise<{
|
|
30
|
+
resolveR3FEntryAdapter?: unknown;
|
|
31
|
+
}>,
|
|
32
|
+
import(/* @vite-ignore */ R3F_RUNTIME_PATH) as Promise<{ projectThree?: unknown }>,
|
|
33
|
+
]);
|
|
34
|
+
if (typeof entry.resolveR3FEntryAdapter !== 'function') {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"The packaged runtime's synthetic R3F entry module did not export " +
|
|
37
|
+
'`resolveR3FEntryAdapter` — see R3F_ENTRY_DOORWAY in vite-plugin-module-doorways.ts.',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (typeof mod.projectThree !== 'object' || mod.projectThree === null) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"The packaged runtime's synthetic R3F module did not export " +
|
|
43
|
+
'`projectThree` — see R3F_DOORWAY in vite-plugin-module-doorways.ts.',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
resolve: entry.resolveR3FEntryAdapter as R3FEntryResolver,
|
|
48
|
+
projectThree: mod.projectThree as typeof THREE,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The packaged editor shell and project modules are separate Vite graphs. The
|
|
54
|
+
* adapter itself comes from the project graph, so its Three host namespace
|
|
55
|
+
* must come from there too: Fiber seeds its intrinsic catalogue from
|
|
56
|
+
* `host.three`, and mixing in the shell's constructors makes applyProps turn a
|
|
57
|
+
* typed `DirectionalLight.color` into the raw JSX string.
|
|
58
|
+
*/
|
|
59
|
+
export function withPackagedProjectThree(
|
|
60
|
+
adapter: RootAdapter,
|
|
61
|
+
projectThree: typeof THREE,
|
|
62
|
+
): RootAdapter {
|
|
63
|
+
return {
|
|
64
|
+
id: adapter.id,
|
|
65
|
+
mount(host: ThreeHostContext) {
|
|
66
|
+
return adapter.mount({ ...host, three: projectThree });
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function resolveR3FEntryAdapterForEditor(entryModule: unknown, rootId: string) {
|
|
72
|
+
if (!(await isPackagedRuntime())) return resolveR3FEntryAdapter(entryModule, rootId);
|
|
73
|
+
if (!cachedPackagedRuntime) cachedPackagedRuntime = packagedRuntime();
|
|
74
|
+
const runtime = await cachedPackagedRuntime;
|
|
75
|
+
const adapter = runtime.resolve(entryModule, rootId);
|
|
76
|
+
return adapter ? withPackagedProjectThree(adapter, runtime.projectThree) : null;
|
|
77
|
+
}
|