@volter/editor-game 0.5.65 → 0.5.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/contributions/audio-unlock.service.ts +2 -2
  2. package/contributions/autoplay.service.ts +1 -1
  3. package/contributions/bridge.command.ts +3 -3
  4. package/contributions/canvas/component-board.service.ts +16 -0
  5. package/contributions/canvas/design-time-mount.service.ts +45 -0
  6. package/contributions/canvas-story-capture.service.ts +12 -0
  7. package/contributions/edit-mode-audio.service.ts +2 -2
  8. package/contributions/edit-mode-networking.service.ts +1 -1
  9. package/contributions/gameplay.command.ts +4 -4
  10. package/contributions/generation.service.ts +3 -3
  11. package/contributions/generations.status.tsx +1 -1
  12. package/contributions/godot.palette.json +99 -0
  13. package/contributions/godot.style.ts +82 -0
  14. package/contributions/godot.view.ts +75 -0
  15. package/contributions/ingest.service.ts +4 -4
  16. package/contributions/instances.command.ts +1 -1
  17. package/contributions/navmesh.menu.ts +1 -1
  18. package/contributions/network-observer.service.ts +14 -0
  19. package/contributions/play.command.ts +11 -5
  20. package/contributions/react/component-board.service.ts +1 -1
  21. package/contributions/react/design-time-mount.service.ts +1 -1
  22. package/contributions/react/react-inspector.service.ts +2 -2
  23. package/contributions/scene-document.service.ts +5 -5
  24. package/contributions/state-watch.menu.ts +1 -1
  25. package/contributions/state-watch.utility.tsx +1 -1
  26. package/contributions/team-playtest.service.ts +4 -4
  27. package/contributions/three/component-board.service.ts +1 -1
  28. package/contributions/three/component-verbs.command.ts +8 -8
  29. package/contributions/three/three-authoring.service.ts +8 -5
  30. package/contributions/three-story-capture.service.ts +12 -0
  31. package/contributions/unity.palette.json +102 -0
  32. package/contributions/unity.style.ts +72 -0
  33. package/contributions/unity.view.ts +82 -0
  34. package/contributions/unreal.palette.json +105 -0
  35. package/contributions/unreal.style.ts +66 -0
  36. package/contributions/unreal.view.ts +93 -0
  37. package/dist-node/serving.mjs +16 -0
  38. package/package.json +33 -16
  39. package/serving/index.ts +27 -0
  40. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  41. package/src/asset-budget/asset-budget-model.ts +4 -4
  42. package/src/asset-budget/optimize-apply.ts +7 -7
  43. package/src/audio/AudioDebuggerPanel.tsx +1 -1
  44. package/src/bridge/dispatch.ts +16 -16
  45. package/src/bridge/live-frames.ts +1 -1
  46. package/src/bridge/screenshot.ts +6 -6
  47. package/src/build/BuildProfilesPanel.tsx +3 -3
  48. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -0
  49. package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
  50. package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
  51. package/src/canvas/canvas-design-mount.ts +524 -0
  52. package/src/canvas/design-time-canvas-mount.ts +79 -0
  53. package/src/coverage/live-authoring-surface.ts +6 -6
  54. package/src/coverage/live-project-verbs.ts +2 -2
  55. package/src/coverage/native-system-coverage.ts +6 -6
  56. package/src/coverage/root-coverage.ts +1 -1
  57. package/src/coverage/session-coverage.ts +5 -5
  58. package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
  59. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +13 -53
  60. package/src/edit-mode/edit-mode-audio.ts +4 -4
  61. package/src/edit-mode/edit-mode-networking.ts +2 -2
  62. package/src/game-document/GameCaptureFrameButton.tsx +2 -2
  63. package/src/game-document/GameDocument.tsx +2 -2
  64. package/src/game-document/GamePanel.tsx +8 -8
  65. package/src/game-document/InstanceInspectorPicker.tsx +2 -2
  66. package/src/game-document/crowd-debug.ts +1 -1
  67. package/src/game-document/device-preview.ts +10 -11
  68. package/src/game-document/physics-debug.ts +1 -1
  69. package/src/generation/GenerationActivity.tsx +6 -8
  70. package/src/generation/generation-documents.tsx +8 -8
  71. package/src/generation/generation-jobs.ts +2 -2
  72. package/src/generation/generation-presentation.ts +1 -1
  73. package/src/host/adapter-reach.ts +1 -1
  74. package/src/host/adapter-runtime-bindings.ts +97 -6
  75. package/src/host/api/configurations.ts +2 -2
  76. package/src/host/authoring/babylon-authoring-adapter.ts +8 -8
  77. package/src/host/authoring/contract-hierarchy-authoring.ts +1 -1
  78. package/src/host/authoring/contract-scenes-stories.ts +1 -1
  79. package/src/host/authoring/creation-site-related.ts +2 -2
  80. package/src/host/authoring/ephemeral-persistence.ts +1 -1
  81. package/src/host/authoring/gesture-persist.ts +2 -2
  82. package/src/host/authoring/ingest-data-writer.ts +1 -1
  83. package/src/host/authoring/ingest-source-persistence.ts +8 -8
  84. package/src/host/authoring/mount-isolated-pixi-screen.ts +1 -1
  85. package/src/host/authoring/mounted-authoring.ts +4 -4
  86. package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
  87. package/src/host/authoring/pixi-authoring-adapter.ts +42 -17
  88. package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
  89. package/src/host/authoring/pixi-live-write-target.ts +7 -7
  90. package/src/host/authoring/pixi-source-identity.ts +3 -3
  91. package/src/host/authoring/pixi-source-write-target.ts +1614 -0
  92. package/src/host/authoring/pixi-still-presentation.ts +2 -2
  93. package/src/host/authoring/pixi-structure-history.ts +3 -3
  94. package/src/host/authoring/pixi-transform-channels.ts +16 -14
  95. package/src/host/authoring/source-persistence-backend.ts +6 -6
  96. package/src/host/authoring/struct-write-pipe.ts +3 -3
  97. package/src/host/binding-resolver.ts +8 -9
  98. package/src/host/browser-transpile.ts +2 -2
  99. package/src/host/canvas-entry-runtime.ts +59 -48
  100. package/src/host/canvas-preview-frames.ts +482 -0
  101. package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
  102. package/src/host/components/HeaderTelemetry.tsx +9 -16
  103. package/src/host/components/PixiIsolationSceneContent.tsx +17 -16
  104. package/src/host/components/ThreeIsolationSceneContent.tsx +5 -5
  105. package/src/host/components/frame-debugger-model.ts +2 -2
  106. package/src/host/components/header-telemetry-model.ts +2 -2
  107. package/src/host/components/scene-document.tsx +29 -61
  108. package/src/host/components/utility-view-state.ts +1 -1
  109. package/src/host/components/world-root-stage-binding.tsx +17 -26
  110. package/src/host/components/world-root-stage.ts +130 -63
  111. package/src/host/coverage/capability-coverage.ts +13 -1
  112. package/src/host/coverage/game-contract-seam-evidence.ts +1 -1
  113. package/src/host/coverage/project-verb-coverage.ts +2 -17
  114. package/src/host/coverage/system-adapter-coverage.ts +4 -5
  115. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +14 -8
  116. package/src/host/document-preview-three.ts +2 -2
  117. package/src/host/entry-adjudication.ts +6 -6
  118. package/src/host/game-css-scope-transform.ts +4 -0
  119. package/src/host/game-module-access.ts +1 -1
  120. package/src/host/game-realm-page.ts +16 -8
  121. package/src/host/game-realm-reclaim.ts +1 -1
  122. package/src/host/gameplay-export.ts +25 -14
  123. package/src/host/gameplay-recording.ts +5 -5
  124. package/src/host/gated-globals.ts +6 -6
  125. package/src/host/history/json-history-resource.ts +3 -3
  126. package/src/host/instance-extract-actions.ts +1 -1
  127. package/src/host/instance-fork-actions.ts +1 -1
  128. package/src/host/projection/dom.ts +2 -2
  129. package/src/host/projection/pixi.ts +25 -3
  130. package/src/host/r3f-entry-runtime.ts +65 -34
  131. package/src/host/react-mount-runtime.ts +8 -49
  132. package/src/host/realm-services.ts +2 -2
  133. package/src/host/roots/canvas-root.tsx +361 -0
  134. package/src/host/roots/module-root.ts +1 -1
  135. package/src/host/roots/r3f-root.tsx +473 -0
  136. package/src/host/roots/react-root.ts +9 -43
  137. package/src/host/scene-view-drawability.ts +2 -2
  138. package/src/host/served-bundle-runtime-modules.ts +1 -19
  139. package/src/host/server-log-bridge.ts +3 -3
  140. package/src/host/stories/mounted-story-viewport-source.ts +1 -1
  141. package/src/host/stories/pixi-story-model.ts +30 -0
  142. package/src/host/stories/story-media-captures.ts +46 -0
  143. package/src/host/stories/story-media-presence.ts +3 -3
  144. package/src/host/stories/story-pixi-preview.ts +408 -0
  145. package/src/host/stories/story-three-preview.ts +806 -0
  146. package/src/host/stories/three-story-captures.ts +35 -0
  147. package/src/host/story-three-preview-runtime.ts +56 -0
  148. package/src/host/three-ingest-runtime.ts +1 -1
  149. package/src/host/use-active-performance-source.ts +3 -3
  150. package/src/host/viewport-pose-memory.ts +1 -1
  151. package/src/host/viewport-root-presentation.ts +6 -5
  152. package/src/ingest/active-ingest.ts +2 -2
  153. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  154. package/src/ingest/authoring/ingest-root-adapter.ts +13 -13
  155. package/src/ingest/capture-wait-report.ts +1 -1
  156. package/src/ingest/deferred-ingest-play.ts +11 -10
  157. package/src/ingest/discovery-public-ingest.ts +2 -2
  158. package/src/ingest/ingest-boot-viewport.ts +6 -8
  159. package/src/ingest/ingest-canvas-scene-document.tsx +17 -16
  160. package/src/ingest/ingest-canvas-scene.ts +5 -8
  161. package/src/ingest/ingest-evidence-hook.ts +1 -1
  162. package/src/ingest/ingest-frame-snapshot.ts +1 -1
  163. package/src/ingest/ingest-play-control.ts +4 -4
  164. package/src/ingest/ingest-render-debug.ts +11 -11
  165. package/src/ingest/ingest-siblings.ts +16 -28
  166. package/src/ingest/module-mode.ts +18 -19
  167. package/src/ingest/mount-canvas-ingest-root.ts +27 -27
  168. package/src/ingest/mount-coverage.ts +4 -4
  169. package/src/ingest/mount-dom-ingest-root.ts +16 -16
  170. package/src/ingest/mount-ingest-root.ts +14 -14
  171. package/src/ingest/mount-three-ingest-root.ts +12 -12
  172. package/src/ingest/resolve-canvas.ts +1 -1
  173. package/src/ingest/served-html-boot.ts +1 -1
  174. package/src/ingest/surface-canvas.ts +1 -1
  175. package/src/ingest/unmount-ingest-root.ts +7 -7
  176. package/src/navmesh/navmesh-handler.ts +24 -16
  177. package/src/network/NetworkInspectorPanel.tsx +449 -14
  178. package/src/network/network-inspector-model.ts +14 -2
  179. package/src/play/play-log-events.ts +1 -1
  180. package/src/play/play-mode.ts +99 -142
  181. package/src/play/play-recording.ts +2 -2
  182. package/src/play/react-play-live-authoring.ts +3 -3
  183. package/src/play/run-selection.ts +93 -0
  184. package/src/play-bar/PlayBar.tsx +20 -39
  185. package/src/profiler/FrameDebuggerPanel.tsx +1 -1
  186. package/src/profiler/PerformancePanel.tsx +4 -4
  187. package/src/react/design-time-react-mount.ts +37 -85
  188. package/src/react/dom-authoring-adapter.ts +11 -11
  189. package/src/react/react-inspector-section.tsx +13 -13
  190. package/src/react/react-world-authoring-adapter.ts +31 -143
  191. package/src/react/story-documents/story-documents.tsx +18 -22
  192. package/src/react/ui-board-document.tsx +10 -11
  193. package/src/react/ui-component-board.ts +5 -5
  194. package/src/runtime/adapter/audio-meter.ts +21 -0
  195. package/src/runtime/adapter/first-party-audio-system.ts +230 -0
  196. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
  197. package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
  198. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
  199. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
  200. package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
  201. package/src/runtime/adapter/native-debug-module.ts +498 -0
  202. package/src/runtime/audio/bus-mixer.ts +161 -0
  203. package/src/runtime/audio/pose-guard.ts +80 -0
  204. package/src/runtime/core/frame-pacing.ts +126 -0
  205. package/src/runtime/core/game-loop.ts +225 -0
  206. package/src/runtime/core/game-scoped-slot.ts +28 -0
  207. package/src/runtime/core/seeded-random.ts +162 -0
  208. package/src/runtime/core/sim-clock.ts +391 -0
  209. package/src/runtime/core/system-runner.ts +269 -0
  210. package/src/runtime/core/types.ts +104 -0
  211. package/src/runtime/create-runtime.ts +1128 -0
  212. package/src/runtime/debug-bridge.ts +570 -0
  213. package/src/runtime/debug-registry.ts +899 -0
  214. package/src/runtime/dev/chrome-trace.ts +153 -0
  215. package/src/runtime/dev/instruments.ts +403 -0
  216. package/src/runtime/dev/logger.ts +119 -0
  217. package/src/runtime/dev/performance-profiler.ts +367 -0
  218. package/src/runtime/dev/register-render-vitals.ts +276 -0
  219. package/src/runtime/dev/render-census.ts +354 -0
  220. package/src/runtime/dev/render-debug-adapter.ts +218 -0
  221. package/src/runtime/dev/render-memory.ts +226 -0
  222. package/src/runtime/dev/render-vitals.ts +338 -0
  223. package/src/runtime/dev/static-batch-advisor.ts +188 -0
  224. package/src/runtime/dev/webgl-frame-capture.ts +366 -0
  225. package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
  226. package/src/runtime/dev-build.ts +47 -0
  227. package/src/runtime/game.ts +1636 -0
  228. package/src/runtime/gameplay-rng-trap.ts +135 -0
  229. package/src/runtime/host-context.ts +64 -0
  230. package/src/runtime/input-router.ts +169 -0
  231. package/src/runtime/mount-manifest.ts +480 -0
  232. package/src/runtime/pixi/authoring.ts +706 -0
  233. package/src/runtime/pixi/ingest.ts +116 -0
  234. package/src/runtime/pixi/physics-registry.ts +49 -0
  235. package/src/runtime/pixi/render-pass-bracket.ts +117 -0
  236. package/src/runtime/pixi/scene-capture.ts +179 -0
  237. package/src/runtime/pixi/system-adapters.ts +69 -0
  238. package/src/runtime/playtest.ts +22 -0
  239. package/src/runtime/presentation.ts +141 -0
  240. package/src/runtime/render-control.ts +642 -0
  241. package/src/runtime/render-seed.ts +77 -0
  242. package/src/runtime/run-ticks-settled.ts +73 -0
  243. package/src/runtime/setup/setup-audio.ts +72 -0
  244. package/src/services/audio-pose-guard.ts +2 -2
  245. package/src/services/game-audio.ts +152 -0
  246. package/src/services/game-network.ts +657 -0
  247. package/src/services/game-physics.ts +334 -0
  248. package/src/state-watch/StateWatchPanel.tsx +1 -1
  249. package/src/three/authoring/camera-runtime-inspector-section.tsx +4 -3
  250. package/src/three/authoring/constraint-inspector-section.tsx +8 -7
  251. package/src/three/authoring/model-asset-inspector-section.tsx +14 -17
  252. package/src/three/authoring/oid-source-persistence.ts +13 -13
  253. package/src/three/authoring/r3f-design-session.ts +79 -61
  254. package/src/three/authoring/r3f-source-authoring-adapter.ts +112 -108
  255. package/src/three/authoring/reflection-probe-inspector-section.tsx +5 -4
  256. package/src/three/authoring/spatial-joint-handles.ts +1 -1
  257. package/src/three/authoring/spatial-lod-handles.ts +1 -1
  258. package/src/three/authoring/spatial-particle-handles.ts +1 -1
  259. package/src/three/authoring/three-authoring-adapter.ts +33 -33
  260. package/src/three/component-verbs/extract-menu.ts +8 -7
  261. package/src/three/component-verbs/fork-menu.ts +8 -7
  262. package/src/three/component-verbs/internals-menu.ts +3 -3
  263. package/src/three/drei-asset-caches.ts +13 -0
  264. package/src/three/story-documents/three-story-documents.tsx +24 -34
  265. package/src/three/three-board/ThreeBoardDocument.tsx +25 -25
  266. package/src/three/three-board/board-scene.ts +7 -7
  267. package/src/three/three-board/three-component-board.ts +5 -5
  268. package/contributions/react/pasteboard.action.ts +0 -41
  269. package/contributions/xstate-behavior.action.ts +0 -42
  270. package/contributions/xstate-behavior.document.tsx +0 -73
  271. package/contributions/xstate-behavior.inspector.tsx +0 -28
  272. package/contributions/xstate-behavior.menu.ts +0 -23
  273. package/src/react/pasteboard-materialize.ts +0 -154
  274. package/src/services/game-audio-unlock.ts +0 -48
  275. package/src/xstate/XStateBehaviorSection.tsx +0 -130
  276. package/src/xstate/XStateMachineInspector.tsx +0 -566
  277. package/src/xstate/character-animation-machine.fixture.ts +0 -74
  278. package/src/xstate/live-behaviors.ts +0 -106
  279. package/src/xstate/use-live-actor-state.ts +0 -44
  280. package/src/xstate/xstate-graph.ts +0 -235
  281. package/src/xstate/xstate-layout.ts +0 -76
@@ -1,130 +0,0 @@
1
- /**
2
- * The compact **Behavior** Inspector section (§4.3): the selected entity's
3
- * live machine — its id, where the actor IS, the machine's FULL state chart
4
- * with the active path marked, and the way into the Behavior document.
5
- *
6
- * It renders ONLY when `xstate-behavior.inspector.tsx`'s `match` found a real
7
- * live behavior mark on the selected node, so a template entity (no machine)
8
- * never sees it.
9
- */
10
-
11
- import { Button, themeVars } from '@volter/editor-sdk/widgets';
12
- import {
13
- subscribeXStateBehaviorInspections,
14
- xstateBehaviorInspectionsVersion,
15
- } from '@volter/threejs-runtime/behavior/xstate-inspection';
16
- import { useSyncExternalStore } from 'react';
17
- import type { AnyStateMachine, AnyStateNode } from 'xstate';
18
- import { liveBehaviorFor, openBehaviorDocument } from './live-behaviors';
19
- import { useLiveActorState } from './use-live-actor-state';
20
-
21
- /** Every descendant state node of the machine (depth-first, root excluded),
22
- * for the full-state-list readout below — the same direct `.states` walk
23
- * `xstate-graph.ts` documents as the sanctioned traversal. */
24
- function allMachineStates(machine: AnyStateMachine): AnyStateNode[] {
25
- const out: AnyStateNode[] = [];
26
- const visit = (node: AnyStateNode): void => {
27
- for (const child of Object.values(node.states)) {
28
- out.push(child);
29
- visit(child);
30
- }
31
- };
32
- visit(machine.root);
33
- return out;
34
- }
35
-
36
- export function XStateMachineSection({ nodeId }: { nodeId: string | null }) {
37
- useSyncExternalStore(
38
- subscribeXStateBehaviorInspections,
39
- xstateBehaviorInspectionsVersion,
40
- xstateBehaviorInspectionsVersion,
41
- );
42
- const behavior = nodeId ? liveBehaviorFor(nodeId) : undefined;
43
- const machine = behavior?.machine;
44
- // Follow live transitions (re-render per snapshot) — the state list and
45
- // layer readout below re-derive from each.
46
- const activeIds = useLiveActorState(machine as AnyStateMachine, behavior?.actor);
47
- if (!behavior || !machine || !nodeId) return null;
48
- const activeIdSet = new Set(activeIds ?? []);
49
- const states = allMachineStates(machine);
50
- return (
51
- <div
52
- data-testid="inspector-xstate-section"
53
- style={{
54
- pointerEvents: 'auto',
55
- padding: 8,
56
- borderBottom: `1px solid ${themeVars.boundary.default}`,
57
- }}
58
- >
59
- <div
60
- style={{
61
- fontSize: 11,
62
- color: themeVars.content.dim,
63
- textTransform: 'uppercase',
64
- marginBottom: 4,
65
- }}
66
- >
67
- Behavior
68
- </div>
69
- <div
70
- style={{ fontSize: 11, color: themeVars.content.primary, fontWeight: 600, marginBottom: 2 }}
71
- >
72
- {machine.id}
73
- </div>
74
- <div style={{ fontSize: 10, color: themeVars.content.muted, marginBottom: 4 }}>
75
- <span style={{ color: themeVars.semantic.success, fontWeight: 700 }}>●</span> Live —{' '}
76
- {activeIds?.map((stateId, index) => (
77
- <span key={stateId}>
78
- {index > 0 && ' · '}
79
- <span
80
- style={{ fontFamily: themeVars.typography.mono, color: themeVars.accent.default }}
81
- >
82
- {stateId.startsWith(`${machine.id}.`)
83
- ? stateId.slice(machine.id.length + 1)
84
- : stateId}
85
- </span>
86
- </span>
87
- ))}
88
- </div>
89
- {/* The FULL state chart, active path highlighted — the section must
90
- show what the machine can do, not only where it is (owner
91
- feedback, 2026-08-06). */}
92
- <div data-testid="xstate-section-states" style={{ marginBottom: 6 }}>
93
- {states.map((state) => {
94
- const relative = state.id.startsWith(`${machine.root.id}.`)
95
- ? state.id.slice(machine.root.id.length + 1)
96
- : state.id;
97
- const depth = relative.split('.').length - 1;
98
- const active = activeIdSet.has(state.id);
99
- return (
100
- <div
101
- key={state.id}
102
- data-testid={`xstate-section-state-${relative}`}
103
- style={{
104
- paddingLeft: 4 + depth * 12,
105
- fontSize: 10,
106
- lineHeight: 1.7,
107
- fontFamily: themeVars.typography.mono,
108
- color: active ? themeVars.accent.default : themeVars.content.muted,
109
- fontWeight: active ? 700 : 400,
110
- }}
111
- >
112
- {active ? '● ' : '○ '}
113
- {state.key}
114
- </div>
115
- );
116
- })}
117
- </div>
118
- <div style={{ fontSize: 10, color: themeVars.content.dim, marginBottom: 6 }}>
119
- Source: hand-authored XState TypeScript — inspect-only.
120
- </div>
121
- <Button
122
- size="compact"
123
- data-testid="xstate-open-graph"
124
- onClick={() => void openBehaviorDocument(nodeId)}
125
- >
126
- Inspect Behavior
127
- </Button>
128
- </div>
129
- );
130
- }
@@ -1,566 +0,0 @@
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
- }