@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,782 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE DOM MEDIUM'S DESIGN-TIME MOUNT (`@editor/authoring/design-time-mount-registry`).
|
|
3
|
+
* A dom world's design-time surface is its project's portable-CSF BOARD: every
|
|
4
|
+
* composed story of the project laid out as an isolated, labeled frame, with
|
|
5
|
+
* the root entry's own story (derived, never labeled — the story whose CSF
|
|
6
|
+
* `meta.component` is the entry's default export) as the frame it opens on.
|
|
7
|
+
* A project with no stories, and the pasteboard document's `entryOnly`
|
|
8
|
+
* descriptor, fall back to mounting the entry once against an inert Game.
|
|
9
|
+
*
|
|
10
|
+
* ## Why this is a package and not the host's
|
|
11
|
+
*
|
|
12
|
+
* It was the `mountReactLayer` half of `@editor/authoring/design-time-layers.ts`,
|
|
13
|
+
* chosen by one literal in one expression — `candidate.kind === 'canvas' ?
|
|
14
|
+
* mountCanvasLayer(…) : mountReactLayer(…)` — against a `LayerMountResult`
|
|
15
|
+
* contract whose own docblock already said the failure/teardown/play lifecycle
|
|
16
|
+
* is "the SAME for both media". Behind that literal the host knew what
|
|
17
|
+
* portable CSF is, imported the story registry and Storybook's preview API
|
|
18
|
+
* with it, and hard-coded this medium's staleness rule besides. The host keeps
|
|
19
|
+
* the layer STACK (epochs, z-order, the eye, the pan transform, the play
|
|
20
|
+
* handoff and Stop's rebuild, the Boundary disclosure a failed mount degrades
|
|
21
|
+
* to) and asks the registry how a medium mounts (WORK.md §The open-source
|
|
22
|
+
* launch item 12, §Stories leave the host edge 9).
|
|
23
|
+
*
|
|
24
|
+
* ## The seam, which is not a new one for this package
|
|
25
|
+
*
|
|
26
|
+
* `@volter/editor-game` already contributes the React/DOM Inspector through the host's
|
|
27
|
+
* own inspector-section registry; this is the second contribution and the
|
|
28
|
+
* fifth registry of the same family (see the registry module's own note for
|
|
29
|
+
* the four it transcribes). The package REGISTERS; the host names no medium.
|
|
30
|
+
*
|
|
31
|
+
* ## What has NOT moved with it, and why — each blocked by a HOST importer
|
|
32
|
+
*
|
|
33
|
+
* - `react-world-authoring-adapter.ts` (the adapter this mint returns) DID
|
|
34
|
+
* move, in the same unit: once this mount left, its only host importer
|
|
35
|
+
* was `dom-authoring-adapter.ts:70`
|
|
36
|
+
* (`cssColorToHex`/`mapBoxEditPatchKey`/`styleProp`), which had no host
|
|
37
|
+
* importer of its own, so the pair moved together. `@volter/editor-game` reaches
|
|
38
|
+
* them as `@volter/editor-game/react/…` and declares the dependency.
|
|
39
|
+
* - `@editor/authoring/react-story-board.ts` (the board geometry) —
|
|
40
|
+
* `@editor/components/RootSelectionOverlay.tsx:98`,
|
|
41
|
+
* `@editor/components/ReactCanvasControls.tsx:15`,
|
|
42
|
+
* `@editor/pasteboard-materialize.ts:20` and
|
|
43
|
+
* `@editor/canvas-board/CanvasBoardDocument.tsx:57` read its frame
|
|
44
|
+
* placements. Host chrome drawn OVER the board, which is a different
|
|
45
|
+
* question from how the board mounts.
|
|
46
|
+
* - `rememberedPortableStory` / `projectStoryBoardDescriptor` — still the
|
|
47
|
+
* host's, because `@editor/components/world-documents.tsx:542,570` reads
|
|
48
|
+
* both, and the host may not import a package. The write half is a door
|
|
49
|
+
* (`rememberPortableStory`).
|
|
50
|
+
* - the story-registry modules under `@editor/stories/` — as of 2026-09-19
|
|
51
|
+
* the only host files left importing them are the THREE-surface board
|
|
52
|
+
* (`three-board/board-scene.ts`, `three-board/ThreeBoardDocument.tsx`) and
|
|
53
|
+
* the canvas board (`canvas-board/canvas-board-model.ts`,
|
|
54
|
+
* `canvas-board/CanvasBoardDocument.tsx`). Neither is this lane's, and
|
|
55
|
+
* neither is the host's either, which is what the measurement says about
|
|
56
|
+
* unit 11's unmet half: it waits on those two surfaces, not on this one.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import type {
|
|
60
|
+
DesignTimeRootDescriptor,
|
|
61
|
+
LayerMountResult,
|
|
62
|
+
} from '@volter/editor-core/authoring/design-time-layers';
|
|
63
|
+
import {
|
|
64
|
+
rememberedPortableStory,
|
|
65
|
+
rememberPortableStory,
|
|
66
|
+
} from '@volter/editor-core/authoring/design-time-layers';
|
|
67
|
+
import type { DesignTimeMountContext } from '@volter/editor-core/authoring/design-time-mount-registry';
|
|
68
|
+
import { formatMountFailureMessage } from '@volter/editor-core/authoring/mount-failure-report';
|
|
69
|
+
import type { ReactStoryBoardSelectionIntent } from '@volter/editor-core/authoring/react-story-board';
|
|
70
|
+
import { createReactStoryBoard } from '@volter/editor-core/authoring/react-story-board';
|
|
71
|
+
import {
|
|
72
|
+
getRootCanvasViewport,
|
|
73
|
+
setRootCanvasViewport,
|
|
74
|
+
} from '@volter/editor-core/authoring/world-canvas-viewport-state';
|
|
75
|
+
import { recordAuthoringConsumerUse } from '@volter/editor-core/coverage/authoring-seam-evidence';
|
|
76
|
+
import { CrashNullBoundary } from '@volter/editor-core/crash-null-boundary';
|
|
77
|
+
import { readProjectTextFile } from '@volter/editor-core/editor-api';
|
|
78
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
79
|
+
import {
|
|
80
|
+
resolveReactRootMountRuntime,
|
|
81
|
+
resolveWorldProviderForProject,
|
|
82
|
+
} from '../host/react-mount-runtime';
|
|
83
|
+
import { activeRealmServices } from '../host/realm-services';
|
|
84
|
+
import { resolveReactAdapterRootComponent } from '../host/roots/react-root';
|
|
85
|
+
import { scopedGameStylesState } from '@volter/editor-core/scoped-game-css';
|
|
86
|
+
import { componentIdentityName } from '@volter/editor-core/stories/compose-project-stories';
|
|
87
|
+
import { mountIsolatedStory } from '@volter/editor-core/stories/StoryPreviewMount';
|
|
88
|
+
import {
|
|
89
|
+
createStoryPresentationIndex,
|
|
90
|
+
storyBoardPresentation,
|
|
91
|
+
} from '@volter/editor-core/stories/story-presentation';
|
|
92
|
+
import {
|
|
93
|
+
getComponentPreviewStory,
|
|
94
|
+
getProjectPreviewStories,
|
|
95
|
+
getProjectStoryModules,
|
|
96
|
+
type ProjectPreviewStory,
|
|
97
|
+
projectStoriesReady,
|
|
98
|
+
refreshProjectStories,
|
|
99
|
+
subscribeProjectStoryModules,
|
|
100
|
+
} from '@volter/editor-core/stories/story-registry';
|
|
101
|
+
import { domStoryBoardMembers } from '@volter/editor-core/stories/three-story-model';
|
|
102
|
+
import { getDesignTokens } from '@volter/editor-core/ui-source/inspect';
|
|
103
|
+
import { tierSourceWriteBackend } from '@volter/editor-core/ui-source/tier-source-write-backend';
|
|
104
|
+
import { recordViewportFirstFrame } from '@volter/editor-core/viewport-activation-timings';
|
|
105
|
+
import { createAssetCache } from '@volter/threejs-runtime/assets';
|
|
106
|
+
import { createGameLoop } from '@volter/game-runtime/core/game-loop';
|
|
107
|
+
import type { ResolvedAdapterRoot } from '@volter/editor-project/manifest/load';
|
|
108
|
+
import { createGame, type GameInternal } from '@volter/game-runtime/runtime/game';
|
|
109
|
+
import { beginProjectMountEpoch } from '@volter/editor-sdk/session/project-module-url';
|
|
110
|
+
import type { ComponentType } from 'react';
|
|
111
|
+
import { type OidElementLike, ReactRootAuthoringAdapter } from './react-world-authoring-adapter';
|
|
112
|
+
import { paintedContentBounds } from './story-paint-bounds';
|
|
113
|
+
|
|
114
|
+
/** Set by the DOM re-projection right before it remounts a
|
|
115
|
+
* DOM candidate: the registry just published, so the mount reads it as is. */
|
|
116
|
+
let skipStoryRefreshForRemount = false;
|
|
117
|
+
|
|
118
|
+
/** Board chrome is also document navigation: apply the story and make its
|
|
119
|
+
* structural row the store selection so global editor surfaces route back to
|
|
120
|
+
* this React adapter rather than the previously selected world. */
|
|
121
|
+
function selectPortableStory(adapter: ReactRootAuthoringAdapter | null, storyId: string): void {
|
|
122
|
+
if (!adapter) return;
|
|
123
|
+
recordAuthoringConsumerUse({
|
|
124
|
+
adapter,
|
|
125
|
+
seam: 'editor.stories.apply',
|
|
126
|
+
stage: 'effect',
|
|
127
|
+
detail: `the design board applied portable story ${storyId}`,
|
|
128
|
+
run: () => adapter.stories.apply(storyId, storyId),
|
|
129
|
+
});
|
|
130
|
+
const selection = adapter.selection;
|
|
131
|
+
if (selection) {
|
|
132
|
+
recordAuthoringConsumerUse({
|
|
133
|
+
adapter,
|
|
134
|
+
seam: 'editor.selection.set',
|
|
135
|
+
stage: 'effect',
|
|
136
|
+
detail: `the design board selected portable story ${storyId}`,
|
|
137
|
+
run: () => selection.set([storyId]),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Seed the session's dom-story mount box from the project's DECLARED
|
|
144
|
+
* resolution, once — the same rung the story CAPTURE lane reads: the screen a
|
|
145
|
+
* UI story's anchors resolve against is a manifest declaration, not a magic
|
|
146
|
+
* "Fill". Seeded only while the session viewport is still untouched, so a
|
|
147
|
+
* size (or Fill) the user picked afterwards always stands, and a project
|
|
148
|
+
* declaring nothing keeps Fill.
|
|
149
|
+
*/
|
|
150
|
+
let rootViewportSeeded = false;
|
|
151
|
+
async function seedRootViewportFromManifest(): Promise<void> {
|
|
152
|
+
if (rootViewportSeeded) return;
|
|
153
|
+
rootViewportSeeded = true;
|
|
154
|
+
const current = getRootCanvasViewport();
|
|
155
|
+
if (current.width !== null || current.height !== null) return;
|
|
156
|
+
try {
|
|
157
|
+
const text = await readProjectTextFile('vgai.project.json');
|
|
158
|
+
if (!text) return;
|
|
159
|
+
const manifest = JSON.parse(text) as {
|
|
160
|
+
resolution?: { width?: unknown; height?: unknown };
|
|
161
|
+
};
|
|
162
|
+
const width = manifest.resolution?.width;
|
|
163
|
+
const height = manifest.resolution?.height;
|
|
164
|
+
if (typeof width !== 'number' || typeof height !== 'number') return;
|
|
165
|
+
if (width < 64 || height < 64 || width > 4096 || height > 4096) return;
|
|
166
|
+
setRootCanvasViewport(width, height);
|
|
167
|
+
} catch {
|
|
168
|
+
// An unreadable manifest declares nothing; Fill stands.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The inert design-time Game stand-in (B1's one genuine design
|
|
173
|
+
* decision). A REAL `Game` — never a hand-typed duck-typed literal, mirroring
|
|
174
|
+
* this repo's own test convention (`packages/editor/test/adapter-conformance-kit.ts`'s
|
|
175
|
+
* `headlessGame()`) — built with a loop that is constructed but NEVER
|
|
176
|
+
* started/ticked, and with NO roots ever registered. This satisfies
|
|
177
|
+
* `useGame`/`useWorldState` (`@volter/game-runtime/react/world-state`, and any project's own
|
|
178
|
+
* re-export of it) without throwing, genuinely inertly:
|
|
179
|
+
* - `state.subscribe` never fires — nothing ever calls
|
|
180
|
+
* `GameStateBridgeInternal.bump()` (only `GameInternal.runFrame` does, and
|
|
181
|
+
* this Game's loop never runs a frame);
|
|
182
|
+
* - `state.frameVersion` stays `0` forever;
|
|
183
|
+
* - `queryByComponent`/`world()`/`roots` are empty/`null` because `roots`
|
|
184
|
+
* stays `[]` (nothing ever calls `registerRoot`).
|
|
185
|
+
* `defaultRoot`/`components`/`input`/`audio` throw descriptively if touched
|
|
186
|
+
* (same as any fresh `createGame()` before a world registers) — a HUD that
|
|
187
|
+
* reaches for those is out of scope for a design-time stand-in and SHOULD
|
|
188
|
+
* throw loudly, caught by the crash-null boundary below, degrading to the
|
|
189
|
+
* #18 error node rather than silently faking a value.
|
|
190
|
+
*
|
|
191
|
+
* B2 swaps this stand-in's DATA for story-sourced data (a `data`/services
|
|
192
|
+
* seam) — this function is the seam B2 replaces/extends, not a permanent
|
|
193
|
+
* fixture; it never constructs a live Game/sockets.
|
|
194
|
+
*/
|
|
195
|
+
function createDesignTimeGame(): GameInternal {
|
|
196
|
+
const loop = createGameLoop({ fixedTimestep: 1 / 60, maxSubSteps: 8, update: () => {} });
|
|
197
|
+
return createGame({ loop, assets: createAssetCache() });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A minimal `ResolvedAdapterRoot` satisfying `resolveReactAdapterRootComponent`
|
|
202
|
+
* (`../binding-resolver.ts`, which reads only `id`/`kind`/`entry` off
|
|
203
|
+
* it) — the fields below it (`adapter`/`capabilities`/`loop`/`description`)
|
|
204
|
+
* are unused by that function but required by the type; filled with the
|
|
205
|
+
* honest "this is a native default-react world" values.
|
|
206
|
+
*
|
|
207
|
+
* `loop: 'gated'` here is the MANIFEST's declared-intent vocabulary
|
|
208
|
+
* (`ResolvedAdapterRoot.loop`, `@volter/editor-project/manifest/load`), and specifically the
|
|
209
|
+
* schema's own `.default('gated')` — not a verdict about anything. It is safe as
|
|
210
|
+
* a declaration precisely because nothing projects it: the only consumer of this
|
|
211
|
+
* stub is `resolveEntryComponent` below, the stub never reaches
|
|
212
|
+
* `resolveAllRoots` (so it mints no `RootMountSpec`), and the measured loop
|
|
213
|
+
* vocabulary that status and coverage speak is a different type entirely
|
|
214
|
+
* (`ingest/same-realm-loop-gate.ts`'s `MeasuredLoop`, which cannot be spelled
|
|
215
|
+
* without a probe's evidence).
|
|
216
|
+
*/
|
|
217
|
+
function buildResolvedAdapterRootStub(worldId: string, entry: string): ResolvedAdapterRoot {
|
|
218
|
+
return {
|
|
219
|
+
id: worldId,
|
|
220
|
+
surface: 'dom',
|
|
221
|
+
description: undefined,
|
|
222
|
+
adapter: { type: 'builtin', identity: 'dom', surface: 'dom' },
|
|
223
|
+
entry,
|
|
224
|
+
zOrder: 0,
|
|
225
|
+
pausable: true,
|
|
226
|
+
loop: 'gated',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
type EntryResolution = { ok: true; Entry: ComponentType } | { ok: false; error: unknown };
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Load a React root's entry component ONCE per layer mount, without throwing.
|
|
234
|
+
* Both of this layer's paths need it: the derived default story is joined
|
|
235
|
+
* against this component's identity, and the inert-Game fallback renders it.
|
|
236
|
+
* A failure is carried rather than thrown so the story board can still open
|
|
237
|
+
* over a project whose entry is broken; only the fallback (which has nothing
|
|
238
|
+
* else to render) rethrows it.
|
|
239
|
+
*/
|
|
240
|
+
async function resolveEntryComponent(
|
|
241
|
+
worldStub: ResolvedAdapterRoot,
|
|
242
|
+
projectRoot: string,
|
|
243
|
+
): Promise<EntryResolution> {
|
|
244
|
+
try {
|
|
245
|
+
// Design-time preview layer: its own mount, therefore its own epoch —
|
|
246
|
+
// and therefore its own realm value over that epoch.
|
|
247
|
+
const realm = await activeRealmServices(projectRoot, beginProjectMountEpoch());
|
|
248
|
+
const Entry = await resolveReactAdapterRootComponent(worldStub, realm);
|
|
249
|
+
return { ok: true, Entry };
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return { ok: false, error };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* React design-time mount — the entry
|
|
257
|
+
* via the EXISTING #31 loader (`resolveReactAdapterRootComponent`, reused
|
|
258
|
+
* verbatim), wrapped in the canonical engine `WorldProvider`
|
|
259
|
+
* (`resolveWorldProviderForProject`) around the inert
|
|
260
|
+
* `createDesignTimeGame()` stand-in and a React error boundary.
|
|
261
|
+
* `flushSync` forces the initial render/commit/error-boundary-recovery to
|
|
262
|
+
* finish before this call returns (mirrors `ingest-siblings.ts`'s
|
|
263
|
+
* `mountDefaultReactSibling`), so a throwing entry is caught HERE —
|
|
264
|
+
* synchronously — rather than discovered later on react's own schedule.
|
|
265
|
+
*
|
|
266
|
+
* Portable CSF stories take the board path above. This branch is only the
|
|
267
|
+
* explicit no-story fallback and mounts the entry once against the inert Game.
|
|
268
|
+
* On success, it mints the world's live `ReactRootAuthoringAdapter` over this
|
|
269
|
+
* layer's now-mounted DOM — the adapter reflects
|
|
270
|
+
* an already-mounted root, it never mounts one itself; this function is the
|
|
271
|
+
* design-time mount and the adapter class itself is reused.
|
|
272
|
+
*
|
|
273
|
+
* MEASURED while moving it (the brief asked): BOTH `ReactRootAuthoringAdapter`
|
|
274
|
+
* constructions in this file are ADOPTIONS, not mounts. The board branch's
|
|
275
|
+
* (`focusedDomRoot`) reflects a board whose frames `mountIsolatedStory`
|
|
276
|
+
* already mounted, through a facade over the ACTIVE frame's live children;
|
|
277
|
+
* the fallback branch's reflects the layer `root.render`/`flushSync` has
|
|
278
|
+
* already committed into. Neither constructor mounts anything — which is why
|
|
279
|
+
* the adapter class is shared with Play's React branch and the ingest DOM
|
|
280
|
+
* surface, and why the mount is what moved here rather than the adapter.
|
|
281
|
+
*/
|
|
282
|
+
export async function mountReactDesignLayer(
|
|
283
|
+
candidate: DesignTimeRootDescriptor,
|
|
284
|
+
layer: HTMLElement,
|
|
285
|
+
context: DesignTimeMountContext,
|
|
286
|
+
): Promise<LayerMountResult> {
|
|
287
|
+
const { projectRootPath: projectRoot, store, activationDocumentId } = context;
|
|
288
|
+
// NOTE the ORDER: the entry is not required to reach the story board, so the
|
|
289
|
+
// "no entry" refusal below happens AFTER discovery, not before it. A board
|
|
290
|
+
// descriptor may have no entry by construction (the rootless UI board). A
|
|
291
|
+
// board's whole subject is its assigned project stories — an entry only ever
|
|
292
|
+
// picks which UI frame opens first. Refusing up front is what made a board
|
|
293
|
+
// impossible without a root, which is precisely what board presence no
|
|
294
|
+
// longer keys on.
|
|
295
|
+
|
|
296
|
+
// D20 — portable Storybook CSF is React's canonical design-time state.
|
|
297
|
+
// Discovery feeds the first-class hierarchy documents below this world.
|
|
298
|
+
// Edit mode lays EVERY composed story of the project out as an isolated,
|
|
299
|
+
// labeled frame on one editor-only board — board membership is
|
|
300
|
+
// project-global, the project's own component gallery. Play still tears
|
|
301
|
+
// this entire surface down and mounts only the manifest entry against the
|
|
302
|
+
// real Game.
|
|
303
|
+
//
|
|
304
|
+
// A remount that the registry's own publish requested already has fresh
|
|
305
|
+
// stories: refreshing again would reload every story module in parallel —
|
|
306
|
+
// the storm the content-write refresh was built to avoid.
|
|
307
|
+
if (skipStoryRefreshForRemount) skipStoryRefreshForRemount = false;
|
|
308
|
+
else await refreshProjectStories({ rootPath: projectRoot });
|
|
309
|
+
// …every story DECLARED as DOM. The registry is project-global and
|
|
310
|
+
// deliberately unfiltered, so THIS board filters its own membership the
|
|
311
|
+
// way the `3D` board filters its own — otherwise a
|
|
312
|
+
// three story is handed to `react-dom`, which renders `<group>`/`<primitive>`
|
|
313
|
+
// as unknown tags into an empty frame.
|
|
314
|
+
const previewStories = getProjectPreviewStories();
|
|
315
|
+
const portableStories = domStoryBoardMembers(previewStories);
|
|
316
|
+
|
|
317
|
+
// The root's composed default preview is DERIVED, never labeled: it is the
|
|
318
|
+
// story whose CSF `meta.component` is this root entry's default-exported
|
|
319
|
+
// component. Resolve that component first (the same loader the inert-Game
|
|
320
|
+
// fallback below reuses), then ask the registry for its story.
|
|
321
|
+
//
|
|
322
|
+
// The join is by component NAME, not function identity, and that is
|
|
323
|
+
// measured rather than assumed: a story module is imported at its own
|
|
324
|
+
// `/@fs/<path>?t=<now>` url (`story-discovery.ts`) while a root entry is
|
|
325
|
+
// imported at the mount epoch's `?vgai-mount=<n>` url
|
|
326
|
+
// (`project-module-url.ts`), and browser ES-module identity is per-url — so
|
|
327
|
+
// the component object a story's meta holds is never the object the entry
|
|
328
|
+
// loader returns. `pickComponentPreviewStory` (`story-registry.ts`) is the
|
|
329
|
+
// join this repo already ships for `three` prefab thumbnails: component
|
|
330
|
+
// name, preferring a story module in the component's own source directory.
|
|
331
|
+
const entryPath = candidate.path;
|
|
332
|
+
const entry: EntryResolution = entryPath
|
|
333
|
+
? await resolveEntryComponent(
|
|
334
|
+
buildResolvedAdapterRootStub(candidate.worldId, entryPath),
|
|
335
|
+
projectRoot,
|
|
336
|
+
)
|
|
337
|
+
: {
|
|
338
|
+
ok: false,
|
|
339
|
+
error: new Error(
|
|
340
|
+
`design-time react layer "${candidate.worldId}": no \`entry\` declared — nothing to mount.`,
|
|
341
|
+
),
|
|
342
|
+
};
|
|
343
|
+
const entryComponentName = entry.ok ? componentIdentityName(entry.Entry) : undefined;
|
|
344
|
+
const derivedDefaultPortableStory = entryComponentName
|
|
345
|
+
? getComponentPreviewStory(entryComponentName, candidate.path)
|
|
346
|
+
: null;
|
|
347
|
+
const defaultPortableStory =
|
|
348
|
+
portableStories.find((story) => story.id === derivedDefaultPortableStory?.id) ?? null;
|
|
349
|
+
// The board exists because the PROJECT has stories, never because the
|
|
350
|
+
// derivation matched: an entry that fails to import, or a project whose
|
|
351
|
+
// stories name other components, still gets its whole gallery (the entry
|
|
352
|
+
// is only what picks the INITIAL frame). Without this the derivation would
|
|
353
|
+
// silently gate the entire design surface — a broken entry would take every
|
|
354
|
+
// story down with it.
|
|
355
|
+
if (portableStories.length > 0 && !candidate.entryOnly) {
|
|
356
|
+
const rememberedStoryId = rememberedPortableStory(projectRoot, candidate.worldId);
|
|
357
|
+
// Remembered choice, else the derived default, else discovery order —
|
|
358
|
+
// the same "no explicit default falls back to first" rule the registry
|
|
359
|
+
// has always used.
|
|
360
|
+
const initialPortableStory =
|
|
361
|
+
portableStories.find((story) => story.id === rememberedStoryId) ??
|
|
362
|
+
defaultPortableStory ??
|
|
363
|
+
portableStories[0]!;
|
|
364
|
+
// Only a board that HAS an entry can be missing a story for it. A rootless
|
|
365
|
+
// board (`projectStoryBoardDescriptor`) has no `path` BY CONSTRUCTION — the
|
|
366
|
+
// project declares no `dom` root, so there is no entry component and
|
|
367
|
+
// "give a *.stories.tsx a `meta.component` of this root's entry component"
|
|
368
|
+
// names an action that cannot be taken. Opening on the first story is that
|
|
369
|
+
// board's documented behaviour, not a degraded one. Measured on the
|
|
370
|
+
// repo-vendored `racing-game` ingest, whose manifest declares one ingest
|
|
371
|
+
// root and no dom root: a permanent unresolvable warning on every `vgai`
|
|
372
|
+
// command, for a board that was working exactly as designed.
|
|
373
|
+
if (candidate.path && !defaultPortableStory) {
|
|
374
|
+
editorConsole.warn(
|
|
375
|
+
`[design-time-layers] React root "${candidate.worldId}" has no story naming its entry ` +
|
|
376
|
+
`component${entryComponentName ? ` "${entryComponentName}"` : ''} — the board opens on ` +
|
|
377
|
+
`"${initialPortableStory.label}". Give a *.stories.tsx a \`meta.component\` of this ` +
|
|
378
|
+
"root's entry component to choose its default preview.",
|
|
379
|
+
'authoring',
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
let disposed = false;
|
|
383
|
+
let adapter: ReactRootAuthoringAdapter | null = null;
|
|
384
|
+
// A story apply can originate from either hierarchy navigation or board
|
|
385
|
+
// interaction. Hierarchy has no pending board intent and therefore
|
|
386
|
+
// recenters; board clicks explicitly choose highlight-only or zoom.
|
|
387
|
+
let pendingBoardSelectionIntent: ReactStoryBoardSelectionIntent | null = null;
|
|
388
|
+
const mountedByStory = new Map<
|
|
389
|
+
string,
|
|
390
|
+
NonNullable<Awaited<ReturnType<typeof mountIsolatedStory>>>
|
|
391
|
+
>();
|
|
392
|
+
const storyPresentationIndex = createStoryPresentationIndex(portableStories);
|
|
393
|
+
await seedRootViewportFromManifest();
|
|
394
|
+
const board = createReactStoryBoard(
|
|
395
|
+
layer,
|
|
396
|
+
portableStories.map((story) => ({
|
|
397
|
+
id: story.id,
|
|
398
|
+
label: story.label,
|
|
399
|
+
modulePath: story.modulePath,
|
|
400
|
+
...(story.title ? { title: story.title } : {}),
|
|
401
|
+
parameters: story.parameters,
|
|
402
|
+
...(story.globals ? { globals: story.globals } : {}),
|
|
403
|
+
...(story.viewportLocked !== undefined ? { viewportLocked: story.viewportLocked } : {}),
|
|
404
|
+
})),
|
|
405
|
+
initialPortableStory.id,
|
|
406
|
+
(storyId, intent) => {
|
|
407
|
+
pendingBoardSelectionIntent = intent;
|
|
408
|
+
selectPortableStory(adapter, storyId);
|
|
409
|
+
},
|
|
410
|
+
// The board never reaches for the CSF implementation itself
|
|
411
|
+
// (`@editor/authoring/story-board-presentation.ts`): this lane hands it
|
|
412
|
+
// one, reusing the index the React adapter is also constructed against.
|
|
413
|
+
storyBoardPresentation(portableStories, storyPresentationIndex),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Frame the subject: after a story commits, measure its painted union and
|
|
418
|
+
* hand the board the content rectangle to present as the cell — the same
|
|
419
|
+
* decision the canvas board makes from `stage.getBounds()`, made from the
|
|
420
|
+
* DOM. A `fullscreen` story presents its whole declared viewport, and so
|
|
421
|
+
* does a story whose painted union effectively spans it (a full-bleed
|
|
422
|
+
* screen needs no crop even when it forgot to declare). Measured twice —
|
|
423
|
+
* one settle turn after commit, and once more a second later — because a
|
|
424
|
+
* lazy()/Suspense screen commits its wrapper before its chunk lands.
|
|
425
|
+
*/
|
|
426
|
+
const measureContent = (story: ProjectPreviewStory): void => {
|
|
427
|
+
if (disposed) return;
|
|
428
|
+
const frame = board.frames.get(story.id);
|
|
429
|
+
if (!frame) return;
|
|
430
|
+
const layout = (story.parameters as { layout?: unknown } | undefined)?.layout;
|
|
431
|
+
if (layout === 'fullscreen') {
|
|
432
|
+
board.setMeasuredFrame(story.id, null);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const mount = { width: frame.content.offsetWidth, height: frame.content.offsetHeight };
|
|
436
|
+
const bounds = mount.width > 0 ? paintedContentBounds(frame.content) : null;
|
|
437
|
+
if (!bounds) {
|
|
438
|
+
board.setMeasuredFrame(story.id, null);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const PAD = 8;
|
|
442
|
+
const MIN = 48;
|
|
443
|
+
let x0 = Math.max(0, bounds.x - PAD);
|
|
444
|
+
let y0 = Math.max(0, bounds.y - PAD);
|
|
445
|
+
let x1 = Math.min(mount.width, bounds.x + bounds.width + PAD);
|
|
446
|
+
let y1 = Math.min(mount.height, bounds.y + bounds.height + PAD);
|
|
447
|
+
if (x1 - x0 < MIN) {
|
|
448
|
+
const grow = (MIN - (x1 - x0)) / 2;
|
|
449
|
+
x0 = Math.max(0, x0 - grow);
|
|
450
|
+
x1 = Math.min(mount.width, x0 + MIN);
|
|
451
|
+
}
|
|
452
|
+
if (y1 - y0 < MIN) {
|
|
453
|
+
const grow = (MIN - (y1 - y0)) / 2;
|
|
454
|
+
y0 = Math.max(0, y0 - grow);
|
|
455
|
+
y1 = Math.min(mount.height, y0 + MIN);
|
|
456
|
+
}
|
|
457
|
+
if (x1 - x0 >= mount.width * 0.9 && y1 - y0 >= mount.height * 0.9) {
|
|
458
|
+
board.setMeasuredFrame(story.id, null);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
board.setMeasuredFrame(story.id, {
|
|
462
|
+
frame: { width: Math.round(x1 - x0), height: Math.round(y1 - y0) },
|
|
463
|
+
mount,
|
|
464
|
+
offset: { x: Math.round(x0), y: Math.round(y0) },
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
const scheduleContentMeasure = (story: ProjectPreviewStory): void => {
|
|
468
|
+
setTimeout(() => measureContent(story), 0);
|
|
469
|
+
setTimeout(() => measureContent(story), 1_000);
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const mountPortable = async (
|
|
473
|
+
story: ProjectPreviewStory,
|
|
474
|
+
args: Record<string, unknown> = story.args,
|
|
475
|
+
): Promise<void> => {
|
|
476
|
+
const content = board.frames.get(story.id)?.content;
|
|
477
|
+
if (!content) return;
|
|
478
|
+
try {
|
|
479
|
+
const next = await mountIsolatedStory(
|
|
480
|
+
content,
|
|
481
|
+
story.modulePath,
|
|
482
|
+
story.name,
|
|
483
|
+
story.Component,
|
|
484
|
+
args,
|
|
485
|
+
);
|
|
486
|
+
// A newer args update may claim this story's frame while its loader is
|
|
487
|
+
// still in flight. The portable mount reports that supersession as
|
|
488
|
+
// null; keep the newer handle intact.
|
|
489
|
+
if (!next) return;
|
|
490
|
+
if (disposed) {
|
|
491
|
+
next.unmount();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
mountedByStory.set(story.id, next);
|
|
495
|
+
board.clearError(story.id);
|
|
496
|
+
scheduleContentMeasure(story);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
const message = formatMountFailureMessage(error);
|
|
499
|
+
board.setError(story.id, message);
|
|
500
|
+
editorConsole.error(
|
|
501
|
+
`[design-time-layers] React root "${candidate.worldId}" failed mounting portable ` +
|
|
502
|
+
`story "${story.name}": ${message}`,
|
|
503
|
+
'authoring',
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
store.notifyIngestEdit();
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Each story owns its own React root and failure boundary. One broken
|
|
510
|
+
// loader therefore marks only its frame instead of blanking the board.
|
|
511
|
+
await Promise.all(portableStories.map((story) => mountPortable(story)));
|
|
512
|
+
// …and the board says so when the project's game CSS could not be applied.
|
|
513
|
+
// `mountIsolatedStory` has already resolved that state by now (it awaits
|
|
514
|
+
// the same per-project install before its first render), so this reads it
|
|
515
|
+
// rather than racing it. A first-party project reports nothing: `note` is
|
|
516
|
+
// non-null only where the host has EVIDENCE of missing page-level styling
|
|
517
|
+
// (`scoped-game-css.ts`).
|
|
518
|
+
board.setNote(scopedGameStylesState().note);
|
|
519
|
+
// A root document opened because the user clicked a hierarchy story must
|
|
520
|
+
// arrive focused on that remembered frame. A first visit with no prior
|
|
521
|
+
// choice keeps the board's fit-all overview.
|
|
522
|
+
if (rememberedStoryId) board.activate(initialPortableStory.id, true);
|
|
523
|
+
|
|
524
|
+
const byId = new Map(portableStories.map((story) => [story.id, story]));
|
|
525
|
+
// The adapter deliberately walks only the focused story's live DOM. All
|
|
526
|
+
// frames stay rendered, but Inspector/OID selection has one unambiguous
|
|
527
|
+
// source tree—the same source-global editing semantics as before.
|
|
528
|
+
const focusedDomRoot: OidElementLike = {
|
|
529
|
+
tagName: 'DIV',
|
|
530
|
+
get children() {
|
|
531
|
+
return board.activeContent().children as unknown as ArrayLike<OidElementLike>;
|
|
532
|
+
},
|
|
533
|
+
getAttribute: (name) => layer.getAttribute(name),
|
|
534
|
+
getBoundingClientRect: () => layer.getBoundingClientRect(),
|
|
535
|
+
get style() {
|
|
536
|
+
return layer.style;
|
|
537
|
+
},
|
|
538
|
+
get textContent() {
|
|
539
|
+
return board.activeContent().textContent;
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
// The recorder is decided by the ROUTE this host serves, never by how the
|
|
543
|
+
// shell bundle was built — see `tier-source-write-backend.ts` for the
|
|
544
|
+
// packaged-editor bug `import.meta.env.DEV` carried here.
|
|
545
|
+
const boardWriteBackend = await tierSourceWriteBackend();
|
|
546
|
+
adapter = new ReactRootAuthoringAdapter(focusedDomRoot, store, {
|
|
547
|
+
...(boardWriteBackend ? { writeBackend: boardWriteBackend } : {}),
|
|
548
|
+
// The facade root above is a plain object, so the adapter's own
|
|
549
|
+
// scope-root token lookup cannot run — resolve tokens from the ACTIVE
|
|
550
|
+
// frame's real element instead (its game-CSS scope root carries the
|
|
551
|
+
// project's custom properties; the editor page root never does).
|
|
552
|
+
designTokens: () => {
|
|
553
|
+
const content = board.activeContent();
|
|
554
|
+
const scopeElement =
|
|
555
|
+
content.closest?.('[data-vgai-game-styles]') ??
|
|
556
|
+
content.querySelector?.('[data-vgai-game-styles]') ??
|
|
557
|
+
content;
|
|
558
|
+
return getDesignTokens(
|
|
559
|
+
getComputedStyle(scopeElement) as unknown as Parameters<typeof getDesignTokens>[0],
|
|
560
|
+
);
|
|
561
|
+
},
|
|
562
|
+
assetRoot: layer as unknown as OidElementLike,
|
|
563
|
+
portableStories: portableStories.map((story) => ({
|
|
564
|
+
id: story.id,
|
|
565
|
+
label: story.label,
|
|
566
|
+
args: story.args,
|
|
567
|
+
name: story.name,
|
|
568
|
+
modulePath: story.modulePath,
|
|
569
|
+
})),
|
|
570
|
+
portableHierarchy: storyPresentationIndex,
|
|
571
|
+
activePortableStoryId: initialPortableStory.id,
|
|
572
|
+
onPortableStoryApplied: (storyId) => {
|
|
573
|
+
const story = (storyId ? byId.get(storyId) : null) ?? initialPortableStory;
|
|
574
|
+
rememberPortableStory(projectRoot, candidate.worldId, story.id);
|
|
575
|
+
const intent = pendingBoardSelectionIntent;
|
|
576
|
+
pendingBoardSelectionIntent = null;
|
|
577
|
+
// Direct provider calls come from hierarchy navigation and retain
|
|
578
|
+
// auto-center. A board single-click only changes active/selection;
|
|
579
|
+
// its double-click is the explicit zoom gesture.
|
|
580
|
+
board.activate(story.id, intent === null || intent === 'zoom');
|
|
581
|
+
},
|
|
582
|
+
onPortableStoryArgsChanged: (storyId, args) => {
|
|
583
|
+
const story = byId.get(storyId);
|
|
584
|
+
if (!story) return;
|
|
585
|
+
void mountPortable(story, args).catch((error: unknown) => {
|
|
586
|
+
editorConsole.error(
|
|
587
|
+
`[design-time-layers] React root "${candidate.worldId}" failed updating portable ` +
|
|
588
|
+
`story "${story.name}" args: ${formatMountFailureMessage(error)}`,
|
|
589
|
+
'authoring',
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
if (activationDocumentId) {
|
|
595
|
+
requestAnimationFrame(() => recordViewportFirstFrame(activationDocumentId));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
adapter,
|
|
600
|
+
dispose: () => {
|
|
601
|
+
disposed = true;
|
|
602
|
+
adapter.disposeReactRootAdapter();
|
|
603
|
+
for (const mounted of mountedByStory.values()) mounted.unmount();
|
|
604
|
+
mountedByStory.clear();
|
|
605
|
+
board.dispose();
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// This board has no assigned stories, so an ordinary UI root's entry is the
|
|
611
|
+
// only thing left to show and a failure to load it is this layer's failure
|
|
612
|
+
// (the caller degrades it to a #18 disclosure node).
|
|
613
|
+
if (!entry.ok) throw entry.error instanceof Error ? entry.error : new Error(String(entry.error));
|
|
614
|
+
|
|
615
|
+
// An entryOnly mount (the pasteboard document) ASKED for exactly this
|
|
616
|
+
// branch — the file is the canvas and its default export the one subject —
|
|
617
|
+
// so "no CSF preview, falling back" would be noise about working-as-designed.
|
|
618
|
+
if (!candidate.entryOnly) {
|
|
619
|
+
editorConsole.warn(
|
|
620
|
+
`[design-time-layers] React root "${candidate.worldId}" has no portable CSF preview — the ` +
|
|
621
|
+
'project declares no UI stories. Add a *.stories.tsx; give it a `meta.component` of ' +
|
|
622
|
+
`this root's entry component${entryComponentName ? ` (${entryComponentName}, ${candidate.path})` : ''} ` +
|
|
623
|
+
'to make it this root’s default preview. Falling back to the inert-Game preview.',
|
|
624
|
+
'authoring',
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const Entry = entry.Entry;
|
|
629
|
+
const WorldProvider = await resolveWorldProviderForProject();
|
|
630
|
+
// C1 (phase-b/c1-react-mount): this layer's own react-world mount is a
|
|
631
|
+
// SECOND call site with the identical packaged-runtime dual-React-instance
|
|
632
|
+
// exposure `binding-resolver.ts`'s `ReactRootAdapter.mount()` has — under
|
|
633
|
+
// the packaged runtime, `createElement`/`createRoot`/`flushSync` must come
|
|
634
|
+
// from the PROJECT's own react, not this module's (removed) static
|
|
635
|
+
// imports. See `resolveReactRootMountRuntime`'s doc comment.
|
|
636
|
+
const { createElement, createRoot, flushSync } = await resolveReactRootMountRuntime();
|
|
637
|
+
const game = createDesignTimeGame();
|
|
638
|
+
const root = createRoot(layer);
|
|
639
|
+
const render = (onCaught: (error: unknown) => void): void => {
|
|
640
|
+
root.render(
|
|
641
|
+
createElement(CrashNullBoundary, {
|
|
642
|
+
key: '__entry__',
|
|
643
|
+
onCaught,
|
|
644
|
+
children: createElement(WorldProvider, { game }, createElement(Entry)),
|
|
645
|
+
}),
|
|
646
|
+
);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
let caught: unknown = null;
|
|
650
|
+
flushSync(() => {
|
|
651
|
+
render((error: unknown) => {
|
|
652
|
+
caught = error;
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
if (caught !== null) {
|
|
656
|
+
root.unmount();
|
|
657
|
+
throw caught instanceof Error ? caught : new Error(String(caught));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Same route-decided recorder as the board branch above.
|
|
661
|
+
const writeBackend = await tierSourceWriteBackend();
|
|
662
|
+
const adapter = new ReactRootAuthoringAdapter(layer, store, {
|
|
663
|
+
...(writeBackend ? { writeBackend } : {}),
|
|
664
|
+
});
|
|
665
|
+
if (activationDocumentId) {
|
|
666
|
+
requestAnimationFrame(() => recordViewportFirstFrame(activationDocumentId));
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
adapter,
|
|
670
|
+
dispose: () => {
|
|
671
|
+
// A4 (spec 27 §3) — release the adapter's `vite:afterUpdate` echo-reconcile
|
|
672
|
+
// subscription before dropping its DOM, so listeners don't accumulate across
|
|
673
|
+
// layer remounts (a leaked one is harmless — it early-returns on an empty echo —
|
|
674
|
+
// but this keeps teardown clean).
|
|
675
|
+
adapter.disposeReactRootAdapter();
|
|
676
|
+
root.unmount();
|
|
677
|
+
game.dispose();
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** A cheap identity for the project's current story MEMBERSHIP — which
|
|
683
|
+
* stories exist, not their content. An unreadable module counts as its own
|
|
684
|
+
* member, so a `.stories.tsx` that starts and stops parsing still moves the
|
|
685
|
+
* signature. */
|
|
686
|
+
function storyMembershipSignature(): string {
|
|
687
|
+
return getProjectStoryModules()
|
|
688
|
+
.map((module_) =>
|
|
689
|
+
module_.ok ? module_.stories.map((story) => story.id).join(',') : `!${module_.modulePath}`,
|
|
690
|
+
)
|
|
691
|
+
.join('|');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* THIS MEDIUM'S STALENESS RULE (`DesignTimeMount.reprojectWhen`), moved here
|
|
696
|
+
* with the mount it is about.
|
|
697
|
+
*
|
|
698
|
+
* Story frames re-render through react-refresh; a host with no HMR channel at
|
|
699
|
+
* all kept showing the module the board mounted with after a source write — a dragged
|
|
700
|
+
* crosshair stayed where the live preview left it, and an UNDO (source back
|
|
701
|
+
* to `left: '50%'`) changed nothing on screen until F5. "Ctrl+Z just blinked"
|
|
702
|
+
* for every Windows tester and, measured on the instrument, for macOS too
|
|
703
|
+
* (runhuman passes 135-142; screen read 2026-09-03).
|
|
704
|
+
*
|
|
705
|
+
* The registry's publish after a content write is the moment the fresh module
|
|
706
|
+
* EXISTS, so that is the signal. The module set is compared because the
|
|
707
|
+
* registry notifies at the START of a refresh too (not ready, old modules):
|
|
708
|
+
* acting on the start notification remounted the board with the stale module
|
|
709
|
+
* and then dropped the real publish as "the mount's own" — the crosshair
|
|
710
|
+
* snapped back to its pre-drag spot with the source already at the new one
|
|
711
|
+
* (preview build 68, screen read).
|
|
712
|
+
*
|
|
713
|
+
* RE-PROJECT rather than remount: the host keeps the old layer up until the
|
|
714
|
+
* new one settles, because an empty board on top of the old one IS the flash
|
|
715
|
+
* (runhuman pass 146).
|
|
716
|
+
*
|
|
717
|
+
* ## THE SECOND RULE THIS CARRIES, arrived 2026-09-19 (WORK.md §The
|
|
718
|
+
* open-source launch item 18a)
|
|
719
|
+
*
|
|
720
|
+
* Story MEMBERSHIP — which stories exist, not what is in them — changing
|
|
721
|
+
* while the board is mounted is the other reason its frames go stale: a story
|
|
722
|
+
* saved from the editor's own header, or a hand-edited `.stories.tsx`, was
|
|
723
|
+
* invisible anywhere until a full reload (blind-walk beat 9). That rule lived
|
|
724
|
+
* in `@editor/components/world-documents.tsx`'s `RootDocumentContent`, which
|
|
725
|
+
* mounts the dom board AND the canvas Scene, so a canvas world's whole
|
|
726
|
+
* `<Application>` was being torn down and rebuilt every time an unrelated dom
|
|
727
|
+
* story was saved. It is a statement about a MEDIUM's mounted layers, so it
|
|
728
|
+
* belongs on that medium's mount, which is what `DesignTimeMount.reprojectWhen`
|
|
729
|
+
* is; the stack binds it per medium and applies it only to that medium's
|
|
730
|
+
* candidates (`design-time-layers.ts:717-742`).
|
|
731
|
+
*
|
|
732
|
+
* The two rules differ by TIER and by SIGNAL, which is why one hook carries
|
|
733
|
+
* both rather than two firing on the same publish: the content rule is
|
|
734
|
+
* the MEMBERSHIP of the story set: nothing adds a frame to a mounted board by
|
|
735
|
+
* itself, so a board re-projects when which stories exist changes. A CONTENT
|
|
736
|
+
* change does not re-project — the session's frames re-render through
|
|
737
|
+
* react-refresh.
|
|
738
|
+
*/
|
|
739
|
+
export function reprojectWhenStoriesRepublish(invalidate: () => void): () => void {
|
|
740
|
+
// The module set the board was last mounted from, and — separately — the
|
|
741
|
+
// MEMBERSHIP of that set (which stories exist, not their content).
|
|
742
|
+
let mountedModules: readonly unknown[] | null = projectStoriesReady()
|
|
743
|
+
? getProjectStoryModules()
|
|
744
|
+
: null;
|
|
745
|
+
let mountedMembership = mountedModules === null ? null : storyMembershipSignature();
|
|
746
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
747
|
+
const stop = subscribeProjectStoryModules(() => {
|
|
748
|
+
if (!projectStoriesReady()) return;
|
|
749
|
+
const modules = getProjectStoryModules();
|
|
750
|
+
const membership = storyMembershipSignature();
|
|
751
|
+
if (mountedModules === null) {
|
|
752
|
+
// The mount's own publish: remember it, re-project nothing.
|
|
753
|
+
mountedModules = modules;
|
|
754
|
+
mountedMembership = membership;
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const membershipChanged = membership !== mountedMembership;
|
|
758
|
+
mountedModules = modules;
|
|
759
|
+
mountedMembership = membership;
|
|
760
|
+
if (!membershipChanged) return;
|
|
761
|
+
if (timer !== null) clearTimeout(timer);
|
|
762
|
+
timer = setTimeout(() => {
|
|
763
|
+
timer = null;
|
|
764
|
+
// The registry just published, so the mount about to run must not
|
|
765
|
+
// refresh it again: reloading every story module in parallel is the
|
|
766
|
+
// storm the content-write refresh was built to avoid. The host's
|
|
767
|
+
// re-projection runs `mountReactDesignLayer` synchronously enough for
|
|
768
|
+
// the flag to still be set when it reads it (it reads before its first
|
|
769
|
+
// `await`), and the clear is a macrotask behind that.
|
|
770
|
+
skipStoryRefreshForRemount = true;
|
|
771
|
+
invalidate();
|
|
772
|
+
setTimeout(() => {
|
|
773
|
+
skipStoryRefreshForRemount = false;
|
|
774
|
+
}, 0);
|
|
775
|
+
}, 250);
|
|
776
|
+
});
|
|
777
|
+
return () => {
|
|
778
|
+
if (timer !== null) clearTimeout(timer);
|
|
779
|
+
timer = null;
|
|
780
|
+
stop();
|
|
781
|
+
};
|
|
782
|
+
}
|