@volter/editor-game 0.5.66 → 0.5.68
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/contributions/audio-unlock.service.ts +2 -2
- package/contributions/autoplay.service.ts +1 -1
- package/contributions/bridge.command.ts +3 -3
- package/contributions/build-player.document.tsx +95 -0
- package/contributions/canvas/component-board.service.ts +16 -0
- package/contributions/canvas/design-time-mount.service.ts +45 -0
- package/contributions/canvas-story-capture.service.ts +12 -0
- package/contributions/edit-mode-audio.service.ts +2 -2
- package/contributions/edit-mode-networking.service.ts +1 -1
- package/contributions/gameplay.command.ts +4 -4
- package/contributions/generation.service.ts +1 -1
- package/contributions/godot.style.ts +26 -6
- package/contributions/godot.view.ts +6 -0
- package/contributions/ingest.service.ts +2 -2
- package/contributions/instances.command.ts +1 -1
- package/contributions/navmesh.menu.ts +1 -1
- package/contributions/network-observer.service.ts +14 -0
- package/contributions/play.command.ts +10 -4
- package/contributions/react/component-board.service.ts +1 -1
- package/contributions/react/design-time-mount.service.ts +1 -1
- package/contributions/scene-document.service.ts +3 -3
- package/contributions/state-watch.menu.ts +1 -1
- package/contributions/state-watch.utility.tsx +1 -1
- package/contributions/team-playtest.service.ts +4 -4
- package/contributions/three/component-board.service.ts +1 -1
- package/contributions/three/component-verbs.command.ts +6 -6
- package/contributions/three/three-authoring.service.ts +8 -5
- package/contributions/three-story-capture.service.ts +12 -0
- package/contributions/unity.style.ts +17 -7
- package/contributions/unity.view.ts +6 -0
- package/contributions/unreal.style.ts +10 -2
- package/contributions/unreal.view.ts +5 -0
- package/package.json +21 -10
- package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
- package/src/asset-budget/asset-budget-model.ts +1 -1
- package/src/audio/AudioDebuggerPanel.tsx +1 -1
- package/src/bridge/dispatch.ts +14 -14
- package/src/bridge/live-frames.ts +1 -1
- package/src/bridge/screenshot.ts +3 -3
- package/src/build/BuildProfilesPanel.tsx +14 -3
- package/src/build/build-session.ts +35 -0
- package/src/canvas/canvas-board/CanvasBoardDocument.tsx +749 -0
- package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
- package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
- package/src/canvas/canvas-design-mount.ts +524 -0
- package/src/canvas/design-time-canvas-mount.ts +79 -0
- package/src/coverage/live-authoring-surface.ts +5 -5
- package/src/coverage/live-project-verbs.ts +1 -1
- package/src/coverage/native-system-coverage.ts +5 -5
- package/src/coverage/root-coverage.ts +1 -1
- package/src/coverage/session-coverage.ts +3 -3
- package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
- package/src/edit-mode/edit-mode-audio.ts +4 -4
- package/src/edit-mode/edit-mode-networking.ts +2 -2
- package/src/game-document/GameCaptureFrameButton.tsx +1 -1
- package/src/game-document/GameDocument.tsx +2 -2
- package/src/game-document/GamePanel.tsx +4 -4
- package/src/game-document/InstanceInspectorPicker.tsx +2 -2
- package/src/game-document/crowd-debug.ts +1 -1
- package/src/game-document/device-preview.ts +10 -11
- package/src/game-document/physics-debug.ts +1 -1
- package/src/generation/GenerationActivity.tsx +3 -3
- package/src/generation/generation-documents.tsx +5 -5
- package/src/generation/generation-jobs.ts +1 -1
- package/src/host/adapter-runtime-bindings.ts +96 -5
- package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
- package/src/host/authoring/gesture-persist.ts +1 -1
- package/src/host/authoring/ingest-data-writer.ts +1 -1
- package/src/host/authoring/ingest-source-persistence.ts +4 -4
- package/src/host/authoring/mounted-authoring.ts +4 -4
- package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
- package/src/host/authoring/pixi-authoring-adapter.ts +178 -31
- package/src/host/authoring/pixi-creatable-kinds.ts +62 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
- package/src/host/authoring/pixi-live-write-target.ts +4 -4
- package/src/host/authoring/pixi-source-identity.ts +3 -3
- package/src/host/authoring/pixi-source-write-target.ts +1709 -0
- package/src/host/authoring/pixi-still-presentation.ts +1 -1
- package/src/host/authoring/pixi-structure-history.ts +2 -2
- package/src/host/authoring/pixi-transform-channels.ts +16 -14
- package/src/host/authoring/source-persistence-backend.ts +3 -3
- package/src/host/authoring/struct-write-pipe.ts +1 -1
- package/src/host/binding-resolver.ts +8 -9
- package/src/host/browser-transpile.ts +1 -1
- package/src/host/canvas-entry-runtime.ts +58 -47
- package/src/host/canvas-preview-frames.ts +482 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
- package/src/host/components/HeaderTelemetry.tsx +4 -4
- package/src/host/components/PixiIsolationSceneContent.tsx +11 -11
- package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
- package/src/host/components/frame-debugger-model.ts +2 -2
- package/src/host/components/header-telemetry-model.ts +2 -2
- package/src/host/components/scene-document.tsx +14 -14
- package/src/host/components/utility-view-state.ts +1 -1
- package/src/host/components/world-root-stage-binding.tsx +12 -12
- package/src/host/components/world-root-stage.ts +70 -49
- package/src/host/coverage/system-adapter-coverage.ts +3 -4
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
- package/src/host/document-preview-three.ts +1 -1
- package/src/host/entry-adjudication.ts +6 -6
- package/src/host/game-css-scope-transform.ts +4 -0
- package/src/host/game-realm-page.ts +1 -1
- package/src/host/gameplay-export.ts +25 -14
- package/src/host/gameplay-recording.ts +5 -5
- package/src/host/gated-globals.ts +2 -2
- package/src/host/history/json-history-resource.ts +1 -1
- package/src/host/projection/pixi.ts +24 -2
- package/src/host/r3f-entry-runtime.ts +65 -34
- package/src/host/react-mount-runtime.ts +7 -48
- package/src/host/realm-services.ts +1 -1
- package/src/host/roots/canvas-root.tsx +373 -0
- package/src/host/roots/r3f-root.tsx +473 -0
- package/src/host/roots/react-root.ts +9 -43
- package/src/host/served-bundle-runtime-modules.ts +3 -19
- package/src/host/server-log-bridge.ts +2 -2
- package/src/host/stories/mounted-story-viewport-source.ts +1 -1
- package/src/host/stories/pixi-story-model.ts +30 -0
- package/src/host/stories/story-media-captures.ts +46 -0
- package/src/host/stories/story-media-presence.ts +3 -3
- package/src/host/stories/story-pixi-preview.ts +408 -0
- package/src/host/stories/story-three-preview.ts +806 -0
- package/src/host/stories/three-story-captures.ts +35 -0
- package/src/host/story-three-preview-runtime.ts +56 -0
- package/src/host/use-active-performance-source.ts +2 -2
- package/src/host/viewport-pose-memory.ts +1 -1
- package/src/host/viewport-root-presentation.ts +6 -5
- package/src/ingest/active-ingest.ts +1 -1
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
- package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
- package/src/ingest/deferred-ingest-play.ts +6 -6
- package/src/ingest/discovery-public-ingest.ts +2 -2
- package/src/ingest/ingest-boot-viewport.ts +2 -2
- package/src/ingest/ingest-canvas-scene-document.tsx +9 -9
- package/src/ingest/ingest-canvas-scene.ts +3 -3
- package/src/ingest/ingest-evidence-hook.ts +1 -1
- package/src/ingest/ingest-frame-snapshot.ts +1 -1
- package/src/ingest/ingest-play-control.ts +1 -1
- package/src/ingest/ingest-render-debug.ts +10 -10
- package/src/ingest/ingest-siblings.ts +13 -25
- package/src/ingest/module-mode.ts +14 -14
- package/src/ingest/mount-canvas-ingest-root.ts +21 -21
- package/src/ingest/mount-coverage.ts +2 -2
- package/src/ingest/mount-dom-ingest-root.ts +11 -11
- package/src/ingest/mount-ingest-root.ts +8 -8
- package/src/ingest/mount-three-ingest-root.ts +8 -8
- package/src/ingest/resolve-canvas.ts +1 -1
- package/src/ingest/served-html-boot.ts +1 -1
- package/src/ingest/surface-canvas.ts +1 -1
- package/src/ingest/unmount-ingest-root.ts +5 -5
- package/src/navmesh/navmesh-handler.ts +24 -16
- package/src/network/NetworkInspectorPanel.tsx +939 -37
- package/src/network/network-inspector-model.ts +20 -2
- package/src/play/play-log-events.ts +1 -1
- package/src/play/play-mode.ts +76 -133
- package/src/play/play-recording.ts +1 -1
- package/src/play/react-play-live-authoring.ts +3 -3
- package/src/play/run-selection.ts +93 -0
- package/src/play-bar/PlayBar.tsx +20 -39
- package/src/profiler/FrameDebuggerPanel.tsx +1 -1
- package/src/profiler/PerformancePanel.tsx +3 -3
- package/src/react/design-time-react-mount.ts +23 -65
- package/src/react/dom-authoring-adapter.ts +9 -9
- package/src/react/react-inspector-section.tsx +5 -5
- package/src/react/react-world-authoring-adapter.ts +11 -11
- package/src/react/story-documents/story-documents.tsx +9 -9
- package/src/react/ui-board-document.tsx +7 -7
- package/src/react/ui-component-board.ts +2 -2
- package/src/runtime/adapter/audio-meter.ts +21 -0
- package/src/runtime/adapter/first-party-audio-system.ts +230 -0
- package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
- package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
- package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
- package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
- package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
- package/src/runtime/adapter/native-debug-module.ts +498 -0
- package/src/runtime/audio/bus-mixer.ts +161 -0
- package/src/runtime/audio/pose-guard.ts +80 -0
- package/src/runtime/core/frame-pacing.ts +126 -0
- package/src/runtime/core/game-loop.ts +225 -0
- package/src/runtime/core/game-scoped-slot.ts +28 -0
- package/src/runtime/core/seeded-random.ts +162 -0
- package/src/runtime/core/sim-clock.ts +391 -0
- package/src/runtime/core/system-runner.ts +269 -0
- package/src/runtime/core/types.ts +104 -0
- package/src/runtime/create-runtime.ts +1128 -0
- package/src/runtime/debug-bridge.ts +570 -0
- package/src/runtime/debug-registry.ts +899 -0
- package/src/runtime/dev/chrome-trace.ts +153 -0
- package/src/runtime/dev/instruments.ts +403 -0
- package/src/runtime/dev/logger.ts +119 -0
- package/src/runtime/dev/performance-profiler.ts +367 -0
- package/src/runtime/dev/register-render-vitals.ts +276 -0
- package/src/runtime/dev/render-census.ts +354 -0
- package/src/runtime/dev/render-debug-adapter.ts +218 -0
- package/src/runtime/dev/render-memory.ts +226 -0
- package/src/runtime/dev/render-vitals.ts +338 -0
- package/src/runtime/dev/static-batch-advisor.ts +188 -0
- package/src/runtime/dev/webgl-frame-capture.ts +366 -0
- package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
- package/src/runtime/dev-build.ts +47 -0
- package/src/runtime/game.ts +1636 -0
- package/src/runtime/gameplay-rng-trap.ts +135 -0
- package/src/runtime/host-context.ts +64 -0
- package/src/runtime/input-router.ts +182 -0
- package/src/runtime/mount-manifest.ts +480 -0
- package/src/runtime/pixi/authoring.ts +706 -0
- package/src/runtime/pixi/ingest.ts +116 -0
- package/src/runtime/pixi/physics-registry.ts +49 -0
- package/src/runtime/pixi/render-pass-bracket.ts +117 -0
- package/src/runtime/pixi/scene-capture.ts +179 -0
- package/src/runtime/pixi/system-adapters.ts +69 -0
- package/src/runtime/playtest.ts +22 -0
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-control.ts +642 -0
- package/src/runtime/render-seed.ts +77 -0
- package/src/runtime/run-ticks-settled.ts +73 -0
- package/src/runtime/setup/setup-audio.ts +72 -0
- package/src/services/audio-pose-guard.ts +2 -2
- package/src/services/game-audio.ts +152 -0
- package/src/services/game-network.ts +767 -0
- package/src/services/game-physics.ts +334 -0
- package/src/state-watch/StateWatchPanel.tsx +1 -1
- package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
- package/src/three/authoring/constraint-inspector-section.tsx +6 -5
- package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
- package/src/three/authoring/oid-source-persistence.ts +7 -7
- package/src/three/authoring/r3f-design-session.ts +58 -54
- package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
- package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
- package/src/three/authoring/three-authoring-adapter.ts +29 -29
- package/src/three/component-verbs/extract-menu.ts +7 -6
- package/src/three/component-verbs/fork-menu.ts +7 -6
- package/src/three/component-verbs/internals-menu.ts +2 -2
- package/src/three/story-documents/three-story-documents.tsx +13 -13
- package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
- package/src/three/three-board/board-scene.ts +6 -6
- package/src/three/three-board/three-component-board.ts +2 -2
- package/src/services/game-audio-unlock.ts +0 -48
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT IS ON THE 2D BOARD — the canvas board's membership and its mount pass,
|
|
3
|
+
* kept off the React tree so both are readable (and testable) without a
|
|
4
|
+
* document.
|
|
5
|
+
*
|
|
6
|
+
* The board itself is DOM: `CanvasBoardDocument.tsx` lays these rows out with
|
|
7
|
+
* the shared story-board substrate (`authoring/react-story-board.ts`), the same
|
|
8
|
+
* one the `UI` board uses. This module answers only WHICH stories are on it,
|
|
9
|
+
* WHAT each frame is, and how big the frame must be.
|
|
10
|
+
*
|
|
11
|
+
* ## Membership — one exhibit per COMPONENT
|
|
12
|
+
*
|
|
13
|
+
* The unit is the component, not the story VARIANT: a prefab with five
|
|
14
|
+
* design-time states appears once, in the state `pickComponentPreviewStory`
|
|
15
|
+
* selects, and its other states live in its own story frames elsewhere. That is
|
|
16
|
+
* the 3D board's rule and the 3D board's join, so no surface can disagree with
|
|
17
|
+
* another about which state represents a component.
|
|
18
|
+
*
|
|
19
|
+
* ## Membership — declared `canvas`, then mounted as an exhibit
|
|
20
|
+
*
|
|
21
|
+
* A story is a candidate iff `declaredStoryMedium` says `canvas`. An
|
|
22
|
+
* undeclared story is a named gap and is never a candidate — the board does
|
|
23
|
+
* not mount to guess a medium. A declared canvas story is then mounted
|
|
24
|
+
* (`stories/story-pixi-preview.ts`) as the exhibit; a mount that yields no
|
|
25
|
+
* content, or throws, lands in {@link CanvasBoard.skipped} with its reason.
|
|
26
|
+
*
|
|
27
|
+
* Mounts run ONE AT A TIME, on the process-wide turn every other story mount
|
|
28
|
+
* takes (`stories/story-mount-turn.ts`): a story's loaders touch process-global
|
|
29
|
+
* state — Pixi's `Assets` cache and the `@pixi/react` component catalogue are
|
|
30
|
+
* both module-global — so overlapping two mounts interleaves their setup.
|
|
31
|
+
*
|
|
32
|
+
* ## Ghost slots — the project's story-LESS canvas components
|
|
33
|
+
*
|
|
34
|
+
* A `canvas`-surface component with NO story must be VISIBLE AS MISSING rather
|
|
35
|
+
* than silently absent. Discovery reuses the Content gallery's own machinery,
|
|
36
|
+
* never a second scan: the caller hands in the `listProjectComponents()` index
|
|
37
|
+
* and the story↔component join is `pickComponentPreviewStory`. A ghost gets a
|
|
38
|
+
* reserved, named frame and deliberately NO fabricated render — an empty frame
|
|
39
|
+
* is honest and a guessed sprite is not.
|
|
40
|
+
*
|
|
41
|
+
* ## Resource ownership (stated here, once)
|
|
42
|
+
*
|
|
43
|
+
* Each exhibit owns ONE story mount, and {@link disposeCanvasBoard} is the ONE
|
|
44
|
+
* teardown path for the set. A mount owns its own react root, its Application,
|
|
45
|
+
* its renderer and its host element; nothing here destroys any of those
|
|
46
|
+
* directly. A build superseded mid-flight frees what it already mounted inside
|
|
47
|
+
* its own turn, so a caller never receives a board it would have to tear down
|
|
48
|
+
* out of turn.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import type { ProjectComponentEntry } from '@volter/editor-sdk/kit/asset-workflow/project-content';
|
|
52
|
+
import { getProjectStoryRegions } from '@volter/editor-sdk/kit/stories/project-story-regions';
|
|
53
|
+
import {
|
|
54
|
+
declaredStoryMedium,
|
|
55
|
+
reportUndeclaredStoryMedium,
|
|
56
|
+
} from '@volter/editor-sdk/kit/stories/story-declared-medium';
|
|
57
|
+
import type { StoryPreviewComponent } from '@volter/editor-sdk/kit/stories/story-preview-component';
|
|
58
|
+
import { mountedStoryHasPixiContent } from '../../host/stories/pixi-story-model';
|
|
59
|
+
import { deriveStoryGroupPath, storyGroupKey } from '@volter/editor-sdk/kit/stories/story-grouping';
|
|
60
|
+
import {
|
|
61
|
+
type MountedStoryPixi,
|
|
62
|
+
type PixiStoryMountInTurn,
|
|
63
|
+
withPixiStoryMountTurn,
|
|
64
|
+
} from '../../host/stories/story-pixi-preview';
|
|
65
|
+
import { type ProjectStoryModule, pickComponentPreviewStory } from '@volter/editor-sdk/kit/stories/story-registry';
|
|
66
|
+
|
|
67
|
+
/** The district every ghost slot belongs to, so the storyless components
|
|
68
|
+
* gather in one trailing block instead of scattering through the museum. */
|
|
69
|
+
export const GHOST_DISTRICT = 'No story yet';
|
|
70
|
+
|
|
71
|
+
/** The square a ghost frame reserves, in px. Not a claim about the component's
|
|
72
|
+
* size — the frame is empty — just enough room to read as a reserved place. */
|
|
73
|
+
export const GHOST_FRAME_SIZE = { width: 160, height: 160 } as const;
|
|
74
|
+
|
|
75
|
+
/** The cell a canvas story gets when its content reports no usable bounds. */
|
|
76
|
+
const FALLBACK_FRAME_SIZE = { width: 240, height: 180 } as const;
|
|
77
|
+
/** Small sprites still need a selectable frame and a non-colliding label. */
|
|
78
|
+
const MIN_CONTENT_FRAME_SIZE = 96;
|
|
79
|
+
|
|
80
|
+
export interface CanvasBoardCrop {
|
|
81
|
+
readonly x: number;
|
|
82
|
+
readonly y: number;
|
|
83
|
+
readonly width: number;
|
|
84
|
+
readonly height: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Who an exhibit is — the story it composes and what its frame is named. */
|
|
88
|
+
export interface CanvasBoardIdentity {
|
|
89
|
+
/** `<modulePath>#<storyName>` — stable within one project. */
|
|
90
|
+
readonly id: string;
|
|
91
|
+
readonly storyName: string;
|
|
92
|
+
readonly modulePath: string;
|
|
93
|
+
/**
|
|
94
|
+
* The DISTRICT this exhibit stands in — the story group path's ancestors
|
|
95
|
+
* (`Gameplay` for a `Gameplay/Hero` story, `prefabs` for an untitled
|
|
96
|
+
* `src/prefabs/Hero.stories.tsx`), empty when the story is ungrouped.
|
|
97
|
+
*
|
|
98
|
+
* The board's unit is the COMPONENT — one exhibit each — so clustering by
|
|
99
|
+
* component would give a column of one-frame blocks. The shelf a museum
|
|
100
|
+
* wants is the group: every Gameplay prefab side by side, comparable at a
|
|
101
|
+
* glance. That is what the district is, and it is derived with the shared
|
|
102
|
+
* grouping model rather than a second one.
|
|
103
|
+
*/
|
|
104
|
+
readonly district: string;
|
|
105
|
+
/** What the frame's name prints. The CSF export name alone is NOT it:
|
|
106
|
+
* `Default` is the conventional export, so a district of five components
|
|
107
|
+
* would name five different things `Default`. The group's leaf names the
|
|
108
|
+
* component; the story's own label is appended only where one leaf
|
|
109
|
+
* contributes more than one exhibit, the only case where it disambiguates. */
|
|
110
|
+
readonly label: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** One placed exhibit: who it is, its live mount, and the content frame the
|
|
114
|
+
* board presents around that mount. */
|
|
115
|
+
export interface CanvasBoardExhibit extends CanvasBoardIdentity {
|
|
116
|
+
readonly mounted: MountedStoryPixi;
|
|
117
|
+
readonly frameSize: { readonly width: number; readonly height: number };
|
|
118
|
+
/** Portion of the mounting canvas the frame presents. The Application is a
|
|
119
|
+
* harness; only a standard Storybook `layout: 'fullscreen'` story presents
|
|
120
|
+
* its entire declared canvas. */
|
|
121
|
+
readonly crop: CanvasBoardCrop;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** One storyless canvas component's reserved slot. */
|
|
125
|
+
export interface CanvasBoardGhost {
|
|
126
|
+
/** `<path>#<name>` — the frame id. */
|
|
127
|
+
readonly key: string;
|
|
128
|
+
readonly name: string;
|
|
129
|
+
/** Project-root-relative source file. */
|
|
130
|
+
readonly path: string;
|
|
131
|
+
readonly line: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A story that could not become an exhibit, and why. */
|
|
135
|
+
export interface CanvasBoardSkip {
|
|
136
|
+
readonly id: string;
|
|
137
|
+
readonly modulePath: string;
|
|
138
|
+
readonly storyName: string;
|
|
139
|
+
readonly reason: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Everything one build produced. */
|
|
143
|
+
export interface CanvasBoard {
|
|
144
|
+
readonly exhibits: readonly CanvasBoardExhibit[];
|
|
145
|
+
readonly ghosts: readonly CanvasBoardGhost[];
|
|
146
|
+
readonly skipped: readonly CanvasBoardSkip[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The skip tally at the granularity that matches reality: a skip is per STORY,
|
|
151
|
+
* but the cause is almost always per MODULE, so a bare count reads as N
|
|
152
|
+
* independent failures when it is one module that simply is not a canvas one.
|
|
153
|
+
*/
|
|
154
|
+
export function summarizeCanvasBoardSkips(skipped: readonly CanvasBoardSkip[]): {
|
|
155
|
+
readonly stories: number;
|
|
156
|
+
readonly modules: number;
|
|
157
|
+
} {
|
|
158
|
+
return {
|
|
159
|
+
stories: skipped.length,
|
|
160
|
+
modules: new Set(skipped.map((entry) => entry.modulePath)).size,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface CanvasBoardCandidate {
|
|
165
|
+
readonly identity: CanvasBoardIdentity;
|
|
166
|
+
readonly Component: StoryPreviewComponent;
|
|
167
|
+
readonly layout: 'centered' | 'padded' | 'fullscreen';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function storyLayout(parameters: Record<string, unknown>): CanvasBoardCandidate['layout'] {
|
|
171
|
+
const layout = parameters['layout'];
|
|
172
|
+
return layout === 'fullscreen' || layout === 'padded' || layout === 'centered'
|
|
173
|
+
? layout
|
|
174
|
+
: 'padded';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* ONE EXHIBIT PER PREFAB — is this story the one that represents its component
|
|
179
|
+
* on the board? The 3D board's rule and the 3D board's join. A story whose CSF
|
|
180
|
+
* declares no `meta.component` is joined to nothing and keeps its own exhibit.
|
|
181
|
+
*/
|
|
182
|
+
function isComponentPreviewStory(
|
|
183
|
+
modules: readonly ProjectStoryModule[],
|
|
184
|
+
modulePath: string,
|
|
185
|
+
story: { name: string; componentName?: string },
|
|
186
|
+
): boolean {
|
|
187
|
+
if (!story.componentName) return true;
|
|
188
|
+
const picked = pickComponentPreviewStory(modules, story.componentName, modulePath);
|
|
189
|
+
return !picked || (picked.modulePath === modulePath && picked.name === story.name);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The composed stories that REPRESENT a component, named through the shared
|
|
193
|
+
* story-grouping model. */
|
|
194
|
+
export function collectCanvasBoardCandidates(
|
|
195
|
+
modules: readonly ProjectStoryModule[],
|
|
196
|
+
): CanvasBoardCandidate[] {
|
|
197
|
+
const draft: {
|
|
198
|
+
identity: Omit<CanvasBoardIdentity, 'label'>;
|
|
199
|
+
leafKey: string;
|
|
200
|
+
leaf: string;
|
|
201
|
+
storyLabel: string;
|
|
202
|
+
Component: StoryPreviewComponent;
|
|
203
|
+
layout: 'centered' | 'padded' | 'fullscreen';
|
|
204
|
+
}[] = [];
|
|
205
|
+
const perLeaf = new Map<string, number>();
|
|
206
|
+
const regions = getProjectStoryRegions();
|
|
207
|
+
for (const module_ of modules) {
|
|
208
|
+
if (!module_.ok) continue;
|
|
209
|
+
for (const story of module_.stories) {
|
|
210
|
+
const declared = declaredStoryMedium({ modulePath: module_.modulePath, regions });
|
|
211
|
+
if (declared.medium !== 'canvas') {
|
|
212
|
+
if (declared.via === 'undeclared') {
|
|
213
|
+
reportUndeclaredStoryMedium(module_.modulePath, declared.reason);
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!isComponentPreviewStory(modules, module_.modulePath, story)) continue;
|
|
218
|
+
const group = deriveStoryGroupPath({
|
|
219
|
+
modulePath: module_.modulePath,
|
|
220
|
+
...(story.title === undefined ? {} : { title: story.title }),
|
|
221
|
+
});
|
|
222
|
+
const district = storyGroupKey(group);
|
|
223
|
+
const leafKey = `${district}/${group.leaf}`;
|
|
224
|
+
perLeaf.set(leafKey, (perLeaf.get(leafKey) ?? 0) + 1);
|
|
225
|
+
draft.push({
|
|
226
|
+
identity: {
|
|
227
|
+
id: `${module_.modulePath}#${story.name}`,
|
|
228
|
+
storyName: story.name,
|
|
229
|
+
modulePath: module_.modulePath,
|
|
230
|
+
district,
|
|
231
|
+
},
|
|
232
|
+
leafKey,
|
|
233
|
+
leaf: group.leaf,
|
|
234
|
+
storyLabel: story.label,
|
|
235
|
+
Component: story.Component as unknown as StoryPreviewComponent,
|
|
236
|
+
layout: storyLayout(story.parameters),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return draft.map(({ identity, leafKey, leaf, storyLabel, Component, layout }) => ({
|
|
241
|
+
identity: {
|
|
242
|
+
...identity,
|
|
243
|
+
label: (perLeaf.get(leafKey) ?? 0) > 1 ? `${leaf} · ${storyLabel}` : leaf,
|
|
244
|
+
},
|
|
245
|
+
Component,
|
|
246
|
+
layout,
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The `canvas`-surface components no story represents — the ghost-slot set.
|
|
252
|
+
* The join is the Content gallery's own, so the two surfaces can never disagree
|
|
253
|
+
* about which components "have" a story. Inline-SVG entries are image assets,
|
|
254
|
+
* not components (`contentKind`), and other surfaces belong to other boards.
|
|
255
|
+
*/
|
|
256
|
+
export function collectStorylessCanvasComponents(
|
|
257
|
+
modules: readonly ProjectStoryModule[],
|
|
258
|
+
components: readonly ProjectComponentEntry[],
|
|
259
|
+
): CanvasBoardGhost[] {
|
|
260
|
+
const seen = new Set<string>();
|
|
261
|
+
const storyless: CanvasBoardGhost[] = [];
|
|
262
|
+
for (const component of components) {
|
|
263
|
+
if (component.surface !== 'canvas') continue;
|
|
264
|
+
if (component.contentKind === 'image') continue;
|
|
265
|
+
const key = `${component.path}#${component.name}`;
|
|
266
|
+
if (seen.has(key)) continue;
|
|
267
|
+
seen.add(key);
|
|
268
|
+
if (pickComponentPreviewStory(modules, component.name, component.path)) continue;
|
|
269
|
+
storyless.push({ key, name: component.name, path: component.path, line: component.line });
|
|
270
|
+
}
|
|
271
|
+
return storyless;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The frame is the story CONTENT, not the `<Application>` needed to mount it.
|
|
275
|
+
* Storybook's standard `fullscreen` layout is the explicit exception. */
|
|
276
|
+
function presentationOf(
|
|
277
|
+
mounted: MountedStoryPixi,
|
|
278
|
+
layout: CanvasBoardCandidate['layout'],
|
|
279
|
+
): { frameSize: { width: number; height: number }; crop: CanvasBoardCrop } {
|
|
280
|
+
const source = layout === 'fullscreen' ? mounted.app.screen : mounted.stage.getBounds();
|
|
281
|
+
const padding = layout === 'fullscreen' ? 0 : 8;
|
|
282
|
+
const crop: { x: number; y: number; width: number; height: number } = {
|
|
283
|
+
x: Math.floor(source.x - padding),
|
|
284
|
+
y: Math.floor(source.y - padding),
|
|
285
|
+
width: Math.ceil(source.width + padding * 2),
|
|
286
|
+
height: Math.ceil(source.height + padding * 2),
|
|
287
|
+
};
|
|
288
|
+
if (
|
|
289
|
+
!Number.isFinite(crop.width) ||
|
|
290
|
+
!Number.isFinite(crop.height) ||
|
|
291
|
+
crop.width < 1 ||
|
|
292
|
+
crop.height < 1
|
|
293
|
+
) {
|
|
294
|
+
return {
|
|
295
|
+
frameSize: { ...FALLBACK_FRAME_SIZE },
|
|
296
|
+
crop: { x: 0, y: 0, ...FALLBACK_FRAME_SIZE },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (layout !== 'fullscreen') {
|
|
300
|
+
const width = Math.max(MIN_CONTENT_FRAME_SIZE, crop.width);
|
|
301
|
+
const height = Math.max(MIN_CONTENT_FRAME_SIZE, crop.height);
|
|
302
|
+
crop.x -= (width - crop.width) / 2;
|
|
303
|
+
crop.y -= (height - crop.height) / 2;
|
|
304
|
+
crop.width = width;
|
|
305
|
+
crop.height = height;
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
frameSize: { width: crop.width, height: crop.height },
|
|
309
|
+
crop,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function describeError(error: unknown): string {
|
|
314
|
+
return error instanceof Error ? error.message : String(error);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function mountCandidates(
|
|
318
|
+
candidates: readonly CanvasBoardCandidate[],
|
|
319
|
+
signal: AbortSignal | undefined,
|
|
320
|
+
mount: PixiStoryMountInTurn,
|
|
321
|
+
): Promise<{ exhibits: CanvasBoardExhibit[]; skipped: CanvasBoardSkip[] }> {
|
|
322
|
+
const exhibits: CanvasBoardExhibit[] = [];
|
|
323
|
+
const skipped: CanvasBoardSkip[] = [];
|
|
324
|
+
for (const { identity, Component, layout } of candidates) {
|
|
325
|
+
if (signal?.aborted) break;
|
|
326
|
+
const skip = (reason: string): void => {
|
|
327
|
+
skipped.push({
|
|
328
|
+
id: identity.id,
|
|
329
|
+
modulePath: identity.modulePath,
|
|
330
|
+
storyName: identity.storyName,
|
|
331
|
+
reason,
|
|
332
|
+
});
|
|
333
|
+
};
|
|
334
|
+
try {
|
|
335
|
+
const mounted = await mount(Component);
|
|
336
|
+
if (!mountedStoryHasPixiContent(mounted.stage)) {
|
|
337
|
+
// Mounted, but rendered nothing — the honest "not a canvas story"
|
|
338
|
+
// outcome for a story that reconciles to an empty stage.
|
|
339
|
+
mounted.dispose();
|
|
340
|
+
skip('The story mounted no Pixi content.');
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
// The Application is the story's mounting harness, not an implicit
|
|
344
|
+
// artboard. Default/padded/centered stories expose only the native stage;
|
|
345
|
+
// prefab stories that want checkerboard transparency author Pixi's own
|
|
346
|
+
// `backgroundAlpha={0}` (as the shipped examples do). `fullscreen` is
|
|
347
|
+
// the explicit Storybook spelling for presenting the whole output
|
|
348
|
+
// surface, native background and all.
|
|
349
|
+
exhibits.push({ ...identity, mounted, ...presentationOf(mounted, layout) });
|
|
350
|
+
} catch (error) {
|
|
351
|
+
skip(describeError(error));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { exhibits, skipped };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Build the whole board from a story-registry snapshot plus the project's
|
|
359
|
+
* component index. Resolves once every story has either become an exhibit or
|
|
360
|
+
* been recorded as skipped, and every storyless `canvas` component has its slot.
|
|
361
|
+
*
|
|
362
|
+
* Builds never overlap. A `signal` supersedes this build: it stops mounting
|
|
363
|
+
* further stories, frees whatever it already mounted INSIDE its own turn, and
|
|
364
|
+
* rejects.
|
|
365
|
+
*/
|
|
366
|
+
export function buildCanvasBoard(
|
|
367
|
+
modules: readonly ProjectStoryModule[],
|
|
368
|
+
components: readonly ProjectComponentEntry[] = [],
|
|
369
|
+
signal?: AbortSignal,
|
|
370
|
+
): Promise<CanvasBoard> {
|
|
371
|
+
return withPixiStoryMountTurn(async (mount) => {
|
|
372
|
+
const { exhibits, skipped } = await mountCandidates(
|
|
373
|
+
collectCanvasBoardCandidates(modules),
|
|
374
|
+
signal,
|
|
375
|
+
mount,
|
|
376
|
+
);
|
|
377
|
+
const board: CanvasBoard = {
|
|
378
|
+
exhibits,
|
|
379
|
+
ghosts: collectStorylessCanvasComponents(modules, components),
|
|
380
|
+
skipped,
|
|
381
|
+
};
|
|
382
|
+
if (signal?.aborted) {
|
|
383
|
+
disposeInTurn(board);
|
|
384
|
+
throw new Error('The 2D board build was superseded by a newer one.');
|
|
385
|
+
}
|
|
386
|
+
return board;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Tear a built board down inside a story-mount turn, so its stories' effect
|
|
391
|
+
* cleanups never run in the middle of another build's mounts. */
|
|
392
|
+
export function disposeCanvasBoard(board: CanvasBoard): Promise<void> {
|
|
393
|
+
return withPixiStoryMountTurn(async () => {
|
|
394
|
+
disposeInTurn(board);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function disposeInTurn(board: CanvasBoard): void {
|
|
399
|
+
for (const exhibit of board.exhibits) {
|
|
400
|
+
try {
|
|
401
|
+
exhibit.mounted.dispose();
|
|
402
|
+
} catch (error) {
|
|
403
|
+
// biome-ignore lint/suspicious/noConsole: a failed unmount must stay diagnosable without stranding the remaining mounts
|
|
404
|
+
console.error('[canvas-board] a story mount failed to dispose.', error);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE `2D` BOARD's registration — one medium's answer to
|
|
3
|
+
* `@volter/editor-sdk/kit/component-board-registry`.
|
|
4
|
+
*
|
|
5
|
+
* The board stays behind a dynamic `import()`: the 2D board is the only board
|
|
6
|
+
* whose renderer initializes Pixi,
|
|
7
|
+
* so a project whose census says it has no canvas content never loads it. The
|
|
8
|
+
* asynchronous `install` contract exists for exactly this board.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ComponentBoard } from '@volter/editor-sdk/kit/component-board-registry';
|
|
12
|
+
import { editorConsole } from '@volter/editor-sdk/kit/editor-console';
|
|
13
|
+
import { CANVAS_COMPONENTS_DOCUMENT_ID } from '@volter/editor-sdk/kit/workspace-document-ids';
|
|
14
|
+
import {
|
|
15
|
+
getStoryMediaPresence,
|
|
16
|
+
subscribeStoryMediaPresence,
|
|
17
|
+
} from '../../host/stories/story-media-presence';
|
|
18
|
+
import { projectStoriesReady, subscribeProjectStoryModules } from '@volter/editor-sdk/kit/stories/story-registry';
|
|
19
|
+
|
|
20
|
+
/** The tab reads `2D`, the peer of `Scene` and `UI` in the center strip. */
|
|
21
|
+
const CANVAS_COMPONENTS_TITLE = '2D';
|
|
22
|
+
|
|
23
|
+
export const canvasComponentBoard: ComponentBoard = {
|
|
24
|
+
medium: 'canvas',
|
|
25
|
+
owner: '@volter/editor-game/canvas/canvas-board',
|
|
26
|
+
documentId: CANVAS_COMPONENTS_DOCUMENT_ID,
|
|
27
|
+
title: CANVAS_COMPONENTS_TITLE,
|
|
28
|
+
presence: () =>
|
|
29
|
+
!projectStoriesReady() ? null : getStoryMediaPresence().canvas ? 'present' : 'absent',
|
|
30
|
+
subscribe: (listener) => {
|
|
31
|
+
const stopPresence = subscribeStoryMediaPresence(listener);
|
|
32
|
+
const stopRegistry = subscribeProjectStoryModules(listener);
|
|
33
|
+
return () => {
|
|
34
|
+
stopPresence();
|
|
35
|
+
stopRegistry();
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
install: async () => {
|
|
39
|
+
try {
|
|
40
|
+
const { installCanvasBoardDocument } = await import('./CanvasBoardDocument');
|
|
41
|
+
installCanvasBoardDocument();
|
|
42
|
+
} catch (error: unknown) {
|
|
43
|
+
editorConsole.error(
|
|
44
|
+
`Failed to load the ${CANVAS_COMPONENTS_TITLE} board: ${String(error)}`,
|
|
45
|
+
'scene',
|
|
46
|
+
);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
absentReason: () =>
|
|
51
|
+
`This project has no ${CANVAS_COMPONENTS_TITLE} board (${CANVAS_COMPONENTS_DOCUMENT_ID}): ` +
|
|
52
|
+
"board presence keys on STORIES, not on roots, and none of this project's stories declares " +
|
|
53
|
+
'the "canvas" medium. Declare it — a manifest root entry naming the story\'s module, or a ' +
|
|
54
|
+
'`vgai.adapter.ts` regionInclude that covers it — and the board appears. `vgai console` ' +
|
|
55
|
+
'names every story it could not place.',
|
|
56
|
+
};
|