@turing-machine-js/visuals 7.0.0-alpha.7 → 7.0.0-alpha.7.1
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 +13 -0
- package/README.md +62 -0
- package/dist/index.cjs +125 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +124 -1
- package/dist/snippetPlayer.d.ts +44 -0
- package/dist/tapeViewport.d.ts +23 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this package will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [7.0.0-alpha.7.1] - 2026-05-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`tapeViewport(snapshot, width, blank): { cells, headIndex }`** — fixed-width window of tape cells centered on the head. The engine's `Tape` class exposes a `viewport` getter for the live tape; this is the equivalent for `TapeSnapshot` (the wire-data shape carried in `Frame.tape`). Cells outside the snapshot's `symbols` array are padded with `blank`, so the result always has exactly `width` entries. `headIndex` is deterministic at `Math.floor(width / 2)` and is exposed for caller convenience (and to leave room for future non-centered policies without a signature break). Throws `RangeError` on non-positive or non-integer width.
|
|
12
|
+
|
|
13
|
+
- **`SnippetPlayer`** — pure-state playback driver for a `Snippet`. `new SnippetPlayer(snippet)` returns an instance exposing `currentFrame` / `frameIndex` / `done` getters plus `forward()` / `back()` / `reset()` / `goTo(idx)`. Mirrors the engine's `DebugSession` shape (a stateful playback driver for live runs); this is the analogous driver for prerecorded runs. Stateless w.r.t. wall-clock — consumers wire their own ticking (`setInterval`, `requestAnimationFrame`, `IntersectionObserver`); renderer-agnostic — consumers read `currentFrame` and apply it however they like (typically `applyHighlight(snippet.graph, frame.highlight, ops)` for the state graph plus app-specific tape rendering). Two players over the same `Snippet` are independent (frame storage is shared and read-only). `forward()` / `back()` return a `boolean` (true if moved, false at end/start — no-op in that case); `goTo(idx)` throws `RangeError` on out-of-bounds.
|
|
14
|
+
|
|
15
|
+
### Compatibility
|
|
16
|
+
|
|
17
|
+
- Engine + builder + library-binary-numbers + library-binary-numbers-bare stay at `7.0.0-alpha.7` — no changes there. Visuals-only follow-up patch, mirroring the alpha.6.1 precedent for additive consumer-package enhancements.
|
|
18
|
+
- Peer dep `@turing-machine-js/machine: ^7.0.0-alpha.7` unchanged (semver-prerelease caret already accepts `alpha.7.1`).
|
|
19
|
+
|
|
7
20
|
## [7.0.0-alpha.7] - 2026-05-30
|
|
8
21
|
|
|
9
22
|
Lockstep re-alignment with the engine 7.0.0-alpha.7 bump (engine [#213](https://github.com/mellonis/turing-machine-js/issues/213) `CallFrame` extraction + [#223](https://github.com/mellonis/turing-machine-js/issues/223) `toMermaid` framed-wrapper emit fix). No source or behavior changes in this package since alpha.6.1. Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.6` → `^7.0.0-alpha.7`.
|
package/README.md
CHANGED
|
@@ -102,6 +102,10 @@ formatStep(machineState): string // alpha.6 MachineState-based formatter; kep
|
|
|
102
102
|
|
|
103
103
|
// Recording
|
|
104
104
|
recordSnippet({ machine, initialState, graph, alphabets, name?, maxSteps?, log? }): Snippet
|
|
105
|
+
|
|
106
|
+
// Playback
|
|
107
|
+
new SnippetPlayer(snippet)
|
|
108
|
+
tapeViewport(snapshot, width, blank): { cells, headIndex }
|
|
105
109
|
```
|
|
106
110
|
|
|
107
111
|
The 16-rule contract `applyHighlight` satisfies is documented at [`docs/graph-highlight-and-breakpoints.md`](./docs/graph-highlight-and-breakpoints.md).
|
|
@@ -238,6 +242,64 @@ const snippet = recordSnippet({
|
|
|
238
242
|
|
|
239
243
|
`Frame.commands` carries both `read` and `write` per tape so a player can step forward (write `write`, move per `movement`, flash if `write !== read`) AND backward (move opposite of `movement`, restore `read`) without diffing neighbouring frames.
|
|
240
244
|
|
|
245
|
+
## Example: playing a snippet
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import {
|
|
249
|
+
applyHighlight, indexGraph, SnippetPlayer,
|
|
250
|
+
type Frame, type HighlightOps,
|
|
251
|
+
} from '@turing-machine-js/visuals';
|
|
252
|
+
|
|
253
|
+
// Build a HighlightOps over your SVG / renderer — see the
|
|
254
|
+
// "Example: applying highlight in a DOM renderer" above for the
|
|
255
|
+
// concrete `domOps(svgRoot)` factory.
|
|
256
|
+
declare const ops: HighlightOps;
|
|
257
|
+
|
|
258
|
+
// Render a frame's tape state into your UI — app-specific (Svelte,
|
|
259
|
+
// React, vanilla, ANSI, …). For a fixed-width centered window padded
|
|
260
|
+
// with the alphabet's blank, see `tapeViewport(snap, width, blank)` below.
|
|
261
|
+
declare function renderTape(tape: Frame['tape']): void;
|
|
262
|
+
|
|
263
|
+
// Buttons your UI exposes — strictly illustrative.
|
|
264
|
+
declare const prevBtn: HTMLButtonElement;
|
|
265
|
+
declare const nextBtn: HTMLButtonElement;
|
|
266
|
+
declare const replayBtn: HTMLButtonElement;
|
|
267
|
+
|
|
268
|
+
const player = new SnippetPlayer(snippet);
|
|
269
|
+
const indexes = indexGraph(snippet.graph);
|
|
270
|
+
let prev: Parameters<typeof applyHighlight>[3] = null;
|
|
271
|
+
|
|
272
|
+
function applyFrame(): void {
|
|
273
|
+
const frame = player.currentFrame;
|
|
274
|
+
// (consumer is responsible for wiping previous highlight classes from
|
|
275
|
+
// the DOM before each applyHighlight call — see the highlight example above.)
|
|
276
|
+
if (frame.highlight) {
|
|
277
|
+
prev = applyHighlight(frame.highlight, snippet.graph, indexes, prev, ops).nextPrev;
|
|
278
|
+
}
|
|
279
|
+
renderTape(frame.tape);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// One AbortController scopes both the auto-play timer and the click
|
|
283
|
+
// listeners — call controller.abort() in your component teardown
|
|
284
|
+
// (Svelte onDestroy / React useEffect cleanup / etc.).
|
|
285
|
+
const controller = new AbortController();
|
|
286
|
+
const { signal } = controller;
|
|
287
|
+
|
|
288
|
+
// Auto-play forward at a fixed cadence:
|
|
289
|
+
const id = setInterval(() => {
|
|
290
|
+
if (!player.forward()) { clearInterval(id); return; }
|
|
291
|
+
applyFrame();
|
|
292
|
+
}, 800);
|
|
293
|
+
signal.addEventListener('abort', () => clearInterval(id), { once: true });
|
|
294
|
+
|
|
295
|
+
// Bi-directional scrub:
|
|
296
|
+
prevBtn.addEventListener('click', () => { if (player.back()) applyFrame(); }, { signal });
|
|
297
|
+
nextBtn.addEventListener('click', () => { if (player.forward()) applyFrame(); }, { signal });
|
|
298
|
+
replayBtn.addEventListener('click', () => { player.reset(); applyFrame(); }, { signal });
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
`SnippetPlayer` is pure state — no timers, no events. Consumers wire `setInterval` / `requestAnimationFrame` / `IntersectionObserver` and call `forward()` / `back()` / `goTo(idx)`. Two players over the same `Snippet` are independent (frame storage is shared and read-only). Mirrors the engine's `DebugSession` shape — stateful playback driver for live runs vs prerecorded runs.
|
|
302
|
+
|
|
241
303
|
## Versioning
|
|
242
304
|
|
|
243
305
|
Engine + builder + libraries bump in **lockstep** via `lerna version`. **Visuals breaks lockstep for additive consumer-package patches** (e.g., `7.0.0-alpha.6.1` added formatter primitives + the token surface without bumping the engine — there were no engine changes to justify ghost releases). Semver-prerelease caret semantics make this safe: peer `^7.0.0-alpha.6` accepts `7.0.0-alpha.6.1+` without consumers having to widen anything. When the next engine alpha needs to ship, all 5 packages bump back to lockstep.
|
package/dist/index.cjs
CHANGED
|
@@ -599,6 +599,130 @@ function recordSnippet(opts) {
|
|
|
599
599
|
};
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
+
var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
603
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
604
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
605
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
606
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
607
|
+
};
|
|
608
|
+
var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
609
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
610
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
611
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
612
|
+
};
|
|
613
|
+
var _SnippetPlayer_snippet, _SnippetPlayer_lastIndex, _SnippetPlayer_index;
|
|
614
|
+
/**
|
|
615
|
+
* Pure-state playback driver for a `Snippet` produced by `recordSnippet`.
|
|
616
|
+
*
|
|
617
|
+
* Holds the current frame index and exposes `forward` / `back` / `reset` /
|
|
618
|
+
* `goTo`. Stateless w.r.t. wall-clock — consumers wire their own ticking
|
|
619
|
+
* (`setInterval`, `requestAnimationFrame`, `IntersectionObserver`, etc.).
|
|
620
|
+
* Renderer-agnostic — consumers read `currentFrame` and apply it however
|
|
621
|
+
* they like (e.g. `applyHighlight(snippet.graph, frame.highlight, ops)`
|
|
622
|
+
* for state-graph highlight, plus app-specific tape rendering).
|
|
623
|
+
*
|
|
624
|
+
* Two players over the same `Snippet` are independent — frame storage is
|
|
625
|
+
* shared and read-only.
|
|
626
|
+
*
|
|
627
|
+
* Mirrors the engine's `DebugSession` shape (a stateful playback driver
|
|
628
|
+
* for live runs); this is the analogous driver for prerecorded runs.
|
|
629
|
+
*/
|
|
630
|
+
class SnippetPlayer {
|
|
631
|
+
constructor(snippet) {
|
|
632
|
+
_SnippetPlayer_snippet.set(this, void 0);
|
|
633
|
+
_SnippetPlayer_lastIndex.set(this, void 0);
|
|
634
|
+
_SnippetPlayer_index.set(this, 0);
|
|
635
|
+
if (snippet.frames.length === 0) {
|
|
636
|
+
throw new Error('SnippetPlayer: snippet has no frames');
|
|
637
|
+
}
|
|
638
|
+
__classPrivateFieldSet(this, _SnippetPlayer_snippet, snippet, "f");
|
|
639
|
+
__classPrivateFieldSet(this, _SnippetPlayer_lastIndex, snippet.frames.length - 1, "f");
|
|
640
|
+
}
|
|
641
|
+
/** The frame at the current index. Live getter — re-reads on every access. */
|
|
642
|
+
get currentFrame() {
|
|
643
|
+
return __classPrivateFieldGet(this, _SnippetPlayer_snippet, "f").frames[__classPrivateFieldGet(this, _SnippetPlayer_index, "f")];
|
|
644
|
+
}
|
|
645
|
+
/** Current frame index (0 = initial state, `snippet.frames.length - 1` = final). */
|
|
646
|
+
get frameIndex() {
|
|
647
|
+
return __classPrivateFieldGet(this, _SnippetPlayer_index, "f");
|
|
648
|
+
}
|
|
649
|
+
/** True when `frameIndex === snippet.frames.length - 1`. */
|
|
650
|
+
get done() {
|
|
651
|
+
return __classPrivateFieldGet(this, _SnippetPlayer_index, "f") === __classPrivateFieldGet(this, _SnippetPlayer_lastIndex, "f");
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Advance one frame. Returns `true` if advanced, `false` if already at the
|
|
655
|
+
* last frame (no-op in that case).
|
|
656
|
+
*/
|
|
657
|
+
forward() {
|
|
658
|
+
if (__classPrivateFieldGet(this, _SnippetPlayer_index, "f") >= __classPrivateFieldGet(this, _SnippetPlayer_lastIndex, "f"))
|
|
659
|
+
return false;
|
|
660
|
+
__classPrivateFieldSet(this, _SnippetPlayer_index, __classPrivateFieldGet(this, _SnippetPlayer_index, "f") + 1, "f");
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Retreat one frame. Returns `true` if retreated, `false` if already at
|
|
665
|
+
* the first frame (no-op in that case).
|
|
666
|
+
*/
|
|
667
|
+
back() {
|
|
668
|
+
if (__classPrivateFieldGet(this, _SnippetPlayer_index, "f") <= 0)
|
|
669
|
+
return false;
|
|
670
|
+
__classPrivateFieldSet(this, _SnippetPlayer_index, __classPrivateFieldGet(this, _SnippetPlayer_index, "f") - 1, "f");
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
/** Jump to frame 0. */
|
|
674
|
+
reset() {
|
|
675
|
+
__classPrivateFieldSet(this, _SnippetPlayer_index, 0, "f");
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Jump to a specific frame index. Throws `RangeError` if out of bounds
|
|
679
|
+
* (negative, beyond the last frame, or not an integer).
|
|
680
|
+
*/
|
|
681
|
+
goTo(frameIndex) {
|
|
682
|
+
if (!Number.isInteger(frameIndex) || frameIndex < 0 || frameIndex > __classPrivateFieldGet(this, _SnippetPlayer_lastIndex, "f")) {
|
|
683
|
+
throw new RangeError(`SnippetPlayer.goTo: frame index ${frameIndex} out of bounds [0, ${__classPrivateFieldGet(this, _SnippetPlayer_lastIndex, "f")}]`);
|
|
684
|
+
}
|
|
685
|
+
__classPrivateFieldSet(this, _SnippetPlayer_index, frameIndex, "f");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
_SnippetPlayer_snippet = new WeakMap(), _SnippetPlayer_lastIndex = new WeakMap(), _SnippetPlayer_index = new WeakMap();
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Compute a fixed-width window of tape cells centered on the head.
|
|
692
|
+
*
|
|
693
|
+
* The engine's `Tape` class exposes a `viewport` getter that does this for
|
|
694
|
+
* the live tape; `tapeViewport` is the equivalent for the wire-data
|
|
695
|
+
* `TapeSnapshot` carried in `Frame.tape`. Cells outside the snapshot's
|
|
696
|
+
* `symbols` array are padded with `blank`, so the result always has
|
|
697
|
+
* exactly `width` entries.
|
|
698
|
+
*
|
|
699
|
+
* The returned `headIndex` is the head's index within `cells` —
|
|
700
|
+
* deterministic at `Math.floor(width / 2)`, but exposed for callers that
|
|
701
|
+
* want to avoid recomputing it (and to leave room for future non-centered
|
|
702
|
+
* policies without a signature break).
|
|
703
|
+
*
|
|
704
|
+
* Pass the tape's blank symbol from `Snippet.alphabets[i]` (by convention
|
|
705
|
+
* the first entry per tape in the visuals/engine pipeline — verify against
|
|
706
|
+
* how the snippet was recorded).
|
|
707
|
+
*/
|
|
708
|
+
function tapeViewport(snapshot, width, blank) {
|
|
709
|
+
if (!Number.isInteger(width) || width <= 0) {
|
|
710
|
+
throw new RangeError(`tapeViewport: width must be a positive integer (got ${width})`);
|
|
711
|
+
}
|
|
712
|
+
const half = Math.floor(width / 2);
|
|
713
|
+
const startTapeIdx = snapshot.position - half;
|
|
714
|
+
const cells = new Array(width);
|
|
715
|
+
for (let i = 0; i < width; i += 1) {
|
|
716
|
+
const tapeIdx = startTapeIdx + i;
|
|
717
|
+
cells[i] =
|
|
718
|
+
tapeIdx >= 0 && tapeIdx < snapshot.symbols.length
|
|
719
|
+
? snapshot.symbols[tapeIdx]
|
|
720
|
+
: blank;
|
|
721
|
+
}
|
|
722
|
+
return { cells, headIndex: half };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
exports.SnippetPlayer = SnippetPlayer;
|
|
602
726
|
exports.applyHighlight = applyHighlight;
|
|
603
727
|
exports.applyIndicator = applyIndicator;
|
|
604
728
|
exports.bareIdOf = bareIdOf;
|
|
@@ -611,4 +735,5 @@ exports.highlightExpand = highlightExpand;
|
|
|
611
735
|
exports.indexGraph = indexGraph;
|
|
612
736
|
exports.recordSnippet = recordSnippet;
|
|
613
737
|
exports.recordingOps = recordingOps;
|
|
738
|
+
exports.tapeViewport = tapeViewport;
|
|
614
739
|
exports.tokenizeStep = tokenizeStep;
|
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export type { GraphHighlight, TapeSnapshot, Frame, Snippet } from './types';
|
|
|
7
7
|
export { applyHighlight, applyIndicator } from './applyHighlight';
|
|
8
8
|
export { formatCommand, formatStep, formatStepNotation, formatTape, tokenizeStep, type StepCommand, type ReadToken, type WriteToken, type StepTokens, } from './format';
|
|
9
9
|
export { recordSnippet, type RecordSnippetOptions } from './recordSnippet';
|
|
10
|
+
export { SnippetPlayer } from './snippetPlayer';
|
|
11
|
+
export { tapeViewport } from './tapeViewport';
|
package/dist/index.mjs
CHANGED
|
@@ -597,4 +597,127 @@ function recordSnippet(opts) {
|
|
|
597
597
|
};
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
-
|
|
600
|
+
var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
601
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
602
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
603
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
604
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
605
|
+
};
|
|
606
|
+
var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
607
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
608
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
609
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
610
|
+
};
|
|
611
|
+
var _SnippetPlayer_snippet, _SnippetPlayer_lastIndex, _SnippetPlayer_index;
|
|
612
|
+
/**
|
|
613
|
+
* Pure-state playback driver for a `Snippet` produced by `recordSnippet`.
|
|
614
|
+
*
|
|
615
|
+
* Holds the current frame index and exposes `forward` / `back` / `reset` /
|
|
616
|
+
* `goTo`. Stateless w.r.t. wall-clock — consumers wire their own ticking
|
|
617
|
+
* (`setInterval`, `requestAnimationFrame`, `IntersectionObserver`, etc.).
|
|
618
|
+
* Renderer-agnostic — consumers read `currentFrame` and apply it however
|
|
619
|
+
* they like (e.g. `applyHighlight(snippet.graph, frame.highlight, ops)`
|
|
620
|
+
* for state-graph highlight, plus app-specific tape rendering).
|
|
621
|
+
*
|
|
622
|
+
* Two players over the same `Snippet` are independent — frame storage is
|
|
623
|
+
* shared and read-only.
|
|
624
|
+
*
|
|
625
|
+
* Mirrors the engine's `DebugSession` shape (a stateful playback driver
|
|
626
|
+
* for live runs); this is the analogous driver for prerecorded runs.
|
|
627
|
+
*/
|
|
628
|
+
class SnippetPlayer {
|
|
629
|
+
constructor(snippet) {
|
|
630
|
+
_SnippetPlayer_snippet.set(this, void 0);
|
|
631
|
+
_SnippetPlayer_lastIndex.set(this, void 0);
|
|
632
|
+
_SnippetPlayer_index.set(this, 0);
|
|
633
|
+
if (snippet.frames.length === 0) {
|
|
634
|
+
throw new Error('SnippetPlayer: snippet has no frames');
|
|
635
|
+
}
|
|
636
|
+
__classPrivateFieldSet(this, _SnippetPlayer_snippet, snippet, "f");
|
|
637
|
+
__classPrivateFieldSet(this, _SnippetPlayer_lastIndex, snippet.frames.length - 1, "f");
|
|
638
|
+
}
|
|
639
|
+
/** The frame at the current index. Live getter — re-reads on every access. */
|
|
640
|
+
get currentFrame() {
|
|
641
|
+
return __classPrivateFieldGet(this, _SnippetPlayer_snippet, "f").frames[__classPrivateFieldGet(this, _SnippetPlayer_index, "f")];
|
|
642
|
+
}
|
|
643
|
+
/** Current frame index (0 = initial state, `snippet.frames.length - 1` = final). */
|
|
644
|
+
get frameIndex() {
|
|
645
|
+
return __classPrivateFieldGet(this, _SnippetPlayer_index, "f");
|
|
646
|
+
}
|
|
647
|
+
/** True when `frameIndex === snippet.frames.length - 1`. */
|
|
648
|
+
get done() {
|
|
649
|
+
return __classPrivateFieldGet(this, _SnippetPlayer_index, "f") === __classPrivateFieldGet(this, _SnippetPlayer_lastIndex, "f");
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Advance one frame. Returns `true` if advanced, `false` if already at the
|
|
653
|
+
* last frame (no-op in that case).
|
|
654
|
+
*/
|
|
655
|
+
forward() {
|
|
656
|
+
if (__classPrivateFieldGet(this, _SnippetPlayer_index, "f") >= __classPrivateFieldGet(this, _SnippetPlayer_lastIndex, "f"))
|
|
657
|
+
return false;
|
|
658
|
+
__classPrivateFieldSet(this, _SnippetPlayer_index, __classPrivateFieldGet(this, _SnippetPlayer_index, "f") + 1, "f");
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Retreat one frame. Returns `true` if retreated, `false` if already at
|
|
663
|
+
* the first frame (no-op in that case).
|
|
664
|
+
*/
|
|
665
|
+
back() {
|
|
666
|
+
if (__classPrivateFieldGet(this, _SnippetPlayer_index, "f") <= 0)
|
|
667
|
+
return false;
|
|
668
|
+
__classPrivateFieldSet(this, _SnippetPlayer_index, __classPrivateFieldGet(this, _SnippetPlayer_index, "f") - 1, "f");
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
/** Jump to frame 0. */
|
|
672
|
+
reset() {
|
|
673
|
+
__classPrivateFieldSet(this, _SnippetPlayer_index, 0, "f");
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Jump to a specific frame index. Throws `RangeError` if out of bounds
|
|
677
|
+
* (negative, beyond the last frame, or not an integer).
|
|
678
|
+
*/
|
|
679
|
+
goTo(frameIndex) {
|
|
680
|
+
if (!Number.isInteger(frameIndex) || frameIndex < 0 || frameIndex > __classPrivateFieldGet(this, _SnippetPlayer_lastIndex, "f")) {
|
|
681
|
+
throw new RangeError(`SnippetPlayer.goTo: frame index ${frameIndex} out of bounds [0, ${__classPrivateFieldGet(this, _SnippetPlayer_lastIndex, "f")}]`);
|
|
682
|
+
}
|
|
683
|
+
__classPrivateFieldSet(this, _SnippetPlayer_index, frameIndex, "f");
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
_SnippetPlayer_snippet = new WeakMap(), _SnippetPlayer_lastIndex = new WeakMap(), _SnippetPlayer_index = new WeakMap();
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Compute a fixed-width window of tape cells centered on the head.
|
|
690
|
+
*
|
|
691
|
+
* The engine's `Tape` class exposes a `viewport` getter that does this for
|
|
692
|
+
* the live tape; `tapeViewport` is the equivalent for the wire-data
|
|
693
|
+
* `TapeSnapshot` carried in `Frame.tape`. Cells outside the snapshot's
|
|
694
|
+
* `symbols` array are padded with `blank`, so the result always has
|
|
695
|
+
* exactly `width` entries.
|
|
696
|
+
*
|
|
697
|
+
* The returned `headIndex` is the head's index within `cells` —
|
|
698
|
+
* deterministic at `Math.floor(width / 2)`, but exposed for callers that
|
|
699
|
+
* want to avoid recomputing it (and to leave room for future non-centered
|
|
700
|
+
* policies without a signature break).
|
|
701
|
+
*
|
|
702
|
+
* Pass the tape's blank symbol from `Snippet.alphabets[i]` (by convention
|
|
703
|
+
* the first entry per tape in the visuals/engine pipeline — verify against
|
|
704
|
+
* how the snippet was recorded).
|
|
705
|
+
*/
|
|
706
|
+
function tapeViewport(snapshot, width, blank) {
|
|
707
|
+
if (!Number.isInteger(width) || width <= 0) {
|
|
708
|
+
throw new RangeError(`tapeViewport: width must be a positive integer (got ${width})`);
|
|
709
|
+
}
|
|
710
|
+
const half = Math.floor(width / 2);
|
|
711
|
+
const startTapeIdx = snapshot.position - half;
|
|
712
|
+
const cells = new Array(width);
|
|
713
|
+
for (let i = 0; i < width; i += 1) {
|
|
714
|
+
const tapeIdx = startTapeIdx + i;
|
|
715
|
+
cells[i] =
|
|
716
|
+
tapeIdx >= 0 && tapeIdx < snapshot.symbols.length
|
|
717
|
+
? snapshot.symbols[tapeIdx]
|
|
718
|
+
: blank;
|
|
719
|
+
}
|
|
720
|
+
return { cells, headIndex: half };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export { SnippetPlayer, applyHighlight, applyIndicator, bareIdOf, equivalentIds, formatCommand, formatStep, formatStepNotation, formatTape, highlightExpand, indexGraph, recordSnippet, recordingOps, tapeViewport, tokenizeStep };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Frame, Snippet } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Pure-state playback driver for a `Snippet` produced by `recordSnippet`.
|
|
4
|
+
*
|
|
5
|
+
* Holds the current frame index and exposes `forward` / `back` / `reset` /
|
|
6
|
+
* `goTo`. Stateless w.r.t. wall-clock — consumers wire their own ticking
|
|
7
|
+
* (`setInterval`, `requestAnimationFrame`, `IntersectionObserver`, etc.).
|
|
8
|
+
* Renderer-agnostic — consumers read `currentFrame` and apply it however
|
|
9
|
+
* they like (e.g. `applyHighlight(snippet.graph, frame.highlight, ops)`
|
|
10
|
+
* for state-graph highlight, plus app-specific tape rendering).
|
|
11
|
+
*
|
|
12
|
+
* Two players over the same `Snippet` are independent — frame storage is
|
|
13
|
+
* shared and read-only.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the engine's `DebugSession` shape (a stateful playback driver
|
|
16
|
+
* for live runs); this is the analogous driver for prerecorded runs.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SnippetPlayer {
|
|
19
|
+
#private;
|
|
20
|
+
constructor(snippet: Snippet);
|
|
21
|
+
/** The frame at the current index. Live getter — re-reads on every access. */
|
|
22
|
+
get currentFrame(): Frame;
|
|
23
|
+
/** Current frame index (0 = initial state, `snippet.frames.length - 1` = final). */
|
|
24
|
+
get frameIndex(): number;
|
|
25
|
+
/** True when `frameIndex === snippet.frames.length - 1`. */
|
|
26
|
+
get done(): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Advance one frame. Returns `true` if advanced, `false` if already at the
|
|
29
|
+
* last frame (no-op in that case).
|
|
30
|
+
*/
|
|
31
|
+
forward(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Retreat one frame. Returns `true` if retreated, `false` if already at
|
|
34
|
+
* the first frame (no-op in that case).
|
|
35
|
+
*/
|
|
36
|
+
back(): boolean;
|
|
37
|
+
/** Jump to frame 0. */
|
|
38
|
+
reset(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Jump to a specific frame index. Throws `RangeError` if out of bounds
|
|
41
|
+
* (negative, beyond the last frame, or not an integer).
|
|
42
|
+
*/
|
|
43
|
+
goTo(frameIndex: number): void;
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TapeSnapshot } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Compute a fixed-width window of tape cells centered on the head.
|
|
4
|
+
*
|
|
5
|
+
* The engine's `Tape` class exposes a `viewport` getter that does this for
|
|
6
|
+
* the live tape; `tapeViewport` is the equivalent for the wire-data
|
|
7
|
+
* `TapeSnapshot` carried in `Frame.tape`. Cells outside the snapshot's
|
|
8
|
+
* `symbols` array are padded with `blank`, so the result always has
|
|
9
|
+
* exactly `width` entries.
|
|
10
|
+
*
|
|
11
|
+
* The returned `headIndex` is the head's index within `cells` —
|
|
12
|
+
* deterministic at `Math.floor(width / 2)`, but exposed for callers that
|
|
13
|
+
* want to avoid recomputing it (and to leave room for future non-centered
|
|
14
|
+
* policies without a signature break).
|
|
15
|
+
*
|
|
16
|
+
* Pass the tape's blank symbol from `Snippet.alphabets[i]` (by convention
|
|
17
|
+
* the first entry per tape in the visuals/engine pipeline — verify against
|
|
18
|
+
* how the snippet was recorded).
|
|
19
|
+
*/
|
|
20
|
+
export declare function tapeViewport(snapshot: TapeSnapshot, width: number, blank: string): {
|
|
21
|
+
cells: string[];
|
|
22
|
+
headIndex: number;
|
|
23
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turing-machine-js/visuals",
|
|
3
|
-
"version": "7.0.0-alpha.7",
|
|
3
|
+
"version": "7.0.0-alpha.7.1",
|
|
4
4
|
"description": "Pure highlight + graph-indexing logic for @turing-machine-js/machine — no DOM, no renderer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"npm": ">=7.0.0"
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"default": "./dist/index.mjs"
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "2adf5ab059319427a9a449370f91b2ea8e926059"
|
|
46
46
|
}
|