@turing-machine-js/visuals 7.0.0-alpha.6
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/CHANGELOG.md +21 -0
- package/LICENSE +674 -0
- package/README.md +21 -0
- package/dist/applyHighlight.d.ts +42 -0
- package/dist/applyHighlight.js +191 -0
- package/dist/format.d.ts +22 -0
- package/dist/format.js +46 -0
- package/dist/graphIndexes.d.ts +31 -0
- package/dist/graphIndexes.js +58 -0
- package/dist/graphUtils.d.ts +37 -0
- package/dist/graphUtils.js +76 -0
- package/dist/highlightOps.d.ts +81 -0
- package/dist/highlightOps.js +36 -0
- package/dist/index.cjs +514 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/dist/index.mjs +503 -0
- package/dist/recordSnippet.d.ts +35 -0
- package/dist/recordSnippet.js +92 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.js +1 -0
- package/docs/graph-highlight-and-breakpoints.md +272 -0
- package/package.json +46 -0
- package/src/applyHighlight.spec.ts +331 -0
- package/src/applyHighlight.ts +217 -0
- package/src/fixtures/graphs/post-walk-mark.json +108 -0
- package/src/fixtures/graphs/turing-callable-subtree.json +108 -0
- package/src/fixtures/graphs/turing-copy-two-tapes.json +87 -0
- package/src/fixtures/graphs/turing-replace-b.json +72 -0
- package/src/format.spec.ts +100 -0
- package/src/format.ts +51 -0
- package/src/graphIndexes.ts +84 -0
- package/src/graphUtils.spec.ts +112 -0
- package/src/graphUtils.ts +74 -0
- package/src/highlightOps.ts +94 -0
- package/src/index.ts +10 -0
- package/src/recordSnippet.spec.ts +275 -0
- package/src/recordSnippet.ts +141 -0
- package/src/types.ts +96 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +10 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Graph } from '@turing-machine-js/machine';
|
|
2
|
+
/**
|
|
3
|
+
* State-graph highlight descriptor (machines-demo#10). MachineView derives it
|
|
4
|
+
* from `executionMode` + the latest pause-response data; MachineGraph reads
|
|
5
|
+
* it to light up the `from → edge → to` triple in the rendered SVG.
|
|
6
|
+
*
|
|
7
|
+
* - `fromId: 'idle'` represents the synthetic `idle([idle])` sentinel that
|
|
8
|
+
* `toMermaid` emits at the entry point. Used in IDLE mode to mark "where
|
|
9
|
+
* execution would start".
|
|
10
|
+
* - `fromId: number` is an engine `GraphNode.id` — the source state.
|
|
11
|
+
* - `toId: number | null` is the destination state's id (or `null` at halt).
|
|
12
|
+
* - `strong` selects which end of the triple gets the bolder/stronger
|
|
13
|
+
* accent. Per the (B) rule: `from` strong at `before` pause; `to` strong
|
|
14
|
+
* at `after` / iter-end pause / IDLE (destination feels current).
|
|
15
|
+
*/
|
|
16
|
+
export type GraphHighlight = {
|
|
17
|
+
fromId: number | 'idle';
|
|
18
|
+
toId: number | null;
|
|
19
|
+
strong: 'from' | 'to';
|
|
20
|
+
/**
|
|
21
|
+
* True when this highlight reflects a paused-event apply (RUNNING_PAUSED),
|
|
22
|
+
* false for per-iter idle applies (RUNNING_AUTO). MachineGraph uses this
|
|
23
|
+
* to detect cross-pause same-state revisits — pulsing the strong node when
|
|
24
|
+
* the current paused apply lands on the same state as the previous paused
|
|
25
|
+
* apply, even when intermediate idle applies pointed elsewhere
|
|
26
|
+
* (e.g., stateA breakpoint → continue → run through stateB/C → stateA
|
|
27
|
+
* breakpoint fires again). The simpler "previous apply's strong matches"
|
|
28
|
+
* check already covers AUTO self-loops; this flag drives the second pulse
|
|
29
|
+
* trigger.
|
|
30
|
+
*/
|
|
31
|
+
paused: boolean;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Per-tape snapshot: the cells visible/usable plus the head's index into them.
|
|
35
|
+
* Same shape as machines-demo's TapeSnapshot. Pure data — no library handles.
|
|
36
|
+
*/
|
|
37
|
+
export type TapeSnapshot = {
|
|
38
|
+
symbols: string[];
|
|
39
|
+
position: number;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* One frame of a recorded snippet — the state of the machine at iter `step`.
|
|
43
|
+
* Frame 0 = initial state (before any transition); frame N = state after iter N's transition.
|
|
44
|
+
*
|
|
45
|
+
* `tape` is per-tape (single-tape machines: length 1). `highlight` describes
|
|
46
|
+
* what to render on the state graph at this moment (null when no highlight).
|
|
47
|
+
* `log` is optional pre-formatted text — a caption / status line consumers can render.
|
|
48
|
+
*/
|
|
49
|
+
export type Frame = {
|
|
50
|
+
step: number;
|
|
51
|
+
tape: TapeSnapshot[];
|
|
52
|
+
/**
|
|
53
|
+
* Per-tape engine command for the iter that produced this frame. Carries
|
|
54
|
+
* both sides of the cell so players can step bi-directionally without
|
|
55
|
+
* recomputing from neighbouring frames:
|
|
56
|
+
*
|
|
57
|
+
* - `movement` — `'L' | 'R' | 'S'`. Forward step slides the tape this way;
|
|
58
|
+
* backward step slides the opposite.
|
|
59
|
+
* - `read` — symbol on the head's cell BEFORE this iter (what the engine
|
|
60
|
+
* matched). Backward step writes this back.
|
|
61
|
+
* - `write` — symbol on the cell AFTER this iter (=== `read` if no write
|
|
62
|
+
* happened). Forward step writes this; UI triggers the per-cell flash
|
|
63
|
+
* iff `write !== read`.
|
|
64
|
+
*
|
|
65
|
+
* Undefined on frame 0 (initial state — no transition has fired yet).
|
|
66
|
+
*/
|
|
67
|
+
commands?: {
|
|
68
|
+
movement: 'L' | 'R' | 'S';
|
|
69
|
+
read: string;
|
|
70
|
+
write: string;
|
|
71
|
+
}[];
|
|
72
|
+
highlight: GraphHighlight | null;
|
|
73
|
+
log?: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Recorded run of a machine — playback artifact for embeds, articles,
|
|
77
|
+
* landing-page panels. Engine-agnostic (no `engine` field; identity lives
|
|
78
|
+
* at the caller bucket level).
|
|
79
|
+
*
|
|
80
|
+
* - `version: 1` — schema integer. Additive fields don't bump it;
|
|
81
|
+
* shape-breaking changes do.
|
|
82
|
+
* - `graph` — engine `State.toGraph` output captured at recording time.
|
|
83
|
+
* - `alphabets` — per-tape alphabet list (single-tape: length 1).
|
|
84
|
+
* - `frames` — length === `stepsApplied + 1`; frame 0 is the initial state.
|
|
85
|
+
*/
|
|
86
|
+
export type Snippet = {
|
|
87
|
+
version: 1;
|
|
88
|
+
name?: string;
|
|
89
|
+
graph: Graph;
|
|
90
|
+
alphabets: string[][];
|
|
91
|
+
frames: Frame[];
|
|
92
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Graph highlight and breakpoint rules
|
|
2
|
+
|
|
3
|
+
> Canonical reference for every CSS class the demo applies to the rendered state-graph SVG, when each class fires, and how the breakpoint engine-side / UI-side state stays consistent. Companion to [`execution-model.md`](./execution-model.md) (modes + transitions) and [`machine-graph-palette.md`](./machine-graph-palette.md) (color tokens). Source files: `src/components/MachineGraph.svelte` (the apply-highlight + cache-build + indicator effects), `src/components/MachineView.svelte` (derives `graphHighlight`, owns the `breakpoints` set), `src/lib/machineWorker.ts` (`onPauseFn` + `toggleBreakpoint`), `src/lib/graphUtils.ts` (canonicalization helpers).
|
|
4
|
+
|
|
5
|
+
## 1. Node-id conventions
|
|
6
|
+
|
|
7
|
+
The engine's `Graph.nodes` keys are positive integer ids assigned at graph-build time. The demo also recognizes three synthetic ids:
|
|
8
|
+
|
|
9
|
+
| Id range | Kind | Where it comes from | Click target? |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `> 0` | Regular state, wrapper, or bare | `State.toGraph` walks reachable States | yes |
|
|
12
|
+
| `0` | Halt singleton (`haltState`) | Engine sentinel, process-wide | no — global, not per-machine |
|
|
13
|
+
| `< 0` | Halt marker (`isHaltMarker: true`, id = `-frameId`) | `toGraph` rewrites in-frame halts so they land inside the subgraph cluster instead of the global singleton | no — collapses to `haltState` at runtime |
|
|
14
|
+
| `'idle'` | Synthetic entry sentinel | `toGraph` always emits a stadium-shape `idle` node with `idle -. enter .-> sN` | no |
|
|
15
|
+
|
|
16
|
+
Click handlers are attached only to nodes whose key is `typeof === 'number' && > 0` — see `MachineGraph.svelte`'s cache-build `$effect`.
|
|
17
|
+
|
|
18
|
+
## 2. Wrapper / bare equivalence
|
|
19
|
+
|
|
20
|
+
A `State.withOverriddenHaltState(continuation)` produces a *wrapper* State that shares its `#debugRef` cell with the bare engine-side (`State.ts:391`: `state.#debugRef = bare.#debugRef`). Setting `wrapper.debug.before = true` and `bare.debug.before = true` mutate the SAME cell.
|
|
21
|
+
|
|
22
|
+
The demo collapses this into a single breakpoint *equivalence class*:
|
|
23
|
+
|
|
24
|
+
- The **canonical id** for a class is the bare's id. `bareIdOf(id, graph)` returns the bare id for a wrapper, self otherwise.
|
|
25
|
+
- The `breakpoints: SvelteMap<number, { before: boolean; after: boolean }>` (in `MachineView`) holds canonical bare ids → per-kind state. Entries with both kinds off are pruned. `breakpointIndicatorSet` is the `$derived` set of "any kind set" canonical ids, passed to the indicator effect.
|
|
26
|
+
- The **indicator effect** (in `MachineGraph`) renders the `mg-breakpoint` mark on every class member (bare + every wrapper sharing that bare via `bareStateId`): for each cached node, it normalizes the node's id via `bareIdOf` and checks the indicator set. The set is kind-agnostic — the future per-kind dots (Layer N) will use the Map directly.
|
|
27
|
+
- The **highlight effect** uses an **asymmetric** expansion via `highlightExpand`:
|
|
28
|
+
|
|
29
|
+
| Source side | Returns | Why |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Wrapper | `[wrapper, bare]` | wrapper-entry pause is the visually joined pair — light up both |
|
|
32
|
+
| Bare | `[bare]` only | when the engine is genuinely on the bare (loop iter), the wrapper isn't active |
|
|
33
|
+
| Regular state | `[self]` | no class membership |
|
|
34
|
+
|
|
35
|
+
This means right-clicking a wrapper or a bare opens the menu over the same engine breakpoint class, both nodes show the indicator, but the strong-highlight only "leaks" wrapper → bare, never bare → wrappers.
|
|
36
|
+
|
|
37
|
+
**Halt class.** The halt singleton (`id 0`) and every halt marker (`id < 0`, one per frame; see §1) form an additional equivalence class — they all collapse to the engine-wide `haltState` at runtime. `bareIdOf` maps any negative id to `0`; `equivalentIds(0, g)` returns `[0, ...all halt markers]`. The breakpoint Map stores canonical `0`; the indicator therefore lights up the singleton AND every halt marker simultaneously. `MachineView`'s `onToggleBreakpoint` also normalizes negative ids to `0` before forwarding to the worker, so the worker (which only has `id 0` in `collectStates`) sees a single canonical toggle regardless of which halt-class node the user right-clicked.
|
|
38
|
+
|
|
39
|
+
**Class membership in the menu.** The context menu (§12) shows a "Shared with: …" info line listing other class members' names — so the user can see at a glance that flipping the BP also flips the sibling. For the halt class it instead shows "Global — affects all halts in the runtime", since `haltState` is a process-wide singleton rather than a per-graph state. Singleton classes (regular states, no wrappers / no sharing) omit the info line.
|
|
40
|
+
|
|
41
|
+
## 3. CSS classes applied to SVG elements
|
|
42
|
+
|
|
43
|
+
All classes are added imperatively by the apply-highlight or indicator `$effect`s in `MachineGraph.svelte`. Styling lives in the same component's `<style>` block; tokens in `app.css`.
|
|
44
|
+
|
|
45
|
+
| Class | Target | When | Cleared by |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| `mg-breakpoint` | `g.node` | Set on every member of an equivalence class whose canonical id is in `breakpoints` | Indicator effect re-runs on `breakpoints` change |
|
|
48
|
+
| `mg-highlight-from` | `g.node` (+ `'idle'` sentinel) | Source side of the just-fired or about-to-fire transition | Cleared at the top of every apply-highlight run |
|
|
49
|
+
| `mg-highlight-to` | `g.node` | Destination side of same | Same |
|
|
50
|
+
| `mg-highlight-strong` | `g.node` | The "focal" node of the highlight triple (set on the side matching `h.strong`, plus any class member via `highlightExpand` if source is wrapper) | Same |
|
|
51
|
+
| `mg-highlight-edge` | `path` + `g.label` of edge | The edge connecting from→to in graph data-id space (`L_${from}_${to}_*`); also wrapper→bare "call" edge when `toEqIds` expanded the pair | Same |
|
|
52
|
+
| `mg-frame-active` | `g.cluster` of a callable-subtree subgraph | The strong node (via canonical id) lives inside the frame — or the destination return chain detected a post-pop arrival from that frame | Same |
|
|
53
|
+
| `mg-hl-arrow-shape` | inside cloned `<marker>` | Materialized once per render at cache-build time; switched in via `marker-end` swap on highlighted paths (browsers without `context-stroke`) | Marker swap reverts via `data-mg-orig-marker-end` on clear |
|
|
54
|
+
|
|
55
|
+
## 4. `graphHighlight` shape (the input to the effect)
|
|
56
|
+
|
|
57
|
+
Derived in `MachineView.svelte` from `(executionMode, currentStateId, nextStateId, prevStateId, pauseBefore, graph)`:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
type GraphHighlight = {
|
|
61
|
+
fromId: number | 'idle'; // source-side node
|
|
62
|
+
toId: number | null; // destination-side node; null = no destination
|
|
63
|
+
strong: 'from' | 'to' | null;
|
|
64
|
+
paused: boolean; // true → eligible for pulse + revisit logic
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Mode | `fromId` | `toId` | `strong` | `paused` |
|
|
69
|
+
|---|---|---|---|---|
|
|
70
|
+
| `RUNNING_AUTO` | `currentStateId` | `nextStateId` | `'from'` | `false` |
|
|
71
|
+
| `RUNNING_PAUSED`, `pauseBefore = true` (debug.before fired) | `prevStateId ?? 'idle'` | `currentStateId` | `'to'` | `true` |
|
|
72
|
+
| `RUNNING_PAUSED`, `pauseBefore = false` (Step / pause-after / click-pause) | `currentStateId` | `nextStateId` | `'from'` | `true` |
|
|
73
|
+
| Anything else | `null` (no highlight) |
|
|
74
|
+
|
|
75
|
+
`currentStateId` / `nextStateId` come from the worker's `paused` / `idle` / `stepped` responses (worker uses `m.state.id` for current, `nextStateIdFromYield` for next).
|
|
76
|
+
|
|
77
|
+
## 5. Halt-target retargeting
|
|
78
|
+
|
|
79
|
+
`toGraph` rewrites halt-bound transitions of in-frame states so they land on the **frame's halt marker** (id `= -frameId`), not the real `haltState` (id `0`). The engine's runtime, however, reports `nextState.id === 0` for any halt. The apply-highlight effect bridges:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
if toId === 0 AND fromId is in some frame F → toId := -F
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This makes the visible edge (`L_sX_cF_*`) the one that lights up. If `fromId` isn't in any frame (e.g. `writeMarker → halt` in the callable-subtree example), `toId` stays `0` and falls into the halt-singleton branch (§7).
|
|
86
|
+
|
|
87
|
+
## 6. Source return chain (engine just halted into a frame)
|
|
88
|
+
|
|
89
|
+
Fires when `toId < 0` (an in-frame halt marker). The engine will pop the stack and resume at the wrapper's override. To visualize the post-pop trajectory **before** the next iter relocates the strong node:
|
|
90
|
+
|
|
91
|
+
- Highlight the return arrow `L_w_${F}_s${wrapperId}` (dotted).
|
|
92
|
+
- Mark each wrapper of frame F with `mg-highlight-to`.
|
|
93
|
+
- Highlight each wrapper's call-target-replacement edge `L_s${wrapperId}_s${overrideId}` and the override node.
|
|
94
|
+
|
|
95
|
+
When multiple wrappers share the bare, the demo highlights all of them — the engine's runtime choice depends on stack state, which the demo doesn't track.
|
|
96
|
+
|
|
97
|
+
## 7. Destination return chain (engine just resumed at an override-target)
|
|
98
|
+
|
|
99
|
+
Mirror of §6, fires when `toId > 0` AND `fromId` is in some frame F AND any wrapper of F has `overriddenHaltStateId === toId`. The engine *just popped* and is paused/running at the override. The straight `bare → override` edge doesn't exist in the graph — light up the actual visible path:
|
|
100
|
+
|
|
101
|
+
- bare (`fromId`, gets `mg-highlight-from` from the standard pass)
|
|
102
|
+
- bare → halt-marker edge `L_s${fromId}_c${F}`
|
|
103
|
+
- halt-marker `c${F}` with `mg-highlight-to`
|
|
104
|
+
- return arrow `L_w_${F}_s${wrapperId}` (dotted)
|
|
105
|
+
- wrapper with `mg-highlight-to`
|
|
106
|
+
- wrapper → override edge `L_s${wrapperId}_s${toId}`
|
|
107
|
+
- override (= `toId`, gets `mg-highlight-to` + strong from standard pass)
|
|
108
|
+
- frame F's cluster gets `mg-frame-active` (strong node lives outside the frame, so the frame-highlight pass otherwise wouldn't fire)
|
|
109
|
+
|
|
110
|
+
## 8. Halt singleton (`toId === 0` with no retarget)
|
|
111
|
+
|
|
112
|
+
If §5's retarget didn't fire (because `fromId` isn't in a frame), `toId` stays `0`. The halt-singleton node is then highlighted via a direct lookup `nodeCache.get(0)` instead of the normal `highlightExpand` path (which skips `id <= 0`). Without this branch the leading edge would light up while the singleton stays grey — visually orphaned.
|
|
113
|
+
|
|
114
|
+
## 9. Frame-active rule
|
|
115
|
+
|
|
116
|
+
`mg-frame-active` lights the cluster border of a callable-subtree subgraph. Fires when **either**:
|
|
117
|
+
|
|
118
|
+
- the strong node's canonical id (`bareIdOf(strongId)`) lives inside the frame (`nodeFrameMap.get`), OR
|
|
119
|
+
- the destination return chain (§7) detected a post-pop arrival from that frame.
|
|
120
|
+
|
|
121
|
+
The first case handles "we're executing inside the subroutine"; the second handles "we just left the subroutine but the return-chain is still visualized."
|
|
122
|
+
|
|
123
|
+
## 10. Wrapper-entry call-edge highlight
|
|
124
|
+
|
|
125
|
+
When the to-side expansion via `highlightExpand` produced `[wrapper, bare]` (length 2), the connector between them is the wrapper→bare **call edge** `L_s${wrapperId}_s${bareId}`. The standard `highlightEdgeByDataId(fromKey, toKey)` call uses `toKey = s${wrapper}` so it wouldn't pick up this edge. An explicit follow-up call highlights it so the joined visual pair has a visible connector.
|
|
126
|
+
|
|
127
|
+
## 11. Pause-revisit pulse
|
|
128
|
+
|
|
129
|
+
A short opacity pulse fires on the strong element when the current paused event lands on the same state as the **immediately previous** paused event. Implementation:
|
|
130
|
+
|
|
131
|
+
- `lastPausedStrongId: number | 'idle' | null` stored at module scope.
|
|
132
|
+
- After applying highlight, if `h.paused && strongId === lastPausedStrongId`, call `strongEl.animate(...)`.
|
|
133
|
+
- If `h.paused`, write `strongId` to `lastPausedStrongId`.
|
|
134
|
+
|
|
135
|
+
**Important: use the raw `strongId`, NOT the canonical bare-id.** Wrapper-pause and bare-pause are visually distinct events even though they share `#debugRef`; pausing at the wrapper then continuing into the bare must NOT pulse (the engine moved between two different nodes). The frame-highlight pass uses the canonical for its lookup, but the pulse comparison uses the raw id.
|
|
136
|
+
|
|
137
|
+
Idle events (`h.paused === false`) never update `lastPausedStrongId`, so an `idle` that happens to report the same state doesn't poison the next paused-event comparison.
|
|
138
|
+
|
|
139
|
+
## 12. Right-click handling + listener lifecycle
|
|
140
|
+
|
|
141
|
+
The cache-build `$effect` walks the rendered SVG and attaches a `contextmenu` listener to every `g.node` whose key is a number (positive state, halt singleton `0`, or negative halt marker — only the synthetic `'idle'` string sentinel is skipped). Left-click stays native (text selection, focus, scroll-tap) — only right-click opens the BP menu, matching IDE convention. Halt nodes participate so the user can set / clear the global haltState breakpoint from any halt-class node (see §2's halt-class paragraph); the per-kind menu items dispatch through `MachineView.onToggleBreakpoint`, which canonicalizes negative ids → `0` before the runner sees them. Because mermaid's `lastSource` cache can skip a re-render when the source is byte-identical (so the SAME DOM persists across builds), the effect must NOT just re-attach — that stacks listeners and a single right-click would trigger N menu opens.
|
|
142
|
+
|
|
143
|
+
Pattern:
|
|
144
|
+
|
|
145
|
+
1. Module-scope `clickListenersController: AbortController | null = null`.
|
|
146
|
+
2. At the top of each cache-build pass: `clickListenersController?.abort()` (removes ALL listeners attached with that signal), then `new AbortController()`.
|
|
147
|
+
3. Pass `{ signal }` to every `addEventListener`.
|
|
148
|
+
4. Component unmount aborts in `onMount`'s cleanup.
|
|
149
|
+
|
|
150
|
+
The right-click handler `preventDefault`s the native context menu, then sets `menuStateId`, `menuX`, `menuY` to render a custom menu at the cursor.
|
|
151
|
+
|
|
152
|
+
### Menu state + lifecycle
|
|
153
|
+
|
|
154
|
+
The menu is two `<button role="menuitem">` items inside a `<div role="menu">`, positioned `fixed` at clamped viewport coordinates. Items show `☑`/`☐` based on the current `before` / `after` bits in `breakpointKinds.get(bareIdOf(stateId))`. Picking an item calls `runner.toggleBreakpoint(stateId, kind)` and closes the menu. The worker echoes `breakpointToggled` with the same `kind`, which updates the `breakpoints` Map via `runner.onBreakpointToggled`.
|
|
155
|
+
|
|
156
|
+
Closing happens via:
|
|
157
|
+
- Picking a menu item (fires `onclick`, then `closeMenu`).
|
|
158
|
+
- ESC keydown (global handler installed while menu open).
|
|
159
|
+
- Outside `mousedown` (global handler; `mousedown` not `click` so item picks land first).
|
|
160
|
+
|
|
161
|
+
A separate `AbortController` (`menuOutsideController`) gates the global handlers so they detach cleanly when the menu closes or the component unmounts.
|
|
162
|
+
|
|
163
|
+
### Edge-clamped positioning
|
|
164
|
+
|
|
165
|
+
After the menu mounts, a `$effect` measures `menuEl.getBoundingClientRect()` and shifts the position so the menu fits in the viewport (8px margin from each edge). The raw `menuX/menuY` are the right-click coordinates; the clamped `menuClampedX/menuClampedY` are what feed the inline `left`/`top` style. Re-runs when the raw coords change (subsequent right-click on a different node).
|
|
166
|
+
|
|
167
|
+
## 13. Breakpoint replay across builds
|
|
168
|
+
|
|
169
|
+
Building a new worker creates fresh State instances — any `debug.before`/`debug.after` set on the previous instances is gone. The `breakpoints` Map in `MachineView` is **user intent**, not run state, and survives:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
async function reloadWorker(source = code) {
|
|
173
|
+
// ... await runner.build(source) ...
|
|
174
|
+
// Prune ids that don't exist in the new graph (user edited code).
|
|
175
|
+
for (const id of [...breakpoints.keys()]) {
|
|
176
|
+
if (!res.graph.nodes[id]) breakpoints.delete(id);
|
|
177
|
+
}
|
|
178
|
+
// Replay surviving kinds via toggleBreakpoint. Fresh States have no
|
|
179
|
+
// debug set, so each toggle flips off→on. One call per stored kind per
|
|
180
|
+
// class — the canonical-id keying prevents double-flips of the shared
|
|
181
|
+
// #debugRef.
|
|
182
|
+
for (const [id, kinds] of breakpoints) {
|
|
183
|
+
if (kinds.before) runner.toggleBreakpoint(id, 'before');
|
|
184
|
+
if (kinds.after) runner.toggleBreakpoint(id, 'after');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
On build **failure** the Map IS cleared — there's no graph to prune against.
|
|
190
|
+
|
|
191
|
+
The runner's echo `onBreakpointToggled` normalizes the echoed `stateId` via `bareIdOf(graph)` and updates only the matching `kind` bit, so the Map always holds canonical ids regardless of which class member the user actually right-clicked.
|
|
192
|
+
|
|
193
|
+
## 13a. After-fire + Step: synthetic-pause suppression
|
|
194
|
+
|
|
195
|
+
The engine's `onIter` fires at end-of-iter — functionally the same execution point as an `onPause(after, K)` fire (both happen after the iter's transition has executed and `onStep` has run). When a user clicks Step from inside an after-fire BP pause, the worker's `onIterFn` would otherwise dispatch a SECOND synthetic pause at the same effective point, producing a duplicate log entry.
|
|
196
|
+
|
|
197
|
+
Suppression: `onPauseFn` sets `dispatchedAfterThisIter = true` whenever the engine pause is after-side (`m.pause?.side === 'after'`). `onIterFn` reads the flag (and resets it) at iter boundary; when set AND `stepRequested` is true, it skips the synthetic dispatch **but keeps `stepRequested`** so the NEXT iter's `onIter` pauses naturally. Net effect: Step from an after-fire BP advances one iter (the right semantic for "next pause point"), instead of bouncing twice at the same point.
|
|
198
|
+
|
|
199
|
+
`before`-fires don't set the flag — they fire mid-iter, distinct from end-of-iter, so the synthetic at end-of-iter is a genuinely different pause point and isn't suppressed.
|
|
200
|
+
|
|
201
|
+
## 14. Worker-side wrapper handling (`onPauseFn`)
|
|
202
|
+
|
|
203
|
+
The engine fires `onPause` on BOTH the wrapper entry (iter K) and the immediately following bare entry (iter K+1) when the shared `#debugRef.before === true`. The worker pauses at the wrapper (the user-facing call site) and suppresses ONE following bare entry via `pendingJoinedBareId: number | null`:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
1. onPause(wrapper): if node.isWrapper && node.bareStateId !== null:
|
|
207
|
+
pendingJoinedBareId := bareStateId; dispatchPause
|
|
208
|
+
2. onPause(bare): if state.id === pendingJoinedBareId:
|
|
209
|
+
clear flag; skip dispatch
|
|
210
|
+
3. onPause(bare K+2): pendingJoinedBareId === null; dispatch normally
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Subsequent bare→bare loop iters pause normally — that's a real iteration, not a joined entry.
|
|
214
|
+
|
|
215
|
+
Also: when pausing at a wrapper, the worker swaps the dispatched `state` field to the bare's `name` (looked up via `currentGraph.nodes[bareStateId].name`). The log reads `paused at state walkToBlank ...` instead of `paused at state walkToBlank(writeMarker) ...` — the composite is engine implementation.
|
|
216
|
+
|
|
217
|
+
`currentGraph` is the engine-v7 `Graph` snapshot captured at build time and retained worker-side specifically so `onPauseFn` can ask `isWrapper?` and look up names. Cleared in `reset()`.
|
|
218
|
+
|
|
219
|
+
## 15. Post-machine differences
|
|
220
|
+
|
|
221
|
+
`@post-machine-js/machine` installs an `Object.defineProperty` lockdown on every non-halt PostMachine-constructed State's `debug` property that funnels DIRECT writes through Post's registry (`pm.setBreakpoint` for un-shared, throw for shared). `haltState` is NOT locked — direct `turing.haltState.debug = boolean` writes go straight to the engine setter (post dropped the module-load halt lockdown alongside engine #207). Post wraps `run`'s `onPause` to filter via that registry — pauses fire only when the registered breakpoint matches.
|
|
222
|
+
|
|
223
|
+
**Direct mutation of the engine's `DebugConfig` (e.g. `state.debug.before = true`) bypasses Post's lockdown** because the getter passes through; Post's wrapper then filters the engine's onPause out entirely. The worker's `toggleBreakpoint` therefore uses the SETTER form, reading both kinds and writing the merged shape so toggling one doesn't lose the other:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const debug = entry.state.debug;
|
|
227
|
+
const currentBefore = debug.before === true;
|
|
228
|
+
const currentAfter = debug.after === true;
|
|
229
|
+
const newBefore = req.kind === 'before' ? !currentBefore : currentBefore;
|
|
230
|
+
const newAfter = req.kind === 'after' ? !currentAfter : currentAfter;
|
|
231
|
+
entry.state.debug = null; // clear first — see below
|
|
232
|
+
if (newBefore || newAfter) {
|
|
233
|
+
entry.state.debug = {
|
|
234
|
+
...(newBefore ? { before: true } : {}),
|
|
235
|
+
...(newAfter ? { after: true } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The intermediate `state.debug = null` is **required for Post**: Post's lockdown's setter PUSHES onto the `#breakpoints` array rather than replacing, so without the clear, repeated toggles accumulate stale entries. Turing's setter just creates a fresh DebugConfig either way — the null assignment is a no-op for Turing but essential for Post.
|
|
241
|
+
|
|
242
|
+
- Turing: setter creates/clears the engine's `DebugConfig` normally.
|
|
243
|
+
- Post: setter is intercepted, routed to `pm.setBreakpoint(path, filter)` or `pm.clearBreakpoint(path)` — registry updated, lockdown's internal write via `withLockdownEscape` then updates the engine ref.
|
|
244
|
+
|
|
245
|
+
Branchless form works for both engines.
|
|
246
|
+
|
|
247
|
+
## 16. Quick rules summary
|
|
248
|
+
|
|
249
|
+
A condensed cheat-sheet for the apply-highlight effect's decisions:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
expand from-side via highlightExpand(fromId) # asymmetric: wrapper → [wrapper, bare]
|
|
253
|
+
expand to-side via highlightExpand(toId) if toId > 0 # bare stays alone
|
|
254
|
+
|
|
255
|
+
mark fromEqIds as highlight-from (+ strong if h.strong === 'from')
|
|
256
|
+
mark toEqIds as highlight-to (+ strong if h.strong === 'to')
|
|
257
|
+
|
|
258
|
+
mark halt-marker (toId < 0) directly with highlight-to (+ strong if matching)
|
|
259
|
+
mark halt-singleton (toId === 0) directly with highlight-to (+ strong if matching)
|
|
260
|
+
|
|
261
|
+
highlight edge L_{fromKey}_{toKey}
|
|
262
|
+
if toEqIds had wrapper+bare: also highlight L_s{wrapper}_s{bare} (call edge)
|
|
263
|
+
|
|
264
|
+
if toId < 0: source return chain (halt-marker entry → wrappers → overrides)
|
|
265
|
+
if toId > 0 AND fromFrame matches some wrapper.overrideId:
|
|
266
|
+
destination return chain (bare → halt-marker → wrapper → override; frame active)
|
|
267
|
+
|
|
268
|
+
mark frame active for canonical(strongId)
|
|
269
|
+
|
|
270
|
+
if h.paused AND strongId === lastPausedStrongId: pulse strongEl
|
|
271
|
+
if h.paused: lastPausedStrongId := strongId # raw, not canonical
|
|
272
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@turing-machine-js/visuals",
|
|
3
|
+
"version": "7.0.0-alpha.6",
|
|
4
|
+
"description": "Pure highlight + graph-indexing logic for @turing-machine-js/machine — no DOM, no renderer.",
|
|
5
|
+
"engines": {
|
|
6
|
+
"npm": ">=7.0.0"
|
|
7
|
+
},
|
|
8
|
+
"author": "Ruslan Gilmullin <mellonis@yandex.ru>",
|
|
9
|
+
"homepage": "https://github.com/mellonis/turing-machine-js#readme",
|
|
10
|
+
"license": "GPL-3.0-or-later",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/mellonis/turing-machine-js.git",
|
|
17
|
+
"directory": "packages/visuals"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/mellonis/turing-machine-js/issues?utf8=✓&q=is%3Aissue+is%3Aopen+label%3A%22pkg%3A+visuals%22+label%3Abug"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"turing",
|
|
24
|
+
"machine",
|
|
25
|
+
"visualization"
|
|
26
|
+
],
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@turing-machine-js/machine": "^7.0.0-alpha.6"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc --build --verbose tsconfig.build.json && node ../../scripts/build-node-entries.mjs --package=@turing-machine-js/visuals",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"main": "dist/index.cjs",
|
|
35
|
+
"module": "dist/index.mjs",
|
|
36
|
+
"types": "dist/index.d.ts",
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.mjs",
|
|
41
|
+
"require": "./dist/index.cjs",
|
|
42
|
+
"default": "./dist/index.mjs"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"gitHead": "7801a33fef2f6c30cf6e1c2780b1fbf58685f064"
|
|
46
|
+
}
|