jssm 5.159.0 → 5.159.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/README.md +7 -7
- package/dist/cdn/instance.js +1 -1
- package/dist/cdn/viz.js +1 -1
- package/dist/cli/fsl-export-system-prompt.cjs +1 -1
- package/dist/cli/fsl-render.cjs +1 -1
- package/dist/cli/fsl.cjs +1 -1
- package/dist/cli/lib.cjs +1 -1
- package/dist/cli/lib.mjs +1 -1
- package/dist/deno/README.md +7 -7
- package/dist/deno/fsl_gif.d.ts +3 -2
- package/dist/deno/jssm.js +1 -1
- package/dist/deno/jssm_viz.d.ts +65 -1
- package/dist/fence/fence.js +269 -95
- package/dist/jssm.es5.cjs +1 -1
- package/dist/jssm.es5.iife.js +1 -1
- package/dist/jssm.es6.mjs +1 -1
- package/dist/jssm_viz.cjs +1 -1
- package/dist/jssm_viz.iife.cjs +1 -1
- package/dist/jssm_viz.mjs +1 -1
- package/jssm.cli.d.cts +15 -3
- package/jssm.cli.d.ts +15 -3
- package/jssm.fence.d.ts +3 -2
- package/jssm_viz.es5.d.cts +65 -1
- package/jssm_viz.es6.d.ts +65 -1
- package/package.json +1 -1
package/dist/deno/jssm_viz.d.ts
CHANGED
|
@@ -65,6 +65,24 @@ declare function vc(col: string): string;
|
|
|
65
65
|
* @internal
|
|
66
66
|
*/
|
|
67
67
|
declare function doublequote(txt: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Reverse {@link doublequote}: turn DOT's `\"` escape back into the literal
|
|
70
|
+
* `"` that graphviz renders into SVG `<text>` content. Used by
|
|
71
|
+
* {@link state_svg_label_texts} to reconstruct a node's on-screen label from
|
|
72
|
+
* the DOT label it was handed, so the fence renderer keys against exactly what
|
|
73
|
+
* was drawn.
|
|
74
|
+
*
|
|
75
|
+
* ```typescript
|
|
76
|
+
* undoublequote('a\\"b'); // 'a"b'
|
|
77
|
+
* undoublequote('safe'); // 'safe'
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @param txt A DOT-escaped attribute string (as produced by `doublequote`).
|
|
81
|
+
* @returns The string with every `\"` collapsed back to `"`.
|
|
82
|
+
*
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
declare function undoublequote(txt: string): string;
|
|
68
86
|
/**
|
|
69
87
|
* Convert a state name into a URL-friendly slug suitable for use as the
|
|
70
88
|
* body of a dot/SVG node identifier. The transformation is:
|
|
@@ -459,6 +477,50 @@ declare type VizRenderOpts = {
|
|
|
459
477
|
* @internal
|
|
460
478
|
*/
|
|
461
479
|
declare function node_block_for<T>(u_jssm: jssm.Machine<T>, l_states: string[], state_index: Map<string, string>, state_kinds: Map<string, StateKind>, hide_labels: boolean, mode: RenderGroups): string;
|
|
480
|
+
/**
|
|
481
|
+
* The per-state group-chip map that {@link node_block_for} appends to labels
|
|
482
|
+
* in a given render mode. Mirrors that function's mode dispatch and calls the
|
|
483
|
+
* very same chip sources — {@link chips_for_all_groups} for `'chips'`,
|
|
484
|
+
* {@link plan_cluster_groups} (with identical inputs) for `'cluster'` — so a
|
|
485
|
+
* reconstructed label can never disagree with the one graphviz was handed.
|
|
486
|
+
* `'off'`, and any machine that declares no groups, yields an empty map.
|
|
487
|
+
*
|
|
488
|
+
* @internal
|
|
489
|
+
*/
|
|
490
|
+
declare function chips_for_render_mode<T>(u_jssm: jssm.Machine<T>, l_states: string[], mode: RenderGroups): Map<string, string[]>;
|
|
491
|
+
/**
|
|
492
|
+
* The exact text graphviz places in each state's SVG `<text>` element(s) when
|
|
493
|
+
* a machine is rendered via {@link machine_to_dot} / {@link fsl_to_svg_string}:
|
|
494
|
+
* the state's display text plus any group chips the node builder appends, with
|
|
495
|
+
* DOT's `\"` escaping undone (SVG carries the literal character). A label that
|
|
496
|
+
* wraps across lines becomes several `<text>` elements; this returns the lines
|
|
497
|
+
* joined by `\n`, exactly how {@link extract_state_fills} reads them back — so
|
|
498
|
+
* the derived key and the extracted key meet at the same string.
|
|
499
|
+
*
|
|
500
|
+
* This is the single source of truth the static fence renderer keys its
|
|
501
|
+
* highlight and recolor lookups against, so those lookups can never drift from
|
|
502
|
+
* what was actually drawn — plain labels, group chips, and multi-line wraps
|
|
503
|
+
* alike. It is built by running the node builder's own
|
|
504
|
+
* `label_with_chips(doublequote(display_text), chips)` and inverting the one
|
|
505
|
+
* escaping step, so it follows any change to the label format for free.
|
|
506
|
+
*
|
|
507
|
+
* @param u_jssm The machine being rendered.
|
|
508
|
+
* @param opts Render flags; only `render_groups` affects the label text
|
|
509
|
+
* (default `'cluster'`, matching `fsl_to_svg_string`).
|
|
510
|
+
* @returns A map from each state name to its rendered SVG label text.
|
|
511
|
+
*
|
|
512
|
+
* ```typescript
|
|
513
|
+
* import { sm } from 'jssm';
|
|
514
|
+
* import { state_svg_label_texts } from 'jssm/viz';
|
|
515
|
+
*
|
|
516
|
+
* // a state in two groups renders a chip suffix in its node label
|
|
517
|
+
* state_svg_label_texts(sm`&g1 : [a b]; &g2 : [a]; a -> b;`).get('a'); // 'a [g1]'
|
|
518
|
+
* state_svg_label_texts(sm`a -> b;`).get('a'); // 'a'
|
|
519
|
+
* ```
|
|
520
|
+
*
|
|
521
|
+
* @see extract_state_fills
|
|
522
|
+
*/
|
|
523
|
+
declare function state_svg_label_texts<T>(u_jssm: jssm.Machine<T>, opts?: VizRenderOpts): Map<string, string>;
|
|
462
524
|
/**
|
|
463
525
|
* Render a {@link jssm.Machine} as a graphviz dot string.
|
|
464
526
|
*
|
|
@@ -578,7 +640,7 @@ declare function machine_to_svg_element<T>(u_jssm: jssm.Machine<T>, opts?: VizRe
|
|
|
578
640
|
* @deprecated Use {@link machine_to_dot} instead.
|
|
579
641
|
*/
|
|
580
642
|
declare function dot<T>(machine: jssm.Machine<T>): string;
|
|
581
|
-
export { configure, dot, dot_to_svg, fsl_to_dot, fsl_to_svg_string, fsl_to_svg_element, machine_to_dot, machine_to_svg_string, machine_to_svg_element, version, build_time };
|
|
643
|
+
export { configure, dot, dot_to_svg, fsl_to_dot, fsl_to_svg_string, fsl_to_svg_element, machine_to_dot, machine_to_svg_string, machine_to_svg_element, state_svg_label_texts, version, build_time };
|
|
582
644
|
export type { VizRenderOpts, RenderGroups };
|
|
583
645
|
/** @internal — test-only access to private helpers. */
|
|
584
646
|
export declare const _test: {
|
|
@@ -594,6 +656,7 @@ export declare const _test: {
|
|
|
594
656
|
style_for_state: typeof style_for_state;
|
|
595
657
|
cluster_id_for: typeof cluster_id_for;
|
|
596
658
|
label_with_chips: typeof label_with_chips;
|
|
659
|
+
undoublequote: typeof undoublequote;
|
|
597
660
|
group_parent_map: typeof group_parent_map;
|
|
598
661
|
group_ancestry: typeof group_ancestry;
|
|
599
662
|
primary_group_for: typeof primary_group_for;
|
|
@@ -601,6 +664,7 @@ export declare const _test: {
|
|
|
601
664
|
groups_to_subgraph_string: typeof groups_to_subgraph_string;
|
|
602
665
|
chips_for_all_groups: typeof chips_for_all_groups;
|
|
603
666
|
node_block_for: typeof node_block_for;
|
|
667
|
+
chips_for_render_mode: typeof chips_for_render_mode;
|
|
604
668
|
edge_attr_for: typeof edge_attr_for;
|
|
605
669
|
edge_defaults_body: typeof edge_defaults_body;
|
|
606
670
|
graph_attr_for: typeof graph_attr_for;
|
package/dist/fence/fence.js
CHANGED
|
@@ -23789,7 +23789,7 @@ function fslSemanticSpans(text) {
|
|
|
23789
23789
|
* Useful for runtime diagnostics and for embedding in serialized machine
|
|
23790
23790
|
* snapshots so that deserializers can detect version-skew.
|
|
23791
23791
|
*/
|
|
23792
|
-
const version = "5.159.
|
|
23792
|
+
const version = "5.159.1";
|
|
23793
23793
|
|
|
23794
23794
|
/**
|
|
23795
23795
|
* The FSL Markdown fence convention parser — pure, host-agnostic logic that
|
|
@@ -29474,6 +29474,26 @@ function vc(col) {
|
|
|
29474
29474
|
function doublequote(txt) {
|
|
29475
29475
|
return txt.replace(/"/g, '\\"');
|
|
29476
29476
|
}
|
|
29477
|
+
/**
|
|
29478
|
+
* Reverse {@link doublequote}: turn DOT's `\"` escape back into the literal
|
|
29479
|
+
* `"` that graphviz renders into SVG `<text>` content. Used by
|
|
29480
|
+
* {@link state_svg_label_texts} to reconstruct a node's on-screen label from
|
|
29481
|
+
* the DOT label it was handed, so the fence renderer keys against exactly what
|
|
29482
|
+
* was drawn.
|
|
29483
|
+
*
|
|
29484
|
+
* ```typescript
|
|
29485
|
+
* undoublequote('a\\"b'); // 'a"b'
|
|
29486
|
+
* undoublequote('safe'); // 'safe'
|
|
29487
|
+
* ```
|
|
29488
|
+
*
|
|
29489
|
+
* @param txt A DOT-escaped attribute string (as produced by `doublequote`).
|
|
29490
|
+
* @returns The string with every `\"` collapsed back to `"`.
|
|
29491
|
+
*
|
|
29492
|
+
* @internal
|
|
29493
|
+
*/
|
|
29494
|
+
function undoublequote(txt) {
|
|
29495
|
+
return txt.replace(/\\"/g, '"');
|
|
29496
|
+
}
|
|
29477
29497
|
/**
|
|
29478
29498
|
* Convert a state name into a URL-friendly slug suitable for use as the
|
|
29479
29499
|
* body of a dot/SVG node identifier. The transformation is:
|
|
@@ -30357,6 +30377,71 @@ function node_block_for(u_jssm, l_states, state_index, state_kinds, hide_labels,
|
|
|
30357
30377
|
const { clusters, ungrouped_nodes } = groups_to_subgraph_string(u_jssm, l_states, state_index, state_kinds, hide_labels);
|
|
30358
30378
|
return `${clusters}${(clusters && ungrouped_nodes) ? ' ' : ''}${ungrouped_nodes}`;
|
|
30359
30379
|
}
|
|
30380
|
+
/**
|
|
30381
|
+
* The per-state group-chip map that {@link node_block_for} appends to labels
|
|
30382
|
+
* in a given render mode. Mirrors that function's mode dispatch and calls the
|
|
30383
|
+
* very same chip sources — {@link chips_for_all_groups} for `'chips'`,
|
|
30384
|
+
* {@link plan_cluster_groups} (with identical inputs) for `'cluster'` — so a
|
|
30385
|
+
* reconstructed label can never disagree with the one graphviz was handed.
|
|
30386
|
+
* `'off'`, and any machine that declares no groups, yields an empty map.
|
|
30387
|
+
*
|
|
30388
|
+
* @internal
|
|
30389
|
+
*/
|
|
30390
|
+
function chips_for_render_mode(u_jssm, l_states, mode) {
|
|
30391
|
+
if ((mode === 'off') || (u_jssm.groups().length === 0)) {
|
|
30392
|
+
return new Map();
|
|
30393
|
+
}
|
|
30394
|
+
if (mode === 'chips') {
|
|
30395
|
+
return chips_for_all_groups(u_jssm, l_states);
|
|
30396
|
+
}
|
|
30397
|
+
const order = u_jssm.groups();
|
|
30398
|
+
const parents = group_parent_map(u_jssm._group_registry, order);
|
|
30399
|
+
return plan_cluster_groups(u_jssm, l_states, order, parents).chips;
|
|
30400
|
+
}
|
|
30401
|
+
/**
|
|
30402
|
+
* The exact text graphviz places in each state's SVG `<text>` element(s) when
|
|
30403
|
+
* a machine is rendered via {@link machine_to_dot} / {@link fsl_to_svg_string}:
|
|
30404
|
+
* the state's display text plus any group chips the node builder appends, with
|
|
30405
|
+
* DOT's `\"` escaping undone (SVG carries the literal character). A label that
|
|
30406
|
+
* wraps across lines becomes several `<text>` elements; this returns the lines
|
|
30407
|
+
* joined by `\n`, exactly how {@link extract_state_fills} reads them back — so
|
|
30408
|
+
* the derived key and the extracted key meet at the same string.
|
|
30409
|
+
*
|
|
30410
|
+
* This is the single source of truth the static fence renderer keys its
|
|
30411
|
+
* highlight and recolor lookups against, so those lookups can never drift from
|
|
30412
|
+
* what was actually drawn — plain labels, group chips, and multi-line wraps
|
|
30413
|
+
* alike. It is built by running the node builder's own
|
|
30414
|
+
* `label_with_chips(doublequote(display_text), chips)` and inverting the one
|
|
30415
|
+
* escaping step, so it follows any change to the label format for free.
|
|
30416
|
+
*
|
|
30417
|
+
* @param u_jssm The machine being rendered.
|
|
30418
|
+
* @param opts Render flags; only `render_groups` affects the label text
|
|
30419
|
+
* (default `'cluster'`, matching `fsl_to_svg_string`).
|
|
30420
|
+
* @returns A map from each state name to its rendered SVG label text.
|
|
30421
|
+
*
|
|
30422
|
+
* ```typescript
|
|
30423
|
+
* import { sm } from 'jssm';
|
|
30424
|
+
* import { state_svg_label_texts } from 'jssm/viz';
|
|
30425
|
+
*
|
|
30426
|
+
* // a state in two groups renders a chip suffix in its node label
|
|
30427
|
+
* state_svg_label_texts(sm`&g1 : [a b]; &g2 : [a]; a -> b;`).get('a'); // 'a [g1]'
|
|
30428
|
+
* state_svg_label_texts(sm`a -> b;`).get('a'); // 'a'
|
|
30429
|
+
* ```
|
|
30430
|
+
*
|
|
30431
|
+
* @see extract_state_fills
|
|
30432
|
+
*/
|
|
30433
|
+
function state_svg_label_texts(u_jssm, opts = {}) {
|
|
30434
|
+
var _a, _b;
|
|
30435
|
+
const l_states = u_jssm.states();
|
|
30436
|
+
const mode = (_a = opts.render_groups) !== null && _a !== void 0 ? _a : 'cluster';
|
|
30437
|
+
const chips = chips_for_render_mode(u_jssm, l_states, mode);
|
|
30438
|
+
const out = new Map();
|
|
30439
|
+
for (const s of l_states) {
|
|
30440
|
+
const dot_label = label_with_chips(doublequote(u_jssm.display_text(s)), (_b = chips.get(s)) !== null && _b !== void 0 ? _b : []);
|
|
30441
|
+
out.set(s, undoublequote(dot_label));
|
|
30442
|
+
}
|
|
30443
|
+
return out;
|
|
30444
|
+
}
|
|
30360
30445
|
/**
|
|
30361
30446
|
* Render a {@link jssm.Machine} as a graphviz dot string.
|
|
30362
30447
|
*
|
|
@@ -30473,6 +30558,17 @@ async function fsl_to_svg_string(fsl, opts = {}) {
|
|
|
30473
30558
|
return dot_to_svg(fsl_to_dot(fsl, opts), opts);
|
|
30474
30559
|
}
|
|
30475
30560
|
|
|
30561
|
+
/**
|
|
30562
|
+
* The render targets supported in v1, in canonical order (the CLI `--target`
|
|
30563
|
+
* enum and `--help` list order). This tuple is the single runtime source of
|
|
30564
|
+
* truth: both the {@link RenderTarget} type and the `fsl-render` CLI's
|
|
30565
|
+
* `--target` enum derive from it, so a new target is declared in exactly one
|
|
30566
|
+
* place. Future targets (mermaid, plantuml, scxml, ascii, fsl) land here in
|
|
30567
|
+
* v0.2+.
|
|
30568
|
+
*
|
|
30569
|
+
* @example
|
|
30570
|
+
* RENDER_TARGETS.includes('gif' as RenderTarget); // true
|
|
30571
|
+
*/
|
|
30476
30572
|
/**
|
|
30477
30573
|
* Base error class for render-time failures.
|
|
30478
30574
|
*/
|
|
@@ -31330,8 +31426,22 @@ function xml_unescape(s) {
|
|
|
31330
31426
|
}
|
|
31331
31427
|
/** Match one graphviz node group; capture [1] the group body. @internal */
|
|
31332
31428
|
const NODE_GROUP_RE = /<g[^>]*\bclass="node"[^>]*>([\s\S]*?)<\/g>/g;
|
|
31333
|
-
const
|
|
31429
|
+
const TEXT_RE_G = /<text[^>]*>([\s\S]*?)<\/text>/g;
|
|
31334
31430
|
const SHAPE_FILL_RE = /(<(?:ellipse|polygon|path)\b[^>]*\bfill=")([^"]*)(")/;
|
|
31431
|
+
/**
|
|
31432
|
+
* The state key for one node group: the XML-unescaped content of *every* one
|
|
31433
|
+
* of its `<text>` elements, joined by `\n`. Graphviz splits a label that
|
|
31434
|
+
* wraps across lines into one `<text>` per line, so reading only the first
|
|
31435
|
+
* would key on a truncated string; joining the lines with `\n` reconstructs
|
|
31436
|
+
* the machine's display text exactly (matching `state_svg_label_texts`).
|
|
31437
|
+
* Returns `null` for a node group carrying no `<text>` element at all.
|
|
31438
|
+
*
|
|
31439
|
+
* @internal
|
|
31440
|
+
*/
|
|
31441
|
+
function node_label_text(body) {
|
|
31442
|
+
const lines = [...body.matchAll(TEXT_RE_G)].map(m => xml_unescape(m[1]));
|
|
31443
|
+
return lines.length === 0 ? null : lines.join('\n');
|
|
31444
|
+
}
|
|
31335
31445
|
/**
|
|
31336
31446
|
* Read each state's current fill color out of a graphviz-rendered machine
|
|
31337
31447
|
* SVG, keyed by state name. States whose shape carries no `fill` attribute
|
|
@@ -31348,13 +31458,10 @@ function extract_state_fills(svg) {
|
|
|
31348
31458
|
const out = new Map();
|
|
31349
31459
|
for (const group of svg.matchAll(NODE_GROUP_RE)) {
|
|
31350
31460
|
const body = group[1];
|
|
31351
|
-
const
|
|
31461
|
+
const label = node_label_text(body);
|
|
31352
31462
|
const shape = body.match(SHAPE_FILL_RE);
|
|
31353
|
-
if (shape !== null &&
|
|
31354
|
-
|
|
31355
|
-
if (state_name) {
|
|
31356
|
-
out.set(state_name, shape[2]);
|
|
31357
|
-
}
|
|
31463
|
+
if (shape !== null && label !== null && label !== '') {
|
|
31464
|
+
out.set(label, shape[2]);
|
|
31358
31465
|
}
|
|
31359
31466
|
}
|
|
31360
31467
|
return out;
|
|
@@ -31379,11 +31486,11 @@ function patch_state_fill(svg, state, fill) {
|
|
|
31379
31486
|
if (done) {
|
|
31380
31487
|
return whole;
|
|
31381
31488
|
}
|
|
31382
|
-
const
|
|
31383
|
-
if (
|
|
31489
|
+
const label = node_label_text(body);
|
|
31490
|
+
if (label === null) {
|
|
31384
31491
|
return whole;
|
|
31385
31492
|
}
|
|
31386
|
-
if (
|
|
31493
|
+
if (label !== state) {
|
|
31387
31494
|
return whole;
|
|
31388
31495
|
}
|
|
31389
31496
|
// Function-form replacements: `fill` is caller-supplied and may itself
|
|
@@ -48858,7 +48965,8 @@ function quantize(rgba, max_colors = 256) {
|
|
|
48858
48965
|
throw new JssmError(undefined, 'quantize: max_colors must be 2..256');
|
|
48859
48966
|
}
|
|
48860
48967
|
const pixel_count = rgba.length / 4;
|
|
48861
|
-
// composite over white, build histogram of packed rgb keys
|
|
48968
|
+
// composite over white, build histogram of packed rgb keys (packed[] retained
|
|
48969
|
+
// so the per-pixel index step below need not recompute the keys)
|
|
48862
48970
|
const packed = new Uint32Array(pixel_count);
|
|
48863
48971
|
const histogram = new Map();
|
|
48864
48972
|
for (let i = 0; i < pixel_count; ++i) {
|
|
@@ -48866,6 +48974,48 @@ function quantize(rgba, max_colors = 256) {
|
|
|
48866
48974
|
packed[i] = key;
|
|
48867
48975
|
histogram.set(key, ((_a = histogram.get(key)) !== null && _a !== void 0 ? _a : 0) + 1);
|
|
48868
48976
|
}
|
|
48977
|
+
const { palette, palette_count, index_of } = palette_from_histogram(histogram, max_colors);
|
|
48978
|
+
const indices = new Uint8Array(pixel_count);
|
|
48979
|
+
for (let i = 0; i < pixel_count; ++i) {
|
|
48980
|
+
indices[i] = index_of.get(packed[i]);
|
|
48981
|
+
}
|
|
48982
|
+
return { palette, palette_count, indices };
|
|
48983
|
+
}
|
|
48984
|
+
/**
|
|
48985
|
+
* Accumulate one RGBA frame's composited-over-white colors into a shared
|
|
48986
|
+
* packed-rgb histogram, so an animation's union palette can be built by
|
|
48987
|
+
* streaming frame-by-frame instead of concatenating every frame's pixels into
|
|
48988
|
+
* one buffer first. Iterating frames in order reproduces the exact key/count
|
|
48989
|
+
* sequence a concatenated buffer would yield, so the resulting palette is
|
|
48990
|
+
* byte-identical to quantizing the concatenation.
|
|
48991
|
+
*
|
|
48992
|
+
* @param rgba - Straight RGBA bytes; length must be a multiple of 4.
|
|
48993
|
+
* @param histogram - Packed 0xRRGGBB key → count, mutated in place.
|
|
48994
|
+
*
|
|
48995
|
+
* @example
|
|
48996
|
+
* const h = new Map<number, number>();
|
|
48997
|
+
* accumulate_histogram(new Uint8Array([255,0,0,255]), h); // h: { 0xff0000 => 1 }
|
|
48998
|
+
*
|
|
48999
|
+
* @internal
|
|
49000
|
+
*/
|
|
49001
|
+
function accumulate_histogram(rgba, histogram) {
|
|
49002
|
+
var _a;
|
|
49003
|
+
const pixel_count = rgba.length / 4;
|
|
49004
|
+
for (let i = 0; i < pixel_count; ++i) {
|
|
49005
|
+
const key = packed_over_white(rgba, i);
|
|
49006
|
+
histogram.set(key, ((_a = histogram.get(key)) !== null && _a !== void 0 ? _a : 0) + 1);
|
|
49007
|
+
}
|
|
49008
|
+
}
|
|
49009
|
+
/**
|
|
49010
|
+
* Derive a color palette from a packed-rgb histogram: when the histogram holds
|
|
49011
|
+
* `max_colors` or fewer distinct colors they are preserved exactly (in
|
|
49012
|
+
* insertion order); otherwise a median-cut partition supplies the palette.
|
|
49013
|
+
* Returned alongside the palette is `index_of`, mapping each source color key
|
|
49014
|
+
* to its palette slot, so callers can translate their own pixels.
|
|
49015
|
+
*
|
|
49016
|
+
* @internal
|
|
49017
|
+
*/
|
|
49018
|
+
function palette_from_histogram(histogram, max_colors) {
|
|
48869
49019
|
const distinct = [...histogram.keys()];
|
|
48870
49020
|
if (distinct.length <= max_colors) {
|
|
48871
49021
|
const index_of = new Map();
|
|
@@ -48876,13 +49026,9 @@ function quantize(rgba, max_colors = 256) {
|
|
|
48876
49026
|
palette[i * 3 + 1] = (key >> 8) & 0xff;
|
|
48877
49027
|
palette[i * 3 + 2] = key & 0xff;
|
|
48878
49028
|
});
|
|
48879
|
-
|
|
48880
|
-
for (let i = 0; i < pixel_count; ++i) {
|
|
48881
|
-
indices[i] = index_of.get(packed[i]);
|
|
48882
|
-
}
|
|
48883
|
-
return { palette, palette_count: distinct.length, indices };
|
|
49029
|
+
return { palette, palette_count: distinct.length, index_of };
|
|
48884
49030
|
}
|
|
48885
|
-
return
|
|
49031
|
+
return median_cut_palette(histogram, max_colors);
|
|
48886
49032
|
}
|
|
48887
49033
|
/** Channel extractors for packed 0xRRGGBB keys. @internal */
|
|
48888
49034
|
const CHANNELS = [
|
|
@@ -48891,13 +49037,15 @@ const CHANNELS = [
|
|
|
48891
49037
|
key => key & 0xff,
|
|
48892
49038
|
];
|
|
48893
49039
|
/**
|
|
48894
|
-
* Median-cut
|
|
48895
|
-
*
|
|
48896
|
-
*
|
|
49040
|
+
* Median-cut palette over a pre-built histogram. Splits the box with the
|
|
49041
|
+
* widest channel range at its count-weighted median until `max_colors` boxes
|
|
49042
|
+
* exist, then averages each box into a palette entry. Returns the palette and
|
|
49043
|
+
* the color→slot map only; per-pixel indices are the caller's concern (the
|
|
49044
|
+
* animation encoder maps each frame separately, so it never needs them here).
|
|
48897
49045
|
*
|
|
48898
49046
|
* @internal
|
|
48899
49047
|
*/
|
|
48900
|
-
function
|
|
49048
|
+
function median_cut_palette(histogram, max_colors) {
|
|
48901
49049
|
const boxes = [[...histogram.keys()]];
|
|
48902
49050
|
while (boxes.length < max_colors) {
|
|
48903
49051
|
// pick the box with the widest single-channel range
|
|
@@ -48954,11 +49102,7 @@ function median_cut(packed, histogram, max_colors) {
|
|
|
48954
49102
|
palette[bi * 3 + 1] = Math.round(g / n);
|
|
48955
49103
|
palette[bi * 3 + 2] = Math.round(b / n);
|
|
48956
49104
|
});
|
|
48957
|
-
|
|
48958
|
-
for (let i = 0; i < packed.length; ++i) {
|
|
48959
|
-
indices[i] = index_of.get(packed[i]);
|
|
48960
|
-
}
|
|
48961
|
-
return { palette, palette_count: boxes.length, indices };
|
|
49105
|
+
return { palette, palette_count: boxes.length, index_of };
|
|
48962
49106
|
}
|
|
48963
49107
|
/**
|
|
48964
49108
|
* GIF-variant LZW compression: emits a leading clear code, grows code width
|
|
@@ -49036,8 +49180,9 @@ function lzw_encode(indices, min_code_size) {
|
|
|
49036
49180
|
* @param frames - At least one frame; all with identical width/height and
|
|
49037
49181
|
* `rgba.length === 4 · width · height`.
|
|
49038
49182
|
*
|
|
49039
|
-
* @throws {JssmError} on zero frames,
|
|
49040
|
-
* buffer whose length contradicts its
|
|
49183
|
+
* @throws {JssmError} on zero frames, a zero-width or zero-height frame,
|
|
49184
|
+
* mismatched dimensions, or an rgba buffer whose length contradicts its
|
|
49185
|
+
* stated dimensions.
|
|
49041
49186
|
*
|
|
49042
49187
|
* @example
|
|
49043
49188
|
* const red = { rgba: new Uint8Array([255,0,0,255]), width: 1, height: 1 };
|
|
@@ -49050,6 +49195,12 @@ function encode_gif(frames, opts = {}) {
|
|
|
49050
49195
|
throw new JssmError(undefined, 'encode_gif: at least one frame is required');
|
|
49051
49196
|
}
|
|
49052
49197
|
const { width, height } = frames[0];
|
|
49198
|
+
if (width === 0 || height === 0) {
|
|
49199
|
+
// A 0×0 (or 0×h / w×0) frame passes the `4·w·h === rgba.length` check with
|
|
49200
|
+
// an empty buffer, then reaches lzw_encode where `indices[0]!` coerces
|
|
49201
|
+
// undefined into a bogus code 0 — a silently-corrupt GIF. Fail loudly here.
|
|
49202
|
+
throw new JssmError(undefined, `encode_gif: frame dimensions must be non-zero (got ${width}x${height})`);
|
|
49203
|
+
}
|
|
49053
49204
|
for (const f of frames) {
|
|
49054
49205
|
if (f.width !== width || f.height !== height) {
|
|
49055
49206
|
throw new JssmError(undefined, `encode_gif: frame dimensions differ (${f.width}x${f.height} vs ${width}x${height})`);
|
|
@@ -49060,25 +49211,30 @@ function encode_gif(frames, opts = {}) {
|
|
|
49060
49211
|
}
|
|
49061
49212
|
const delay_cs = (_a = opts.delay_cs) !== null && _a !== void 0 ? _a : 70;
|
|
49062
49213
|
const loop = (_b = opts.loop) !== null && _b !== void 0 ? _b : 0;
|
|
49063
|
-
//
|
|
49064
|
-
|
|
49065
|
-
|
|
49214
|
+
// Global palette over the UNION of every frame, built by streaming each
|
|
49215
|
+
// frame's histogram rather than concatenating all frames into one buffer
|
|
49216
|
+
// first (64 frames × w×h×4 can reach hundreds of MB on a big machine).
|
|
49217
|
+
// Frames are visited in order, so the key/count sequence — and therefore the
|
|
49218
|
+
// palette — is byte-identical to quantizing the concatenation.
|
|
49219
|
+
const union_histogram = new Map();
|
|
49066
49220
|
for (const f of frames) {
|
|
49067
|
-
|
|
49068
|
-
pos += f.rgba.length;
|
|
49221
|
+
accumulate_histogram(f.rgba, union_histogram);
|
|
49069
49222
|
}
|
|
49070
|
-
const
|
|
49071
|
-
const gct_bits = Math.max(1, Math.ceil(Math.log2(Math.max(2,
|
|
49223
|
+
const { palette, palette_count } = palette_from_histogram(union_histogram, 256);
|
|
49224
|
+
const gct_bits = Math.max(1, Math.ceil(Math.log2(Math.max(2, palette_count))));
|
|
49072
49225
|
const gct_size = 1 << gct_bits;
|
|
49073
49226
|
const min_code_size = Math.max(2, gct_bits);
|
|
49074
|
-
|
|
49227
|
+
// One nearest-color cache for the whole encode: the global palette is
|
|
49228
|
+
// identical across frames, so a color's best index never changes frame to
|
|
49229
|
+
// frame — rebuilding the cache per frame only re-did the same work.
|
|
49230
|
+
const palette_cache = new Map();
|
|
49231
|
+
/** Map a frame's pixels to nearest entries of the union global palette. @internal */
|
|
49075
49232
|
const map_to_palette = (rgba) => {
|
|
49076
49233
|
const n = rgba.length / 4;
|
|
49077
49234
|
const out = new Uint8Array(n);
|
|
49078
|
-
const cache = new Map();
|
|
49079
49235
|
for (let i = 0; i < n; ++i) {
|
|
49080
49236
|
const key = packed_over_white(rgba, i);
|
|
49081
|
-
const hit =
|
|
49237
|
+
const hit = palette_cache.get(key);
|
|
49082
49238
|
if (hit !== undefined) {
|
|
49083
49239
|
out[i] = hit;
|
|
49084
49240
|
continue;
|
|
@@ -49087,67 +49243,83 @@ function encode_gif(frames, opts = {}) {
|
|
|
49087
49243
|
const g = (key >> 8) & 0xff;
|
|
49088
49244
|
const b = key & 0xff;
|
|
49089
49245
|
let best = 0, best_d = Infinity;
|
|
49090
|
-
for (let p = 0; p <
|
|
49091
|
-
const dr = r -
|
|
49092
|
-
const dg = g -
|
|
49093
|
-
const db = b -
|
|
49246
|
+
for (let p = 0; p < palette_count; ++p) {
|
|
49247
|
+
const dr = r - palette[p * 3];
|
|
49248
|
+
const dg = g - palette[p * 3 + 1];
|
|
49249
|
+
const db = b - palette[p * 3 + 2];
|
|
49094
49250
|
const d = dr * dr + dg * dg + db * db;
|
|
49095
49251
|
if (d < best_d) {
|
|
49096
49252
|
best_d = d;
|
|
49097
49253
|
best = p;
|
|
49098
49254
|
}
|
|
49099
49255
|
}
|
|
49100
|
-
|
|
49256
|
+
palette_cache.set(key, best);
|
|
49101
49257
|
out[i] = best;
|
|
49102
49258
|
}
|
|
49103
49259
|
return out;
|
|
49104
49260
|
};
|
|
49105
|
-
|
|
49106
|
-
|
|
49261
|
+
// Output accumulates into a growable Uint8Array (doubling on demand) rather
|
|
49262
|
+
// than a boxed-integer number[]; the final GIF is `out.subarray(0, len)`.
|
|
49263
|
+
let out = new Uint8Array(1024);
|
|
49264
|
+
let len = 0;
|
|
49265
|
+
const need = (extra) => {
|
|
49266
|
+
if (len + extra > out.length) {
|
|
49267
|
+
const bigger = new Uint8Array(Math.max(out.length * 2, len + extra));
|
|
49268
|
+
bigger.set(out.subarray(0, len));
|
|
49269
|
+
out = bigger;
|
|
49270
|
+
}
|
|
49271
|
+
};
|
|
49272
|
+
const push = (...vals) => {
|
|
49273
|
+
need(vals.length);
|
|
49274
|
+
for (let k = 0; k < vals.length; ++k) {
|
|
49275
|
+
out[len++] = vals[k] & 0xff;
|
|
49276
|
+
}
|
|
49277
|
+
};
|
|
49278
|
+
const push_u16 = (v) => { push(v & 0xff, (v >> 8) & 0xff); };
|
|
49107
49279
|
// header + logical screen descriptor
|
|
49108
|
-
|
|
49280
|
+
push(0x47, 0x49, 0x46, 0x38, 0x39, 0x61); // "GIF89a"
|
|
49109
49281
|
push_u16(width);
|
|
49110
49282
|
push_u16(height);
|
|
49111
|
-
|
|
49112
|
-
|
|
49283
|
+
push(0x80 | 0x70 | (gct_bits - 1)); // GCT present, color res 8-bit, size
|
|
49284
|
+
push(0, 0); // background index, aspect
|
|
49113
49285
|
// global color table, padded to 2^gct_bits entries
|
|
49114
49286
|
for (let p = 0; p < gct_size; ++p) {
|
|
49115
|
-
if (p <
|
|
49116
|
-
|
|
49287
|
+
if (p < palette_count) {
|
|
49288
|
+
push(palette[p * 3], palette[p * 3 + 1], palette[p * 3 + 2]);
|
|
49117
49289
|
}
|
|
49118
49290
|
else {
|
|
49119
|
-
|
|
49291
|
+
push(0, 0, 0);
|
|
49120
49292
|
}
|
|
49121
49293
|
}
|
|
49122
49294
|
// Netscape looping extension
|
|
49123
|
-
|
|
49124
|
-
|
|
49125
|
-
|
|
49295
|
+
push(0x21, 0xFF, 0x0B);
|
|
49296
|
+
push(...'NETSCAPE2.0'.split('').map(c => c.charCodeAt(0)));
|
|
49297
|
+
push(0x03, 0x01);
|
|
49126
49298
|
push_u16(loop);
|
|
49127
|
-
|
|
49299
|
+
push(0x00);
|
|
49128
49300
|
for (let fi = 0; fi < frames.length; ++fi) {
|
|
49129
49301
|
// graphics control extension: disposal 1 (leave in place), no transparency
|
|
49130
|
-
|
|
49302
|
+
push(0x21, 0xF9, 0x04, 0x04);
|
|
49131
49303
|
push_u16(delay_cs);
|
|
49132
|
-
|
|
49304
|
+
push(0x00, 0x00);
|
|
49133
49305
|
// image descriptor: full frame, no local color table
|
|
49134
|
-
|
|
49306
|
+
push(0x2C);
|
|
49135
49307
|
push_u16(0);
|
|
49136
49308
|
push_u16(0);
|
|
49137
49309
|
push_u16(width);
|
|
49138
49310
|
push_u16(height);
|
|
49139
|
-
|
|
49311
|
+
push(0x00);
|
|
49140
49312
|
const indices = map_to_palette(frames[fi].rgba);
|
|
49141
|
-
const
|
|
49142
|
-
|
|
49143
|
-
for (let at = 0; at <
|
|
49144
|
-
const chunk =
|
|
49145
|
-
|
|
49313
|
+
const packed_lzw = lzw_encode(indices, min_code_size);
|
|
49314
|
+
push(min_code_size);
|
|
49315
|
+
for (let at = 0; at < packed_lzw.length; at += 255) {
|
|
49316
|
+
const chunk = packed_lzw.slice(at, at + 255);
|
|
49317
|
+
push(chunk.length, ...chunk);
|
|
49146
49318
|
}
|
|
49147
|
-
|
|
49319
|
+
push(0x00); // block terminator
|
|
49148
49320
|
}
|
|
49149
|
-
|
|
49150
|
-
return
|
|
49321
|
+
push(0x3B); // trailer
|
|
49322
|
+
return out.slice(0, len);
|
|
49151
49323
|
}
|
|
49152
49324
|
|
|
49153
49325
|
/**
|
|
@@ -49178,32 +49350,33 @@ function error_box(source, message) {
|
|
|
49178
49350
|
/** Note comment, invisible but inspectable. @internal */
|
|
49179
49351
|
const note_comment = (note) => `<!-- fsl-fence: ${escape_html(note)} -->`;
|
|
49180
49352
|
/**
|
|
49181
|
-
* Remap
|
|
49182
|
-
* {@link extract_state_fills} from a rendered SVG) to be keyed by state
|
|
49183
|
-
*
|
|
49184
|
-
*
|
|
49185
|
-
*
|
|
49186
|
-
*
|
|
49187
|
-
*
|
|
49188
|
-
*
|
|
49189
|
-
*
|
|
49190
|
-
*
|
|
49191
|
-
*
|
|
49192
|
-
*
|
|
49193
|
-
*
|
|
49194
|
-
*
|
|
49195
|
-
* @param
|
|
49196
|
-
*
|
|
49353
|
+
* Remap an SVG-label-keyed fill map (as produced by
|
|
49354
|
+
* {@link extract_state_fills} from a rendered SVG) to be keyed by state NAME
|
|
49355
|
+
* instead. Graphviz labels each node with its full rendered label text —
|
|
49356
|
+
* display text, plus any group chips, plus any multi-line wrap — while
|
|
49357
|
+
* `highlight_fsl_html`'s `state_colors` option is keyed by the state's own
|
|
49358
|
+
* name (the code highlighter's semantic spans carry names, not labels).
|
|
49359
|
+
* Without this remap, any state whose label diverges from its name silently
|
|
49360
|
+
* fails to carry its diagram color to its code span.
|
|
49361
|
+
*
|
|
49362
|
+
* Both sides of the join derive the SVG label text from the same source:
|
|
49363
|
+
* `label_texts` comes from {@link state_svg_label_texts} (the viz's own label
|
|
49364
|
+
* builder) and `fills`'s keys are read straight off the SVG, so group chips
|
|
49365
|
+
* and multi-line labels are handled without either side drifting.
|
|
49366
|
+
*
|
|
49367
|
+
* @param label_texts - State name → its rendered SVG label text, from
|
|
49368
|
+
* `state_svg_label_texts`.
|
|
49369
|
+
* @param fills - SVG-label-keyed fills, from `extract_state_fills`.
|
|
49197
49370
|
* @returns Fills re-keyed by state name; states with no matching fill are omitted.
|
|
49198
49371
|
*
|
|
49199
49372
|
* @internal
|
|
49200
49373
|
*/
|
|
49201
|
-
function state_colors_by_name(
|
|
49374
|
+
function state_colors_by_name(label_texts, fills) {
|
|
49202
49375
|
const out = new Map();
|
|
49203
|
-
for (const
|
|
49204
|
-
const fill = fills.get(
|
|
49376
|
+
for (const [state, label] of label_texts) {
|
|
49377
|
+
const fill = fills.get(label);
|
|
49205
49378
|
if (fill !== undefined) {
|
|
49206
|
-
out.set(
|
|
49379
|
+
out.set(state, fill);
|
|
49207
49380
|
}
|
|
49208
49381
|
}
|
|
49209
49382
|
return out;
|
|
@@ -49246,7 +49419,7 @@ async function render_fence_html(source, info, opts = {}) {
|
|
|
49246
49419
|
}
|
|
49247
49420
|
const svg = await fsl_to_svg_string(source);
|
|
49248
49421
|
const fills = extract_state_fills(svg);
|
|
49249
|
-
const state_colors = state_colors_by_name(machine, fills);
|
|
49422
|
+
const state_colors = state_colors_by_name(state_svg_label_texts(machine), fills);
|
|
49250
49423
|
const chunks = desc.notes.map(note_comment);
|
|
49251
49424
|
for (const part of desc.parts) {
|
|
49252
49425
|
if (INTERACTIVE_PARTS.has(part)) {
|
|
@@ -49381,14 +49554,15 @@ async function render_fence_gif(source, opts = {}) {
|
|
|
49381
49554
|
const base_svg = await fsl_to_svg_string(source);
|
|
49382
49555
|
const highlight = (_b = opts.highlight_fill) !== null && _b !== void 0 ? _b : '#ff9930';
|
|
49383
49556
|
const scale = (_c = opts.scale) !== null && _c !== void 0 ? _c : 100;
|
|
49557
|
+
const label_texts = state_svg_label_texts(machine);
|
|
49384
49558
|
const frames = [];
|
|
49385
49559
|
for (const state of walk) {
|
|
49386
|
-
// base_svg's nodes are labeled with
|
|
49387
|
-
//
|
|
49388
|
-
//
|
|
49389
|
-
//
|
|
49390
|
-
//
|
|
49391
|
-
const patched = patch_state_fill(base_svg,
|
|
49560
|
+
// base_svg's nodes are labeled with their full SVG label text (display
|
|
49561
|
+
// text + any group chips + any multi-line wrap), not the walk's state
|
|
49562
|
+
// NAME — patch by the shared derivation or a labeled/chipped/wrapped
|
|
49563
|
+
// state's frame silently no-ops. `walk` is drawn from the machine's own
|
|
49564
|
+
// states, so every entry has a label_texts key.
|
|
49565
|
+
const patched = patch_state_fill(base_svg, label_texts.get(state), highlight);
|
|
49392
49566
|
const raster = await rasterizeRgba(patched, { scale });
|
|
49393
49567
|
frames.push({ rgba: raster.rgba, width: raster.width, height: raster.height });
|
|
49394
49568
|
}
|