@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,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E3 + E4 (spec §11) — a visual, READ-ONLY inspector for a real XState v5 machine: states
|
|
3
|
+
* render as boxes (nodes), transitions as labeled arrows (edges), and the
|
|
4
|
+
* currently active state(s) are highlighted. XState behavior machines are
|
|
5
|
+
* hand-authored TypeScript; this component INSPECTS them, it never edits/generates/round-trips
|
|
6
|
+
* machine config. That boundary is also stated inline in the panel itself (the "Inspect-only"
|
|
7
|
+
* badge below) — not just in a doc a user might not read.
|
|
8
|
+
*
|
|
9
|
+
* Pure presentation: `buildXStateGraph`/`layoutXStateGraph` (this directory)
|
|
10
|
+
* do all the machine-introspection and layout work; this component only
|
|
11
|
+
* renders their output plus the small amount of interaction state
|
|
12
|
+
* (hover/selection for the per-state metadata detail panel).
|
|
13
|
+
*
|
|
14
|
+
* Visual language (house style — see DataPanel.tsx for the canonical dock
|
|
15
|
+
* tab): chrome is the sans default; monospace is reserved for CODE — state
|
|
16
|
+
* ids and event tokens on edges. Blue
|
|
17
|
+
* (the palette accent) is selection (the clicked node); green (the `success` token) is strictly
|
|
18
|
+
* the LIVE status color (the running state's border + "● ACTIVE" pip and the
|
|
19
|
+
* active transition edge) — two different signals, two different colors.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { faCircleInfo, faLock } from '@fortawesome/free-solid-svg-icons';
|
|
23
|
+
import {
|
|
24
|
+
Button,
|
|
25
|
+
EditorIcon,
|
|
26
|
+
fontSizeVar,
|
|
27
|
+
spaceVar,
|
|
28
|
+
TONE_WARNING,
|
|
29
|
+
themeVars,
|
|
30
|
+
} from '@volter/editor-sdk/widgets';
|
|
31
|
+
import type { InspectableXStateActor } from '@volter/threejs-runtime/behavior/xstate-inspection';
|
|
32
|
+
import { useMemo, useState } from 'react';
|
|
33
|
+
import type { AnyStateMachine } from 'xstate';
|
|
34
|
+
import { useLiveActorState } from './use-live-actor-state';
|
|
35
|
+
import { buildXStateGraph, type XStateGraphNode } from './xstate-graph';
|
|
36
|
+
import { layoutXStateGraph } from './xstate-layout';
|
|
37
|
+
|
|
38
|
+
export interface XStateMachineInspectorProps {
|
|
39
|
+
machine: AnyStateMachine;
|
|
40
|
+
/**
|
|
41
|
+
* Static "current state id" highlight (e.g. `"character-animation.idle"`).
|
|
42
|
+
* Ignored while `actor` is provided AND has emitted at least one snapshot
|
|
43
|
+
* (the live actor becomes the source of truth for the highlight once
|
|
44
|
+
* available). Useful for tests and for callers with no live actor.
|
|
45
|
+
*/
|
|
46
|
+
currentStateId?: string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* A LIVE xstate actor to highlight and follow in real time (play mode).
|
|
49
|
+
* Optional — when omitted, the panel is a static graph view of the machine
|
|
50
|
+
* shape, optionally highlighted via `currentStateId`. When provided, this
|
|
51
|
+
* component subscribes to it (`actor.subscribe`) and re-highlights on every
|
|
52
|
+
* transition, unsubscribing on unmount/actor change.
|
|
53
|
+
*/
|
|
54
|
+
actor?: InspectableXStateActor | undefined;
|
|
55
|
+
/** Optional heading override (defaults to the machine's root id). */
|
|
56
|
+
title?: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// U6 sweep: this used to be a fully hand-typed local palette re-spelling the
|
|
60
|
+
// token values; the chrome slots now IMPORT the tokens they always mirrored.
|
|
61
|
+
// Graph paint follows the same semantic roles as surrounding editor chrome.
|
|
62
|
+
// State/data identity is expressed through labels and graph structure, not a
|
|
63
|
+
// private syntax palette that becomes illegible under alternate themes.
|
|
64
|
+
const COLORS = {
|
|
65
|
+
bg: themeVars.surface.inset,
|
|
66
|
+
panelBg: themeVars.surface.panel,
|
|
67
|
+
border: themeVars.boundary.default,
|
|
68
|
+
innerBorder: themeVars.boundary.default,
|
|
69
|
+
nodeBg: themeVars.surface.panel,
|
|
70
|
+
nodeHoverBg: themeVars.surface.chrome,
|
|
71
|
+
nodeBorder: themeVars.boundary.strong,
|
|
72
|
+
/** Green = LIVE status only (running state / active edge), never selection.
|
|
73
|
+
* Now the shared `success` token (§3 absorbs the old one-off `#5ec26a`). */
|
|
74
|
+
live: themeVars.semantic.success,
|
|
75
|
+
text: themeVars.content.primary,
|
|
76
|
+
dim: themeVars.content.dim,
|
|
77
|
+
edge: themeVars.content.dim,
|
|
78
|
+
guard: themeVars.semantic.warning,
|
|
79
|
+
/** Blue = selection (the clicked node) / interactive accent. */
|
|
80
|
+
accent: themeVars.accent.default,
|
|
81
|
+
/** Clip names / event tokens remain code, using the theme accent. */
|
|
82
|
+
codeBlue: themeVars.accent.default,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Monospace is for CODE only: state ids, event tokens, clip names, JSON.
|
|
86
|
+
* (U6: the one shared editor mono stack, not a fifth local spelling.) */
|
|
87
|
+
const MONO = themeVars.typography.mono;
|
|
88
|
+
|
|
89
|
+
/** DataPanel-style uppercase eyebrow label (detail-pane section headers). */
|
|
90
|
+
const eyebrowStyle: React.CSSProperties = {
|
|
91
|
+
fontSize: fontSizeVar.sm,
|
|
92
|
+
fontWeight: 600,
|
|
93
|
+
textTransform: 'uppercase',
|
|
94
|
+
letterSpacing: 0.4,
|
|
95
|
+
color: COLORS.dim,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function nodeTestId(id: string): string {
|
|
99
|
+
return `xstate-node-${id.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One state box. Purely presentational — position comes from the layout. */
|
|
103
|
+
function StateNodeBox({
|
|
104
|
+
n,
|
|
105
|
+
x,
|
|
106
|
+
y,
|
|
107
|
+
width,
|
|
108
|
+
height,
|
|
109
|
+
isActive,
|
|
110
|
+
isSelected,
|
|
111
|
+
onSelect,
|
|
112
|
+
}: {
|
|
113
|
+
n: XStateGraphNode;
|
|
114
|
+
x: number;
|
|
115
|
+
y: number;
|
|
116
|
+
width: number;
|
|
117
|
+
height: number;
|
|
118
|
+
isActive: boolean;
|
|
119
|
+
isSelected: boolean;
|
|
120
|
+
onSelect: () => void;
|
|
121
|
+
}) {
|
|
122
|
+
// Selection (blue) wins the border; live (green) keeps its pip either way.
|
|
123
|
+
return (
|
|
124
|
+
<Button
|
|
125
|
+
className="vgai-xstate-node"
|
|
126
|
+
variant="ghost"
|
|
127
|
+
data-testid={nodeTestId(n.id)}
|
|
128
|
+
data-active={String(isActive)}
|
|
129
|
+
data-selected={isSelected || undefined}
|
|
130
|
+
onClick={onSelect}
|
|
131
|
+
style={{
|
|
132
|
+
position: 'absolute',
|
|
133
|
+
left: x,
|
|
134
|
+
top: y,
|
|
135
|
+
width,
|
|
136
|
+
height,
|
|
137
|
+
}}
|
|
138
|
+
title={n.id}
|
|
139
|
+
>
|
|
140
|
+
<span
|
|
141
|
+
className="vgai-xstate-node-type"
|
|
142
|
+
style={{
|
|
143
|
+
...eyebrowStyle,
|
|
144
|
+
fontSize: fontSizeVar.xs,
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{n.type}
|
|
148
|
+
{n.isInitial ? ' · initial' : ''}
|
|
149
|
+
</span>
|
|
150
|
+
<span
|
|
151
|
+
style={{
|
|
152
|
+
fontWeight: 600,
|
|
153
|
+
fontSize: fontSizeVar.md,
|
|
154
|
+
display: 'flex',
|
|
155
|
+
alignItems: 'center',
|
|
156
|
+
gap: spaceVar[3],
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
{n.key}
|
|
160
|
+
{isActive && (
|
|
161
|
+
<span
|
|
162
|
+
data-testid={`xstate-node-active-badge-${n.key}`}
|
|
163
|
+
style={{
|
|
164
|
+
color: COLORS.live,
|
|
165
|
+
fontSize: fontSizeVar.xs,
|
|
166
|
+
fontWeight: 700,
|
|
167
|
+
letterSpacing: 0.4,
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
● ACTIVE
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
</span>
|
|
174
|
+
</Button>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** SVG arrow connecting two laid-out node boxes, labeled with its event/guard. */
|
|
179
|
+
function EdgeArrow({
|
|
180
|
+
x1,
|
|
181
|
+
y1,
|
|
182
|
+
x2,
|
|
183
|
+
y2,
|
|
184
|
+
label,
|
|
185
|
+
guard,
|
|
186
|
+
isEventless,
|
|
187
|
+
isActive,
|
|
188
|
+
}: {
|
|
189
|
+
x1: number;
|
|
190
|
+
y1: number;
|
|
191
|
+
x2: number;
|
|
192
|
+
y2: number;
|
|
193
|
+
label: string;
|
|
194
|
+
guard: string | undefined;
|
|
195
|
+
isEventless: boolean;
|
|
196
|
+
isActive: boolean;
|
|
197
|
+
}) {
|
|
198
|
+
const midX = (x1 + x2) / 2;
|
|
199
|
+
const midY = (y1 + y2) / 2;
|
|
200
|
+
const stroke = isActive ? COLORS.live : isEventless ? COLORS.guard : COLORS.edge;
|
|
201
|
+
const labelColor = isActive ? COLORS.live : isEventless ? COLORS.guard : COLORS.codeBlue;
|
|
202
|
+
// DISPLAY-only guard tidy-up (the full guardLabel stays in the detail
|
|
203
|
+
// panel's transitions list): an inline arrow-predicate guard reads better
|
|
204
|
+
// on a chip as just its body (`context.speed <= 0.1`), and anything still
|
|
205
|
+
// longer than ~30 chars gets an ellipsis so chips stay near the line's
|
|
206
|
+
// midpoint instead of sprawling across node columns.
|
|
207
|
+
const guardBody = guard?.includes('=>') ? guard.slice(guard.indexOf('=>') + 2).trim() : guard;
|
|
208
|
+
const guardDisplay =
|
|
209
|
+
guardBody && guardBody.length > 30 ? `${guardBody.slice(0, 29)}…` : guardBody;
|
|
210
|
+
// Size the label chip from the FULL displayed text (event + guard suffix) —
|
|
211
|
+
// 9px monospace is ~5.6px/char.
|
|
212
|
+
const fullLabel = guardDisplay ? `${label} [${guardDisplay}]` : label;
|
|
213
|
+
const chipWidth = fullLabel.length * 5.6 + 14;
|
|
214
|
+
return (
|
|
215
|
+
<g data-testid="xstate-edge" opacity={isActive ? 1 : 0.8}>
|
|
216
|
+
<line
|
|
217
|
+
x1={x1}
|
|
218
|
+
y1={y1}
|
|
219
|
+
x2={x2}
|
|
220
|
+
y2={y2}
|
|
221
|
+
stroke={stroke}
|
|
222
|
+
strokeWidth={isActive ? 1.5 : 1}
|
|
223
|
+
markerEnd={isActive ? 'url(#xstate-arrowhead-active)' : 'url(#xstate-arrowhead)'}
|
|
224
|
+
/>
|
|
225
|
+
<rect
|
|
226
|
+
x={midX - chipWidth / 2}
|
|
227
|
+
y={midY - 8}
|
|
228
|
+
width={chipWidth}
|
|
229
|
+
height={16}
|
|
230
|
+
fill={COLORS.panelBg}
|
|
231
|
+
stroke={COLORS.border}
|
|
232
|
+
strokeWidth={1}
|
|
233
|
+
rx={4}
|
|
234
|
+
/>
|
|
235
|
+
<text
|
|
236
|
+
x={midX}
|
|
237
|
+
y={midY + 3}
|
|
238
|
+
fill={labelColor}
|
|
239
|
+
fontSize={9}
|
|
240
|
+
textAnchor="middle"
|
|
241
|
+
fontFamily={MONO}
|
|
242
|
+
>
|
|
243
|
+
{label}
|
|
244
|
+
{guardDisplay && <tspan fill={COLORS.guard}>{` [${guardDisplay}]`}</tspan>}
|
|
245
|
+
</text>
|
|
246
|
+
</g>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function displayedActiveIds(
|
|
251
|
+
liveIds: string[] | null,
|
|
252
|
+
currentStateId: string | undefined,
|
|
253
|
+
): string[] {
|
|
254
|
+
if (liveIds) return liveIds;
|
|
255
|
+
return currentStateId ? [currentStateId] : [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function XStateMachineInspector({
|
|
259
|
+
machine,
|
|
260
|
+
currentStateId,
|
|
261
|
+
actor,
|
|
262
|
+
title,
|
|
263
|
+
}: XStateMachineInspectorProps) {
|
|
264
|
+
const graph = useMemo(() => buildXStateGraph(machine), [machine]);
|
|
265
|
+
const layout = useMemo(() => layoutXStateGraph(graph), [graph]);
|
|
266
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
267
|
+
|
|
268
|
+
// Live actor (play mode) takes over the highlight once it has emitted a
|
|
269
|
+
// snapshot; otherwise fall back to the static `currentStateId` prop.
|
|
270
|
+
const liveActiveIds = useLiveActorState(machine, actor);
|
|
271
|
+
const activeIds = displayedActiveIds(liveActiveIds, currentStateId);
|
|
272
|
+
const activeSet = useMemo(() => new Set(activeIds), [activeIds]);
|
|
273
|
+
|
|
274
|
+
const selectedNode = graph.nodes.find((n) => n.id === selectedId) ?? null;
|
|
275
|
+
const selectedEdges = selectedNode ? graph.edges.filter((e) => e.source === selectedNode.id) : [];
|
|
276
|
+
const context = actor?.getSnapshot().context;
|
|
277
|
+
const contextEntries =
|
|
278
|
+
context && typeof context === 'object' && !Array.isArray(context)
|
|
279
|
+
? Object.entries(context as Record<string, unknown>)
|
|
280
|
+
: [];
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
// Responsive by construction (no width measuring): the graph and the detail
|
|
284
|
+
// pane sit SIDE BY SIDE when there is room and WRAP — detail STACKS BELOW —
|
|
285
|
+
// when the container is narrow (the inspector rail). `flex-wrap` plus a
|
|
286
|
+
// per-pane min-width floor forces the wrap instead of shrinking either pane
|
|
287
|
+
// to an unusable sliver; `min(…, 100%)` keeps the floor from overflowing a
|
|
288
|
+
// container narrower than the floor itself. The graph is an inherently
|
|
289
|
+
// absolute-positioned 2D canvas, so it is BOUNDED in a fixed-min-height box
|
|
290
|
+
// that scrolls internally (below) — it never forces the section wider.
|
|
291
|
+
<div
|
|
292
|
+
data-testid="xstate-inspector-panel"
|
|
293
|
+
style={{
|
|
294
|
+
display: 'flex',
|
|
295
|
+
flexDirection: 'row',
|
|
296
|
+
flexWrap: 'wrap',
|
|
297
|
+
alignContent: 'flex-start',
|
|
298
|
+
height: '100%',
|
|
299
|
+
minHeight: 0,
|
|
300
|
+
overflowY: 'auto',
|
|
301
|
+
background: COLORS.bg,
|
|
302
|
+
color: COLORS.text,
|
|
303
|
+
fontSize: fontSizeVar.md,
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
<div
|
|
307
|
+
style={{
|
|
308
|
+
flex: '1 1 260px',
|
|
309
|
+
minWidth: 'min(260px, 100%)',
|
|
310
|
+
display: 'flex',
|
|
311
|
+
flexDirection: 'column',
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
{/* Header: machine id + the E4 inspect-only badge/explanation. */}
|
|
315
|
+
<div
|
|
316
|
+
style={{
|
|
317
|
+
display: 'flex',
|
|
318
|
+
alignItems: 'center',
|
|
319
|
+
gap: spaceVar[5],
|
|
320
|
+
padding: `${spaceVar[3]} ${spaceVar[5]}`,
|
|
321
|
+
borderBottom: `1px solid ${COLORS.border}`,
|
|
322
|
+
flexShrink: 0,
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
<span style={{ fontWeight: 600, fontSize: fontSizeVar.md }}>
|
|
326
|
+
{title ?? graph.machineId}
|
|
327
|
+
</span>
|
|
328
|
+
<span
|
|
329
|
+
data-testid="xstate-inspect-only-badge"
|
|
330
|
+
title="XState behavior is hand-authored TypeScript. This view derives the live statechart for debugging and never creates a second machine document."
|
|
331
|
+
style={{
|
|
332
|
+
display: 'inline-flex',
|
|
333
|
+
alignItems: 'center',
|
|
334
|
+
gap: spaceVar[2],
|
|
335
|
+
padding: `${spaceVar[1]} ${spaceVar[4]}`,
|
|
336
|
+
borderRadius: themeVars.shape.full,
|
|
337
|
+
// U6: the shared warning banner tone (banner-tones.ts), not a
|
|
338
|
+
// fourth hand-mixed amber triple.
|
|
339
|
+
background: TONE_WARNING.bg,
|
|
340
|
+
color: TONE_WARNING.fg,
|
|
341
|
+
border: `1px solid ${TONE_WARNING.border}`,
|
|
342
|
+
fontSize: fontSizeVar.xs,
|
|
343
|
+
fontWeight: 700,
|
|
344
|
+
textTransform: 'uppercase',
|
|
345
|
+
letterSpacing: 0.4,
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
<EditorIcon icon={faLock} style={{ fontSize: fontSizeVar.xs }} />
|
|
349
|
+
Inspect-only
|
|
350
|
+
</span>
|
|
351
|
+
<span style={{ color: COLORS.dim, fontSize: fontSizeVar.sm, flex: 1 }}>
|
|
352
|
+
Live behavior statechart · source-owned TypeScript
|
|
353
|
+
</span>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{/* Graph canvas */}
|
|
357
|
+
<div
|
|
358
|
+
data-testid="xstate-graph-canvas"
|
|
359
|
+
// `flex: 1` fills a tall panel; `minHeight` is the floor that keeps
|
|
360
|
+
// the graph a usable, internally-scrolled box when the container is
|
|
361
|
+
// auto-height (a section body in the inspector column has no fixed
|
|
362
|
+
// height to fill), so a wide graph pans inside this box rather than
|
|
363
|
+
// pushing the section open.
|
|
364
|
+
style={{ flex: '1 1 auto', minHeight: 300, overflow: 'auto', position: 'relative' }}
|
|
365
|
+
>
|
|
366
|
+
<div style={{ position: 'relative', width: layout.width, height: layout.height }}>
|
|
367
|
+
<svg
|
|
368
|
+
width={layout.width}
|
|
369
|
+
height={layout.height}
|
|
370
|
+
style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}
|
|
371
|
+
>
|
|
372
|
+
<defs>
|
|
373
|
+
<marker
|
|
374
|
+
id="xstate-arrowhead"
|
|
375
|
+
markerWidth="8"
|
|
376
|
+
markerHeight="8"
|
|
377
|
+
refX="7"
|
|
378
|
+
refY="4"
|
|
379
|
+
orient="auto"
|
|
380
|
+
>
|
|
381
|
+
<path d="M0,0 L8,4 L0,8 Z" fill={COLORS.edge} />
|
|
382
|
+
</marker>
|
|
383
|
+
<marker
|
|
384
|
+
id="xstate-arrowhead-active"
|
|
385
|
+
markerWidth="8"
|
|
386
|
+
markerHeight="8"
|
|
387
|
+
refX="7"
|
|
388
|
+
refY="4"
|
|
389
|
+
orient="auto"
|
|
390
|
+
>
|
|
391
|
+
<path d="M0,0 L8,4 L0,8 Z" fill={COLORS.live} />
|
|
392
|
+
</marker>
|
|
393
|
+
</defs>
|
|
394
|
+
{graph.edges.map((edge) => {
|
|
395
|
+
const from = layout.positionById.get(edge.source);
|
|
396
|
+
const to = layout.positionById.get(edge.target);
|
|
397
|
+
if (!from || !to) return null;
|
|
398
|
+
const isActive = activeSet.has(edge.source) && activeSet.has(edge.target);
|
|
399
|
+
// Anchor at the right/left edges of the boxes when moving
|
|
400
|
+
// forward in depth, else box-center — keeps arrows readable
|
|
401
|
+
// without a full routing algorithm (deliberately simple, per
|
|
402
|
+
// the "no fancy graph engine required" AC).
|
|
403
|
+
const x1 = from.x + from.width;
|
|
404
|
+
const y1 = from.y + from.height / 2;
|
|
405
|
+
const x2 = to.x;
|
|
406
|
+
const y2 = to.y + to.height / 2;
|
|
407
|
+
return (
|
|
408
|
+
<EdgeArrow
|
|
409
|
+
key={edge.id}
|
|
410
|
+
x1={x1}
|
|
411
|
+
y1={y1}
|
|
412
|
+
x2={x2}
|
|
413
|
+
y2={y2}
|
|
414
|
+
label={edge.eventLabel}
|
|
415
|
+
guard={edge.guardLabel}
|
|
416
|
+
isEventless={edge.isEventless}
|
|
417
|
+
isActive={isActive}
|
|
418
|
+
/>
|
|
419
|
+
);
|
|
420
|
+
})}
|
|
421
|
+
</svg>
|
|
422
|
+
{layout.nodes.map(({ node, x, y, width, height }) => (
|
|
423
|
+
<StateNodeBox
|
|
424
|
+
key={node.id}
|
|
425
|
+
n={node}
|
|
426
|
+
x={x}
|
|
427
|
+
y={y}
|
|
428
|
+
width={width}
|
|
429
|
+
height={height}
|
|
430
|
+
isActive={activeSet.has(node.id)}
|
|
431
|
+
isSelected={selectedId === node.id}
|
|
432
|
+
onSelect={() => setSelectedId(node.id)}
|
|
433
|
+
/>
|
|
434
|
+
))}
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
{/* Per-state metadata detail panel — beside the graph when wide, wrapped
|
|
440
|
+
BELOW it when narrow (its min-width floor is what forces the wrap). It
|
|
441
|
+
does not grow past `maxWidth`, so a wide panel keeps the graph
|
|
442
|
+
dominant rather than handing half the width to the detail list. */}
|
|
443
|
+
<div
|
|
444
|
+
data-testid="xstate-detail-panel"
|
|
445
|
+
style={{
|
|
446
|
+
flex: '1 1 240px',
|
|
447
|
+
minWidth: 'min(240px, 100%)',
|
|
448
|
+
maxWidth: 320,
|
|
449
|
+
borderLeft: `1px solid ${COLORS.border}`,
|
|
450
|
+
padding: spaceVar[5],
|
|
451
|
+
overflowY: 'auto',
|
|
452
|
+
fontSize: fontSizeVar.md,
|
|
453
|
+
}}
|
|
454
|
+
>
|
|
455
|
+
<div
|
|
456
|
+
style={{
|
|
457
|
+
...eyebrowStyle,
|
|
458
|
+
display: 'flex',
|
|
459
|
+
alignItems: 'center',
|
|
460
|
+
gap: spaceVar[2],
|
|
461
|
+
marginBottom: spaceVar[4],
|
|
462
|
+
}}
|
|
463
|
+
>
|
|
464
|
+
<EditorIcon icon={faCircleInfo} />
|
|
465
|
+
State detail
|
|
466
|
+
</div>
|
|
467
|
+
{!selectedNode ? (
|
|
468
|
+
<div style={{ color: COLORS.dim }}>Click a state node to inspect its metadata.</div>
|
|
469
|
+
) : (
|
|
470
|
+
<div data-testid="xstate-detail-content">
|
|
471
|
+
<div
|
|
472
|
+
style={{
|
|
473
|
+
fontFamily: MONO,
|
|
474
|
+
fontWeight: 600,
|
|
475
|
+
fontSize: fontSizeVar.base,
|
|
476
|
+
marginBottom: spaceVar[2],
|
|
477
|
+
wordBreak: 'break-all',
|
|
478
|
+
}}
|
|
479
|
+
>
|
|
480
|
+
{selectedNode.id}
|
|
481
|
+
</div>
|
|
482
|
+
<div
|
|
483
|
+
style={{ color: COLORS.dim, fontSize: fontSizeVar.base, marginBottom: spaceVar[5] }}
|
|
484
|
+
>
|
|
485
|
+
{selectedNode.type}
|
|
486
|
+
{selectedNode.isInitial ? ' · initial' : ''}
|
|
487
|
+
{activeSet.has(selectedNode.id) ? (
|
|
488
|
+
<span style={{ color: COLORS.live, fontWeight: 700, letterSpacing: 0.4 }}>
|
|
489
|
+
{' · ● '}ACTIVE
|
|
490
|
+
</span>
|
|
491
|
+
) : (
|
|
492
|
+
''
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<div>
|
|
497
|
+
<div style={{ ...eyebrowStyle, marginBottom: spaceVar[2] }}>Outgoing transitions</div>
|
|
498
|
+
{selectedEdges.length === 0 ? (
|
|
499
|
+
<div style={{ color: COLORS.dim, fontSize: fontSizeVar.base }}>None.</div>
|
|
500
|
+
) : (
|
|
501
|
+
selectedEdges.map((e) => (
|
|
502
|
+
<div key={e.id} style={{ marginBottom: 5, fontSize: fontSizeVar.base }}>
|
|
503
|
+
<span
|
|
504
|
+
style={{
|
|
505
|
+
fontFamily: MONO,
|
|
506
|
+
fontSize: fontSizeVar.sm,
|
|
507
|
+
color: COLORS.accent,
|
|
508
|
+
background: COLORS.panelBg,
|
|
509
|
+
border: `1px solid ${COLORS.innerBorder}`,
|
|
510
|
+
borderRadius: themeVars.shape.small,
|
|
511
|
+
padding: '1px 5px',
|
|
512
|
+
}}
|
|
513
|
+
>
|
|
514
|
+
{e.eventLabel}
|
|
515
|
+
</span>
|
|
516
|
+
<span style={{ color: COLORS.dim }}>{' → '}</span>
|
|
517
|
+
<span style={{ fontFamily: MONO, fontSize: fontSizeVar.sm }}>{e.target}</span>
|
|
518
|
+
{e.guardLabel && (
|
|
519
|
+
<div style={{ color: COLORS.guard, fontSize: fontSizeVar.sm, marginTop: 1 }}>
|
|
520
|
+
guard: <span style={{ fontFamily: MONO }}>{e.guardLabel}</span>
|
|
521
|
+
</div>
|
|
522
|
+
)}
|
|
523
|
+
</div>
|
|
524
|
+
))
|
|
525
|
+
)}
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
{contextEntries.length > 0 && (
|
|
529
|
+
<div style={{ marginTop: 14 }} data-testid="xstate-context-table">
|
|
530
|
+
<div style={{ ...eyebrowStyle, marginBottom: 5 }}>Context</div>
|
|
531
|
+
{contextEntries.map(([key, value]) => (
|
|
532
|
+
<div
|
|
533
|
+
key={key}
|
|
534
|
+
style={{
|
|
535
|
+
display: 'grid',
|
|
536
|
+
gridTemplateColumns: 'minmax(70px, 1fr) minmax(60px, 1fr)',
|
|
537
|
+
gap: spaceVar[4],
|
|
538
|
+
padding: '3px 0',
|
|
539
|
+
borderBottom: `1px solid ${COLORS.innerBorder}`,
|
|
540
|
+
fontSize: fontSizeVar.sm,
|
|
541
|
+
}}
|
|
542
|
+
>
|
|
543
|
+
<span style={{ color: COLORS.dim }}>{key}</span>
|
|
544
|
+
<span
|
|
545
|
+
style={{ fontFamily: MONO, overflow: 'hidden', textOverflow: 'ellipsis' }}
|
|
546
|
+
>
|
|
547
|
+
{typeof value === 'string' ||
|
|
548
|
+
typeof value === 'number' ||
|
|
549
|
+
typeof value === 'boolean'
|
|
550
|
+
? String(value)
|
|
551
|
+
: value == null
|
|
552
|
+
? 'null'
|
|
553
|
+
: Array.isArray(value)
|
|
554
|
+
? `${value.length} items`
|
|
555
|
+
: 'object'}
|
|
556
|
+
</span>
|
|
557
|
+
</div>
|
|
558
|
+
))}
|
|
559
|
+
</div>
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
)}
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E3/E4 demo + test fixture — mirrors `characterAnimationMachine`
|
|
3
|
+
* (`examples/third-person/src/components.ts`) EXACTLY (same states, events,
|
|
4
|
+
* guards, `meta.animation`). Not a re-export: that machine is a module-
|
|
5
|
+
* private `const` in the example game (never exported), and
|
|
6
|
+
* `examples/third-person` is outside `packages/editor`'s tsconfig `include`
|
|
7
|
+
* (no path alias reaches it — confirmed: `packages/editor/tsconfig*.json`
|
|
8
|
+
* only maps the runtime packages), so the editor package cannot
|
|
9
|
+
* import it directly without either widening the example's public surface or
|
|
10
|
+
* adding a new cross-package path alias — both bigger changes than this lean
|
|
11
|
+
* inspector unit warrants. A representative fixture machine is
|
|
12
|
+
* spec-sanctioned for exactly this case ("import it or a representative
|
|
13
|
+
* fixture machine").
|
|
14
|
+
*
|
|
15
|
+
* Keep this in sync with `examples/third-person/src/components.ts` by hand if
|
|
16
|
+
* that machine's shape ever changes — there is no structural coupling
|
|
17
|
+
* enforcing it (a comment-only contract, same as any other fixture mirroring
|
|
18
|
+
* real game code).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { assign, setup as xstateSetup } from 'xstate';
|
|
22
|
+
|
|
23
|
+
export const characterAnimationMachine = xstateSetup({
|
|
24
|
+
types: {
|
|
25
|
+
context: {} as { speed: number; grounded: boolean },
|
|
26
|
+
events: {} as { type: 'UPDATE'; speed: number; grounded: boolean } | { type: 'JUMP' },
|
|
27
|
+
},
|
|
28
|
+
}).createMachine({
|
|
29
|
+
id: 'character-animation',
|
|
30
|
+
initial: 'idle',
|
|
31
|
+
context: { speed: 0, grounded: true },
|
|
32
|
+
on: {
|
|
33
|
+
UPDATE: {
|
|
34
|
+
actions: assign({
|
|
35
|
+
speed: ({ event }) => event.speed,
|
|
36
|
+
grounded: ({ event }) => event.grounded,
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
states: {
|
|
41
|
+
idle: {
|
|
42
|
+
meta: { animation: { clip: 'idle', loop: true, crossfade: { duration: 0.2 } } },
|
|
43
|
+
on: { JUMP: 'airborne' },
|
|
44
|
+
always: { guard: ({ context }) => context.speed > 0.1, target: 'locomotion' },
|
|
45
|
+
},
|
|
46
|
+
locomotion: {
|
|
47
|
+
meta: {
|
|
48
|
+
animation: {
|
|
49
|
+
blendTree: {
|
|
50
|
+
type: '1D',
|
|
51
|
+
parameter: 'speed',
|
|
52
|
+
children: [
|
|
53
|
+
{ clip: 'walk', threshold: 1 },
|
|
54
|
+
{ clip: 'run', threshold: 5 },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
crossfade: { duration: 0.2 },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
on: { JUMP: 'airborne' },
|
|
61
|
+
always: { guard: ({ context }) => context.speed <= 0.1, target: 'idle' },
|
|
62
|
+
},
|
|
63
|
+
airborne: {
|
|
64
|
+
meta: { animation: { clip: 'jump', loop: false, crossfade: { duration: 0.1 } } },
|
|
65
|
+
always: [
|
|
66
|
+
{
|
|
67
|
+
guard: ({ context }) => context.grounded && context.speed > 0.1,
|
|
68
|
+
target: 'locomotion',
|
|
69
|
+
},
|
|
70
|
+
{ guard: ({ context }) => context.grounded, target: 'idle' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|