@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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Reveal Internals" / "Hide Internals" — the instance row's toggle,
|
|
3
|
+
* registered through the host's hierarchy contribution seam.
|
|
4
|
+
*
|
|
5
|
+
* Same split as `extract-menu.ts`: `GameHierarchy` stays contract-only and
|
|
6
|
+
* never learns that a three world has hidden internals; THIS module probes the
|
|
7
|
+
* active adapter for the optional capability and owns the session state. The
|
|
8
|
+
* projection math and the read-only rule are pure and stay in the HIERARCHY
|
|
9
|
+
* PANEL's own modules (`@editor/hierarchy-internals`,
|
|
10
|
+
* `@editor/hierarchy-row-model`), which the panel itself reads while rendering
|
|
11
|
+
* — `components/GameHierarchy.tsx:158` imports `internalsProjection` and
|
|
12
|
+
* `applyRevealExpansionDefaults` directly. The projection is the panel's; only
|
|
13
|
+
* the MENU that toggles it is this lane's.
|
|
14
|
+
*
|
|
15
|
+
* NO OWNER WALK IS NEEDED HERE (unlike the source menu's
|
|
16
|
+
* `instanceSourceLocatorFor`): the composite ROUTES
|
|
17
|
+
* `hasInternals`/`internalChildren` to the owning child by `route(id)`, so
|
|
18
|
+
* probing the active adapter is already asking the right one.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
|
|
22
|
+
import {
|
|
23
|
+
type InternalsSource,
|
|
24
|
+
internalsProjection,
|
|
25
|
+
isInternalsRevealed,
|
|
26
|
+
revealedInternalsIds,
|
|
27
|
+
toggleInternalsRevealed,
|
|
28
|
+
} from '@volter/editor-core/hierarchy-internals';
|
|
29
|
+
import { registerHierarchyMenuItems } from '@volter/editor-core/hierarchy-menu-registry';
|
|
30
|
+
import type { AuthoringAdapter } from '@volter/editor-project/adapter';
|
|
31
|
+
|
|
32
|
+
/** Menu capitalization follows the panel's Title Case idiom (`Rename`,
|
|
33
|
+
* `Go to Callsite`). */
|
|
34
|
+
export const REVEAL_INTERNALS_LABEL = 'Reveal Internals';
|
|
35
|
+
export const HIDE_INTERNALS_LABEL = 'Hide Internals';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether THIS row may be revealed/hidden.
|
|
39
|
+
*
|
|
40
|
+
* Two conditions, and the second is the one that is easy to miss: the row must
|
|
41
|
+
* hide something (`hasInternals`), and the row must not ITSELF be a revealed
|
|
42
|
+
* internal. An internal row's own children are already on screen — the reveal
|
|
43
|
+
* walk is transitive — so offering "Reveal Internals" there would be a toggle
|
|
44
|
+
* with no effect and a second, meaningless piece of state.
|
|
45
|
+
*/
|
|
46
|
+
export function internalsToggleFor(
|
|
47
|
+
adapter: AuthoringAdapter,
|
|
48
|
+
nodeId: string,
|
|
49
|
+
): { label: string; toggle: () => void } | null {
|
|
50
|
+
const source = adapter as unknown as InternalsSource;
|
|
51
|
+
if (typeof source.hasInternals !== 'function' || !source.hasInternals(nodeId)) return null;
|
|
52
|
+
if (internalsProjection(source, revealedInternalsIds()).isInternal(nodeId)) return null;
|
|
53
|
+
return {
|
|
54
|
+
label: isInternalsRevealed(nodeId) ? HIDE_INTERNALS_LABEL : REVEAL_INTERNALS_LABEL,
|
|
55
|
+
toggle: () => {
|
|
56
|
+
toggleInternalsRevealed(nodeId);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let unregister: (() => void) | null = null;
|
|
62
|
+
|
|
63
|
+
/** Idempotent install of the internals item (see `component-verbs.service.ts`). */
|
|
64
|
+
export function ensureInstanceInternalsMenuRegistered(): void {
|
|
65
|
+
if (unregister) return;
|
|
66
|
+
unregister = registerHierarchyMenuItems(
|
|
67
|
+
({ nodeId, store }) => internalsToggleFor(getActiveAuthoring(store), nodeId) !== null,
|
|
68
|
+
({ nodeId, store }) => {
|
|
69
|
+
const entry = internalsToggleFor(getActiveAuthoring(store), nodeId);
|
|
70
|
+
if (!entry) return [];
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
label: entry.label,
|
|
74
|
+
action: () => {
|
|
75
|
+
entry.toggle();
|
|
76
|
+
// The revealed set is module state the panel reads while rendering;
|
|
77
|
+
// this is the notify every source-write path already uses to make
|
|
78
|
+
// the hierarchy re-read its world.
|
|
79
|
+
store.notifyIngestEdit();
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Drop the registration — the service contribution's stop. */
|
|
88
|
+
export function unregisterInstanceInternalsMenu(): void {
|
|
89
|
+
unregister?.();
|
|
90
|
+
unregister = null;
|
|
91
|
+
}
|
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-story 3D document the `3D` board opens: ONE mounted `three`
|
|
3
|
+
* story, presented on the editor's existing Object3D document surface
|
|
4
|
+
* (`StageHost.tsx`'s `Object3DDocumentViewport` — real
|
|
5
|
+
* EditorViewport, orbit camera, source-backed R3F hierarchy and transforms,
|
|
6
|
+
* the shared `Object3DDocumentToolbar`). There is deliberately no
|
|
7
|
+
* new viewport class here: a mounted story IS an `Object3D`, so it opens on the
|
|
8
|
+
* surface every other native `Object3D` opens on.
|
|
9
|
+
*
|
|
10
|
+
* ## What this module actually owns
|
|
11
|
+
*
|
|
12
|
+
* This module owns each asynchronous story mount. A viewport borrows it until
|
|
13
|
+
* its content replacement has settled; retiring a source releases the fiber
|
|
14
|
+
* root only after the last viewport borrow returns. A source revision never
|
|
15
|
+
* closes the document or resets its editor state.
|
|
16
|
+
*
|
|
17
|
+
* ## Args → Inspector
|
|
18
|
+
*
|
|
19
|
+
* A registered Inspector section (the same `inspector-section-registry` seam
|
|
20
|
+
* `story-documents.tsx`'s `Story Args` uses), whose editable set and widget
|
|
21
|
+
* types come from the SHARED `storyArgPropertyDescriptors`
|
|
22
|
+
* (`stories/story-arg-descriptors.ts`, extracted from
|
|
23
|
+
* `react-world-authoring-adapter.ts`'s `portableStoryArgDescriptors`) — so a
|
|
24
|
+
* story arg is editable in exactly one place's opinion, whichever surface shows
|
|
25
|
+
* it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Object3DDocumentViewport } from '@volter/editor-core/components/Object3DDocumentViewport';
|
|
29
|
+
import type { SourceDocumentAuthoringFactory } from '@volter/editor-core/components/StageHost';
|
|
30
|
+
import { SurfaceStateOverlay } from '@volter/editor-core/components/SurfaceStateOverlay';
|
|
31
|
+
import { STANDARD_COMPONENT_CAMERA_DIRECTION } from '@volter/editor-core/components/standard-viewport-dressing';
|
|
32
|
+
import { registerDocumentOpener } from '@volter/editor-core/document-open-registry';
|
|
33
|
+
import type { ViewportTab } from '@volter/editor-core/editor-shell-store';
|
|
34
|
+
import { createHmrRegistrationGroup } from '@volter/editor-core/hmr-registration-group';
|
|
35
|
+
import {
|
|
36
|
+
CONTRIBUTED_SECTION_ORDER,
|
|
37
|
+
STORY_ARGS_SECTION_ID,
|
|
38
|
+
STORY_ARGS_SECTION_TITLE,
|
|
39
|
+
} from '@volter/editor-core/inspection/model';
|
|
40
|
+
import { registerInspectorSections } from '@volter/editor-core/inspector-section-registry';
|
|
41
|
+
import type { ComposedProjectStory } from '@volter/editor-core/stories/compose-project-stories';
|
|
42
|
+
import {
|
|
43
|
+
type MountedStoryViewportSource,
|
|
44
|
+
mountedStoryViewportSource,
|
|
45
|
+
} from '../../host/stories/mounted-story-viewport-source';
|
|
46
|
+
import { getProjectStoryRegions } from '@volter/editor-core/stories/project-story-regions';
|
|
47
|
+
import { storyArgPropertyDescriptors } from '../../host/stories/story-arg-descriptors';
|
|
48
|
+
import { declaredStoryMedium } from '@volter/editor-core/stories/story-declared-medium';
|
|
49
|
+
import {
|
|
50
|
+
type StoryDocumentOpenRequest,
|
|
51
|
+
THREE_STORY_DOCUMENT_OPENER,
|
|
52
|
+
} from '@volter/editor-core/stories/story-document-openers';
|
|
53
|
+
import {
|
|
54
|
+
getProjectStoryModules,
|
|
55
|
+
subscribeProjectStoryModules,
|
|
56
|
+
} from '@volter/editor-core/stories/story-registry';
|
|
57
|
+
import {
|
|
58
|
+
disposeStoryObject3D,
|
|
59
|
+
mountStoryObject3D,
|
|
60
|
+
type StoryPreviewComponent,
|
|
61
|
+
} from '@volter/editor-core/stories/story-three-preview';
|
|
62
|
+
import { mountedStoryHasThreeContent } from '@volter/editor-core/stories/three-story-model';
|
|
63
|
+
import { explainSurface } from '@volter/editor-core/surface-state';
|
|
64
|
+
import { sourceWriteBackendIfPrimed } from '@volter/editor-core/ui-source/tier-source-write-backend';
|
|
65
|
+
import {
|
|
66
|
+
activeWorkspaceDocument,
|
|
67
|
+
closeWorkspaceDocument,
|
|
68
|
+
openWorkspaceDocument,
|
|
69
|
+
type WorkspaceDocumentContentProps,
|
|
70
|
+
} from '@volter/editor-core/workspace-document-registry';
|
|
71
|
+
import { registerWorkspaceDocumentRestorer } from '@volter/editor-core/workspace-document-restore';
|
|
72
|
+
import { STORY_ARGS_SECTION_ICON } from '../../react/story-documents/story-args-section';
|
|
73
|
+
import {
|
|
74
|
+
prepareStoryDocumentRestore,
|
|
75
|
+
storyDocumentId,
|
|
76
|
+
} from '../../react/story-documents/story-documents';
|
|
77
|
+
import { Checkbox, TextInput, themeVars } from '@volter/editor-sdk/widgets';
|
|
78
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
79
|
+
import { R3fSourceAuthoringAdapter } from '../authoring/r3f-source-authoring-adapter';
|
|
80
|
+
|
|
81
|
+
/** Inspector path namespace for this document's story args. Its own, because
|
|
82
|
+
* the path namespace belongs to the node the values are read back through. */
|
|
83
|
+
const THREE_STORY_ARG_PATH_PREFIX = 'threeStory.arg.';
|
|
84
|
+
|
|
85
|
+
/** The narrow store surface a three-story document needs — the T6.3 input-gate
|
|
86
|
+
* hand-off, identical to `story-documents.tsx`'s `StoryDocumentStore`. */
|
|
87
|
+
export interface ThreeStoryDocumentStore {
|
|
88
|
+
setActiveViewportTab(tab: ViewportTab): void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ThreeStoryDocumentState {
|
|
92
|
+
readonly modulePath: string;
|
|
93
|
+
readonly storyName: string;
|
|
94
|
+
argOverrides: Record<string, unknown>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const _states = new Map<string, ThreeStoryDocumentState>();
|
|
98
|
+
let _version = 0;
|
|
99
|
+
const _listeners = new Set<() => void>();
|
|
100
|
+
|
|
101
|
+
function notifyChanged(): void {
|
|
102
|
+
_version++;
|
|
103
|
+
for (const fn of _listeners) fn();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function subscribeThreeStoryDocuments(fn: () => void): () => void {
|
|
107
|
+
_listeners.add(fn);
|
|
108
|
+
return () => {
|
|
109
|
+
_listeners.delete(fn);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function threeStoryDocumentsVersion(): number {
|
|
114
|
+
return _version;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Stable identity for a portable story rendered on the declared Three medium. */
|
|
118
|
+
export function threeStoryDocumentId(modulePath: string, storyName: string): string {
|
|
119
|
+
return `three-story:${modulePath}#${storyName}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The ACTIVE center document's three-story state, or `null`. */
|
|
123
|
+
function activeThreeStoryDocument(): (ThreeStoryDocumentState & { id: string }) | null {
|
|
124
|
+
const active = activeWorkspaceDocument();
|
|
125
|
+
if (!active) return null;
|
|
126
|
+
const state = _states.get(active.descriptor.id);
|
|
127
|
+
return state ? { ...state, id: active.descriptor.id } : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Set one arg override — remounts this story's Object3D. `undefined` CLEARS
|
|
131
|
+
* the override (the `delete`-the-key convention `setStoryArgOverride` uses,
|
|
132
|
+
* for the same reason: an absent key falls back to the authored arg, a stored
|
|
133
|
+
* `NaN` would win over it). */
|
|
134
|
+
export function setThreeStoryArgOverride(id: string, key: string, value: unknown): void {
|
|
135
|
+
const state = _states.get(id);
|
|
136
|
+
if (!state) return;
|
|
137
|
+
if (value === undefined) {
|
|
138
|
+
const next = { ...state.argOverrides };
|
|
139
|
+
delete next[key];
|
|
140
|
+
state.argOverrides = next;
|
|
141
|
+
} else {
|
|
142
|
+
state.argOverrides = { ...state.argOverrides, [key]: value };
|
|
143
|
+
}
|
|
144
|
+
notifyChanged();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Open (or activate) the 3D document for one `three` story. The board's
|
|
149
|
+
* exhibit click-through target.
|
|
150
|
+
*/
|
|
151
|
+
export function openThreeStoryDocument(
|
|
152
|
+
store: ThreeStoryDocumentStore,
|
|
153
|
+
modulePath: string,
|
|
154
|
+
storyName: string,
|
|
155
|
+
title = storyName,
|
|
156
|
+
// A click-through OPENS AND FOCUSES, which is why activation is the default.
|
|
157
|
+
// The Edit tab row (`scene-documents.tsx`) opens a story-reached scene as a
|
|
158
|
+
// STANDING tab instead: it is offered, never imposed, so it passes `false`
|
|
159
|
+
// for the same reason the boards do.
|
|
160
|
+
options: { activate?: boolean } = {},
|
|
161
|
+
): string {
|
|
162
|
+
ensureThreeStoryContributionsRegistered();
|
|
163
|
+
// One portable story has one preview document. Close a stale DOM preview
|
|
164
|
+
// of the same story before opening its resolved Object3D medium; otherwise
|
|
165
|
+
// the hidden DOM renderer still mounts the R3F component and reports a
|
|
166
|
+
// misleading "Hooks can only be used within Canvas" crash.
|
|
167
|
+
closeWorkspaceDocument(storyDocumentId(modulePath, storyName));
|
|
168
|
+
const id = threeStoryDocumentId(modulePath, storyName);
|
|
169
|
+
if (!_states.has(id)) {
|
|
170
|
+
_states.set(id, { modulePath, storyName, argOverrides: {} });
|
|
171
|
+
notifyChanged();
|
|
172
|
+
}
|
|
173
|
+
return openWorkspaceDocument(
|
|
174
|
+
{
|
|
175
|
+
id,
|
|
176
|
+
title,
|
|
177
|
+
kind: 'world',
|
|
178
|
+
workspaceRole: 'authored-subject',
|
|
179
|
+
provenance: { sourcePath: modulePath },
|
|
180
|
+
Content: ThreeStoryDocumentContent,
|
|
181
|
+
closeable: true,
|
|
182
|
+
presentation: () => ({ kind: 'story', modulePath, storyName, mode: 'preview' }),
|
|
183
|
+
// A per-story 3D document is SESSION-CREATED — nothing at boot
|
|
184
|
+
// re-derives it from the composite, unlike a manifest world — so it
|
|
185
|
+
// writes itself down or comes back as a titled panel with no content.
|
|
186
|
+
// The LIVE tab text goes with it: the opening board names an exhibit by
|
|
187
|
+
// a board-scoped rule (`board-scene.ts`) that one story cannot
|
|
188
|
+
// re-derive.
|
|
189
|
+
persist: ({ title: liveTitle }) => ({ modulePath, storyName, title: liveTitle }),
|
|
190
|
+
onActivate: () => store.setActiveViewportTab('edit'),
|
|
191
|
+
onDispose: (documentId) => {
|
|
192
|
+
if (_states.delete(documentId)) notifyChanged();
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
{ activate: options.activate !== false },
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** One open three-story document's identity, or `undefined`. A story is a
|
|
200
|
+
* STABLE, ADDRESSABLE state (module path + story name), so this is everything
|
|
201
|
+
* `workspace-state-persistence` needs to write the document down and reopen
|
|
202
|
+
* it on the next boot — the same shape, and the same reason, as
|
|
203
|
+
* `story-documents.tsx`'s `storyDocumentState`. */
|
|
204
|
+
export function threeStoryDocumentState(
|
|
205
|
+
id: string,
|
|
206
|
+
): { readonly modulePath: string; readonly storyName: string } | undefined {
|
|
207
|
+
const state = _states.get(id);
|
|
208
|
+
return state ? { modulePath: state.modulePath, storyName: state.storyName } : undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Reopen a persisted three-story document at boot, or refuse. VERIFIES first
|
|
213
|
+
* (the persistence module's "persist+restore only documents that can
|
|
214
|
+
* truthfully reopen" rule): a module that no longer loads, or no longer
|
|
215
|
+
* exports that story, restores NOTHING and the stale document is dropped
|
|
216
|
+
* by the workspace's ordinary reconciliation. Callers must have settled the
|
|
217
|
+
* story registry (`refreshProjectStories`) first — this reads it synchronously.
|
|
218
|
+
*/
|
|
219
|
+
export function restoreThreeStoryDocument(
|
|
220
|
+
store: ThreeStoryDocumentStore,
|
|
221
|
+
modulePath: string,
|
|
222
|
+
storyName: string,
|
|
223
|
+
title?: string,
|
|
224
|
+
): boolean {
|
|
225
|
+
const { story } = findComposedStory(modulePath, storyName);
|
|
226
|
+
if (!story) return false;
|
|
227
|
+
if (declaredStoryMedium({ modulePath, regions: getProjectStoryRegions() }).medium !== 'three') {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
// `title` is the tab's saved text. The board names an exhibit by a
|
|
231
|
+
// BOARD-SCOPED rule (`board-scene.ts`: the component leaf, disambiguated
|
|
232
|
+
// with the story label only where one leaf contributes several exhibits),
|
|
233
|
+
// which no single story can re-derive — so the tab is restored with the text
|
|
234
|
+
// it had, falling back to the registry's own label for a record that
|
|
235
|
+
// predates this or came from another opener.
|
|
236
|
+
openThreeStoryDocument(store, modulePath, storyName, title ?? story.label);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The turntable's address (`document-open-registry.ts`) — a story mounted as
|
|
241
|
+
* an Object3D on the standard 3D document surface. */
|
|
242
|
+
registerDocumentOpener<StoryDocumentOpenRequest>({
|
|
243
|
+
id: THREE_STORY_DOCUMENT_OPENER,
|
|
244
|
+
owner: 'three-story-documents',
|
|
245
|
+
open: (store, request) =>
|
|
246
|
+
openThreeStoryDocument(
|
|
247
|
+
store,
|
|
248
|
+
request.modulePath,
|
|
249
|
+
request.storyName,
|
|
250
|
+
request.title ?? request.storyName,
|
|
251
|
+
request.activate === undefined ? {} : { activate: request.activate },
|
|
252
|
+
),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
/** Reopen a persisted 3D story document. `restoreThreeStoryDocument` is the
|
|
256
|
+
* verification (module loads, story still exported, medium still `three`)
|
|
257
|
+
* and answers `false` when any of it has moved. */
|
|
258
|
+
registerWorkspaceDocumentRestorer({
|
|
259
|
+
kind: 'world',
|
|
260
|
+
owner: 'three-story-documents',
|
|
261
|
+
prepare: prepareStoryDocumentRestore,
|
|
262
|
+
restore: ({ state, store }) => {
|
|
263
|
+
const record = state as
|
|
264
|
+
| { modulePath?: unknown; storyName?: unknown; title?: unknown }
|
|
265
|
+
| null
|
|
266
|
+
| undefined;
|
|
267
|
+
const modulePath = typeof record?.modulePath === 'string' ? record.modulePath : null;
|
|
268
|
+
const storyName = typeof record?.storyName === 'string' ? record.storyName : null;
|
|
269
|
+
if (!modulePath || !storyName) return false;
|
|
270
|
+
const title = typeof record?.title === 'string' ? record.title : undefined;
|
|
271
|
+
return restoreThreeStoryDocument(store, modulePath, storyName, title);
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
/** Test-only reset — drops all three-story document state. */
|
|
276
|
+
export function __resetThreeStoryDocumentsForTest(): void {
|
|
277
|
+
_states.clear();
|
|
278
|
+
notifyChanged();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function findComposedStory(
|
|
282
|
+
modulePath: string,
|
|
283
|
+
storyName: string,
|
|
284
|
+
): { story: ComposedProjectStory | undefined; moduleError: string | undefined } {
|
|
285
|
+
const module_ = getProjectStoryModules().find((m) => m.modulePath === modulePath);
|
|
286
|
+
if (!module_) return { story: undefined, moduleError: undefined };
|
|
287
|
+
if (!module_.ok) return { story: undefined, moduleError: module_.error };
|
|
288
|
+
return { story: module_.stories.find((s) => s.name === storyName), moduleError: undefined };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- Document content -----------------------------------------------------
|
|
292
|
+
|
|
293
|
+
function ThreeStoryDocumentContent({ documentId, active }: WorkspaceDocumentContentProps) {
|
|
294
|
+
useSyncExternalStore(subscribeThreeStoryDocuments, threeStoryDocumentsVersion);
|
|
295
|
+
// Re-resolve on every registry refresh (project change, `story-files-changed`
|
|
296
|
+
// SSE) — the same live-rescan physics every other story surface uses.
|
|
297
|
+
useSyncExternalStore(subscribeProjectStoryModules, getProjectStoryModules);
|
|
298
|
+
const state = _states.get(documentId);
|
|
299
|
+
const [source, setSource] = useState<MountedStoryViewportSource | null>(null);
|
|
300
|
+
const [error, setError] = useState<string | null>(null);
|
|
301
|
+
const ownedSources = useRef(new Set<MountedStoryViewportSource>());
|
|
302
|
+
// Keep the displayed mount alive while its replacement compiles and settles.
|
|
303
|
+
// Retire old mounts only after React has committed the new viewport source.
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (!source) return;
|
|
306
|
+
for (const owned of ownedSources.current) {
|
|
307
|
+
// Set insertion order is publication order. A newer result may already
|
|
308
|
+
// be queued for React; never retire it from an older commit's effect.
|
|
309
|
+
if (owned === source) break;
|
|
310
|
+
owned.dispose();
|
|
311
|
+
ownedSources.current.delete(owned);
|
|
312
|
+
}
|
|
313
|
+
}, [source]);
|
|
314
|
+
useEffect(
|
|
315
|
+
() => () => {
|
|
316
|
+
for (const owned of ownedSources.current) owned.dispose();
|
|
317
|
+
ownedSources.current.clear();
|
|
318
|
+
},
|
|
319
|
+
[],
|
|
320
|
+
);
|
|
321
|
+
// The layout host mounts every open document, so a session
|
|
322
|
+
// restored with several story tabs would otherwise mount every one of their
|
|
323
|
+
// fiber roots at boot. Mount on FIRST activation and retain the mount
|
|
324
|
+
// afterwards — latching rather than tracking `active` is the point: a mount
|
|
325
|
+
// freed on deactivation is exactly the black-panel defect
|
|
326
|
+
// `mountedStoryViewportSource` documents.
|
|
327
|
+
const [visited, setVisited] = useState(active);
|
|
328
|
+
// This document is a mounted project-owned R3F composition whose stamped
|
|
329
|
+
// JSX source is its write authority, so THIS caller supplies the source
|
|
330
|
+
// adapter — the Object3D document constructs no source lane itself.
|
|
331
|
+
// Memoized on the identity it closes over: the factory is an activation
|
|
332
|
+
// dependency of the viewport and must not change per render.
|
|
333
|
+
const modulePath = state?.modulePath;
|
|
334
|
+
const sourceAuthoring = useMemo<SourceDocumentAuthoringFactory | undefined>(
|
|
335
|
+
() =>
|
|
336
|
+
modulePath
|
|
337
|
+
? ({ store, scene, hierarchyRoots }) => {
|
|
338
|
+
const adapter = new R3fSourceAuthoringAdapter(store, scene, {
|
|
339
|
+
worldId: documentId,
|
|
340
|
+
entryPath: modulePath,
|
|
341
|
+
writeBackend: sourceWriteBackendIfPrimed('The Asset Editor'),
|
|
342
|
+
hierarchyMode: 'definition',
|
|
343
|
+
...(hierarchyRoots ? { hierarchyRoots } : {}),
|
|
344
|
+
});
|
|
345
|
+
return { adapter, dispose: () => adapter.dispose() };
|
|
346
|
+
}
|
|
347
|
+
: undefined,
|
|
348
|
+
[documentId, modulePath],
|
|
349
|
+
);
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
if (active) setVisited(true);
|
|
352
|
+
}, [active]);
|
|
353
|
+
const { story, moduleError } = state
|
|
354
|
+
? findComposedStory(state.modulePath, state.storyName)
|
|
355
|
+
: { story: undefined, moduleError: undefined };
|
|
356
|
+
const overridesKey = state ? JSON.stringify(state.argOverrides) : '';
|
|
357
|
+
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
if (!visited || !story || !state) return;
|
|
360
|
+
let cancelled = false;
|
|
361
|
+
setError(null);
|
|
362
|
+
void mountStoryObject3D(story.Component as StoryPreviewComponent, {
|
|
363
|
+
...state.argOverrides,
|
|
364
|
+
})
|
|
365
|
+
.then((mounted) => {
|
|
366
|
+
if (cancelled) {
|
|
367
|
+
void disposeStoryObject3D(mounted);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// A story that reconciles to an EMPTY subtree is the one remaining way
|
|
371
|
+
// this document could paint a black viewport with nothing to say. Same
|
|
372
|
+
// predicate every three surface qualifies on, so they cannot disagree
|
|
373
|
+
// about what "mounted as three" means — and here it
|
|
374
|
+
// becomes a reported failure rather than an empty picture.
|
|
375
|
+
if (!mountedStoryHasThreeContent(mounted.root)) {
|
|
376
|
+
void disposeStoryObject3D(mounted);
|
|
377
|
+
setError(
|
|
378
|
+
'The story mounted with no three content — its first R3F commit rendered an empty ' +
|
|
379
|
+
'subtree. A <Suspense> boundary around three content is the usual cause: the ' +
|
|
380
|
+
'boundary commits its fallback first, so the real children arrive on a later ' +
|
|
381
|
+
'commit this document never sees.',
|
|
382
|
+
);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const next = mountedStoryViewportSource(mounted);
|
|
386
|
+
ownedSources.current.add(next);
|
|
387
|
+
setSource(next);
|
|
388
|
+
})
|
|
389
|
+
.catch((reason: unknown) => {
|
|
390
|
+
if (cancelled) return;
|
|
391
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
392
|
+
});
|
|
393
|
+
return () => {
|
|
394
|
+
cancelled = true;
|
|
395
|
+
// Cancel obsolete builds. The displayed source remains owned until a
|
|
396
|
+
// replacement commits or the document closes; HMR is not a close.
|
|
397
|
+
};
|
|
398
|
+
// `story.Component` identity changes on registry refresh, `overridesKey` on
|
|
399
|
+
// an arg edit — both remount, which is exactly the wanted behavior.
|
|
400
|
+
// `visited` only ever goes false → true, so it never tears a mount down.
|
|
401
|
+
}, [visited, story, state, overridesKey]);
|
|
402
|
+
|
|
403
|
+
if (!state) {
|
|
404
|
+
return (
|
|
405
|
+
<div style={{ padding: 12, fontSize: 12, color: themeVars.content.muted }}>
|
|
406
|
+
This story document is no longer available.
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if ((!story || error) && !source) {
|
|
412
|
+
const rootId = `${state.modulePath}#${state.storyName}`;
|
|
413
|
+
return (
|
|
414
|
+
<div
|
|
415
|
+
data-testid="three-story-document-error"
|
|
416
|
+
style={{ position: 'relative', flex: 1, minHeight: 0 }}
|
|
417
|
+
>
|
|
418
|
+
<SurfaceStateOverlay
|
|
419
|
+
testId="three-story-surface-status"
|
|
420
|
+
explanation={{
|
|
421
|
+
tone: 'error',
|
|
422
|
+
title: `Story ${rootId} failed`,
|
|
423
|
+
description:
|
|
424
|
+
error ?? moduleError ?? 'It may have been renamed or removed; close this document.',
|
|
425
|
+
}}
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!source) {
|
|
432
|
+
return (
|
|
433
|
+
<div
|
|
434
|
+
data-testid="three-story-document-loading"
|
|
435
|
+
style={{ position: 'relative', flex: 1, minHeight: 0 }}
|
|
436
|
+
>
|
|
437
|
+
<SurfaceStateOverlay
|
|
438
|
+
testId="three-story-surface-status"
|
|
439
|
+
explanation={explainSurface({
|
|
440
|
+
surface: 'Story',
|
|
441
|
+
rootIds: [`${state.modulePath}#${state.storyName}`],
|
|
442
|
+
phase: 'loading',
|
|
443
|
+
content: 'unknown',
|
|
444
|
+
})}
|
|
445
|
+
/>
|
|
446
|
+
</div>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div style={{ position: 'relative', display: 'flex', flex: 1, minHeight: 0 }}>
|
|
452
|
+
<Object3DDocumentViewport
|
|
453
|
+
documentId={documentId}
|
|
454
|
+
sourcePath={state.modulePath}
|
|
455
|
+
build={source.build}
|
|
456
|
+
displayName={story?.label ?? state.storyName}
|
|
457
|
+
active={active}
|
|
458
|
+
assetType="component"
|
|
459
|
+
{...(sourceAuthoring ? { sourceAuthoring } : {})}
|
|
460
|
+
cameraDirection={STANDARD_COMPONENT_CAMERA_DIRECTION}
|
|
461
|
+
/>
|
|
462
|
+
{(!story || error) && (
|
|
463
|
+
<SurfaceStateOverlay
|
|
464
|
+
testId="three-story-surface-status"
|
|
465
|
+
explanation={{
|
|
466
|
+
tone: 'error',
|
|
467
|
+
title: 'Source update failed',
|
|
468
|
+
description:
|
|
469
|
+
error ??
|
|
470
|
+
moduleError ??
|
|
471
|
+
'This story is unavailable. The last working view is retained.',
|
|
472
|
+
}}
|
|
473
|
+
/>
|
|
474
|
+
)}
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// --- Inspector "Story Args" section for a three-story document -------------
|
|
480
|
+
|
|
481
|
+
function ThreeStoryArgsSection() {
|
|
482
|
+
useSyncExternalStore(subscribeThreeStoryDocuments, threeStoryDocumentsVersion);
|
|
483
|
+
useSyncExternalStore(subscribeProjectStoryModules, getProjectStoryModules);
|
|
484
|
+
const active = activeThreeStoryDocument();
|
|
485
|
+
if (!active) return null;
|
|
486
|
+
const { story } = findComposedStory(active.modulePath, active.storyName);
|
|
487
|
+
const args = story ? { ...story.args, ...active.argOverrides } : {};
|
|
488
|
+
// One owner for "which args are editable, and as what" — shared with the
|
|
489
|
+
// React world adapter's story nodes.
|
|
490
|
+
const descriptors = storyArgPropertyDescriptors(args, THREE_STORY_ARG_PATH_PREFIX);
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<div
|
|
494
|
+
data-testid="inspector-story-args-three"
|
|
495
|
+
style={{
|
|
496
|
+
pointerEvents: 'auto',
|
|
497
|
+
padding: 8,
|
|
498
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
499
|
+
}}
|
|
500
|
+
>
|
|
501
|
+
{/* No mini-label of its own: the projection heads this section with its
|
|
502
|
+
title and icon (`components/InspectionProjection.tsx`). */}
|
|
503
|
+
<div
|
|
504
|
+
style={{ fontSize: 11, color: themeVars.content.primary, fontWeight: 600, marginBottom: 4 }}
|
|
505
|
+
>
|
|
506
|
+
{active.storyName}
|
|
507
|
+
</div>
|
|
508
|
+
{descriptors.length === 0 && (
|
|
509
|
+
<div style={{ fontSize: 11, color: themeVars.content.dim }}>
|
|
510
|
+
{story ? 'This story declares no editable args.' : 'Story source unavailable.'}
|
|
511
|
+
</div>
|
|
512
|
+
)}
|
|
513
|
+
{descriptors.map((descriptor) => (
|
|
514
|
+
<div
|
|
515
|
+
key={descriptor.path}
|
|
516
|
+
style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '3px 0' }}
|
|
517
|
+
>
|
|
518
|
+
<span
|
|
519
|
+
title={descriptor.label}
|
|
520
|
+
style={{
|
|
521
|
+
width: 90,
|
|
522
|
+
flexShrink: 0,
|
|
523
|
+
fontSize: 11,
|
|
524
|
+
color: themeVars.content.muted,
|
|
525
|
+
overflow: 'hidden',
|
|
526
|
+
textOverflow: 'ellipsis',
|
|
527
|
+
}}
|
|
528
|
+
>
|
|
529
|
+
{descriptor.label}
|
|
530
|
+
</span>
|
|
531
|
+
{descriptor.type === 'boolean' ? (
|
|
532
|
+
<Checkbox
|
|
533
|
+
className="vgai-checkbox"
|
|
534
|
+
data-testid={`three-story-arg-${descriptor.label}`}
|
|
535
|
+
checked={args[descriptor.label] === true}
|
|
536
|
+
onChange={(e) =>
|
|
537
|
+
setThreeStoryArgOverride(active.id, descriptor.label, e.target.checked)
|
|
538
|
+
}
|
|
539
|
+
/>
|
|
540
|
+
) : descriptor.type === 'number' ? (
|
|
541
|
+
<TextInput
|
|
542
|
+
type="number"
|
|
543
|
+
className="vgai-input"
|
|
544
|
+
data-testid={`three-story-arg-${descriptor.label}`}
|
|
545
|
+
value={args[descriptor.label] as number}
|
|
546
|
+
onChange={(e) => {
|
|
547
|
+
// Empty/unparseable clears the override rather than storing
|
|
548
|
+
// NaN — same reason as `setThreeStoryArgOverride`'s doc.
|
|
549
|
+
const raw = e.target.value;
|
|
550
|
+
const parsed = Number.parseFloat(raw);
|
|
551
|
+
setThreeStoryArgOverride(
|
|
552
|
+
active.id,
|
|
553
|
+
descriptor.label,
|
|
554
|
+
raw === '' || Number.isNaN(parsed) ? undefined : parsed,
|
|
555
|
+
);
|
|
556
|
+
}}
|
|
557
|
+
style={{ width: 80 }}
|
|
558
|
+
/>
|
|
559
|
+
) : descriptor.type === 'string' ? (
|
|
560
|
+
<TextInput
|
|
561
|
+
type="text"
|
|
562
|
+
className="vgai-input"
|
|
563
|
+
data-testid={`three-story-arg-${descriptor.label}`}
|
|
564
|
+
value={args[descriptor.label] as string}
|
|
565
|
+
onChange={(e) =>
|
|
566
|
+
setThreeStoryArgOverride(active.id, descriptor.label, e.target.value)
|
|
567
|
+
}
|
|
568
|
+
style={{ flex: 1, minWidth: 0 }}
|
|
569
|
+
/>
|
|
570
|
+
) : (
|
|
571
|
+
<span
|
|
572
|
+
data-testid={`three-story-arg-${descriptor.label}`}
|
|
573
|
+
style={{ fontSize: 11, color: themeVars.content.dim }}
|
|
574
|
+
>
|
|
575
|
+
{JSON.stringify(args[descriptor.label])}
|
|
576
|
+
</span>
|
|
577
|
+
)}
|
|
578
|
+
</div>
|
|
579
|
+
))}
|
|
580
|
+
</div>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const registrationGroup = createHmrRegistrationGroup(import.meta.hot, 'three-story-contributions');
|
|
585
|
+
|
|
586
|
+
/** Idempotent one-time registration of the three-story Inspector section.
|
|
587
|
+
* Called from `openThreeStoryDocument` so merely importing this module in a
|
|
588
|
+
* test does not mutate shared registries. */
|
|
589
|
+
export function ensureThreeStoryContributionsRegistered(): void {
|
|
590
|
+
registrationGroup.ensure((track) => {
|
|
591
|
+
track(
|
|
592
|
+
registerInspectorSections({
|
|
593
|
+
match: () => activeThreeStoryDocument() !== null,
|
|
594
|
+
id: STORY_ARGS_SECTION_ID,
|
|
595
|
+
title: STORY_ARGS_SECTION_TITLE,
|
|
596
|
+
icon: STORY_ARGS_SECTION_ICON,
|
|
597
|
+
order: CONTRIBUTED_SECTION_ORDER,
|
|
598
|
+
Section: ThreeStoryArgsSection,
|
|
599
|
+
}),
|
|
600
|
+
);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Test-only: allow re-registration after a registry reset. */
|
|
605
|
+
export function __resetThreeStoryContributionsForTest(): void {
|
|
606
|
+
registrationGroup.reset();
|
|
607
|
+
}
|