@volter/editor-game 0.5.65 → 0.5.67
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/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 +3 -3
- package/contributions/generations.status.tsx +1 -1
- package/contributions/godot.palette.json +99 -0
- package/contributions/godot.style.ts +82 -0
- package/contributions/godot.view.ts +75 -0
- package/contributions/ingest.service.ts +4 -4
- 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 +11 -5
- package/contributions/react/component-board.service.ts +1 -1
- package/contributions/react/design-time-mount.service.ts +1 -1
- package/contributions/react/react-inspector.service.ts +2 -2
- package/contributions/scene-document.service.ts +5 -5
- 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 +8 -8
- package/contributions/three/three-authoring.service.ts +8 -5
- package/contributions/three-story-capture.service.ts +12 -0
- package/contributions/unity.palette.json +102 -0
- package/contributions/unity.style.ts +72 -0
- package/contributions/unity.view.ts +82 -0
- package/contributions/unreal.palette.json +105 -0
- package/contributions/unreal.style.ts +66 -0
- package/contributions/unreal.view.ts +93 -0
- package/dist-node/serving.mjs +16 -0
- package/package.json +33 -16
- package/serving/index.ts +27 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
- package/src/asset-budget/asset-budget-model.ts +4 -4
- package/src/asset-budget/optimize-apply.ts +7 -7
- package/src/audio/AudioDebuggerPanel.tsx +1 -1
- package/src/bridge/dispatch.ts +16 -16
- package/src/bridge/live-frames.ts +1 -1
- package/src/bridge/screenshot.ts +6 -6
- package/src/build/BuildProfilesPanel.tsx +3 -3
- package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -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 +6 -6
- package/src/coverage/live-project-verbs.ts +2 -2
- package/src/coverage/native-system-coverage.ts +6 -6
- package/src/coverage/root-coverage.ts +1 -1
- package/src/coverage/session-coverage.ts +5 -5
- package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +13 -53
- 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 +2 -2
- package/src/game-document/GameDocument.tsx +2 -2
- package/src/game-document/GamePanel.tsx +8 -8
- 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 +6 -8
- package/src/generation/generation-documents.tsx +8 -8
- package/src/generation/generation-jobs.ts +2 -2
- package/src/generation/generation-presentation.ts +1 -1
- package/src/host/adapter-reach.ts +1 -1
- package/src/host/adapter-runtime-bindings.ts +97 -6
- package/src/host/api/configurations.ts +2 -2
- package/src/host/authoring/babylon-authoring-adapter.ts +8 -8
- package/src/host/authoring/contract-hierarchy-authoring.ts +1 -1
- package/src/host/authoring/contract-scenes-stories.ts +1 -1
- package/src/host/authoring/creation-site-related.ts +2 -2
- package/src/host/authoring/ephemeral-persistence.ts +1 -1
- package/src/host/authoring/gesture-persist.ts +2 -2
- package/src/host/authoring/ingest-data-writer.ts +1 -1
- package/src/host/authoring/ingest-source-persistence.ts +8 -8
- package/src/host/authoring/mount-isolated-pixi-screen.ts +1 -1
- 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 +42 -17
- package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
- package/src/host/authoring/pixi-live-write-target.ts +7 -7
- package/src/host/authoring/pixi-source-identity.ts +3 -3
- package/src/host/authoring/pixi-source-write-target.ts +1614 -0
- package/src/host/authoring/pixi-still-presentation.ts +2 -2
- package/src/host/authoring/pixi-structure-history.ts +3 -3
- package/src/host/authoring/pixi-transform-channels.ts +16 -14
- package/src/host/authoring/source-persistence-backend.ts +6 -6
- package/src/host/authoring/struct-write-pipe.ts +3 -3
- package/src/host/binding-resolver.ts +8 -9
- package/src/host/browser-transpile.ts +2 -2
- package/src/host/canvas-entry-runtime.ts +59 -48
- package/src/host/canvas-preview-frames.ts +482 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
- package/src/host/components/HeaderTelemetry.tsx +9 -16
- package/src/host/components/PixiIsolationSceneContent.tsx +17 -16
- package/src/host/components/ThreeIsolationSceneContent.tsx +5 -5
- 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 +29 -61
- package/src/host/components/utility-view-state.ts +1 -1
- package/src/host/components/world-root-stage-binding.tsx +17 -26
- package/src/host/components/world-root-stage.ts +130 -63
- package/src/host/coverage/capability-coverage.ts +13 -1
- package/src/host/coverage/game-contract-seam-evidence.ts +1 -1
- package/src/host/coverage/project-verb-coverage.ts +2 -17
- package/src/host/coverage/system-adapter-coverage.ts +4 -5
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +14 -8
- package/src/host/document-preview-three.ts +2 -2
- package/src/host/entry-adjudication.ts +6 -6
- package/src/host/game-css-scope-transform.ts +4 -0
- package/src/host/game-module-access.ts +1 -1
- package/src/host/game-realm-page.ts +16 -8
- package/src/host/game-realm-reclaim.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 +6 -6
- package/src/host/history/json-history-resource.ts +3 -3
- package/src/host/instance-extract-actions.ts +1 -1
- package/src/host/instance-fork-actions.ts +1 -1
- package/src/host/projection/dom.ts +2 -2
- package/src/host/projection/pixi.ts +25 -3
- package/src/host/r3f-entry-runtime.ts +65 -34
- package/src/host/react-mount-runtime.ts +8 -49
- package/src/host/realm-services.ts +2 -2
- package/src/host/roots/canvas-root.tsx +361 -0
- package/src/host/roots/module-root.ts +1 -1
- package/src/host/roots/r3f-root.tsx +473 -0
- package/src/host/roots/react-root.ts +9 -43
- package/src/host/scene-view-drawability.ts +2 -2
- package/src/host/served-bundle-runtime-modules.ts +1 -19
- package/src/host/server-log-bridge.ts +3 -3
- 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/three-ingest-runtime.ts +1 -1
- package/src/host/use-active-performance-source.ts +3 -3
- package/src/host/viewport-pose-memory.ts +1 -1
- package/src/host/viewport-root-presentation.ts +6 -5
- package/src/ingest/active-ingest.ts +2 -2
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
- package/src/ingest/authoring/ingest-root-adapter.ts +13 -13
- package/src/ingest/capture-wait-report.ts +1 -1
- package/src/ingest/deferred-ingest-play.ts +11 -10
- package/src/ingest/discovery-public-ingest.ts +2 -2
- package/src/ingest/ingest-boot-viewport.ts +6 -8
- package/src/ingest/ingest-canvas-scene-document.tsx +17 -16
- package/src/ingest/ingest-canvas-scene.ts +5 -8
- 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 +4 -4
- package/src/ingest/ingest-render-debug.ts +11 -11
- package/src/ingest/ingest-siblings.ts +16 -28
- package/src/ingest/module-mode.ts +18 -19
- package/src/ingest/mount-canvas-ingest-root.ts +27 -27
- package/src/ingest/mount-coverage.ts +4 -4
- package/src/ingest/mount-dom-ingest-root.ts +16 -16
- package/src/ingest/mount-ingest-root.ts +14 -14
- package/src/ingest/mount-three-ingest-root.ts +12 -12
- 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 +7 -7
- package/src/navmesh/navmesh-handler.ts +24 -16
- package/src/network/NetworkInspectorPanel.tsx +449 -14
- package/src/network/network-inspector-model.ts +14 -2
- package/src/play/play-log-events.ts +1 -1
- package/src/play/play-mode.ts +99 -142
- package/src/play/play-recording.ts +2 -2
- 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 +4 -4
- package/src/react/design-time-react-mount.ts +37 -85
- package/src/react/dom-authoring-adapter.ts +11 -11
- package/src/react/react-inspector-section.tsx +13 -13
- package/src/react/react-world-authoring-adapter.ts +31 -143
- package/src/react/story-documents/story-documents.tsx +18 -22
- package/src/react/ui-board-document.tsx +10 -11
- package/src/react/ui-component-board.ts +5 -5
- 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 +169 -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 +657 -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 +4 -3
- package/src/three/authoring/constraint-inspector-section.tsx +8 -7
- package/src/three/authoring/model-asset-inspector-section.tsx +14 -17
- package/src/three/authoring/oid-source-persistence.ts +13 -13
- package/src/three/authoring/r3f-design-session.ts +79 -61
- package/src/three/authoring/r3f-source-authoring-adapter.ts +112 -108
- package/src/three/authoring/reflection-probe-inspector-section.tsx +5 -4
- package/src/three/authoring/spatial-joint-handles.ts +1 -1
- package/src/three/authoring/spatial-lod-handles.ts +1 -1
- package/src/three/authoring/spatial-particle-handles.ts +1 -1
- package/src/three/authoring/three-authoring-adapter.ts +33 -33
- package/src/three/component-verbs/extract-menu.ts +8 -7
- package/src/three/component-verbs/fork-menu.ts +8 -7
- package/src/three/component-verbs/internals-menu.ts +3 -3
- package/src/three/drei-asset-caches.ts +13 -0
- package/src/three/story-documents/three-story-documents.tsx +24 -34
- package/src/three/three-board/ThreeBoardDocument.tsx +25 -25
- package/src/three/three-board/board-scene.ts +7 -7
- package/src/three/three-board/three-component-board.ts +5 -5
- package/contributions/react/pasteboard.action.ts +0 -41
- package/contributions/xstate-behavior.action.ts +0 -42
- package/contributions/xstate-behavior.document.tsx +0 -73
- package/contributions/xstate-behavior.inspector.tsx +0 -28
- package/contributions/xstate-behavior.menu.ts +0 -23
- package/src/react/pasteboard-materialize.ts +0 -154
- package/src/services/game-audio-unlock.ts +0 -48
- package/src/xstate/XStateBehaviorSection.tsx +0 -130
- package/src/xstate/XStateMachineInspector.tsx +0 -566
- package/src/xstate/character-animation-machine.fixture.ts +0 -74
- package/src/xstate/live-behaviors.ts +0 -106
- package/src/xstate/use-live-actor-state.ts +0 -44
- package/src/xstate/xstate-graph.ts +0 -235
- package/src/xstate/xstate-layout.ts +0 -76
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 2D COMPONENTS board — every qualifying canvas story laid out as a frame
|
|
3
|
+
* on the project's design canvas, each frame hosting that story's OWN mounted
|
|
4
|
+
* Pixi `Application`, playing its cycle, framed to the component's content.
|
|
5
|
+
*
|
|
6
|
+
* It is a PEER OF THE `UI` BOARD, not of the `3D` one. A canvas story renders a
|
|
7
|
+
* `<canvas>` — it is DOM — so it is an ordinary frame on the shared story-board
|
|
8
|
+
* substrate, and everything a Figma-style board needs is already there and
|
|
9
|
+
* reused verbatim:
|
|
10
|
+
*
|
|
11
|
+
* - frame layout, group clustering and view fitting — `authoring/react-story-board.ts`
|
|
12
|
+
* - frame/group names, zoom-independent and collision-free — `authoring/story-board-chrome-fit.ts`
|
|
13
|
+
* - pan + zoom — the ONE shared singleton `authoring/world-pan-state.ts`,
|
|
14
|
+
* driven by `authoring/react-canvas-navigation.ts`
|
|
15
|
+
* - the dot-grid canvas and the alpha checkerboard — `authoring/react-design-canvas-style.ts`
|
|
16
|
+
* - districts — `stories/story-grouping.ts`
|
|
17
|
+
*
|
|
18
|
+
* What is genuinely this board's own is only what goes IN a frame (an adopted
|
|
19
|
+
* Pixi host instead of a React render) and WHO is on it
|
|
20
|
+
* (`canvas-board-model.ts`).
|
|
21
|
+
*
|
|
22
|
+
* ## True texel scale
|
|
23
|
+
*
|
|
24
|
+
* A frame is the mounted Pixi content's bounds with a small transparent inset.
|
|
25
|
+
* The `<Application width height>` is only a mounting harness; it becomes the
|
|
26
|
+
* frame when the story opts into Storybook's standard `layout: 'fullscreen'`.
|
|
27
|
+
* There is no scale channel between art and frame. Magnification is the
|
|
28
|
+
* board's shared zoom, and while it magnifies, exhibits' canvases are told to
|
|
29
|
+
* magnify TEXELS rather than blur them (see `applyView` below).
|
|
30
|
+
*
|
|
31
|
+
* ## One clock, and one context per exhibit
|
|
32
|
+
*
|
|
33
|
+
* Each exhibit arrives STOPPED (`stories/story-pixi-preview.ts` never lets a
|
|
34
|
+
* design-time story run on wall time) and is driven from THIS document's single
|
|
35
|
+
* rAF loop, which runs only while the document is the active tab. Each exhibit
|
|
36
|
+
* keeps its own renderer, which is what draws its own canvas — so the exhibit
|
|
37
|
+
* count is bounded by the browser's WebGL context budget (order of sixteen per
|
|
38
|
+
* page), not by layout. That ceiling is stated in `MountedStoryPixi.app`.
|
|
39
|
+
*
|
|
40
|
+
* ## Ghost slots open nothing
|
|
41
|
+
*
|
|
42
|
+
* A storyless canvas component gets a reserved, named frame with a dashed edge
|
|
43
|
+
* and NO fabricated render. The board says what is missing where the missing
|
|
44
|
+
* thing would be.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { installReactCanvasNavigation } from '@volter/editor-sdk/kit/authoring/react-canvas-navigation';
|
|
48
|
+
import {
|
|
49
|
+
REACT_DESIGN_CANVAS_COLOR,
|
|
50
|
+
REACT_DESIGN_CANVAS_DOT,
|
|
51
|
+
} from '@volter/editor-sdk/kit/authoring/react-design-canvas-style';
|
|
52
|
+
import {
|
|
53
|
+
createReactStoryBoard,
|
|
54
|
+
type ReactStoryBoard,
|
|
55
|
+
type ReactStoryBoardStory,
|
|
56
|
+
} from '@volter/editor-sdk/kit/authoring/react-story-board';
|
|
57
|
+
import {
|
|
58
|
+
getRootPan,
|
|
59
|
+
panRootBy,
|
|
60
|
+
panTransformValue,
|
|
61
|
+
subscribeRootPan,
|
|
62
|
+
} from '@volter/editor-sdk/kit/world-pan-state';
|
|
63
|
+
import { registerPresentedPixiApps } from '../../host/canvas-preview-frames';
|
|
64
|
+
import { ReactCanvasControls } from '@volter/editor-sdk/kit/components/ReactCanvasControls';
|
|
65
|
+
import { themeVars } from '@volter/editor-sdk/widgets';
|
|
66
|
+
import { listProjectComponents } from '@volter/editor-sdk/kit/api/assets';
|
|
67
|
+
import { editorConsole } from '@volter/editor-sdk/kit/editor-console';
|
|
68
|
+
import { publishDocumentInspectionSubject } from '@volter/editor-sdk/kit/inspection/document-subject';
|
|
69
|
+
import { CANVAS_COMPONENTS_DOCUMENT_ID } from '@volter/editor-sdk/kit/workspace-document-ids';
|
|
70
|
+
import { registerAvailableWorkspaceDocument } from '@volter/editor-sdk/kit/workspace-available-documents';
|
|
71
|
+
import {
|
|
72
|
+
registerWorkspaceDocumentSelection,
|
|
73
|
+
type WorkspaceDocumentContentProps,
|
|
74
|
+
} from '@volter/editor-sdk/kit/workspace-document-registry';
|
|
75
|
+
import { storyBoardPresentation } from '@volter/editor-sdk/kit/stories/story-presentation';
|
|
76
|
+
import { getProjectStoryModules, subscribeProjectStoryModules } from '@volter/editor-sdk/kit/stories/story-registry';
|
|
77
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
78
|
+
import {
|
|
79
|
+
buildCanvasBoard,
|
|
80
|
+
type CanvasBoard,
|
|
81
|
+
disposeCanvasBoard,
|
|
82
|
+
GHOST_DISTRICT,
|
|
83
|
+
GHOST_FRAME_SIZE,
|
|
84
|
+
summarizeCanvasBoardSkips,
|
|
85
|
+
} from './canvas-board-model';
|
|
86
|
+
|
|
87
|
+
/** The board is a project-wide singleton: one generated view of one registry.
|
|
88
|
+
* Symmetric with the other pinned boards — a stable project-level id, so a
|
|
89
|
+
* persisted layout never strands the panel when a file is renamed. */
|
|
90
|
+
export { CANVAS_COMPONENTS_DOCUMENT_ID };
|
|
91
|
+
|
|
92
|
+
/** The tab reads `2D`, the peer of `3D` and `UI` in the center strip. */
|
|
93
|
+
const CANVAS_COMPONENTS_TITLE = '2D';
|
|
94
|
+
|
|
95
|
+
/** The narrow store surface this document needs. */
|
|
96
|
+
type BuildState =
|
|
97
|
+
| { readonly status: 'idle' }
|
|
98
|
+
| { readonly status: 'building' }
|
|
99
|
+
| { readonly status: 'ready'; readonly board: CanvasBoard }
|
|
100
|
+
| { readonly status: 'error'; readonly message: string };
|
|
101
|
+
|
|
102
|
+
/** How far a press may travel and still count as a click rather than a pan. */
|
|
103
|
+
const BOARD_CLICK_SLOP_PX = 4;
|
|
104
|
+
|
|
105
|
+
/** A live presentation of one built board over one container. */
|
|
106
|
+
interface CanvasBoardPresentation {
|
|
107
|
+
/** Drive (or freeze) every exhibit's own clock. */
|
|
108
|
+
setPlaying(playing: boolean): void;
|
|
109
|
+
/** Select the frame under a client point; false when the point hit no
|
|
110
|
+
* inactive frame. The whole frame is the target, not only its label. */
|
|
111
|
+
selectAt(clientX: number, clientY: number): boolean;
|
|
112
|
+
/** Recenter on the frame under a client point. */
|
|
113
|
+
zoomAt(clientX: number, clientY: number): boolean;
|
|
114
|
+
/** The exhibit the board is currently showing as active, for the Inspector. */
|
|
115
|
+
activeStoryId(): string | null;
|
|
116
|
+
dispose(): void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The exhibits' rows, then the ghosts'.
|
|
121
|
+
*
|
|
122
|
+
* A row's `title` is the substrate's ORGANIZATION input, and this board's
|
|
123
|
+
* blocks are DISTRICTS rather than components: the board already shows one
|
|
124
|
+
* exhibit per component, so clustering by component would stack a column of
|
|
125
|
+
* one-frame blocks instead of the shelf a museum wants. So each row is titled
|
|
126
|
+
* with its district, which makes the block "Gameplay" and the frames inside it
|
|
127
|
+
* "Hero", "Grunt", "Brute". Ghosts share ONE trailing district, so the
|
|
128
|
+
* storyless components land after every real block — a "still to do" shelf,
|
|
129
|
+
* never interleaved with the museum.
|
|
130
|
+
*
|
|
131
|
+
* An ungrouped exhibit (no district) carries no title and falls back to the
|
|
132
|
+
* substrate's own module-path derivation, exactly like an untitled dom story.
|
|
133
|
+
*/
|
|
134
|
+
function boardRows(board: CanvasBoard): ReactStoryBoardStory[] {
|
|
135
|
+
return [
|
|
136
|
+
...board.exhibits.map((exhibit) => ({
|
|
137
|
+
id: exhibit.id,
|
|
138
|
+
label: exhibit.label,
|
|
139
|
+
modulePath: exhibit.modulePath,
|
|
140
|
+
...(exhibit.district ? { title: exhibit.district } : {}),
|
|
141
|
+
frameSize: exhibit.frameSize,
|
|
142
|
+
})),
|
|
143
|
+
...board.ghosts.map((ghost) => ({
|
|
144
|
+
id: ghost.key,
|
|
145
|
+
label: ghost.name,
|
|
146
|
+
modulePath: ghost.path,
|
|
147
|
+
title: GHOST_DISTRICT,
|
|
148
|
+
frameSize: GHOST_FRAME_SIZE,
|
|
149
|
+
})),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Mount one built board into `container`: the shared design canvas' own
|
|
155
|
+
* transformed layer, the shared story board over it, and each story's mounted
|
|
156
|
+
* host adopted into its frame.
|
|
157
|
+
*
|
|
158
|
+
* Returns null for a board with nothing on it — the substrate requires at least
|
|
159
|
+
* one frame, and an empty board is a status line rather than a canvas.
|
|
160
|
+
*/
|
|
161
|
+
function presentCanvasBoard(
|
|
162
|
+
container: HTMLElement,
|
|
163
|
+
board: CanvasBoard,
|
|
164
|
+
onSelectionChanged?: () => void,
|
|
165
|
+
): CanvasBoardPresentation | null {
|
|
166
|
+
const rows = boardRows(board);
|
|
167
|
+
const first = rows[0];
|
|
168
|
+
if (!first) return null;
|
|
169
|
+
|
|
170
|
+
// The transformed world layer — the same role `design-time-layers.ts`'s
|
|
171
|
+
// layer plays for the UI board, and the same ONE shared transform value
|
|
172
|
+
// (`panTransformValue(getRootPan())`), so both boards' cameras are literally
|
|
173
|
+
// one camera and neither can drift from the other.
|
|
174
|
+
const layer = document.createElement('div');
|
|
175
|
+
layer.dataset['testid'] = 'canvas-board-layer';
|
|
176
|
+
layer.style.position = 'absolute';
|
|
177
|
+
layer.style.left = '0';
|
|
178
|
+
layer.style.top = '0';
|
|
179
|
+
layer.style.transformOrigin = '0 0';
|
|
180
|
+
container.appendChild(layer);
|
|
181
|
+
|
|
182
|
+
let storyBoard: ReactStoryBoard | null = null;
|
|
183
|
+
let selectedStoryId: string | null = first.id;
|
|
184
|
+
storyBoard = createReactStoryBoard(
|
|
185
|
+
layer,
|
|
186
|
+
rows,
|
|
187
|
+
first.id,
|
|
188
|
+
(storyId, intent) => {
|
|
189
|
+
// No authoring adapter stands behind this board: a frame click
|
|
190
|
+
// highlights, a double-click recenters on it. Both are the substrate's
|
|
191
|
+
// own. What the click DOES carry is identity, which the Inspector can
|
|
192
|
+
// say something honest about — see the document subject published below.
|
|
193
|
+
selectedStoryId = storyId;
|
|
194
|
+
storyBoard?.activate(storyId, intent === 'zoom');
|
|
195
|
+
onSelectionChanged?.();
|
|
196
|
+
},
|
|
197
|
+
// The substrate is host chrome's and does not import portable CSF; the
|
|
198
|
+
// lane that mounts a board supplies how its frames are organized and
|
|
199
|
+
// sized (`@volter/editor-sdk/kit/authoring/story-board-presentation.ts`).
|
|
200
|
+
storyBoardPresentation(rows),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const canvases: HTMLCanvasElement[] = [];
|
|
204
|
+
for (const exhibit of board.exhibits) {
|
|
205
|
+
const frame = storyBoard.frames.get(exhibit.id);
|
|
206
|
+
if (!frame) continue;
|
|
207
|
+
// The story's OWN host element, not its canvas: react owns the host's
|
|
208
|
+
// children, and moving a react-rendered node out from under its root makes
|
|
209
|
+
// unmount throw (`MountedStoryPixi.host`).
|
|
210
|
+
const host = exhibit.mounted.host;
|
|
211
|
+
host.setAttribute(
|
|
212
|
+
'style',
|
|
213
|
+
`position:absolute;left:${-exhibit.crop.x}px;top:${-exhibit.crop.y}px;` +
|
|
214
|
+
`width:${exhibit.mounted.app.screen.width}px;height:${exhibit.mounted.app.screen.height}px;` +
|
|
215
|
+
'overflow:visible;contain:layout paint;',
|
|
216
|
+
);
|
|
217
|
+
const canvas = exhibit.mounted.app.canvas as HTMLCanvasElement;
|
|
218
|
+
canvas.style.display = 'block';
|
|
219
|
+
// 1 declared px = 1 CSS px. Pixi's own `autoDensity` may have written a
|
|
220
|
+
// device-pixel-ratio size here; the frame is authored space, so it is the
|
|
221
|
+
// declared cell that goes on screen.
|
|
222
|
+
canvas.style.width = `${exhibit.mounted.app.screen.width}px`;
|
|
223
|
+
canvas.style.height = `${exhibit.mounted.app.screen.height}px`;
|
|
224
|
+
frame.content.appendChild(host);
|
|
225
|
+
canvases.push(canvas);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const ghost of board.ghosts) {
|
|
229
|
+
const frame = storyBoard.frames.get(ghost.key);
|
|
230
|
+
if (!frame) continue;
|
|
231
|
+
frame.element.style.border = `1px dashed ${themeVars.boundary.strong}`;
|
|
232
|
+
const empty = document.createElement('div');
|
|
233
|
+
empty.dataset['testid'] = 'canvas-board-ghost';
|
|
234
|
+
empty.textContent = 'No story yet';
|
|
235
|
+
empty.style.position = 'absolute';
|
|
236
|
+
empty.style.inset = '0';
|
|
237
|
+
empty.style.display = 'flex';
|
|
238
|
+
empty.style.alignItems = 'center';
|
|
239
|
+
empty.style.justifyContent = 'center';
|
|
240
|
+
empty.style.color = themeVars.content.dim;
|
|
241
|
+
empty.style.font = `11px ${themeVars.typography.sans}`;
|
|
242
|
+
empty.style.pointerEvents = 'none';
|
|
243
|
+
frame.content.appendChild(empty);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The board's exhibits are what `capture-active-document` photographs, and a
|
|
247
|
+
// story's own `<Application>` has no `preserveDrawingBuffer` — so the shared
|
|
248
|
+
// composite lane needs Pixi's own extraction for exactly these canvases.
|
|
249
|
+
const unregisterApps = registerPresentedPixiApps(
|
|
250
|
+
board.exhibits.map((exhibit) => exhibit.mounted.app),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const applyView = (): void => {
|
|
254
|
+
const view = getRootPan();
|
|
255
|
+
layer.style.transform = panTransformValue(view) ?? '';
|
|
256
|
+
// MAGNIFY TEXELS, NEVER BLUR THEM. The board's zoom is a CSS scale over an
|
|
257
|
+
// already-rendered canvas, and a browser resamples that bilinearly by
|
|
258
|
+
// default — which would smooth away the nearest-neighbour sheet the game
|
|
259
|
+
// actually ships. Below 1× the opposite is true: dropping texels aliases,
|
|
260
|
+
// so the overview keeps the browser's own filtering.
|
|
261
|
+
const rendering = view.zoom >= 1 ? 'pixelated' : 'auto';
|
|
262
|
+
for (const canvas of canvases) canvas.style.imageRendering = rendering;
|
|
263
|
+
};
|
|
264
|
+
const unsubscribePan = subscribeRootPan(applyView);
|
|
265
|
+
applyView();
|
|
266
|
+
|
|
267
|
+
// The board's subject is the whole shelf, so it opens on the overview rather
|
|
268
|
+
// than on one frame (the substrate's own default, which suits a UI board
|
|
269
|
+
// being edited a state at a time). A frame this size at editing zoom would
|
|
270
|
+
// show one sprite and no neighbours to compare it with. After a frame, so it
|
|
271
|
+
// lands on the substrate's own initial fit rather than under it.
|
|
272
|
+
const openingFit = requestAnimationFrame(() => storyBoard?.fitAll());
|
|
273
|
+
|
|
274
|
+
let playing = false;
|
|
275
|
+
let frameHandle = 0;
|
|
276
|
+
let lastFrameMs = 0;
|
|
277
|
+
let elapsedMs = 0;
|
|
278
|
+
const tick = (now: number): void => {
|
|
279
|
+
frameHandle = requestAnimationFrame(tick);
|
|
280
|
+
// Exhibit clocks advance by REAL elapsed time, clamped so a backgrounded
|
|
281
|
+
// tab does not resume with one enormous step through every cycle.
|
|
282
|
+
elapsedMs += lastFrameMs === 0 ? 16 : Math.min(100, now - lastFrameMs);
|
|
283
|
+
lastFrameMs = now;
|
|
284
|
+
for (const exhibit of board.exhibits) {
|
|
285
|
+
try {
|
|
286
|
+
exhibit.mounted.ticker.update(elapsedMs);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// biome-ignore lint/suspicious/noConsole: a broken exhibit must stay diagnosable without stopping the board
|
|
289
|
+
console.error('[canvas-board] an exhibit failed to tick.', error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
// THE WHOLE FRAME IS THE TARGET, not only its label. The substrate has
|
|
296
|
+
// always exposed this hit test — `selectReactStoryFrameAtPoint`'s own note
|
|
297
|
+
// calls it "the expected Figma-style click target" — but this board never
|
|
298
|
+
// wired it, so a frame could only be selected by clicking its small name
|
|
299
|
+
// (runhuman pass 86: "you have to click it by the name; if you click it by
|
|
300
|
+
// the card, you can't actually select it"). The UI board gets this through
|
|
301
|
+
// `RootSelectionOverlay`, which no adapter-less board mounts.
|
|
302
|
+
selectAt(clientX: number, clientY: number): boolean {
|
|
303
|
+
return storyBoard?.selectAtClientPoint(clientX, clientY) ?? false;
|
|
304
|
+
},
|
|
305
|
+
activeStoryId(): string | null {
|
|
306
|
+
return selectedStoryId;
|
|
307
|
+
},
|
|
308
|
+
zoomAt(clientX: number, clientY: number): boolean {
|
|
309
|
+
return storyBoard?.zoomAtClientPoint(clientX, clientY) ?? false;
|
|
310
|
+
},
|
|
311
|
+
setPlaying(next: boolean): void {
|
|
312
|
+
if (next === playing) return;
|
|
313
|
+
playing = next;
|
|
314
|
+
if (next) {
|
|
315
|
+
lastFrameMs = 0;
|
|
316
|
+
frameHandle = requestAnimationFrame(tick);
|
|
317
|
+
} else {
|
|
318
|
+
cancelAnimationFrame(frameHandle);
|
|
319
|
+
frameHandle = 0;
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
dispose(): void {
|
|
323
|
+
cancelAnimationFrame(frameHandle);
|
|
324
|
+
cancelAnimationFrame(openingFit);
|
|
325
|
+
unsubscribePan();
|
|
326
|
+
unregisterApps();
|
|
327
|
+
// Every adopted host goes home BEFORE the board's own DOM does: the host
|
|
328
|
+
// belongs to the story's mount, which is the only thing entitled to free
|
|
329
|
+
// it (the mount's own `dispose`, run by `disposeCanvasBoard`).
|
|
330
|
+
for (const exhibit of board.exhibits) exhibit.mounted.host.remove();
|
|
331
|
+
storyBoard?.dispose();
|
|
332
|
+
storyBoard = null;
|
|
333
|
+
layer.remove();
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function CanvasBoardContent({ active }: WorkspaceDocumentContentProps) {
|
|
339
|
+
const modules = useSyncExternalStore(subscribeProjectStoryModules, getProjectStoryModules);
|
|
340
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
341
|
+
const presentationRef = useRef<CanvasBoardPresentation | null>(null);
|
|
342
|
+
/** Where the current press started, so a pan is never read as a click. */
|
|
343
|
+
const pressRef = useRef<{ x: number; y: number } | null>(null);
|
|
344
|
+
/** The previous pointer position while a drag-pan is running. */
|
|
345
|
+
const dragRef = useRef<{ x: number; y: number } | null>(null);
|
|
346
|
+
const [state, setState] = useState<BuildState>({ status: 'idle' });
|
|
347
|
+
|
|
348
|
+
// The board is a STANDING tab whose panel Dockview mounts whether or not it
|
|
349
|
+
// is the active one, so mounting is not the trigger — FIRST ACTIVATION is.
|
|
350
|
+
// Building mounts every story off-screen, one at a time, and a tab nobody
|
|
351
|
+
// has looked at must cost nothing at boot. It latches: once shown, the board
|
|
352
|
+
// survives tab switches rather than being rebuilt on every return.
|
|
353
|
+
const [revealed, setRevealed] = useState(active);
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
if (active) setRevealed(true);
|
|
356
|
+
}, [active]);
|
|
357
|
+
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
const container = containerRef.current;
|
|
360
|
+
if (!container) return;
|
|
361
|
+
return installReactCanvasNavigation(container);
|
|
362
|
+
}, []);
|
|
363
|
+
|
|
364
|
+
// Rebuild on reveal and on every story-registry change; the previous board's
|
|
365
|
+
// ONE teardown runs here and nowhere else.
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
if (!revealed) return;
|
|
368
|
+
const controller = new AbortController();
|
|
369
|
+
let built: CanvasBoard | null = null;
|
|
370
|
+
setState({ status: 'building' });
|
|
371
|
+
// The component index rides along so storyless canvas components get their
|
|
372
|
+
// ghost frames — the Content gallery's own discovery, never a second scan.
|
|
373
|
+
// An index failure degrades to "no ghosts", not to a failed board.
|
|
374
|
+
void listProjectComponents()
|
|
375
|
+
.then((listing) => {
|
|
376
|
+
// Still "no ghosts, not a failed board" — but SAID OUT LOUD, so a
|
|
377
|
+
// failed index does not look like a project with no components.
|
|
378
|
+
if (!listing.ok) {
|
|
379
|
+
editorConsole.warn(
|
|
380
|
+
`Component index unavailable, so this board shows no ghost frames: ${listing.reason}`,
|
|
381
|
+
'canvas-board',
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return buildCanvasBoard(modules, listing.ok ? listing.entries : [], controller.signal);
|
|
385
|
+
})
|
|
386
|
+
.then(
|
|
387
|
+
(board) => {
|
|
388
|
+
// A build can RESOLVE in the same turn that unmount aborts it, when
|
|
389
|
+
// cleanup already saw `built === null` — so a success landing after
|
|
390
|
+
// an abort frees itself here or it leaks.
|
|
391
|
+
if (controller.signal.aborted) {
|
|
392
|
+
void disposeCanvasBoard(board);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
built = board;
|
|
396
|
+
setState({ status: 'ready', board });
|
|
397
|
+
},
|
|
398
|
+
(error: unknown) => {
|
|
399
|
+
// A superseded build already freed itself in its own turn.
|
|
400
|
+
if (controller.signal.aborted) return;
|
|
401
|
+
setState({
|
|
402
|
+
status: 'error',
|
|
403
|
+
message: error instanceof Error ? error.message : String(error),
|
|
404
|
+
});
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
return () => {
|
|
408
|
+
controller.abort();
|
|
409
|
+
if (built) void disposeCanvasBoard(built);
|
|
410
|
+
};
|
|
411
|
+
}, [modules, revealed]);
|
|
412
|
+
|
|
413
|
+
const board = state.status === 'ready' ? state.board : null;
|
|
414
|
+
const [selectedStoryId, setSelectedStoryId] = useState<string | null>(null);
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
const container = containerRef.current;
|
|
417
|
+
if (!container || !board) return;
|
|
418
|
+
const presentation = presentCanvasBoard(container, board, () => {
|
|
419
|
+
setSelectedStoryId(presentationRef.current?.activeStoryId() ?? null);
|
|
420
|
+
});
|
|
421
|
+
presentationRef.current = presentation;
|
|
422
|
+
setSelectedStoryId(presentation?.activeStoryId() ?? null);
|
|
423
|
+
return () => {
|
|
424
|
+
presentationRef.current = null;
|
|
425
|
+
presentation?.dispose();
|
|
426
|
+
};
|
|
427
|
+
}, [board]);
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* WHAT THE SELECTED FRAME IS, said in the Inspector.
|
|
431
|
+
*
|
|
432
|
+
* This board stands behind no authoring adapter — a frame carries no
|
|
433
|
+
* editable transform — so clicking one used to leave the Inspector
|
|
434
|
+
* completely empty, which reads as the click having failed (runhuman pass
|
|
435
|
+
* 86). A frame still HAS an identity, and identity is exactly what a
|
|
436
|
+
* document may describe about itself: the component the exhibit shows, the
|
|
437
|
+
* story composing it, and the module both come from. Read-only by
|
|
438
|
+
* construction: no io, no fields, nothing that implies an edit this surface
|
|
439
|
+
* cannot perform.
|
|
440
|
+
*/
|
|
441
|
+
const selectedExhibit = board?.exhibits.find((exhibit) => exhibit.id === selectedStoryId) ?? null;
|
|
442
|
+
// "I AM THE SUBJECT" — the half without which publishing is inert. With no
|
|
443
|
+
// registered selection the Inspector falls back to the shared entity
|
|
444
|
+
// selection, so the board would show whatever was picked in the scene
|
|
445
|
+
// before its tab was opened, and a board with nothing picked in it would
|
|
446
|
+
// never reach the no-selection seam at all
|
|
447
|
+
// (`workspace-document-registry.ts`'s `WorkspaceDocumentSelection`, and
|
|
448
|
+
// `components/AssetEditorShell.tsx`, which registers the same pair).
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (!active) return;
|
|
451
|
+
return registerWorkspaceDocumentSelection(CANVAS_COMPONENTS_DOCUMENT_ID, () => ({
|
|
452
|
+
adapter: null,
|
|
453
|
+
nodeId: null,
|
|
454
|
+
}));
|
|
455
|
+
}, [active]);
|
|
456
|
+
useEffect(() => {
|
|
457
|
+
if (!active || !selectedExhibit) return;
|
|
458
|
+
return publishDocumentInspectionSubject(CANVAS_COMPONENTS_DOCUMENT_ID, () => ({
|
|
459
|
+
id: selectedExhibit.id,
|
|
460
|
+
title: selectedExhibit.label,
|
|
461
|
+
kindLabel: 'Canvas story',
|
|
462
|
+
note: {
|
|
463
|
+
text: `${selectedExhibit.storyName} · ${selectedExhibit.modulePath}`,
|
|
464
|
+
testId: 'canvas-board-subject-note',
|
|
465
|
+
},
|
|
466
|
+
sections: [],
|
|
467
|
+
}));
|
|
468
|
+
}, [active, selectedExhibit]);
|
|
469
|
+
|
|
470
|
+
// The exhibits' clocks run only while this document is the visible subject.
|
|
471
|
+
// Keyed on the BOARD as well as on `active`: a board that finishes building
|
|
472
|
+
// while its tab is already the active one never sees `active` change, and
|
|
473
|
+
// without this its exhibits would mount frozen on their first frame.
|
|
474
|
+
useEffect(() => {
|
|
475
|
+
presentationRef.current?.setPlaying(active);
|
|
476
|
+
return () => presentationRef.current?.setPlaying(false);
|
|
477
|
+
}, [active, board]);
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<div
|
|
481
|
+
data-testid="canvas-board"
|
|
482
|
+
data-vgai-backdrop-color={active ? REACT_DESIGN_CANVAS_COLOR : undefined}
|
|
483
|
+
data-vgai-backdrop-policy={active ? 'dark-frost' : undefined}
|
|
484
|
+
style={{
|
|
485
|
+
position: 'absolute',
|
|
486
|
+
inset: 0,
|
|
487
|
+
overflow: 'hidden',
|
|
488
|
+
pointerEvents: 'auto',
|
|
489
|
+
touchAction: 'none',
|
|
490
|
+
// A DRAG PANS; IT NEVER SWEEPS TEXT. Now that dragging moves the
|
|
491
|
+
// canvas, the browser's own text selection ran alongside it and left
|
|
492
|
+
// frame labels highlighted, which visibly fought the next pan — "you
|
|
493
|
+
// have to double- or triple-click somewhere else for the text to be
|
|
494
|
+
// unhighlighted" (runhuman pass 89). Board chrome is a surface to
|
|
495
|
+
// navigate, not prose to select.
|
|
496
|
+
userSelect: 'none',
|
|
497
|
+
WebkitUserSelect: 'none',
|
|
498
|
+
backgroundColor: REACT_DESIGN_CANVAS_COLOR,
|
|
499
|
+
}}
|
|
500
|
+
ref={containerRef}
|
|
501
|
+
// DRAG PANS, AND A CLICK STILL SELECTS. The board's navigation was
|
|
502
|
+
// wheel-only (wheel pans, cmd-wheel zooms — Figma's own model), so
|
|
503
|
+
// dragging did nothing at all and a tester reported "you can't pan" as
|
|
504
|
+
// their number one fix (runhuman pass 87). Any button drags the canvas,
|
|
505
|
+
// because this board has no marquee and no movable frames, so there is
|
|
506
|
+
// nothing else a drag could mean. The press point is remembered so a
|
|
507
|
+
// drag that ends over a frame stays a pan rather than selecting it —
|
|
508
|
+
// the movement threshold the Scene's own grammar uses, not a modifier.
|
|
509
|
+
onPointerDown={(event) => {
|
|
510
|
+
pressRef.current = { x: event.clientX, y: event.clientY };
|
|
511
|
+
dragRef.current = { x: event.clientX, y: event.clientY };
|
|
512
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
513
|
+
}}
|
|
514
|
+
onPointerMove={(event) => {
|
|
515
|
+
const from = dragRef.current;
|
|
516
|
+
if (!from) return;
|
|
517
|
+
const dx = event.clientX - from.x;
|
|
518
|
+
const dy = event.clientY - from.y;
|
|
519
|
+
if (dx === 0 && dy === 0) return;
|
|
520
|
+
dragRef.current = { x: event.clientX, y: event.clientY };
|
|
521
|
+
panRootBy(dx, dy);
|
|
522
|
+
}}
|
|
523
|
+
onPointerUp={(event) => {
|
|
524
|
+
const press = pressRef.current;
|
|
525
|
+
pressRef.current = null;
|
|
526
|
+
dragRef.current = null;
|
|
527
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
528
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
529
|
+
}
|
|
530
|
+
if (!press) return;
|
|
531
|
+
const moved = Math.hypot(event.clientX - press.x, event.clientY - press.y);
|
|
532
|
+
if (moved > BOARD_CLICK_SLOP_PX) return;
|
|
533
|
+
presentationRef.current?.selectAt(event.clientX, event.clientY);
|
|
534
|
+
}}
|
|
535
|
+
onPointerCancel={() => {
|
|
536
|
+
pressRef.current = null;
|
|
537
|
+
dragRef.current = null;
|
|
538
|
+
}}
|
|
539
|
+
onDoubleClick={(event) => {
|
|
540
|
+
presentationRef.current?.zoomAt(event.clientX, event.clientY);
|
|
541
|
+
}}
|
|
542
|
+
>
|
|
543
|
+
<DesignCanvasBackdrop />
|
|
544
|
+
{board ? (
|
|
545
|
+
<CanvasBoardStatus board={board} />
|
|
546
|
+
) : (
|
|
547
|
+
<div
|
|
548
|
+
data-testid="canvas-board-status"
|
|
549
|
+
style={{ padding: 16, fontSize: 12, color: themeVars.content.muted }}
|
|
550
|
+
>
|
|
551
|
+
{state.status === 'error' && state.message}
|
|
552
|
+
{state.status === 'building' && 'Mounting every canvas story off-screen…'}
|
|
553
|
+
{state.status === 'idle' && 'Open this tab to lay out every canvas story at 1× texels.'}
|
|
554
|
+
</div>
|
|
555
|
+
)}
|
|
556
|
+
<ReactCanvasControls containerRef={containerRef} context={null} boardOnly />
|
|
557
|
+
</div>
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Infinite editor canvas behind the frames — the same dot grid, following the
|
|
562
|
+
* same shared transform, as the UI board's. */
|
|
563
|
+
function DesignCanvasBackdrop() {
|
|
564
|
+
const view = useSyncExternalStore(subscribeRootPan, getRootPan);
|
|
565
|
+
const spacing = Math.max(8, 24 * view.zoom);
|
|
566
|
+
const dotRadius = Math.max(0.7, Math.min(1.4, view.zoom));
|
|
567
|
+
return (
|
|
568
|
+
<div
|
|
569
|
+
data-testid="react-design-canvas-backdrop"
|
|
570
|
+
aria-hidden="true"
|
|
571
|
+
style={{
|
|
572
|
+
position: 'absolute',
|
|
573
|
+
inset: 0,
|
|
574
|
+
pointerEvents: 'none',
|
|
575
|
+
backgroundColor: REACT_DESIGN_CANVAS_COLOR,
|
|
576
|
+
backgroundImage: `radial-gradient(circle, ${REACT_DESIGN_CANVAS_DOT} ${dotRadius}px, transparent ${dotRadius}px)`,
|
|
577
|
+
backgroundPosition: `${view.x}px ${view.y}px`,
|
|
578
|
+
backgroundSize: `${spacing}px ${spacing}px`,
|
|
579
|
+
}}
|
|
580
|
+
/>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Counts, plus the honest list of stories that produced no Pixi content.
|
|
585
|
+
* Ghosts and skips are DIFFERENT facts — a ghost is a component with no story,
|
|
586
|
+
* a skip is a story that mounted nothing 2D — and the footer states both. */
|
|
587
|
+
function CanvasBoardStatus({ board }: { readonly board: CanvasBoard }) {
|
|
588
|
+
const [open, setOpen] = useState(false);
|
|
589
|
+
const skips = summarizeCanvasBoardSkips(board.skipped);
|
|
590
|
+
return (
|
|
591
|
+
<div
|
|
592
|
+
data-testid="canvas-board-status"
|
|
593
|
+
style={{
|
|
594
|
+
position: 'absolute',
|
|
595
|
+
left: 8,
|
|
596
|
+
bottom: 8,
|
|
597
|
+
maxWidth: 420,
|
|
598
|
+
padding: '4px 8px',
|
|
599
|
+
borderRadius: themeVars.shape.small,
|
|
600
|
+
background: themeVars.surface.raised,
|
|
601
|
+
border: `1px solid ${themeVars.boundary.default}`,
|
|
602
|
+
fontSize: 10,
|
|
603
|
+
color: themeVars.content.muted,
|
|
604
|
+
pointerEvents: 'auto',
|
|
605
|
+
}}
|
|
606
|
+
>
|
|
607
|
+
{board.exhibits.length} exhibit{board.exhibits.length === 1 ? '' : 's'}
|
|
608
|
+
{board.ghosts.length > 0 && (
|
|
609
|
+
<>
|
|
610
|
+
{' · '}
|
|
611
|
+
{board.ghosts.length} component{board.ghosts.length === 1 ? '' : 's'} without stories
|
|
612
|
+
</>
|
|
613
|
+
)}
|
|
614
|
+
{board.skipped.length > 0 && (
|
|
615
|
+
<>
|
|
616
|
+
{' · '}
|
|
617
|
+
{/* Inline disclosure toggle — the role-annotated span is the
|
|
618
|
+
guard-sanctioned shape for text-link chrome. */}
|
|
619
|
+
<span
|
|
620
|
+
role="button"
|
|
621
|
+
tabIndex={0}
|
|
622
|
+
aria-expanded={open}
|
|
623
|
+
onClick={() => setOpen((value) => !value)}
|
|
624
|
+
onKeyDown={(event) => {
|
|
625
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
626
|
+
event.preventDefault();
|
|
627
|
+
setOpen((value) => !value);
|
|
628
|
+
}
|
|
629
|
+
}}
|
|
630
|
+
style={{ cursor: 'pointer', color: themeVars.accent.default }}
|
|
631
|
+
>
|
|
632
|
+
{skips.stories} {skips.stories === 1 ? 'story' : 'stories'} in {skips.modules}{' '}
|
|
633
|
+
{skips.modules === 1 ? 'module' : 'modules'} not on this board
|
|
634
|
+
</span>
|
|
635
|
+
</>
|
|
636
|
+
)}
|
|
637
|
+
{open && (
|
|
638
|
+
<div style={{ marginTop: 4, maxHeight: 160, overflow: 'auto' }}>
|
|
639
|
+
{board.skipped.map((entry) => (
|
|
640
|
+
<div key={entry.id} style={{ padding: '1px 0' }}>
|
|
641
|
+
<code style={{ color: themeVars.content.dim }}>
|
|
642
|
+
{entry.modulePath}#{entry.storyName}
|
|
643
|
+
</code>{' '}
|
|
644
|
+
— {entry.reason}
|
|
645
|
+
</div>
|
|
646
|
+
))}
|
|
647
|
+
</div>
|
|
648
|
+
)}
|
|
649
|
+
</div>
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Offer the `2D` board as a document the person can open, the way its `UI` and `3D` peers are
|
|
655
|
+
* offered: the component-board registry installs it when the project has canvas stories
|
|
656
|
+
* (`host/stories/story-media-presence.ts`). It never steals focus, and it costs nothing until
|
|
657
|
+
* looked at.
|
|
658
|
+
*/
|
|
659
|
+
export function installCanvasBoardDocument(): string {
|
|
660
|
+
return registerAvailableWorkspaceDocument(
|
|
661
|
+
{
|
|
662
|
+
id: CANVAS_COMPONENTS_DOCUMENT_ID,
|
|
663
|
+
title: CANVAS_COMPONENTS_TITLE,
|
|
664
|
+
// A generated browse-over-the-project's-content view, not an authored
|
|
665
|
+
// subject: it is regenerated from the story registry, never persisted.
|
|
666
|
+
kind: 'content-browser',
|
|
667
|
+
workspaceRole: 'workspace-reference',
|
|
668
|
+
Content: CanvasBoardContent,
|
|
669
|
+
closeable: false,
|
|
670
|
+
readOnly: true,
|
|
671
|
+
presentation: () => ({ kind: 'workspace', id: CANVAS_COMPONENTS_DOCUMENT_ID }),
|
|
672
|
+
},
|
|
673
|
+
// A board never steals focus; a canvas root's Scene is the project's default document.
|
|
674
|
+
{ category: 'canvas' },
|
|
675
|
+
);
|
|
676
|
+
}
|