@mettascript/grapher 2.0.0

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/dist/index.js ADDED
@@ -0,0 +1,4014 @@
1
+ // src/editor.ts
2
+ import { MeTTa as MeTTa2, ExpressionAtom as ExpressionAtom7, E as E5 } from "@mettascript/hyperon";
3
+
4
+ // src/ids.ts
5
+ var counter = 0;
6
+ function nextId() {
7
+ return `n${++counter}`;
8
+ }
9
+
10
+ // src/model.ts
11
+ var X_EPSILON = 0.01;
12
+ var Graph = class _Graph {
13
+ nodes = /* @__PURE__ */ new Map();
14
+ childIds = /* @__PURE__ */ new Map();
15
+ // parent -> ordered child ids
16
+ parentIds = /* @__PURE__ */ new Map();
17
+ // child -> parent ids
18
+ /** Add a node. An explicit `id` is preserved (for loading); otherwise a fresh one is minted. Defaults:
19
+ * kind `symbol`, position `(0, 0)`. */
20
+ add(spec) {
21
+ const node = {
22
+ id: spec.id ?? nextId(),
23
+ name: spec.name,
24
+ kind: spec.kind ?? "symbol",
25
+ x: spec.x ?? 0,
26
+ y: spec.y ?? 0
27
+ };
28
+ this.nodes.set(node.id, node);
29
+ return node;
30
+ }
31
+ /** Remove a node and every edge touching it. */
32
+ remove(id) {
33
+ for (const p of this.parentsOf(id)) this.disconnect(p, id);
34
+ for (const c of this.childrenOf(id)) this.disconnect(id, c);
35
+ this.nodes.delete(id);
36
+ this.childIds.delete(id);
37
+ this.parentIds.delete(id);
38
+ }
39
+ /** Move a node to a new position. */
40
+ move(id, x, y) {
41
+ const node = this.nodes.get(id);
42
+ if (node) {
43
+ node.x = x;
44
+ node.y = y;
45
+ }
46
+ }
47
+ /** This node's child ids, in insertion order. */
48
+ childrenOf(id) {
49
+ return [...this.childIds.get(id) ?? []];
50
+ }
51
+ /** This node's parent ids, in insertion order. */
52
+ parentsOf(id) {
53
+ return [...this.parentIds.get(id) ?? []];
54
+ }
55
+ /** Whether `parent -> child` would be a legal edge: not a self-loop, a duplicate, an unknown node, or a
56
+ * cycle (the parent already sitting below the child). Lets the editor reject a drag before committing it. */
57
+ canConnect(parentId, childId) {
58
+ if (parentId === childId) return false;
59
+ if (!this.nodes.has(parentId) || !this.nodes.has(childId)) return false;
60
+ if ((this.childIds.get(parentId) ?? []).includes(childId)) return false;
61
+ if (this.reaches(childId, parentId)) return false;
62
+ return true;
63
+ }
64
+ /** Connect `parent` to `child`. Returns false and does nothing when the edge would be illegal (see
65
+ * {@link canConnect}). */
66
+ connect(parentId, childId) {
67
+ if (!this.canConnect(parentId, childId)) return false;
68
+ this.childIds.set(parentId, [...this.childIds.get(parentId) ?? [], childId]);
69
+ this.parentIds.set(childId, [...this.parentIds.get(childId) ?? [], parentId]);
70
+ return true;
71
+ }
72
+ /** Remove the `parent -> child` edge if present. */
73
+ disconnect(parentId, childId) {
74
+ const kids = this.childIds.get(parentId);
75
+ if (kids)
76
+ this.childIds.set(
77
+ parentId,
78
+ kids.filter((k) => k !== childId)
79
+ );
80
+ const parents = this.parentIds.get(childId);
81
+ if (parents)
82
+ this.parentIds.set(
83
+ childId,
84
+ parents.filter((p) => p !== parentId)
85
+ );
86
+ }
87
+ /** Children of `id` in screen order: by x, ties (within {@link X_EPSILON}) broken by y. This is how
88
+ * argument order is read off the canvas. */
89
+ sortedChildren(id) {
90
+ const kids = this.childrenOf(id).map((k) => this.nodes.get(k)).filter((n) => n !== void 0);
91
+ return kids.sort((a, b) => Math.abs(a.x - b.x) < X_EPSILON ? a.y - b.y : a.x - b.x);
92
+ }
93
+ /** The roots (parentless nodes) reachable by walking up from `id`. A parentless node is its own head. */
94
+ findHeads(id) {
95
+ const heads = /* @__PURE__ */ new Map();
96
+ const seen = /* @__PURE__ */ new Set();
97
+ const visit = (cur) => {
98
+ if (seen.has(cur)) return;
99
+ seen.add(cur);
100
+ const parents = this.parentIds.get(cur) ?? [];
101
+ if (parents.length === 0) {
102
+ const node = this.nodes.get(cur);
103
+ if (node) heads.set(cur, node);
104
+ } else {
105
+ for (const p of parents) visit(p);
106
+ }
107
+ };
108
+ visit(id);
109
+ return [...heads.values()];
110
+ }
111
+ /** Every root in the graph (nodes with no parents). */
112
+ heads() {
113
+ return [...this.nodes.values()].filter((n) => (this.parentIds.get(n.id) ?? []).length === 0);
114
+ }
115
+ /** A deep copy: same node ids, positions, and edges. */
116
+ clone() {
117
+ const g = new _Graph();
118
+ for (const n of this.nodes.values()) g.add({ ...n });
119
+ for (const [p, kids] of this.childIds) for (const c of kids) g.connect(p, c);
120
+ return g;
121
+ }
122
+ /** Can `from` reach `to` by following child edges downward? */
123
+ reaches(from, to) {
124
+ const seen = /* @__PURE__ */ new Set();
125
+ const stack = [from];
126
+ while (stack.length > 0) {
127
+ const cur = stack.pop();
128
+ if (cur === void 0) break;
129
+ if (cur === to) return true;
130
+ if (seen.has(cur)) continue;
131
+ seen.add(cur);
132
+ for (const c of this.childIds.get(cur) ?? []) stack.push(c);
133
+ }
134
+ return false;
135
+ }
136
+ };
137
+
138
+ // src/color.ts
139
+ var GLYPHS = {
140
+ "*": "\xD7",
141
+ // ×
142
+ "-": "\u2212",
143
+ // −
144
+ "/": "\xF7",
145
+ // ÷
146
+ "%": "mod",
147
+ ">=": "\u2265",
148
+ // ≥
149
+ "<=": "\u2264",
150
+ // ≤
151
+ "!=": "\u2260",
152
+ // ≠
153
+ "->": "\u2192",
154
+ // →
155
+ "=": "\u2261",
156
+ // ≡
157
+ and: "\u2227",
158
+ // ∧
159
+ or: "\u2228",
160
+ // ∨
161
+ not: "\xAC",
162
+ // ¬
163
+ xor: "\u2295",
164
+ // ⊕
165
+ superpose: "\u222A"
166
+ // ∪
167
+ };
168
+ function displayGlyph(name) {
169
+ return GLYPHS[name] ?? name;
170
+ }
171
+ var NUMERIC = /^[+-]?(\d+\.?\d*|\.\d+)$/;
172
+ var OPERATORS = /* @__PURE__ */ new Set([
173
+ "=",
174
+ ":",
175
+ "->",
176
+ "==",
177
+ "!=",
178
+ "!",
179
+ "+",
180
+ "-",
181
+ "*",
182
+ "/",
183
+ "%",
184
+ "<",
185
+ ">",
186
+ ">=",
187
+ "<=",
188
+ "and",
189
+ "or",
190
+ "not",
191
+ "xor"
192
+ ]);
193
+ var CONTROL = /* @__PURE__ */ new Set(["if", "case", "cond", "match", "switch", "unify"]);
194
+ var BOOLEANS = /* @__PURE__ */ new Set(["True", "False"]);
195
+ function roleOf(name) {
196
+ if (name.startsWith("$")) return "variable";
197
+ if (name.startsWith("&")) return "spaceref";
198
+ if (name.startsWith("@")) return "at";
199
+ if (name.startsWith('"')) return "string";
200
+ if (NUMERIC.test(name)) return "number";
201
+ if (BOOLEANS.has(name)) return "boolean";
202
+ if (OPERATORS.has(name)) return "operator";
203
+ if (CONTROL.has(name)) return "control";
204
+ return "symbol";
205
+ }
206
+ var DARK = "#0d1117";
207
+ var PALETTE = {
208
+ // a variable is drawn hollow (a transparent circle), so its fill is used as the outline and its text is
209
+ // the same color, visible on the canvas rather than dark-on-fill.
210
+ variable: { fill: "#ffa657", text: "#ffa657" },
211
+ spaceref: { fill: "#ffa657", text: DARK },
212
+ at: { fill: "#d2a8ff", text: DARK },
213
+ number: { fill: "#79c0ff", text: DARK },
214
+ string: { fill: "#a5d6ff", text: DARK },
215
+ boolean: { fill: "#39c5cf", text: DARK },
216
+ operator: { fill: "#ff7b72", text: DARK },
217
+ control: { fill: "#f2cc60", text: DARK },
218
+ paren: { fill: "#7ee787", text: DARK },
219
+ symbol: { fill: "#454c5a", text: "#e6edf3" }
220
+ };
221
+ function colorFor(node) {
222
+ if (node.kind === "list") return PALETTE.paren;
223
+ if (node.kind === "dot") return { fill: "#6e7681", text: "#e6edf3" };
224
+ return PALETTE[roleOf(node.name)];
225
+ }
226
+ var srgbToLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
227
+ var linearToSrgb = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
228
+ function hexToOklab(hex) {
229
+ const m = /^#([0-9a-fA-F]{6})$/.exec(hex);
230
+ if (m === null) return null;
231
+ const v = parseInt(m[1], 16);
232
+ const r = srgbToLinear((v >> 16 & 255) / 255);
233
+ const g = srgbToLinear((v >> 8 & 255) / 255);
234
+ const b = srgbToLinear((v & 255) / 255);
235
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
236
+ const m2 = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
237
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
238
+ return [
239
+ 0.2104542553 * l + 0.793617785 * m2 - 0.0040720468 * s,
240
+ 1.9779984951 * l - 2.428592205 * m2 + 0.4505937099 * s,
241
+ 0.0259040371 * l + 0.7827717662 * m2 - 0.808675766 * s
242
+ ];
243
+ }
244
+ function oklabToHex([L, A, B]) {
245
+ const l_ = L + 0.3963377774 * A + 0.2158037573 * B;
246
+ const m_ = L - 0.1055613458 * A - 0.0638541728 * B;
247
+ const s_ = L - 0.0894841775 * A - 1.291485548 * B;
248
+ const l = l_ * l_ * l_;
249
+ const m = m_ * m_ * m_;
250
+ const s = s_ * s_ * s_;
251
+ const r = linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
252
+ const g = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
253
+ const b = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s);
254
+ const hx = (c) => Math.max(0, Math.min(255, Math.round(c * 255))).toString(16).padStart(2, "0");
255
+ return `#${hx(r)}${hx(g)}${hx(b)}`;
256
+ }
257
+ function lerpColor(a, b, t) {
258
+ const ca = hexToOklab(a);
259
+ const cb = hexToOklab(b);
260
+ if (ca === null || cb === null) return t < 0.5 ? a : b;
261
+ return oklabToHex([
262
+ ca[0] + (cb[0] - ca[0]) * t,
263
+ ca[1] + (cb[1] - ca[1]) * t,
264
+ ca[2] + (cb[2] - ca[2]) * t
265
+ ]);
266
+ }
267
+ function heatColor(t) {
268
+ const u = Math.max(0, Math.min(1, t));
269
+ return u < 0.5 ? lerpColor("#3fb950", "#f2cc60", u * 2) : lerpColor("#f2cc60", "#f85149", (u - 0.5) * 2);
270
+ }
271
+
272
+ // src/measure.ts
273
+ var NODE_H = 22;
274
+ var CHAR_W = 7;
275
+ var PAD_X = 12;
276
+ var MIN_W = 24;
277
+ function displayText(node) {
278
+ if (node.kind === "list") return "( )";
279
+ if (node.kind === "dot") return "\u2022";
280
+ return node.name.length > 0 ? displayGlyph(node.name) : "?";
281
+ }
282
+ function nodeWidth(node) {
283
+ return Math.max(MIN_W, displayText(node).length * CHAR_W + PAD_X);
284
+ }
285
+
286
+ // src/variables.ts
287
+ function variableLinks(graph) {
288
+ const groups = /* @__PURE__ */ new Map();
289
+ for (const node of graph.nodes.values()) {
290
+ if (node.kind !== "symbol" || !node.name.startsWith("$")) continue;
291
+ const head = graph.findHeads(node.id)[0];
292
+ if (head === void 0) continue;
293
+ const key = `${head.id}\0${node.name}`;
294
+ const arr = groups.get(key);
295
+ if (arr) arr.push(node.id);
296
+ else groups.set(key, [node.id]);
297
+ }
298
+ const links = [];
299
+ for (const ids of groups.values()) {
300
+ if (ids.length < 2) continue;
301
+ const sorted = ids.map((id) => graph.nodes.get(id)).filter((n) => n !== void 0).sort((a, b) => a.x - b.x || a.y - b.y);
302
+ for (let i = 0; i + 1 < sorted.length; i++) links.push([sorted[i].id, sorted[i + 1].id]);
303
+ }
304
+ return links;
305
+ }
306
+
307
+ // src/anim.ts
308
+ var lerp = (a, b, t) => a + (b - a) * t;
309
+ function ease(t) {
310
+ return t * t * t * (t * (t * 6 - 15) + 10);
311
+ }
312
+ var DEFAULT_TRACE_MS = 550;
313
+ function arcPoint(ax, ay, bx, by, t) {
314
+ const mx = lerp(ax, bx, t);
315
+ const my = lerp(ay, by, t);
316
+ const dx = bx - ax;
317
+ const dy = by - ay;
318
+ const dist = Math.hypot(dx, dy);
319
+ if (dist < 1) return { x: mx, y: my };
320
+ const off = Math.sin(Math.PI * t) * Math.min(dist * 0.16, 36);
321
+ return { x: mx + -dy / dist * off, y: my + dx / dist * off };
322
+ }
323
+
324
+ // src/shapes.ts
325
+ var CORNER_SEG = 6;
326
+ function shapePolygon(role, w) {
327
+ const hw = w / 2;
328
+ const hh = NODE_H / 2;
329
+ switch (role) {
330
+ case "operator":
331
+ case "control":
332
+ return [
333
+ [0, -hh],
334
+ [hw, 0],
335
+ [0, hh],
336
+ [-hw, 0]
337
+ ];
338
+ // diamond
339
+ case "variable":
340
+ return ellipse(hw, hh);
341
+ // hollow circle
342
+ case "number":
343
+ case "string":
344
+ return roundedRect(hw, hh, hh);
345
+ // pill
346
+ case "boolean":
347
+ return roundedRect(hw, hh, 3);
348
+ // square
349
+ case "list":
350
+ return [
351
+ [-hw + 7, -hh],
352
+ [hw - 7, -hh],
353
+ [hw, 0],
354
+ [hw - 7, hh],
355
+ [-hw + 7, hh],
356
+ [-hw, 0]
357
+ ];
358
+ // hexagon
359
+ default:
360
+ return roundedRect(hw, hh, 5);
361
+ }
362
+ }
363
+ function ellipse(hw, hh) {
364
+ const n = 24;
365
+ const out = [];
366
+ for (let i = 0; i < n; i++) {
367
+ const a = 2 * Math.PI * i / n;
368
+ out.push([hw * Math.cos(a), hh * Math.sin(a)]);
369
+ }
370
+ return out;
371
+ }
372
+ function roundedRect(hw, hh, r) {
373
+ const rr = Math.min(r, hw, hh);
374
+ const out = [];
375
+ const corners = [
376
+ [hw - rr, hh - rr, 0],
377
+ [-(hw - rr), hh - rr, Math.PI / 2],
378
+ [-(hw - rr), -(hh - rr), Math.PI],
379
+ [hw - rr, -(hh - rr), 3 * Math.PI / 2]
380
+ ];
381
+ for (const [cx, cy, a0] of corners)
382
+ for (let i = 0; i <= CORNER_SEG; i++) {
383
+ const a = a0 + i / CORNER_SEG * (Math.PI / 2);
384
+ out.push([cx + rr * Math.cos(a), cy + rr * Math.sin(a)]);
385
+ }
386
+ return out;
387
+ }
388
+ function raycast(poly, ang) {
389
+ const dx = Math.cos(ang);
390
+ const dy = Math.sin(ang);
391
+ let best = Infinity;
392
+ let hit = [0, 0];
393
+ for (let i = 0; i < poly.length; i++) {
394
+ const [x1, y1] = poly[i];
395
+ const [x2, y2] = poly[(i + 1) % poly.length];
396
+ const ex = x2 - x1;
397
+ const ey = y2 - y1;
398
+ const det = ex * dy - dx * ey;
399
+ if (Math.abs(det) < 1e-9) continue;
400
+ const t = (ex * y1 - ey * x1) / det;
401
+ const s = (dx * y1 - dy * x1) / det;
402
+ if (t > 0 && s >= -1e-6 && s <= 1 + 1e-6 && t < best) {
403
+ best = t;
404
+ hit = [t * dx, t * dy];
405
+ }
406
+ }
407
+ return hit;
408
+ }
409
+ function shapePoints(node, n) {
410
+ const role = node.kind === "symbol" ? roleOf(node.name) : node.kind;
411
+ const poly = shapePolygon(role, nodeWidth(node));
412
+ const out = [];
413
+ for (let i = 0; i < n; i++) out.push(raycast(poly, 2 * Math.PI * i / n));
414
+ return out;
415
+ }
416
+ function pointsAttr(points) {
417
+ return points.map(([x, y]) => `${x.toFixed(2)},${y.toFixed(2)}`).join(" ");
418
+ }
419
+
420
+ // src/render.ts
421
+ var SVG_NS = "http://www.w3.org/2000/svg";
422
+ var PORT_R = 4;
423
+ var TRACE_DURATION = DEFAULT_TRACE_MS;
424
+ var SHAPE_N = 40;
425
+ var CANVAS_BG = "#1b1d23";
426
+ var CSS = `
427
+ .mg-svg { width: 100%; height: 100%; display: block; overflow: clip; background: ${CANVAS_BG}; user-select: none; font-family: ui-monospace, monospace; }
428
+ .mg-edge { stroke-width: 1.6; }
429
+ .mg-var-link { stroke: #ffa657; stroke-width: 1; stroke-dasharray: 3 4; opacity: 0.45; }
430
+ .mg-node text { font-size: 12px; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
431
+ .mg-node .box { stroke: #00000055; stroke-width: 1; cursor: grab; }
432
+ .mg-node .var { fill: none; stroke-width: 1.8; cursor: grab; }
433
+ .mg-sel { fill: none; stroke: #38bdf8; stroke-width: 1.5; }
434
+ .mg-sel.primary { stroke: #f59e0b; }
435
+ .mg-port { fill: #cbd5e1; stroke: #1b1d23; stroke-width: 1; cursor: crosshair; }
436
+ .mg-result { font-size: 11px; fill: #9ca3af; }
437
+ .mg-result.error { fill: #f87171; }
438
+ .mg-viz-hi { fill: none; stroke: #f2cc60; stroke-width: 2.5; }
439
+ .mg-viz-label { font-size: 11px; fill: #f2cc60; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
440
+ .mg-overlay { pointer-events: none; }
441
+ `;
442
+ function svgEl(tag, attrs) {
443
+ const node = document.createElementNS(SVG_NS, tag);
444
+ for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
445
+ return node;
446
+ }
447
+ function blendEdge(x1, y1, x2, y2, cFrom, cTo, opacity = 1) {
448
+ const n = 8;
449
+ const out = [];
450
+ for (let i = 0; i < n; i++) {
451
+ const t0 = i / n;
452
+ const t1 = (i + 1) / n;
453
+ out.push(
454
+ svgEl("line", {
455
+ class: "mg-edge",
456
+ x1: lerp(x1, x2, t0),
457
+ y1: lerp(y1, y2, t0),
458
+ x2: lerp(x1, x2, t1),
459
+ y2: lerp(y1, y2, t1),
460
+ stroke: lerpColor(cFrom, cTo, (i + 0.5) / n),
461
+ opacity
462
+ })
463
+ );
464
+ }
465
+ return out;
466
+ }
467
+ function nodeShape(node, w, fill) {
468
+ const hw = w / 2;
469
+ const hh = NODE_H / 2;
470
+ const poly = (points) => svgEl("polygon", { class: "box", points, fill });
471
+ const rect = (rx) => svgEl("rect", { class: "box", x: -hw, y: -hh, width: w, height: NODE_H, rx, fill });
472
+ const role = node.kind === "symbol" ? roleOf(node.name) : node.kind;
473
+ switch (role) {
474
+ case "operator":
475
+ case "control":
476
+ return poly(`0,${-hh} ${hw},0 0,${hh} ${-hw},0`);
477
+ // diamond (a flowchart decision)
478
+ case "variable":
479
+ return svgEl("ellipse", { class: "var", cx: 0, cy: 0, rx: hw, ry: hh, stroke: fill });
480
+ case "number":
481
+ case "string":
482
+ return rect(hh);
483
+ // pill
484
+ case "boolean":
485
+ return rect(3);
486
+ // a square constant box
487
+ case "list":
488
+ return poly(
489
+ `${-hw + 7},${-hh} ${hw - 7},${-hh} ${hw},0 ${hw - 7},${hh} ${-hw + 7},${hh} ${-hw},0`
490
+ );
491
+ // hexagon
492
+ default:
493
+ return rect(5);
494
+ }
495
+ }
496
+ var Renderer = class {
497
+ svg;
498
+ viewportG;
499
+ haloG;
500
+ edgesG;
501
+ mergeG;
502
+ nodesG;
503
+ // The last shown playthrough frame, so the next step can glide from it.
504
+ tracePrev = null;
505
+ traceRaf = 0;
506
+ // How long one step's morph takes, so a slower playback slows the animation itself, not just the pauses.
507
+ traceDuration = TRACE_DURATION;
508
+ // The canvas background, so the redex glow can keep enough contrast against whatever theme is set.
509
+ bg = CANVAS_BG;
510
+ constructor(container) {
511
+ this.svg = svgEl("svg", { class: "mg-svg" });
512
+ const style = document.createElementNS(SVG_NS, "style");
513
+ style.textContent = CSS;
514
+ this.svg.appendChild(style);
515
+ const goo = svgEl("filter", {
516
+ id: "mg-goo",
517
+ x: "-50%",
518
+ y: "-50%",
519
+ width: "200%",
520
+ height: "200%"
521
+ });
522
+ goo.append(
523
+ svgEl("feGaussianBlur", { in: "SourceGraphic", stdDeviation: "6", result: "b" }),
524
+ svgEl("feColorMatrix", {
525
+ in: "b",
526
+ type: "matrix",
527
+ values: "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -8",
528
+ result: "goo"
529
+ }),
530
+ // A liquid warble on top of the meld: a noise field (feTurbulence) displaces the blob's edge, so it
531
+ // ripples like a fluid and a wide label like `fact` or `True` loses its rigid box edge instead of
532
+ // staying a stiff rounded rectangle.
533
+ svgEl("feTurbulence", {
534
+ type: "fractalNoise",
535
+ baseFrequency: "0.018",
536
+ numOctaves: "2",
537
+ seed: "7",
538
+ result: "noise"
539
+ }),
540
+ svgEl("feDisplacementMap", {
541
+ in: "goo",
542
+ in2: "noise",
543
+ scale: "14",
544
+ xChannelSelector: "R",
545
+ yChannelSelector: "G"
546
+ })
547
+ );
548
+ const defs = svgEl("defs", {});
549
+ defs.appendChild(goo);
550
+ this.svg.appendChild(defs);
551
+ this.viewportG = svgEl("g", { class: "mg-viewport" });
552
+ this.haloG = svgEl("g", { class: "mg-halo" });
553
+ this.edgesG = svgEl("g", { class: "mg-edges" });
554
+ this.mergeG = svgEl("g", { class: "mg-merge", filter: "url(#mg-goo)" });
555
+ this.nodesG = svgEl("g", { class: "mg-nodes" });
556
+ this.viewportG.appendChild(this.haloG);
557
+ this.viewportG.appendChild(this.edgesG);
558
+ this.viewportG.appendChild(this.mergeG);
559
+ this.viewportG.appendChild(this.nodesG);
560
+ this.svg.appendChild(this.viewportG);
561
+ container.appendChild(this.svg);
562
+ }
563
+ /** Set the canvas background. The redex glow re-reads it, so its contrast stays right under any theme. */
564
+ setBackground(bg) {
565
+ this.bg = bg;
566
+ this.svg.style.background = bg;
567
+ }
568
+ /** Set how long a step's morph takes (ms), so playback speed slows the animation, not just the pauses. */
569
+ setTraceDuration(ms) {
570
+ this.traceDuration = Math.max(1, ms);
571
+ }
572
+ /** Redraw the whole frame. */
573
+ render(state) {
574
+ const { viewport: v } = state;
575
+ this.viewportG.setAttribute("transform", `translate(${v.panX} ${v.panY}) scale(${v.scale})`);
576
+ this.haloG.replaceChildren();
577
+ this.mergeG.replaceChildren();
578
+ this.edgesG.replaceChildren(...this.edges(state.graph), ...this.varNet(state.graph));
579
+ this.nodesG.replaceChildren(...this.nodeGroups(state));
580
+ }
581
+ /** Forget the last playthrough frame, so the next one starts without gliding from a stale step. */
582
+ clearTrace() {
583
+ if (this.traceRaf !== 0) {
584
+ cancelAnimationFrame(this.traceRaf);
585
+ this.traceRaf = 0;
586
+ }
587
+ this.tracePrev = null;
588
+ this.haloG.replaceChildren();
589
+ this.mergeG.replaceChildren();
590
+ }
591
+ /** Show one reduction step. With `animate` and a previous step, shared subterms arc from their old place
592
+ * to their new one, the parts that appear or vanish fade, and the viewport eases, the way a math
593
+ * animation morphs one expression into the next. Otherwise it paints at once. */
594
+ showTrace(state, animate) {
595
+ const next = traceFrame(state.graph, state.viewport);
596
+ const prev = this.tracePrev;
597
+ this.tracePrev = next;
598
+ if (this.traceRaf !== 0) {
599
+ cancelAnimationFrame(this.traceRaf);
600
+ this.traceRaf = 0;
601
+ }
602
+ if (!animate || prev === null || typeof requestAnimationFrame === "undefined") {
603
+ this.paintTrace(prev ?? next, next, 1);
604
+ return;
605
+ }
606
+ const start = performance.now();
607
+ const dur = this.traceDuration;
608
+ const step = (now) => {
609
+ const p = Math.min(1, (now - start) / dur);
610
+ this.paintTrace(prev, next, ease(p));
611
+ if (p < 1) {
612
+ this.traceRaf = requestAnimationFrame(step);
613
+ } else {
614
+ this.traceRaf = 0;
615
+ this.paintTrace(next, next, 1);
616
+ }
617
+ };
618
+ this.traceRaf = requestAnimationFrame(step);
619
+ }
620
+ /** Paint an interpolated frame between two steps. The interpolation itself is pure ({@link
621
+ * interpolateTrace}); this just draws the result. */
622
+ paintTrace(from, to, t) {
623
+ const frame = interpolateTrace(from, to, t);
624
+ const { panX, panY, scale } = frame.viewport;
625
+ this.viewportG.setAttribute("transform", `translate(${panX} ${panY}) scale(${scale})`);
626
+ this.haloG.replaceChildren(
627
+ ...frame.redex !== void 0 && frame.redex.op > 0.01 ? haloEls(frame.redex, this.bg) : []
628
+ );
629
+ const mergeEls = [];
630
+ for (const p of frame.nodes) {
631
+ if (!p.merging || p.op <= 0.02) continue;
632
+ mergeEls.push(
633
+ svgEl("polygon", {
634
+ points: pointsAttr(p.points),
635
+ transform: `translate(${p.x.toFixed(1)} ${p.y.toFixed(1)})`,
636
+ fill: p.fill,
637
+ opacity: p.op.toFixed(3)
638
+ })
639
+ );
640
+ }
641
+ this.mergeG.replaceChildren(...mergeEls);
642
+ const edgeEls = [];
643
+ for (const e of frame.edges) {
644
+ if (e.colorA === "")
645
+ edgeEls.push(
646
+ svgEl("line", {
647
+ class: "mg-var-link",
648
+ x1: e.x1,
649
+ y1: e.y1,
650
+ x2: e.x2,
651
+ y2: e.y2,
652
+ opacity: e.op
653
+ })
654
+ );
655
+ else edgeEls.push(...blendEdge(e.x1, e.y1, e.x2, e.y2, e.colorA, e.colorB, e.op));
656
+ }
657
+ this.edgesG.replaceChildren(...edgeEls);
658
+ const nodeEls = frame.nodes.map((p) => {
659
+ const g = svgEl("g", {
660
+ class: "mg-node",
661
+ transform: `translate(${p.x} ${p.y})`,
662
+ opacity: p.op
663
+ });
664
+ if (!p.merging)
665
+ g.appendChild(
666
+ svgEl("polygon", {
667
+ points: pointsAttr(p.points),
668
+ fill: p.fill,
669
+ "fill-opacity": p.fillOp.toFixed(3),
670
+ stroke: p.fill,
671
+ "stroke-opacity": (1 - p.fillOp * 0.7).toFixed(3),
672
+ "stroke-width": 1.6
673
+ })
674
+ );
675
+ for (const tx of p.texts) {
676
+ const text = svgEl("text", { x: 0, y: 0, fill: tx.color, opacity: tx.op });
677
+ text.textContent = tx.text;
678
+ g.appendChild(text);
679
+ }
680
+ return g;
681
+ });
682
+ this.nodesG.replaceChildren(...nodeEls);
683
+ }
684
+ /** Faint dashed links between the occurrences of each variable within a rule. */
685
+ varNet(graph) {
686
+ const out = [];
687
+ for (const [aId, bId] of variableLinks(graph)) {
688
+ const a = graph.nodes.get(aId);
689
+ const b = graph.nodes.get(bId);
690
+ if (a === void 0 || b === void 0) continue;
691
+ out.push(svgEl("line", { class: "mg-var-link", x1: a.x, y1: a.y, x2: b.x, y2: b.y }));
692
+ }
693
+ return out;
694
+ }
695
+ edges(graph) {
696
+ const out = [];
697
+ for (const parent of graph.nodes.values()) {
698
+ const kids = graph.childrenOf(parent.id);
699
+ if (kids.length === 0) continue;
700
+ const pColor = colorFor(parent).fill;
701
+ for (const childId of kids) {
702
+ const child = graph.nodes.get(childId);
703
+ if (child === void 0) continue;
704
+ out.push(...blendEdge(parent.x, parent.y, child.x, child.y, pColor, colorFor(child).fill));
705
+ }
706
+ }
707
+ return out;
708
+ }
709
+ nodeGroups(state) {
710
+ const { graph, selection, labels, primaryId, viz } = state;
711
+ const groups = [];
712
+ for (const node of graph.nodes.values()) {
713
+ const w = nodeWidth(node);
714
+ const color = colorFor(node);
715
+ const vz = viz.get(node.id);
716
+ const g = svgEl("g", {
717
+ class: "mg-node",
718
+ "data-id": node.id,
719
+ transform: vz?.sizeScale !== void 0 ? `translate(${node.x} ${node.y}) scale(${vz.sizeScale.toFixed(3)})` : `translate(${node.x} ${node.y})`
720
+ });
721
+ if (selection.has(node.id)) {
722
+ g.appendChild(
723
+ svgEl("rect", {
724
+ class: node.id === primaryId ? "mg-sel primary" : "mg-sel",
725
+ x: -w / 2 - 3,
726
+ y: -NODE_H / 2 - 3,
727
+ width: w + 6,
728
+ height: NODE_H + 6,
729
+ rx: 7
730
+ })
731
+ );
732
+ }
733
+ if (vz?.highlight === true) {
734
+ g.appendChild(
735
+ svgEl("rect", {
736
+ class: "mg-viz-hi",
737
+ x: -w / 2 - 5,
738
+ y: -NODE_H / 2 - 5,
739
+ width: w + 10,
740
+ height: NODE_H + 10,
741
+ rx: 9
742
+ })
743
+ );
744
+ }
745
+ g.appendChild(nodeShape(node, w, vz?.color ?? color.fill));
746
+ const text = svgEl("text", { x: 0, y: 0 });
747
+ text.textContent = displayText(node);
748
+ text.setAttribute("fill", color.text);
749
+ g.appendChild(text);
750
+ if (vz?.label !== void 0 && vz.label.length > 0) {
751
+ const vl = svgEl("text", { class: "mg-viz-label", x: 0, y: -NODE_H / 2 - 10 });
752
+ vl.textContent = vz.label;
753
+ g.appendChild(vl);
754
+ }
755
+ g.appendChild(
756
+ svgEl("circle", { class: "mg-port", "data-port": "1", cx: 0, cy: -NODE_H / 2, r: PORT_R })
757
+ );
758
+ const label = labels.get(node.id);
759
+ if (label !== void 0 && label.text.length > 0) {
760
+ const result = svgEl("text", {
761
+ class: label.error ? "mg-result error" : "mg-result",
762
+ x: 0,
763
+ y: NODE_H / 2 + 13
764
+ });
765
+ result.textContent = label.text;
766
+ g.appendChild(result);
767
+ }
768
+ groups.push(g);
769
+ }
770
+ return groups;
771
+ }
772
+ };
773
+ function pointsRadius(points) {
774
+ let r = 0;
775
+ for (const [x, y] of points) r = Math.max(r, Math.hypot(x, y));
776
+ return r;
777
+ }
778
+ function traceKeyMap(graph) {
779
+ const keys = /* @__PURE__ */ new Map();
780
+ const walk = (id, path) => {
781
+ keys.set(id, path);
782
+ graph.sortedChildren(id).forEach((c, i) => walk(c.id, `${path}.${i}`));
783
+ };
784
+ graph.heads().forEach((h, i) => walk(h.id, `h${i}`));
785
+ return keys;
786
+ }
787
+ function traceFrame(graph, viewport) {
788
+ const keys = traceKeyMap(graph);
789
+ const slots = [];
790
+ for (const n of graph.nodes.values()) {
791
+ const key = keys.get(n.id);
792
+ if (key === void 0) continue;
793
+ const color = colorFor(n);
794
+ const points = shapePoints(n, SHAPE_N);
795
+ slots.push({
796
+ key,
797
+ leaf: graph.childrenOf(n.id).length === 0,
798
+ x: n.x,
799
+ y: n.y,
800
+ points,
801
+ radius: pointsRadius(points),
802
+ fill: color.fill,
803
+ text: displayText(n),
804
+ textColor: color.text,
805
+ hollow: (n.kind === "symbol" ? roleOf(n.name) : n.kind) === "variable"
806
+ });
807
+ }
808
+ const edges = [];
809
+ for (const parent of graph.nodes.values()) {
810
+ const pk = keys.get(parent.id);
811
+ if (pk === void 0) continue;
812
+ const pColor = colorFor(parent).fill;
813
+ for (const cid of graph.childrenOf(parent.id)) {
814
+ const child = graph.nodes.get(cid);
815
+ const ck = keys.get(cid);
816
+ if (child !== void 0 && ck !== void 0)
817
+ edges.push({
818
+ key: `${pk}>${ck}`,
819
+ a: pk,
820
+ b: ck,
821
+ colorA: pColor,
822
+ colorB: colorFor(child).fill
823
+ });
824
+ }
825
+ }
826
+ for (const [aId, bId] of variableLinks(graph)) {
827
+ const ak = keys.get(aId);
828
+ const bk = keys.get(bId);
829
+ if (ak !== void 0 && bk !== void 0)
830
+ edges.push({ key: `v:${ak}:${bk}`, a: ak, b: bk, colorA: "", colorB: "" });
831
+ }
832
+ return { slots, edges, viewport };
833
+ }
834
+ function interpolateTrace(from, to, t) {
835
+ const fromByKey = new Map(from.slots.map((s) => [s.key, s]));
836
+ const toNodes = new Map(to.slots.map((s) => [s.key, s]));
837
+ const used = /* @__PURE__ */ new Set();
838
+ const matchOf = /* @__PURE__ */ new Map();
839
+ for (const s of to.slots) {
840
+ const f2 = fromByKey.get(s.key);
841
+ if (f2 !== void 0 && !(f2.leaf && !s.leaf)) {
842
+ matchOf.set(s.key, f2);
843
+ used.add(f2);
844
+ }
845
+ }
846
+ const byTo = /* @__PURE__ */ new Map();
847
+ const byFrom = /* @__PURE__ */ new Map();
848
+ const rendered = [];
849
+ for (const s of to.slots) {
850
+ const f2 = matchOf.get(s.key);
851
+ const p = f2 !== void 0 ? morphPlacement(f2, s, t) : singlePlacement(s, t);
852
+ byTo.set(s.key, p);
853
+ if (f2 !== void 0) byFrom.set(f2.key, p);
854
+ rendered.push(p);
855
+ }
856
+ for (const s of from.slots) {
857
+ if (used.has(s)) continue;
858
+ const target2 = toNodes.get(s.key) ?? ancestorTarget(s.key, toNodes);
859
+ const p = target2 !== void 0 ? collapsePlacement(s, target2, t) : singlePlacement(s, 1 - t);
860
+ byFrom.set(s.key, p);
861
+ rendered.push(p);
862
+ }
863
+ const edges = [];
864
+ const pushEdge = (e, a, b, pres = 1) => {
865
+ if (a === void 0 || b === void 0) return;
866
+ const op = Math.min(a.op, b.op, pres);
867
+ if (op > 0.02)
868
+ edges.push({ colorA: e.colorA, colorB: e.colorB, x1: a.x, y1: a.y, x2: b.x, y2: b.y, op });
869
+ };
870
+ const toEdgeKeys = new Set(to.edges.map((e) => e.key));
871
+ const fromEdgeKeys = new Set(from.edges.map((e) => e.key));
872
+ for (const e of to.edges)
873
+ pushEdge(e, byTo.get(e.a), byTo.get(e.b), fromEdgeKeys.has(e.key) ? 1 : t);
874
+ for (const e of from.edges)
875
+ if (!toEdgeKeys.has(e.key)) pushEdge(e, byFrom.get(e.a), byFrom.get(e.b), 1 - t);
876
+ const viewport = {
877
+ panX: lerp(from.viewport.panX, to.viewport.panX, t),
878
+ panY: lerp(from.viewport.panY, to.viewport.panY, t),
879
+ scale: lerp(from.viewport.scale, to.viewport.scale, t)
880
+ };
881
+ return {
882
+ nodes: rendered.filter((p) => p.op > 0.02),
883
+ edges,
884
+ viewport,
885
+ redex: findRedex(from, used, toNodes, t)
886
+ };
887
+ }
888
+ function findRedex(from, used, toNodes, t) {
889
+ const op = Math.max(0, 1 - t * 1.7);
890
+ if (op <= 0.01) return void 0;
891
+ let root;
892
+ let depth = Infinity;
893
+ for (const f2 of from.slots) {
894
+ const toAt = toNodes.get(f2.key);
895
+ const changed = !used.has(f2) || toAt !== void 0 && toAt.text !== f2.text;
896
+ const d = f2.key.split(".").length;
897
+ if (changed && d < depth) {
898
+ depth = d;
899
+ root = f2;
900
+ }
901
+ }
902
+ if (root === void 0) return void 0;
903
+ const prefix = root.key + ".";
904
+ let minX = Infinity;
905
+ let minY = Infinity;
906
+ let maxX = -Infinity;
907
+ let maxY = -Infinity;
908
+ for (const f2 of from.slots)
909
+ if (f2.key === root.key || f2.key.startsWith(prefix)) {
910
+ const ext = f2.radius;
911
+ minX = Math.min(minX, f2.x - ext);
912
+ maxX = Math.max(maxX, f2.x + ext);
913
+ minY = Math.min(minY, f2.y - ext);
914
+ maxY = Math.max(maxY, f2.y + ext);
915
+ }
916
+ return {
917
+ x: (minX + maxX) / 2,
918
+ y: (minY + maxY) / 2,
919
+ rx: (maxX - minX) / 2 + 8,
920
+ ry: (maxY - minY) / 2 + 8,
921
+ op,
922
+ color: root.fill
923
+ // the raw role color; the glow tints it against the live background at paint time
924
+ };
925
+ }
926
+ function haloEls(redex, bg) {
927
+ const color = haloTint(redex.color, bg);
928
+ const grad = svgEl("radialGradient", { id: "mg-halo" });
929
+ const stop = (offset, op) => svgEl("stop", { offset, "stop-color": color, "stop-opacity": op });
930
+ grad.append(stop("0%", "0.5"), stop("55%", "0.2"), stop("100%", "0"));
931
+ return [
932
+ grad,
933
+ svgEl("ellipse", {
934
+ cx: redex.x.toFixed(2),
935
+ cy: redex.y.toFixed(2),
936
+ rx: redex.rx.toFixed(2),
937
+ ry: redex.ry.toFixed(2),
938
+ fill: "url(#mg-halo)",
939
+ opacity: redex.op.toFixed(3)
940
+ })
941
+ ];
942
+ }
943
+ function luminance(hex) {
944
+ const m = /^#([0-9a-fA-F]{6})$/.exec(hex);
945
+ if (m === null) return null;
946
+ const v = parseInt(m[1], 16);
947
+ return 0.299 * (v >> 16 & 255) + 0.587 * (v >> 8 & 255) + 0.114 * (v & 255);
948
+ }
949
+ function haloTint(hex, bg) {
950
+ const lc = luminance(hex);
951
+ const lb = luminance(bg);
952
+ if (lc === null || lb === null || Math.abs(lc - lb) >= 80) return hex;
953
+ return lerpColor(hex, lb < 128 ? "#ffffff" : "#000000", 0.55);
954
+ }
955
+ function morphPlacement(f2, s, t) {
956
+ const p = arcPoint(f2.x, f2.y, s.x, s.y, t);
957
+ const points = s.points.map((pt, i) => {
958
+ const q = f2.points[i] ?? pt;
959
+ return [lerp(q[0], pt[0], t), lerp(q[1], pt[1], t)];
960
+ });
961
+ const texts = f2.text === s.text ? [{ text: s.text, color: lerpColor(f2.textColor, s.textColor, t), op: 1 }] : [
962
+ { text: f2.text, color: f2.textColor, op: 1 - t },
963
+ { text: s.text, color: s.textColor, op: t }
964
+ ];
965
+ const fillOp = lerp(f2.hollow ? 0 : 1, s.hollow ? 0 : 1, t);
966
+ return { x: p.x, y: p.y, op: 1, points, fill: lerpColor(f2.fill, s.fill, t), fillOp, texts };
967
+ }
968
+ function singlePlacement(s, op) {
969
+ return {
970
+ x: s.x,
971
+ y: s.y,
972
+ op,
973
+ points: s.points,
974
+ fill: s.fill,
975
+ fillOp: s.hollow ? 0 : 1,
976
+ texts: [{ text: s.text, color: s.textColor, op: 1 }]
977
+ };
978
+ }
979
+ function ancestorTarget(key, toNodes) {
980
+ let k = key;
981
+ for (; ; ) {
982
+ const dot = k.lastIndexOf(".");
983
+ if (dot < 0) return void 0;
984
+ k = k.slice(0, dot);
985
+ const s = toNodes.get(k);
986
+ if (s !== void 0) return s;
987
+ }
988
+ }
989
+ function collapsePlacement(s, target2, t) {
990
+ const startDist = Math.hypot(s.x - target2.x, s.y - target2.y);
991
+ const sR = s.radius;
992
+ const tR = target2.radius;
993
+ const text = [{ text: s.text, color: s.textColor, op: 1 }];
994
+ if (startDist < sR + tR)
995
+ return {
996
+ x: s.x,
997
+ y: s.y,
998
+ op: 1 - t,
999
+ points: s.points,
1000
+ fill: s.fill,
1001
+ fillOp: s.hollow ? 0 : 1,
1002
+ texts: text
1003
+ };
1004
+ const x = lerp(s.x, target2.x, t);
1005
+ const y = lerp(s.y, target2.y, t);
1006
+ const m = Math.max(0, Math.min(1, 1 - (1 - t) * startDist / (sR + tR)));
1007
+ const points = m <= 0 ? s.points : target2.points.map((pt, i) => {
1008
+ const q = s.points[i] ?? pt;
1009
+ return [lerp(q[0], pt[0], m), lerp(q[1], pt[1], m)];
1010
+ });
1011
+ const op = m < 0.7 ? 1 : (1 - m) / 0.3;
1012
+ return {
1013
+ x,
1014
+ y,
1015
+ op,
1016
+ points,
1017
+ fill: lerpColor(s.fill, target2.fill, m),
1018
+ fillOp: s.hollow ? 0 : 1,
1019
+ texts: text,
1020
+ merging: m > 1e-3
1021
+ // in the gooey layer only once it has reached the result and begun to meld
1022
+ };
1023
+ }
1024
+
1025
+ // src/viewport.ts
1026
+ var MIN_SCALE = 0.1;
1027
+ var MAX_SCALE = 4;
1028
+ var clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
1029
+ function initialViewport() {
1030
+ return { panX: 0, panY: 0, scale: 1 };
1031
+ }
1032
+ function toWorld(v, sx, sy) {
1033
+ return { x: (sx - v.panX) / v.scale, y: (sy - v.panY) / v.scale };
1034
+ }
1035
+ function toScreen(v, wx, wy) {
1036
+ return { x: wx * v.scale + v.panX, y: wy * v.scale + v.panY };
1037
+ }
1038
+ function pan(v, dx, dy) {
1039
+ return { scale: v.scale, panX: v.panX + dx, panY: v.panY + dy };
1040
+ }
1041
+ function zoomAt(v, sx, sy, factor) {
1042
+ const world = toWorld(v, sx, sy);
1043
+ const scale = clamp(v.scale * factor, MIN_SCALE, MAX_SCALE);
1044
+ return { scale, panX: sx - world.x * scale, panY: sy - world.y * scale };
1045
+ }
1046
+
1047
+ // src/controller.ts
1048
+ var ZOOM_STEP = 1.0015;
1049
+ var Controller = class {
1050
+ constructor(host) {
1051
+ this.host = host;
1052
+ this.overlay = svgEl("g", { class: "mg-overlay" });
1053
+ host.svg.appendChild(this.overlay);
1054
+ host.container.tabIndex = 0;
1055
+ host.svg.addEventListener("pointerdown", this.onDown);
1056
+ host.svg.addEventListener("pointermove", this.onMove);
1057
+ host.svg.addEventListener("pointerup", this.onUp);
1058
+ host.svg.addEventListener("wheel", this.onWheel, { passive: false });
1059
+ host.svg.addEventListener("dblclick", this.onDblClick);
1060
+ host.svg.addEventListener("contextmenu", this.onContext);
1061
+ host.container.addEventListener("keydown", this.onKeyDown);
1062
+ host.container.addEventListener("keyup", this.onKeyUp);
1063
+ }
1064
+ host;
1065
+ mode = "idle";
1066
+ lastScreen = { x: 0, y: 0 };
1067
+ connectFrom = null;
1068
+ rubberStart = { x: 0, y: 0 };
1069
+ spaceHeld = false;
1070
+ clip = null;
1071
+ overlay;
1072
+ input = null;
1073
+ menu = null;
1074
+ createAt = { x: 0, y: 0 };
1075
+ highlight = -1;
1076
+ onDown = (e) => this.pointerDown(e);
1077
+ onMove = (e) => this.pointerMove(e);
1078
+ onUp = (e) => this.pointerUp(e);
1079
+ onWheel = (e) => this.wheel(e);
1080
+ onDblClick = (e) => this.dblClick(e);
1081
+ onKeyDown = (e) => this.keyDown(e);
1082
+ onKeyUp = (e) => this.keyUp(e);
1083
+ onContext = (e) => e.preventDefault();
1084
+ /** Detach every listener and remove transient UI. */
1085
+ destroy() {
1086
+ this.host.svg.removeEventListener("pointerdown", this.onDown);
1087
+ this.host.svg.removeEventListener("pointermove", this.onMove);
1088
+ this.host.svg.removeEventListener("pointerup", this.onUp);
1089
+ this.host.svg.removeEventListener("wheel", this.onWheel);
1090
+ this.host.svg.removeEventListener("dblclick", this.onDblClick);
1091
+ this.host.svg.removeEventListener("contextmenu", this.onContext);
1092
+ this.host.container.removeEventListener("keydown", this.onKeyDown);
1093
+ this.host.container.removeEventListener("keyup", this.onKeyUp);
1094
+ this.closeInput();
1095
+ this.overlay.remove();
1096
+ }
1097
+ // ---- coordinate helpers ---------------------------------------------------------------------
1098
+ screenOf(e) {
1099
+ const r = this.host.svg.getBoundingClientRect();
1100
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
1101
+ }
1102
+ worldOf(e) {
1103
+ const s = this.screenOf(e);
1104
+ return toWorld(this.host.viewport, s.x, s.y);
1105
+ }
1106
+ nodeIdAt(target2) {
1107
+ const el2 = target2 instanceof Element ? target2.closest("[data-id]") : null;
1108
+ return el2?.getAttribute("data-id") ?? null;
1109
+ }
1110
+ // ---- pointer --------------------------------------------------------------------------------
1111
+ pointerDown(e) {
1112
+ this.host.container.focus({ preventScroll: true });
1113
+ this.host.svg.setPointerCapture(e.pointerId);
1114
+ this.lastScreen = this.screenOf(e);
1115
+ if (this.host.isTracing()) {
1116
+ this.mode = "pan";
1117
+ return;
1118
+ }
1119
+ const onPort = target(e) && target(e).getAttribute("data-port") === "1";
1120
+ const nodeId = this.nodeIdAt(e.target);
1121
+ if (e.button === 2 || this.spaceHeld) {
1122
+ this.mode = "pan";
1123
+ return;
1124
+ }
1125
+ if (onPort && nodeId !== null) {
1126
+ this.mode = "connect";
1127
+ this.connectFrom = nodeId;
1128
+ return;
1129
+ }
1130
+ if (nodeId !== null) {
1131
+ if (e.shiftKey) this.toggle(nodeId);
1132
+ else if (!this.host.selection.has(nodeId)) this.selectOnly(nodeId);
1133
+ this.host.primaryId = nodeId;
1134
+ this.mode = "move";
1135
+ this.host.render();
1136
+ return;
1137
+ }
1138
+ if (!e.shiftKey) this.clearSelection();
1139
+ if (this.host.panOnLeftDrag && !e.shiftKey) {
1140
+ this.mode = "pan";
1141
+ this.host.render();
1142
+ return;
1143
+ }
1144
+ this.mode = "rubber";
1145
+ this.rubberStart = this.worldOf(e);
1146
+ this.host.render();
1147
+ }
1148
+ pointerMove(e) {
1149
+ const screen = this.screenOf(e);
1150
+ const dx = screen.x - this.lastScreen.x;
1151
+ const dy = screen.y - this.lastScreen.y;
1152
+ if (this.mode === "pan") {
1153
+ this.host.viewport = pan(this.host.viewport, dx, dy);
1154
+ this.host.render();
1155
+ } else if (this.mode === "move") {
1156
+ const k = this.host.viewport.scale;
1157
+ for (const id of this.host.selection) {
1158
+ const n = this.host.graph.nodes.get(id);
1159
+ if (n) this.host.graph.move(id, n.x + dx / k, n.y + dy / k);
1160
+ }
1161
+ this.host.render();
1162
+ } else if (this.mode === "connect") {
1163
+ const over = this.nodeIdAt(document.elementFromPoint(e.clientX, e.clientY));
1164
+ this.drawConnectLine(this.worldOf(e), over);
1165
+ } else if (this.mode === "rubber") {
1166
+ this.drawRubber(this.worldOf(e));
1167
+ }
1168
+ this.lastScreen = screen;
1169
+ }
1170
+ pointerUp(e) {
1171
+ if (this.mode === "connect" && this.connectFrom !== null) {
1172
+ const el2 = document.elementFromPoint(e.clientX, e.clientY);
1173
+ const dropped = this.nodeIdAt(el2);
1174
+ if (dropped !== null) {
1175
+ if (this.host.graph.connect(dropped, this.connectFrom)) this.host.changed();
1176
+ } else if (el2 !== null && this.host.svg.contains(el2)) {
1177
+ const parents = this.host.graph.parentsOf(this.connectFrom);
1178
+ for (const p of parents) this.host.graph.disconnect(p, this.connectFrom);
1179
+ if (parents.length > 0) this.host.changed();
1180
+ }
1181
+ } else if (this.mode === "rubber") {
1182
+ this.selectInRubber(this.worldOf(e), e.shiftKey);
1183
+ } else if (this.mode === "move") {
1184
+ this.host.changed();
1185
+ }
1186
+ this.connectFrom = null;
1187
+ this.mode = "idle";
1188
+ this.overlay.replaceChildren();
1189
+ this.host.render();
1190
+ }
1191
+ wheel(e) {
1192
+ if (!e.ctrlKey && !e.metaKey) return;
1193
+ e.preventDefault();
1194
+ const s = this.screenOf(e);
1195
+ this.host.viewport = zoomAt(this.host.viewport, s.x, s.y, ZOOM_STEP ** -e.deltaY);
1196
+ this.host.render();
1197
+ }
1198
+ dblClick(e) {
1199
+ if (this.host.isTracing()) return;
1200
+ const nodeId = this.nodeIdAt(e.target);
1201
+ if (nodeId !== null) this.host.evaluate(nodeId);
1202
+ else this.openInput(e);
1203
+ }
1204
+ // ---- selection ------------------------------------------------------------------------------
1205
+ selectOnly(id) {
1206
+ this.host.selection.clear();
1207
+ this.host.selection.add(id);
1208
+ }
1209
+ toggle(id) {
1210
+ if (this.host.selection.has(id)) this.host.selection.delete(id);
1211
+ else this.host.selection.add(id);
1212
+ }
1213
+ clearSelection() {
1214
+ this.host.selection.clear();
1215
+ this.host.primaryId = null;
1216
+ }
1217
+ selectInRubber(end, additive) {
1218
+ const x1 = Math.min(this.rubberStart.x, end.x);
1219
+ const x2 = Math.max(this.rubberStart.x, end.x);
1220
+ const y1 = Math.min(this.rubberStart.y, end.y);
1221
+ const y2 = Math.max(this.rubberStart.y, end.y);
1222
+ if (!additive) this.host.selection.clear();
1223
+ for (const n of this.host.graph.nodes.values())
1224
+ if (n.x >= x1 && n.x <= x2 && n.y >= y1 && n.y <= y2) this.host.selection.add(n.id);
1225
+ }
1226
+ // ---- overlays -------------------------------------------------------------------------------
1227
+ drawRubber(end) {
1228
+ const a = this.toScreen(this.rubberStart);
1229
+ const b = this.toScreen(end);
1230
+ const rect = svgEl("rect", {
1231
+ fill: "#38bdf822",
1232
+ stroke: "#38bdf8",
1233
+ "stroke-dasharray": "4 3",
1234
+ x: Math.min(a.x, b.x),
1235
+ y: Math.min(a.y, b.y),
1236
+ width: Math.abs(a.x - b.x),
1237
+ height: Math.abs(a.y - b.y)
1238
+ });
1239
+ this.overlay.replaceChildren(rect);
1240
+ }
1241
+ drawConnectLine(end, over) {
1242
+ if (this.connectFrom === null) return;
1243
+ const from = this.host.graph.nodes.get(this.connectFrom);
1244
+ if (from === void 0) return;
1245
+ const a = this.toScreen({ x: from.x, y: from.y - NODE_H / 2 });
1246
+ const b = this.toScreen(end);
1247
+ const color = over === null || over === this.connectFrom ? "#cbd5e1" : this.host.graph.canConnect(over, this.connectFrom) ? "#3fb950" : "#f85149";
1248
+ this.overlay.replaceChildren(
1249
+ svgEl("line", { stroke: color, "stroke-width": 2, x1: a.x, y1: a.y, x2: b.x, y2: b.y })
1250
+ );
1251
+ }
1252
+ toScreen(p) {
1253
+ const v = this.host.viewport;
1254
+ return { x: p.x * v.scale + v.panX, y: p.y * v.scale + v.panY };
1255
+ }
1256
+ // ---- keyboard -------------------------------------------------------------------------------
1257
+ keyDown(e) {
1258
+ if (this.input !== null) return;
1259
+ if (e.key === " ") {
1260
+ this.spaceHeld = true;
1261
+ e.preventDefault();
1262
+ return;
1263
+ }
1264
+ if (this.host.isTracing()) return;
1265
+ if (e.key === "Delete" || e.key === "Backspace") {
1266
+ for (const id of [...this.host.selection]) this.host.graph.remove(id);
1267
+ this.clearSelection();
1268
+ this.host.changed();
1269
+ this.host.render();
1270
+ e.preventDefault();
1271
+ return;
1272
+ }
1273
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "c") {
1274
+ this.copy();
1275
+ e.preventDefault();
1276
+ } else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "v") {
1277
+ this.paste();
1278
+ e.preventDefault();
1279
+ } else if (e.key === "Tab" && this.host.primaryId !== null) {
1280
+ this.host.evaluate(this.host.primaryId);
1281
+ e.preventDefault();
1282
+ }
1283
+ }
1284
+ keyUp(e) {
1285
+ if (e.key === " ") this.spaceHeld = false;
1286
+ }
1287
+ // ---- copy and paste -------------------------------------------------------------------------
1288
+ copy() {
1289
+ const ids = new Set(this.host.selection);
1290
+ if (ids.size === 0) return;
1291
+ const nodes = [...ids].map((id) => ({ ...this.host.graph.nodes.get(id) })).filter(Boolean);
1292
+ const edges = [];
1293
+ for (const id of ids)
1294
+ for (const c of this.host.graph.childrenOf(id)) if (ids.has(c)) edges.push([id, c]);
1295
+ this.clip = { nodes, edges };
1296
+ }
1297
+ paste() {
1298
+ if (this.clip === null || this.clip.nodes.length === 0) return;
1299
+ const remap = /* @__PURE__ */ new Map();
1300
+ this.host.selection.clear();
1301
+ for (const n of this.clip.nodes) {
1302
+ const copy = this.host.graph.add({ name: n.name, kind: n.kind, x: n.x + 30, y: n.y + 30 });
1303
+ remap.set(n.id, copy.id);
1304
+ this.host.selection.add(copy.id);
1305
+ }
1306
+ for (const [p, c] of this.clip.edges) {
1307
+ const np = remap.get(p);
1308
+ const nc = remap.get(c);
1309
+ if (np !== void 0 && nc !== void 0) this.host.graph.connect(np, nc);
1310
+ }
1311
+ this.host.changed();
1312
+ this.host.render();
1313
+ }
1314
+ // ---- node creation --------------------------------------------------------------------------
1315
+ openInput(e) {
1316
+ this.closeInput();
1317
+ this.createAt = this.worldOf(e);
1318
+ const s = this.screenOf(e);
1319
+ const input = document.createElement("input");
1320
+ input.className = "mg-input";
1321
+ input.setAttribute(
1322
+ "style",
1323
+ `position:absolute;left:${s.x}px;top:${s.y}px;transform:translate(-50%,-50%);min-width:80px;padding:4px 6px;border:2px solid #38bdf8;border-radius:6px;background:#0f1116;color:#fff;font-family:ui-monospace,monospace;font-size:14px;z-index:10;`
1324
+ );
1325
+ input.placeholder = "name, empty = ( )";
1326
+ input.addEventListener("input", () => this.updateMenu());
1327
+ input.addEventListener("keydown", (ev) => this.inputKey(ev));
1328
+ this.host.container.appendChild(input);
1329
+ this.input = input;
1330
+ input.focus({ preventScroll: true });
1331
+ this.updateMenu();
1332
+ }
1333
+ inputKey(e) {
1334
+ e.stopPropagation();
1335
+ const list = this.host.completions(this.input?.value ?? "");
1336
+ if (e.key === "Escape") {
1337
+ this.closeInput();
1338
+ e.preventDefault();
1339
+ } else if (e.key === "ArrowDown") {
1340
+ this.highlight = Math.min(this.highlight + 1, list.length - 1);
1341
+ this.renderMenu(list);
1342
+ e.preventDefault();
1343
+ } else if (e.key === "ArrowUp") {
1344
+ this.highlight = Math.max(this.highlight - 1, -1);
1345
+ this.renderMenu(list);
1346
+ e.preventDefault();
1347
+ } else if (e.key === "Enter") {
1348
+ const chosen = this.highlight >= 0 ? list[this.highlight] : void 0;
1349
+ this.commit(chosen ?? this.input?.value ?? "");
1350
+ e.preventDefault();
1351
+ }
1352
+ }
1353
+ commit(raw) {
1354
+ const value = raw.trim();
1355
+ const kind = value === "" ? "list" : value === "." ? "dot" : "symbol";
1356
+ const name = kind === "symbol" ? value : "";
1357
+ const node = this.host.graph.add({ name, kind, x: this.createAt.x, y: this.createAt.y });
1358
+ this.selectOnly(node.id);
1359
+ this.host.primaryId = node.id;
1360
+ this.closeInput();
1361
+ this.host.changed();
1362
+ this.host.render();
1363
+ }
1364
+ updateMenu() {
1365
+ this.highlight = -1;
1366
+ this.renderMenu(this.host.completions(this.input?.value ?? ""));
1367
+ }
1368
+ renderMenu(list) {
1369
+ if (this.input === null) return;
1370
+ this.menu?.remove();
1371
+ if (list.length === 0) {
1372
+ this.menu = null;
1373
+ return;
1374
+ }
1375
+ const menu = document.createElement("div");
1376
+ menu.setAttribute(
1377
+ "style",
1378
+ `position:absolute;left:${this.input.style.left};top:calc(${this.input.style.top} + 20px);background:#0f1116;border:1px solid #333;border-radius:6px;z-index:10;font-family:ui-monospace,monospace;font-size:13px;`
1379
+ );
1380
+ list.forEach((name, i) => {
1381
+ const item = document.createElement("div");
1382
+ item.textContent = name;
1383
+ item.setAttribute(
1384
+ "style",
1385
+ `padding:3px 8px;cursor:pointer;color:${i === this.highlight ? "#38bdf8" : "#cbd5e1"};`
1386
+ );
1387
+ item.addEventListener("mousedown", (ev) => {
1388
+ ev.preventDefault();
1389
+ this.commit(name);
1390
+ });
1391
+ menu.appendChild(item);
1392
+ });
1393
+ this.host.container.appendChild(menu);
1394
+ this.menu = menu;
1395
+ }
1396
+ closeInput() {
1397
+ this.input?.remove();
1398
+ this.menu?.remove();
1399
+ this.input = null;
1400
+ this.menu = null;
1401
+ this.highlight = -1;
1402
+ }
1403
+ };
1404
+ function target(e) {
1405
+ return e.target instanceof Element ? e.target : null;
1406
+ }
1407
+
1408
+ // src/atom.ts
1409
+ import {
1410
+ SymbolAtom,
1411
+ VariableAtom,
1412
+ ExpressionAtom,
1413
+ GroundedAtom,
1414
+ S,
1415
+ E
1416
+ } from "@mettascript/hyperon";
1417
+
1418
+ // src/parse.ts
1419
+ import { SExprParser, standardTokenizer } from "@mettascript/hyperon";
1420
+ var sharedTokenizer;
1421
+ var tokenizer = () => sharedTokenizer ??= standardTokenizer();
1422
+ function parseProgram(src) {
1423
+ return new SExprParser(src).parseAll(tokenizer());
1424
+ }
1425
+ function parseLeaf(token) {
1426
+ const atoms = parseProgram(token);
1427
+ return atoms.length === 1 ? atoms[0] : void 0;
1428
+ }
1429
+
1430
+ // src/layout.ts
1431
+ var ROW = NODE_H + 26;
1432
+ var GAP = 14;
1433
+ var HEAD_GAP = 3 * NODE_H;
1434
+ function layout(graph, opts) {
1435
+ const originX = opts?.originX ?? 0;
1436
+ const originY = opts?.originY ?? 0;
1437
+ const scaleOf = opts?.scaleOf ?? (() => 1);
1438
+ const visited = /* @__PURE__ */ new Set();
1439
+ let cursorX = originX;
1440
+ const place2 = (id, depth) => {
1441
+ const node = graph.nodes.get(id);
1442
+ if (node === void 0) return cursorX;
1443
+ if (visited.has(id)) return node.x;
1444
+ visited.add(id);
1445
+ node.y = originY + depth * ROW;
1446
+ const kids = graph.childrenOf(id);
1447
+ if (kids.length === 0) {
1448
+ const w = nodeWidth(node) * scaleOf(node);
1449
+ node.x = cursorX + w / 2;
1450
+ cursorX += w + GAP;
1451
+ return node.x;
1452
+ }
1453
+ const xs = kids.map((k) => place2(k, depth + 1));
1454
+ node.x = (Math.min(...xs) + Math.max(...xs)) / 2;
1455
+ return node.x;
1456
+ };
1457
+ for (const root of graph.heads()) {
1458
+ place2(root.id, 0);
1459
+ cursorX += HEAD_GAP;
1460
+ }
1461
+ }
1462
+
1463
+ // src/atom.ts
1464
+ function compose(graph, node) {
1465
+ const kids = graph.sortedChildren(node.id).map((c) => compose(graph, c)).filter((a) => a !== null);
1466
+ switch (node.kind) {
1467
+ case "list":
1468
+ return E(...kids);
1469
+ case "dot":
1470
+ return kids.length === 0 ? null : kids.length === 1 ? kids[0] : E(...kids);
1471
+ case "symbol": {
1472
+ const leaf = parseLeaf(node.name) ?? S(node.name);
1473
+ return kids.length > 0 ? E(leaf, ...kids) : leaf;
1474
+ }
1475
+ }
1476
+ }
1477
+ function composeAtom(graph, nodeId) {
1478
+ const node = graph.nodes.get(nodeId);
1479
+ return node ? compose(graph, node) : null;
1480
+ }
1481
+ function graphToAtoms(graph) {
1482
+ return graph.heads().map((h) => compose(graph, h)).filter((a) => a !== null);
1483
+ }
1484
+ function atomToGraph(atoms, graph = new Graph()) {
1485
+ for (const atom of atoms) buildNode(graph, atom);
1486
+ layout(graph);
1487
+ return graph;
1488
+ }
1489
+ function isAtomic(atom) {
1490
+ return atom instanceof SymbolAtom || atom instanceof VariableAtom || atom instanceof GroundedAtom;
1491
+ }
1492
+ function leafName(atom) {
1493
+ return atom.toString();
1494
+ }
1495
+ function buildNode(graph, atom) {
1496
+ if (atom instanceof ExpressionAtom) {
1497
+ const items = atom.children();
1498
+ const head = items[0];
1499
+ if (items.length >= 2 && head !== void 0 && isAtomic(head)) {
1500
+ const node2 = graph.add({ name: leafName(head), kind: "symbol" });
1501
+ for (const child of items.slice(1)) graph.connect(node2.id, buildNode(graph, child));
1502
+ return node2.id;
1503
+ }
1504
+ const node = graph.add({ name: "", kind: "list" });
1505
+ for (const child of items) graph.connect(node.id, buildNode(graph, child));
1506
+ return node.id;
1507
+ }
1508
+ return graph.add({ name: leafName(atom), kind: "symbol" }).id;
1509
+ }
1510
+
1511
+ // src/serialize.ts
1512
+ function toJson(graph) {
1513
+ const nodes = [...graph.nodes.values()].map((n) => ({ ...n }));
1514
+ const edges = [];
1515
+ for (const n of graph.nodes.values())
1516
+ for (const c of graph.childrenOf(n.id)) edges.push([n.id, c]);
1517
+ return { nodes, edges };
1518
+ }
1519
+ function fromJson(json) {
1520
+ const graph = new Graph();
1521
+ for (const n of json.nodes) graph.add({ ...n });
1522
+ for (const [parent, child] of json.edges) graph.connect(parent, child);
1523
+ return graph;
1524
+ }
1525
+ function toSource(graph) {
1526
+ return graphToAtoms(graph).map((a) => a.toString()).join("\n");
1527
+ }
1528
+ function fromSource(src) {
1529
+ return atomToGraph(parseProgram(src));
1530
+ }
1531
+
1532
+ // src/evaluate.ts
1533
+ import { MeTTa, atomIsError } from "@mettascript/hyperon";
1534
+ function loadProgram(graph, metta = new MeTTa()) {
1535
+ const space = metta.space();
1536
+ for (const atom of graphToAtoms(graph)) space.addAtom(atom);
1537
+ return metta;
1538
+ }
1539
+ var MAX_LABEL = 80;
1540
+ function nothing() {
1541
+ return { atoms: [], label: "", error: false };
1542
+ }
1543
+ function summarize(results) {
1544
+ const text = results.map(String).join(", ");
1545
+ const label = text.length > MAX_LABEL ? `${text.slice(0, MAX_LABEL - 1)}\u2026` : text;
1546
+ return { atoms: results, label, error: results.some(atomIsError) };
1547
+ }
1548
+ function evaluateHead(graph, headId, metta) {
1549
+ const atom = composeAtom(graph, headId);
1550
+ if (atom === null) return nothing();
1551
+ try {
1552
+ return summarize(metta.evaluateAtom(atom));
1553
+ } catch (e) {
1554
+ return { atoms: [], label: String(e), error: true };
1555
+ }
1556
+ }
1557
+ async function evaluateHeadAsync(graph, headId, metta) {
1558
+ const atom = composeAtom(graph, headId);
1559
+ if (atom === null) return nothing();
1560
+ try {
1561
+ return summarize(await metta.evaluateAtomAsync(atom));
1562
+ } catch (e) {
1563
+ return { atoms: [], label: String(e), error: true };
1564
+ }
1565
+ }
1566
+
1567
+ // src/completions.ts
1568
+ import { SymbolAtom as SymbolAtom2 } from "@mettascript/hyperon";
1569
+ var STDLIB = [
1570
+ "if",
1571
+ "case",
1572
+ "let",
1573
+ "let*",
1574
+ "match",
1575
+ "superpose",
1576
+ "collapse",
1577
+ "empty",
1578
+ "unify",
1579
+ "quote",
1580
+ "sealed",
1581
+ "car-atom",
1582
+ "cdr-atom",
1583
+ "cons-atom",
1584
+ "decons-atom",
1585
+ "get-type",
1586
+ "get-metatype",
1587
+ "assertEqual",
1588
+ "assertAlphaEqual",
1589
+ "add-atom",
1590
+ "remove-atom",
1591
+ "bind!",
1592
+ "new-space",
1593
+ "&self",
1594
+ "+",
1595
+ "-",
1596
+ "*",
1597
+ "/",
1598
+ "%",
1599
+ "==",
1600
+ "!=",
1601
+ "<",
1602
+ ">",
1603
+ "<=",
1604
+ ">=",
1605
+ "and",
1606
+ "or",
1607
+ "not"
1608
+ ];
1609
+ function spaceSymbols(metta) {
1610
+ const names = /* @__PURE__ */ new Set();
1611
+ for (const atom of metta.space().getAtoms())
1612
+ for (const descendant of atom.iterate())
1613
+ if (descendant instanceof SymbolAtom2) names.add(descendant.name());
1614
+ return names;
1615
+ }
1616
+ function isSubsequence(q, name) {
1617
+ let i = 0;
1618
+ for (const ch of name) if (i < q.length && ch === q[i]) i++;
1619
+ return i === q.length;
1620
+ }
1621
+ function matchScore(name, q) {
1622
+ if (name === q) return 100;
1623
+ if (name.startsWith(q)) return 80;
1624
+ if (name.includes(q)) return 50;
1625
+ return isSubsequence(q, name) ? 20 : 0;
1626
+ }
1627
+ function completionsFor(prefix, metta, limit = 9) {
1628
+ const q = prefix.toLowerCase();
1629
+ if (q === "") return [];
1630
+ const pool = new Set(STDLIB);
1631
+ for (const s of spaceSymbols(metta)) pool.add(s);
1632
+ return [...pool].map((name) => ({ name, score: matchScore(name.toLowerCase(), q) })).filter((x) => x.score > 0).sort(
1633
+ (a, b) => b.score - a.score || a.name.length - b.name.length || a.name.localeCompare(b.name)
1634
+ ).slice(0, limit).map((x) => x.name);
1635
+ }
1636
+
1637
+ // src/reduce.ts
1638
+ import { E as E2, S as S2, ExpressionAtom as ExpressionAtom2 } from "@mettascript/hyperon";
1639
+ var MAX_WIDTH = 24;
1640
+ function lazyPositions(head, metta, cache) {
1641
+ const key = head.toString();
1642
+ const hit = cache.get(key);
1643
+ if (hit !== void 0) return hit;
1644
+ const out = /* @__PURE__ */ new Set();
1645
+ const types = metta.evaluateAtom(E2(S2("get-type"), head));
1646
+ const type = types[0];
1647
+ if (type instanceof ExpressionAtom2) {
1648
+ const items = type.children();
1649
+ if (items[0]?.toString() === "->")
1650
+ for (let i = 1; i < items.length - 1; i++) {
1651
+ const t = items[i].toString();
1652
+ if (t === "Atom" || t === "Expression") out.add(i);
1653
+ }
1654
+ }
1655
+ cache.set(key, out);
1656
+ return out;
1657
+ }
1658
+ function stepAll(atom, metta) {
1659
+ const results = metta.evaluateAtom(E2(S2("eval"), atom));
1660
+ const src = atom.toString();
1661
+ const out = [];
1662
+ const seen = /* @__PURE__ */ new Set();
1663
+ for (const r of results) {
1664
+ if (r instanceof ExpressionAtom2) {
1665
+ const items = r.children();
1666
+ if (items.length === 2 && items[0].toString() === "eval" && items[1].toString() === src)
1667
+ continue;
1668
+ }
1669
+ const s = r.toString();
1670
+ if (s === src || seen.has(s)) continue;
1671
+ seen.add(s);
1672
+ out.push(r);
1673
+ }
1674
+ return out.length === 0 ? null : out;
1675
+ }
1676
+ function reduceStep(atom, metta, cache = /* @__PURE__ */ new Map()) {
1677
+ if (atom instanceof ExpressionAtom2) {
1678
+ const items = atom.children();
1679
+ const head = items[0];
1680
+ const lazy = head !== void 0 ? lazyPositions(head, metta, cache) : /* @__PURE__ */ new Set();
1681
+ for (let i = 1; i < items.length; i++) {
1682
+ if (lazy.has(i)) continue;
1683
+ const reduced = reduceStep(items[i], metta, cache);
1684
+ if (reduced !== null)
1685
+ return reduced.map((r) => {
1686
+ const next = [...items];
1687
+ next[i] = r;
1688
+ return E2(...next);
1689
+ });
1690
+ }
1691
+ }
1692
+ return stepAll(atom, metta);
1693
+ }
1694
+ function sameSet(a, b) {
1695
+ if (a.length !== b.length) return false;
1696
+ const as = a.map(String).sort();
1697
+ const bs = b.map(String).sort();
1698
+ return as.every((s, i) => s === bs[i]);
1699
+ }
1700
+ function reduceTrace(atom, metta, maxSteps = 300) {
1701
+ const cache = /* @__PURE__ */ new Map();
1702
+ const frontiers = [[atom]];
1703
+ let frontier = [atom];
1704
+ let settled = false;
1705
+ for (let i = 0; i < maxSteps; i++) {
1706
+ const next = [];
1707
+ const seen = /* @__PURE__ */ new Set();
1708
+ let changed = false;
1709
+ for (const term of frontier) {
1710
+ const step = reduceStep(term, metta, cache);
1711
+ if (step !== null) changed = true;
1712
+ for (const s of step ?? [term]) {
1713
+ const key = s.toString();
1714
+ if (!seen.has(key)) {
1715
+ seen.add(key);
1716
+ next.push(s);
1717
+ }
1718
+ }
1719
+ }
1720
+ if (!changed) {
1721
+ settled = true;
1722
+ break;
1723
+ }
1724
+ frontier = next.length > MAX_WIDTH ? next.slice(0, MAX_WIDTH) : next;
1725
+ frontiers.push(frontier);
1726
+ }
1727
+ if (settled) {
1728
+ const result = metta.evaluateAtom(atom);
1729
+ if (result.length > 0 && !sameSet(frontier, result))
1730
+ frontiers.push(result.length > MAX_WIDTH ? result.slice(0, MAX_WIDTH) : result);
1731
+ }
1732
+ return frontiers;
1733
+ }
1734
+
1735
+ // src/skeleton.ts
1736
+ import { E as E3, S as S3, V, ExpressionAtom as ExpressionAtom3, VariableAtom as VariableAtom2 } from "@mettascript/hyperon";
1737
+ function withSilhouettes(frontiers, metta) {
1738
+ const out = [];
1739
+ for (let i = 0; i < frontiers.length; i++) {
1740
+ out.push(frontiers[i]);
1741
+ const cur = frontiers[i];
1742
+ const next = frontiers[i + 1];
1743
+ if (next !== void 0 && cur.length === 1 && next.length === 1) {
1744
+ const body = skeletonize(cur[0], next[0], metta);
1745
+ if (body !== null) out.push([body]);
1746
+ }
1747
+ }
1748
+ return out;
1749
+ }
1750
+ function skeletonize(before, after, metta) {
1751
+ if (before instanceof ExpressionAtom3 && after instanceof ExpressionAtom3) {
1752
+ const b = before.children();
1753
+ const a = after.children();
1754
+ if (b.length === a.length) {
1755
+ const diff = [];
1756
+ for (let i = 0; i < a.length; i++) if (b[i].toString() !== a[i].toString()) diff.push(i);
1757
+ if (diff.length === 1) {
1758
+ const i = diff[0];
1759
+ const sub = skeletonize(b[i], a[i], metta);
1760
+ if (sub === null) return null;
1761
+ const next = [...a];
1762
+ next[i] = sub;
1763
+ return E3(...next);
1764
+ }
1765
+ }
1766
+ }
1767
+ return ruleBody(before, after, metta);
1768
+ }
1769
+ function ruleBody(redex, reduct, metta) {
1770
+ if (!(redex instanceof ExpressionAtom3)) return null;
1771
+ const kids = redex.children();
1772
+ if (kids.length < 2) return null;
1773
+ const pattern = E3(kids[0], ...kids.slice(1).map((_, i) => V("v" + String(i))));
1774
+ const bodies = metta.evaluateAtom(
1775
+ E3(S3("match"), S3("&self"), E3(S3("="), pattern, V("body")), V("body"))
1776
+ );
1777
+ for (const cand of bodies)
1778
+ if (cand instanceof ExpressionAtom3 && hasVar(cand) && fits(cand, reduct))
1779
+ return cleanVars(cand);
1780
+ return null;
1781
+ }
1782
+ function fits(pattern, ground) {
1783
+ if (pattern instanceof VariableAtom2) return true;
1784
+ if (pattern instanceof ExpressionAtom3 && ground instanceof ExpressionAtom3) {
1785
+ const p = pattern.children();
1786
+ const g = ground.children();
1787
+ return p.length === g.length && p.every((c, i) => fits(c, g[i]));
1788
+ }
1789
+ return pattern.toString() === ground.toString();
1790
+ }
1791
+ function hasVar(atom) {
1792
+ if (atom instanceof VariableAtom2) return true;
1793
+ if (atom instanceof ExpressionAtom3) return atom.children().some(hasVar);
1794
+ return false;
1795
+ }
1796
+ function cleanVars(atom) {
1797
+ if (atom instanceof VariableAtom2) return V(atom.name().replace(/#\d+$/, ""));
1798
+ if (atom instanceof ExpressionAtom3) return E3(...atom.children().map(cleanVars));
1799
+ return atom;
1800
+ }
1801
+
1802
+ // src/block/view.ts
1803
+ import { S as S4 } from "@mettascript/hyperon";
1804
+
1805
+ // src/block/settings.ts
1806
+ var SITE_PALETTE = {
1807
+ canvas: "#1b1d23",
1808
+ bkgColor: "#21262d",
1809
+ backgroundBlockColor: "#2b313b",
1810
+ outlineBlockColor: "#3d444d",
1811
+ formColor: "#e6edf3",
1812
+ identifierColor: "#e6edf3",
1813
+ literalColor: "#79c0ff",
1814
+ stringColor: "#a5d6ff",
1815
+ operatorColor: "#ff7b72",
1816
+ spacerefColor: "#ffa657",
1817
+ atColor: "#d2a8ff",
1818
+ holeFill: "#ffa657",
1819
+ holeSide: "#ffa657",
1820
+ holeText: "#1b1d23",
1821
+ selectedColor: "#f2cc60",
1822
+ selectedAtomColor: "#1b1d23"
1823
+ };
1824
+ var TEAL_PALETTE = {
1825
+ canvas: "#002F36",
1826
+ bkgColor: "#002F36",
1827
+ backgroundBlockColor: "#003A42",
1828
+ outlineBlockColor: "#005A65",
1829
+ formColor: "#0082D6",
1830
+ identifierColor: "#30A1B6",
1831
+ literalColor: "#FFC732",
1832
+ stringColor: "#79E7B0",
1833
+ operatorColor: "#0082D6",
1834
+ spacerefColor: "#30A1B6",
1835
+ atColor: "#30A1B6",
1836
+ holeFill: "#FCE13E",
1837
+ holeSide: "#C17317",
1838
+ holeText: "#002F36",
1839
+ selectedColor: "#E60000",
1840
+ selectedAtomColor: "#FFFFFF"
1841
+ };
1842
+ var CUTOFF = 14;
1843
+ function makeSettings(fontSize, unitWidth, palette = SITE_PALETTE) {
1844
+ const unitHeight = Math.round(fontSize * 1.55);
1845
+ const radius = Math.max(2, Math.round(unitHeight / 2) - 1);
1846
+ const radiusAdj = Math.max(2, Math.round(radius * 5 / 7));
1847
+ return { fontSize, unitWidth, unitHeight, radius, radiusAdj, cutoff: CUTOFF, ...palette };
1848
+ }
1849
+
1850
+ // src/block/layout.ts
1851
+ import {
1852
+ ExpressionAtom as ExpressionAtom4,
1853
+ VariableAtom as VariableAtom3,
1854
+ SymbolAtom as SymbolAtom3,
1855
+ GroundedAtom as GroundedAtom2
1856
+ } from "@mettascript/hyperon";
1857
+
1858
+ // src/block/color.ts
1859
+ function glyphColor(name, isHead, s) {
1860
+ switch (roleOf(name)) {
1861
+ case "number":
1862
+ return s.literalColor;
1863
+ case "string":
1864
+ return s.stringColor;
1865
+ case "operator":
1866
+ case "control":
1867
+ return s.operatorColor;
1868
+ case "spaceref":
1869
+ return s.spacerefColor;
1870
+ case "at":
1871
+ return s.atColor;
1872
+ default:
1873
+ return isHead ? s.formColor : s.identifierColor;
1874
+ }
1875
+ }
1876
+ function blockColors(depth, s) {
1877
+ const fill = depth % 2 === 0 ? s.backgroundBlockColor : s.bkgColor;
1878
+ return { fill, outline: s.outlineBlockColor };
1879
+ }
1880
+
1881
+ // src/block/layout.ts
1882
+ var IF_LIKE = /* @__PURE__ */ new Set(["if", "and", "or"]);
1883
+ var LAMBDA_LIKE = /* @__PURE__ */ new Set([
1884
+ "=",
1885
+ "let",
1886
+ "let*",
1887
+ "match",
1888
+ "function",
1889
+ "lambda",
1890
+ "\\",
1891
+ "sealed",
1892
+ "chain"
1893
+ ]);
1894
+ var COND_LIKE = /* @__PURE__ */ new Set(["case", "cond", "superpose", "collapse", "unify"]);
1895
+ function classify(head, u) {
1896
+ if (IF_LIKE.has(head))
1897
+ return { headerItems: 2, indent: u, straightLeft: true, headerException: false };
1898
+ if (LAMBDA_LIKE.has(head))
1899
+ return { headerItems: 2, indent: u, straightLeft: true, headerException: true };
1900
+ if (COND_LIKE.has(head))
1901
+ return { headerItems: 1, indent: u, straightLeft: true, headerException: false };
1902
+ return null;
1903
+ }
1904
+ function isHeaded(atom) {
1905
+ const items = atom.children();
1906
+ const head = items[0];
1907
+ return items.length >= 2 && head !== void 0 && isLeaf(head);
1908
+ }
1909
+ function isLeaf(atom) {
1910
+ return atom instanceof SymbolAtom3 || atom instanceof VariableAtom3 || atom instanceof GroundedAtom2;
1911
+ }
1912
+ function needsTail(box) {
1913
+ return box.kind !== "expr";
1914
+ }
1915
+ function layoutRow(boxes, startX, u) {
1916
+ let x = startX;
1917
+ let h = 0;
1918
+ let right = startX;
1919
+ for (const b of boxes) {
1920
+ b.x = x;
1921
+ b.y = 0;
1922
+ right = x + b.w;
1923
+ h = Math.max(h, b.h);
1924
+ x = right + u;
1925
+ }
1926
+ return { right, h };
1927
+ }
1928
+ function build(atom, depth, path, isHead, s) {
1929
+ const u = s.unitWidth;
1930
+ if (atom instanceof VariableAtom3) {
1931
+ const text = atom.toString();
1932
+ return {
1933
+ kind: "hole",
1934
+ path,
1935
+ x: 0,
1936
+ y: 0,
1937
+ w: text.length * u + u,
1938
+ // a half unit of breathing room on each side of the name
1939
+ h: s.unitHeight,
1940
+ depth,
1941
+ text
1942
+ };
1943
+ }
1944
+ if (!(atom instanceof ExpressionAtom4)) {
1945
+ const name = atom.toString();
1946
+ const text = displayGlyph(name);
1947
+ return {
1948
+ kind: "atom",
1949
+ path,
1950
+ x: 0,
1951
+ y: 0,
1952
+ w: text.length * u,
1953
+ h: s.unitHeight,
1954
+ depth,
1955
+ text,
1956
+ color: glyphColor(name, isHead, s)
1957
+ };
1958
+ }
1959
+ const items = atom.children();
1960
+ if (items.length === 0) {
1961
+ const { fill: fill2, outline: outline2 } = blockColors(depth, s);
1962
+ const w2 = 3 * u;
1963
+ const h = s.unitHeight;
1964
+ return {
1965
+ kind: "expr",
1966
+ path,
1967
+ x: 0,
1968
+ y: 0,
1969
+ w: w2,
1970
+ h,
1971
+ depth,
1972
+ orient: "h",
1973
+ fill: fill2,
1974
+ outline: outline2,
1975
+ headerException: false,
1976
+ children: [],
1977
+ rightProfile: [{ x: w2, h }],
1978
+ leftProfile: [{ x: 0, h }]
1979
+ };
1980
+ }
1981
+ const headed = isHeaded(atom);
1982
+ const children = items.map((child, k) => {
1983
+ const childIsHead = headed && k === 0;
1984
+ return build(child, childIsHead ? depth : depth + 1, [...path, k], childIsHead, s);
1985
+ });
1986
+ const { fill, outline } = blockColors(depth, s);
1987
+ const totalChars = children.reduce((sum, b) => sum + b.w, 0) / u;
1988
+ const headName = headed ? items[0].toString() : "";
1989
+ const cls = headed && children.length >= 2 ? classify(headName, u) : null;
1990
+ const stacks = cls !== null && totalChars >= s.cutoff && children.length > cls.headerItems;
1991
+ if (!stacks) {
1992
+ const { right, h } = layoutRow(children, u, u);
1993
+ const last = children[children.length - 1];
1994
+ const w2 = right + (last !== void 0 && needsTail(last) ? u : 0);
1995
+ return {
1996
+ kind: "expr",
1997
+ path,
1998
+ x: 0,
1999
+ y: 0,
2000
+ w: w2,
2001
+ h,
2002
+ depth,
2003
+ orient: "h",
2004
+ fill,
2005
+ outline,
2006
+ headerException: false,
2007
+ children,
2008
+ rightProfile: [{ x: w2, h }],
2009
+ leftProfile: [{ x: 0, h }]
2010
+ };
2011
+ }
2012
+ const c = cls;
2013
+ const header = children.slice(0, c.headerItems);
2014
+ const body = children.slice(c.headerItems);
2015
+ const head = layoutRow(header, u, u);
2016
+ const lastHeader = header[header.length - 1];
2017
+ const rightProfile = [
2018
+ { x: head.right + (lastHeader !== void 0 && needsTail(lastHeader) ? u : 0), h: head.h }
2019
+ ];
2020
+ const leftProfile = [{ x: 0, h: head.h }];
2021
+ let yCursor = head.h;
2022
+ for (const b of body) {
2023
+ b.x = c.indent;
2024
+ b.y = yCursor;
2025
+ yCursor += b.h;
2026
+ rightProfile.push({ x: c.indent + b.w + (needsTail(b) ? u : 0), h: b.h });
2027
+ leftProfile.push({ x: c.straightLeft ? 0 : c.indent, h: b.h });
2028
+ }
2029
+ const w = Math.max(...rightProfile.map((r) => r.x));
2030
+ return {
2031
+ kind: "expr",
2032
+ path,
2033
+ x: 0,
2034
+ y: 0,
2035
+ w,
2036
+ h: yCursor,
2037
+ depth,
2038
+ orient: "v",
2039
+ fill,
2040
+ outline,
2041
+ headerException: c.headerException,
2042
+ children,
2043
+ rightProfile,
2044
+ leftProfile
2045
+ };
2046
+ }
2047
+ function place(box, ox, oy) {
2048
+ box.x += ox;
2049
+ box.y += oy;
2050
+ if (box.kind === "expr") for (const child of box.children) place(child, box.x, box.y);
2051
+ }
2052
+ function layoutAtom(atom, s, path = []) {
2053
+ const box = build(atom, 0, path, false, s);
2054
+ place(box, 0, 0);
2055
+ return box;
2056
+ }
2057
+ function placeProgram(atoms, s) {
2058
+ const gap = s.unitHeight;
2059
+ const boxes = [];
2060
+ let y = 0;
2061
+ for (let i = 0; i < atoms.length; i++) {
2062
+ const box = build(atoms[i], 0, [i], false, s);
2063
+ place(box, 0, y);
2064
+ boxes.push(box);
2065
+ y += box.h + gap;
2066
+ }
2067
+ return boxes;
2068
+ }
2069
+
2070
+ // src/block/geometry.ts
2071
+ var T = 0.39;
2072
+ function rot(x, y, deg) {
2073
+ const a = deg * Math.PI / 180;
2074
+ const c = Math.cos(a);
2075
+ const s = Math.sin(a);
2076
+ return { x: x * c - y * s, y: x * s + y * c };
2077
+ }
2078
+ function f(n) {
2079
+ return (Math.round(n * 100) / 100).toString();
2080
+ }
2081
+ function pulledPointsToPath(points) {
2082
+ const n = points.length;
2083
+ if (n === 0) return "";
2084
+ const first = points[0];
2085
+ let d = `M ${f(first.x)} ${f(first.y)}`;
2086
+ for (let i = 0; i < n; i++) {
2087
+ const prev = points[i];
2088
+ const cur = points[(i + 1) % n];
2089
+ const vx = cur.x - prev.x;
2090
+ const vy = cur.y - prev.y;
2091
+ const r1 = rot(vx, vy, prev.rangle);
2092
+ const c1x = prev.x + prev.rpull * r1.x;
2093
+ const c1y = prev.y + prev.rpull * r1.y;
2094
+ const r2 = rot(-vx, -vy, cur.langle);
2095
+ const c2x = cur.x + cur.lpull * r2.x;
2096
+ const c2y = cur.y + cur.lpull * r2.y;
2097
+ d += ` C ${f(c1x)} ${f(c1y)} ${f(c2x)} ${f(c2y)} ${f(cur.x)} ${f(cur.y)}`;
2098
+ }
2099
+ return d + " Z";
2100
+ }
2101
+ function roundedRectPoints(width, height, initR) {
2102
+ const r = width < 2 * initR ? Math.floor(width / 2) : Math.round(initR);
2103
+ const sl = Math.max(0, width - 2 * r);
2104
+ const sh = Math.max(0, height - 2 * r);
2105
+ return [
2106
+ { lpull: 0, langle: 0, x: 0, y: r, rpull: T, rangle: -45 },
2107
+ { lpull: T, langle: 45, x: r, y: 0, rpull: 0, rangle: 0 },
2108
+ { lpull: 0, langle: 0, x: r + sl, y: 0, rpull: T, rangle: -45 },
2109
+ { lpull: T, langle: 45, x: 2 * r + sl, y: r, rpull: 0, rangle: 0 },
2110
+ { lpull: 0, langle: 0, x: 2 * r + sl, y: r + sh, rpull: T, rangle: -45 },
2111
+ { lpull: T, langle: 45, x: r + sl, y: 2 * r + sh, rpull: 0, rangle: 0 },
2112
+ { lpull: 0, langle: 0, x: r, y: 2 * r + sh, rpull: T, rangle: -45 },
2113
+ { lpull: T, langle: 45, x: 0, y: r + sh, rpull: T, rangle: 0 }
2114
+ ];
2115
+ }
2116
+ function roundedRectPath(width, height, r) {
2117
+ return pulledPointsToPath(roundedRectPoints(width, height, r));
2118
+ }
2119
+ function sign3(a, b, polarity) {
2120
+ if (a > b) return polarity;
2121
+ if (a === b) return 0;
2122
+ return -polarity;
2123
+ }
2124
+ function augment(polarity, init, rows) {
2125
+ const withLtp = [];
2126
+ let prev = init;
2127
+ for (const row of rows) {
2128
+ withLtp.push({ w: row.x, h: row.h, ltp: sign3(row.x, prev, polarity), ltn: 0 });
2129
+ prev = row.x;
2130
+ }
2131
+ let next = init;
2132
+ for (let i = withLtp.length - 1; i >= 0; i--) {
2133
+ const row = withLtp[i];
2134
+ row.ltn = sign3(row.w, next, polarity);
2135
+ next = row.w;
2136
+ }
2137
+ return withLtp;
2138
+ }
2139
+ function fourPointRow(p, n, offset, ltp, ltn, initH, finalH, r) {
2140
+ const y1 = 2 * n * r + initH;
2141
+ const y2 = 2 * n * r + p * r + initH;
2142
+ const y3 = 2 * n * r + p * r + finalH;
2143
+ const y4 = 2 * n * r + p * 2 * r + finalH;
2144
+ const x1 = offset + -p * ltp * r;
2145
+ const x2 = offset;
2146
+ const x3 = offset;
2147
+ const x4 = offset + -p * ltn * r;
2148
+ return [
2149
+ { lpull: T, langle: 0, x: x1, y: y1, rpull: T, rangle: ltp * -45 },
2150
+ { lpull: T, langle: ltp * 45, x: x2, y: y2, rpull: T, rangle: 0 },
2151
+ { lpull: T, langle: 0, x: x3, y: y3, rpull: T, rangle: ltn * -45 },
2152
+ { lpull: T, langle: ltn * 45, x: x4, y: y4, rpull: T, rangle: 0 }
2153
+ ];
2154
+ }
2155
+ function calcRows(p, init, numRows, totalH, rows, headerException, r) {
2156
+ const aug = augment(p, init, rows);
2157
+ if (headerException && aug.length > 0) aug[0].ltn = Math.abs(aug[0].ltn);
2158
+ const points = [];
2159
+ let n = numRows;
2160
+ let h = totalH;
2161
+ for (const row of aug) {
2162
+ points.push(...fourPointRow(p, n, row.w, row.ltp, row.ltn, h, h + p * row.h, r));
2163
+ n = n + p;
2164
+ h = h + p * row.h;
2165
+ }
2166
+ return { points, numRows: n, totalH: h };
2167
+ }
2168
+ function roundedBackingPoints(rightProfile, leftProfile, r, headerException) {
2169
+ const srcRight = rightProfile.map((row) => ({ x: row.x, h: row.h - 2 * r }));
2170
+ const srcLeft = leftProfile.map((row) => ({ x: row.x, h: row.h - 2 * r }));
2171
+ const right = calcRows(1, 0, 0, 0, srcRight, headerException, r);
2172
+ const left = calcRows(
2173
+ -1,
2174
+ Infinity,
2175
+ right.numRows,
2176
+ right.totalH,
2177
+ [...srcLeft].reverse(),
2178
+ false,
2179
+ r
2180
+ );
2181
+ return [...right.points, ...left.points];
2182
+ }
2183
+ function roundedBackingPath(rightProfile, leftProfile, r, headerException) {
2184
+ return pulledPointsToPath(roundedBackingPoints(rightProfile, leftProfile, r, headerException));
2185
+ }
2186
+
2187
+ // src/block/animate.ts
2188
+ var backingD = (box, s) => box.orient === "h" ? roundedRectPath(box.w, box.h, s.radius) : roundedBackingPath(box.rightProfile, box.leftProfile, s.radius, box.headerException);
2189
+ function boxesToPrims(boxes, s, selectedPath) {
2190
+ const out = [];
2191
+ const draw = (box) => {
2192
+ const pk = JSON.stringify(box.path);
2193
+ if (box.kind === "expr") {
2194
+ out.push({
2195
+ t: "path",
2196
+ key: `b:${pk}`,
2197
+ d: backingD(box, s),
2198
+ tx: box.x,
2199
+ ty: box.y,
2200
+ fill: box.fill,
2201
+ stroke: box.outline,
2202
+ op: 1,
2203
+ dataPath: pk
2204
+ });
2205
+ if (box.children.length === 0)
2206
+ out.push({
2207
+ t: "text",
2208
+ key: `t:${pk}`,
2209
+ x: box.x + box.w / 2,
2210
+ y: box.y + box.h / 2,
2211
+ text: "()",
2212
+ fill: s.identifierColor,
2213
+ size: s.fontSize,
2214
+ weight: false,
2215
+ op: 1
2216
+ });
2217
+ for (const c of box.children) draw(c);
2218
+ return;
2219
+ }
2220
+ if (box.kind === "hole") {
2221
+ const ph = s.fontSize + 6;
2222
+ const py = box.y + (box.h - ph) / 2;
2223
+ out.push({
2224
+ t: "path",
2225
+ key: `b:${pk}`,
2226
+ d: roundedRectPath(box.w, ph, ph / 2),
2227
+ tx: box.x,
2228
+ ty: py,
2229
+ fill: s.holeFill,
2230
+ stroke: "none",
2231
+ op: 1,
2232
+ dataPath: pk
2233
+ });
2234
+ out.push({
2235
+ t: "text",
2236
+ key: `t:${pk}`,
2237
+ x: box.x + box.w / 2,
2238
+ y: box.y + box.h / 2,
2239
+ text: box.text,
2240
+ fill: s.holeText,
2241
+ size: s.fontSize,
2242
+ weight: true,
2243
+ op: 1
2244
+ });
2245
+ return;
2246
+ }
2247
+ out.push({ t: "hit", key: `r:${pk}`, x: box.x, y: box.y, w: box.w, h: box.h, dataPath: pk });
2248
+ out.push({
2249
+ t: "text",
2250
+ key: `t:${pk}`,
2251
+ x: box.x + box.w / 2,
2252
+ y: box.y + box.h / 2,
2253
+ text: box.text,
2254
+ fill: box.color,
2255
+ size: s.fontSize,
2256
+ weight: false,
2257
+ op: 1
2258
+ });
2259
+ };
2260
+ for (const b of boxes) draw(b);
2261
+ if (selectedPath !== null) {
2262
+ const sel = findBox(boxes, selectedPath);
2263
+ if (sel !== null) {
2264
+ const selD = sel.kind === "expr" ? backingD(sel, s) : roundedRectPath(sel.w, sel.h, sel.kind === "hole" ? s.radiusAdj : s.radius);
2265
+ out.push({
2266
+ t: "path",
2267
+ key: "sel",
2268
+ d: selD,
2269
+ tx: sel.x,
2270
+ ty: sel.y,
2271
+ fill: "none",
2272
+ stroke: s.selectedColor,
2273
+ op: 1
2274
+ });
2275
+ const hh = s.unitHeight;
2276
+ const hw = s.unitWidth * 1.8;
2277
+ const hx = sel.x - hw - s.unitWidth * 0.4;
2278
+ const hy = sel.y + sel.h / 2 - hh / 2;
2279
+ out.push({
2280
+ t: "path",
2281
+ key: "hbg",
2282
+ d: roundedRectPath(hw, hh, s.radiusAdj),
2283
+ tx: hx,
2284
+ ty: hy,
2285
+ fill: s.selectedColor,
2286
+ stroke: "none",
2287
+ op: 1
2288
+ });
2289
+ out.push({
2290
+ t: "text",
2291
+ key: "harrow",
2292
+ x: hx + hw / 2,
2293
+ y: hy + hh / 2,
2294
+ text: "\u2192",
2295
+ fill: s.selectedAtomColor,
2296
+ size: s.fontSize,
2297
+ weight: false,
2298
+ op: 1
2299
+ });
2300
+ }
2301
+ }
2302
+ return out;
2303
+ }
2304
+ function findBox(boxes, path) {
2305
+ const same = (a) => a.length === path.length && a.every((v, i) => v === path[i]);
2306
+ const walk = (b) => {
2307
+ if (same(b.path)) return b;
2308
+ if (b.kind === "expr")
2309
+ for (const c of b.children) {
2310
+ const hit = walk(c);
2311
+ if (hit !== null) return hit;
2312
+ }
2313
+ return null;
2314
+ };
2315
+ for (const b of boxes) {
2316
+ const hit = walk(b);
2317
+ if (hit !== null) return hit;
2318
+ }
2319
+ return null;
2320
+ }
2321
+ var NUM = /-?\d+(\.\d+)?/g;
2322
+ function lerpD(a, b, t) {
2323
+ const na = a.match(NUM);
2324
+ const nb = b.match(NUM);
2325
+ if (na === null || nb === null || na.length !== nb.length) return b;
2326
+ let i = 0;
2327
+ return b.replace(NUM, () => {
2328
+ const v = lerp(Number(na[i]), Number(nb[i]), t);
2329
+ i++;
2330
+ return (Math.round(v * 100) / 100).toString();
2331
+ });
2332
+ }
2333
+ function compatible(p, n) {
2334
+ if (p.t !== n.t) return false;
2335
+ if (p.t === "path" && n.t === "path")
2336
+ return (p.d.match(NUM)?.length ?? 0) === (n.d.match(NUM)?.length ?? 0);
2337
+ if (p.t === "text" && n.t === "text") return p.text === n.text;
2338
+ return true;
2339
+ }
2340
+ function faded(p, op) {
2341
+ return p.t === "hit" ? p : { ...p, op };
2342
+ }
2343
+ function morph(p, n, t) {
2344
+ if (p.t === "path" && n.t === "path")
2345
+ return {
2346
+ ...n,
2347
+ d: lerpD(p.d, n.d, t),
2348
+ tx: lerp(p.tx, n.tx, t),
2349
+ ty: lerp(p.ty, n.ty, t),
2350
+ op: lerp(p.op, n.op, t)
2351
+ };
2352
+ if (p.t === "text" && n.t === "text")
2353
+ return {
2354
+ ...n,
2355
+ x: lerp(p.x, n.x, t),
2356
+ y: lerp(p.y, n.y, t),
2357
+ size: lerp(p.size, n.size, t),
2358
+ op: lerp(p.op, n.op, t)
2359
+ };
2360
+ return n;
2361
+ }
2362
+ function push(m, k, v) {
2363
+ const arr = m.get(k);
2364
+ if (arr === void 0) m.set(k, [v]);
2365
+ else arr.push(v);
2366
+ }
2367
+ function interpolate(prev, next, t) {
2368
+ const prevMap = new Map(prev.map((p) => [p.key, p]));
2369
+ const nextKeys = new Set(next.map((n) => n.key));
2370
+ const exitingByText = /* @__PURE__ */ new Map();
2371
+ const enteringByText = /* @__PURE__ */ new Map();
2372
+ for (const p of prev)
2373
+ if (p.t === "text" && !p.weight && !nextKeys.has(p.key)) push(exitingByText, p.text, p);
2374
+ for (const n of next)
2375
+ if (n.t === "text" && !n.weight && !prevMap.has(n.key)) push(enteringByText, n.text, n);
2376
+ const glideFrom = /* @__PURE__ */ new Map();
2377
+ const glided = /* @__PURE__ */ new Set();
2378
+ for (const [txt, ns] of enteringByText) {
2379
+ const ps = exitingByText.get(txt);
2380
+ if (ns.length === 1 && ps !== void 0 && ps.length === 1) {
2381
+ glideFrom.set(ns[0].key, ps[0]);
2382
+ glided.add(ps[0].key);
2383
+ }
2384
+ }
2385
+ const out = [];
2386
+ for (const n of next) {
2387
+ if (n.t === "hit") continue;
2388
+ const p = prevMap.get(n.key) ?? glideFrom.get(n.key);
2389
+ if (p === void 0) {
2390
+ out.push(faded(n, t));
2391
+ } else if (compatible(p, n)) {
2392
+ out.push(morph(p, n, t));
2393
+ } else {
2394
+ out.push(faded(n, t));
2395
+ out.push(faded({ ...p, key: `${p.key}:x` }, 1 - t));
2396
+ }
2397
+ }
2398
+ for (const p of prev) {
2399
+ if (p.t === "hit") continue;
2400
+ if (!nextKeys.has(p.key) && !glided.has(p.key)) out.push(faded(p, 1 - t));
2401
+ }
2402
+ return out;
2403
+ }
2404
+
2405
+ // src/block/render.ts
2406
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
2407
+ var DURATION = 320;
2408
+ var CSS2 = `
2409
+ .blk-svg { width: 100%; height: 100%; display: block; overflow: clip; user-select: none;
2410
+ font-family: Iosevka, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
2411
+ .blk-block { stroke-width: 1; }
2412
+ .blk-glyph { text-anchor: middle; dominant-baseline: central; pointer-events: none; }
2413
+ .blk-hole-text { text-anchor: middle; dominant-baseline: central; pointer-events: none; font-weight: 600; }
2414
+ .blk-hit { fill: transparent; }
2415
+ .blk-sel { fill: none; stroke-width: 2; }
2416
+ `;
2417
+ function el(tag, attrs) {
2418
+ const node = document.createElementNS(SVG_NS2, tag);
2419
+ for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
2420
+ return node;
2421
+ }
2422
+ function measureUnitWidth(fontSize) {
2423
+ const canvas = document.createElement("canvas");
2424
+ const ctx = canvas.getContext("2d");
2425
+ if (ctx === null) return fontSize * 0.6;
2426
+ ctx.font = `${fontSize}px Iosevka, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`;
2427
+ const w = ctx.measureText("M").width;
2428
+ return w > 0 ? w : fontSize * 0.6;
2429
+ }
2430
+ function findByPath(boxes, path) {
2431
+ const same = (a) => a.length === path.length && a.every((v, i) => v === path[i]);
2432
+ const walk = (box) => {
2433
+ if (same(box.path)) return box;
2434
+ if (box.kind === "expr")
2435
+ for (const c of box.children) {
2436
+ const hit = walk(c);
2437
+ if (hit !== null) return hit;
2438
+ }
2439
+ return null;
2440
+ };
2441
+ for (const b of boxes) {
2442
+ const hit = walk(b);
2443
+ if (hit !== null) return hit;
2444
+ }
2445
+ return null;
2446
+ }
2447
+ var BlockRenderer = class {
2448
+ svg;
2449
+ scene;
2450
+ settled = [];
2451
+ pending = null;
2452
+ settledRect = null;
2453
+ raf = 0;
2454
+ // User zoom/pan, layered on top of the auto-fit view box as a transform on the scene group.
2455
+ viewport = { scale: 1, panX: 0, panY: 0 };
2456
+ shownRect = { x: 0, y: 0, w: 1, h: 1 };
2457
+ constructor(container) {
2458
+ this.svg = el("svg", { class: "blk-svg" });
2459
+ const style = document.createElementNS(SVG_NS2, "style");
2460
+ style.textContent = CSS2;
2461
+ this.svg.appendChild(style);
2462
+ this.scene = el("g", { class: "blk-scene" });
2463
+ this.svg.appendChild(this.scene);
2464
+ container.appendChild(this.svg);
2465
+ }
2466
+ /** Redraw the program. When `animate` is set and a previous frame exists, tween into the new one. */
2467
+ render(boxes, s, selectedPath, animate = false) {
2468
+ this.svg.style.background = s.canvas;
2469
+ const next = boxesToPrims(boxes, s, selectedPath);
2470
+ const nextRect = this.rectFor(boxes, s);
2471
+ const canAnimate = animate && this.settled.length > 0 && this.settledRect !== null && typeof requestAnimationFrame !== "undefined";
2472
+ if (!canAnimate) {
2473
+ if (this.raf !== 0) {
2474
+ cancelAnimationFrame(this.raf);
2475
+ this.raf = 0;
2476
+ }
2477
+ this.paint(next);
2478
+ this.setViewBox(nextRect);
2479
+ this.settled = next;
2480
+ this.settledRect = nextRect;
2481
+ this.pending = null;
2482
+ return;
2483
+ }
2484
+ if (this.raf !== 0) {
2485
+ cancelAnimationFrame(this.raf);
2486
+ this.raf = 0;
2487
+ if (this.pending !== null) this.settled = this.pending;
2488
+ }
2489
+ const from = this.settled;
2490
+ const fromRect = this.settledRect ?? nextRect;
2491
+ this.pending = next;
2492
+ const start = performance.now();
2493
+ const step = (now) => {
2494
+ const p = Math.min(1, (now - start) / DURATION);
2495
+ const t = ease(p);
2496
+ this.paint(interpolate(from, next, t));
2497
+ this.setViewBox(lerpRect(fromRect, nextRect, t));
2498
+ if (p < 1) {
2499
+ this.raf = requestAnimationFrame(step);
2500
+ } else {
2501
+ this.raf = 0;
2502
+ this.paint(next);
2503
+ this.setViewBox(nextRect);
2504
+ this.settled = next;
2505
+ this.settledRect = nextRect;
2506
+ this.pending = null;
2507
+ }
2508
+ };
2509
+ this.raf = requestAnimationFrame(step);
2510
+ }
2511
+ paint(prims) {
2512
+ const out = [];
2513
+ for (const p of prims) {
2514
+ if (p.t === "path") {
2515
+ const attrs = {
2516
+ class: p.key === "sel" ? "blk-sel" : "blk-block",
2517
+ d: p.d,
2518
+ transform: `translate(${p.tx} ${p.ty})`,
2519
+ fill: p.fill,
2520
+ stroke: p.stroke,
2521
+ opacity: p.op
2522
+ };
2523
+ if (p.dataPath !== void 0) attrs["data-path"] = p.dataPath;
2524
+ out.push(el("path", attrs));
2525
+ } else if (p.t === "text") {
2526
+ const node = el("text", {
2527
+ class: p.weight ? "blk-hole-text" : "blk-glyph",
2528
+ x: p.x,
2529
+ y: p.y,
2530
+ "font-size": p.size,
2531
+ fill: p.fill,
2532
+ opacity: p.op
2533
+ });
2534
+ node.textContent = p.text;
2535
+ out.push(node);
2536
+ } else {
2537
+ out.push(
2538
+ el("rect", {
2539
+ class: "blk-hit",
2540
+ "data-path": p.dataPath,
2541
+ x: p.x,
2542
+ y: p.y,
2543
+ width: p.w,
2544
+ height: p.h
2545
+ })
2546
+ );
2547
+ }
2548
+ }
2549
+ this.scene.replaceChildren(...out);
2550
+ }
2551
+ setViewBox(r) {
2552
+ this.shownRect = r;
2553
+ this.svg.setAttribute("viewBox", `${r.x} ${r.y} ${r.w} ${r.h}`);
2554
+ this.svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
2555
+ this.applyViewport();
2556
+ }
2557
+ /** Apply the user zoom/pan as a transform on the scene group, centered on the shown view box so zoom
2558
+ * grows toward the middle rather than a corner. Re-applied on every frame (the group survives a repaint
2559
+ * and keeps its transform), so a zoomed view stays zoomed as the program reduces. */
2560
+ applyViewport() {
2561
+ const { x, y, w, h } = this.shownRect;
2562
+ const cx = x + w / 2;
2563
+ const cy = y + h / 2;
2564
+ const { scale, panX, panY } = this.viewport;
2565
+ const tx = cx - cx * scale + panX;
2566
+ const ty = cy - cy * scale + panY;
2567
+ this.scene.setAttribute("transform", `translate(${tx} ${ty}) scale(${scale})`);
2568
+ }
2569
+ /** Zoom in (factor > 1) or out (factor < 1), around the center. */
2570
+ zoomBy(factor) {
2571
+ this.viewport.scale = Math.min(5, Math.max(0.2, this.viewport.scale * factor));
2572
+ this.applyViewport();
2573
+ }
2574
+ /** Pan by a screen-space delta, converted into view-box units through the current fit. */
2575
+ panBy(dx, dy) {
2576
+ const { w, h } = this.shownRect;
2577
+ const sw = this.svg.clientWidth || 800;
2578
+ const sh = this.svg.clientHeight || 440;
2579
+ const fit = Math.min(sw / w, sh / h) || 1;
2580
+ this.viewport.panX += dx / fit;
2581
+ this.viewport.panY += dy / fit;
2582
+ this.applyViewport();
2583
+ }
2584
+ /** Reset zoom/pan so the auto-fit frames the whole program again. */
2585
+ resetViewport() {
2586
+ this.viewport.scale = 1;
2587
+ this.viewport.panX = 0;
2588
+ this.viewport.panY = 0;
2589
+ this.applyViewport();
2590
+ }
2591
+ /** The view box that frames the whole program with padding and room for the selection handle. */
2592
+ rectFor(boxes, s) {
2593
+ if (boxes.length === 0) return { x: 0, y: 0, w: 1, h: 1 };
2594
+ let minX = Infinity;
2595
+ let minY = Infinity;
2596
+ let maxX = -Infinity;
2597
+ let maxY = -Infinity;
2598
+ for (const b of boxes) {
2599
+ minX = Math.min(minX, b.x);
2600
+ minY = Math.min(minY, b.y);
2601
+ maxX = Math.max(maxX, b.x + b.w);
2602
+ maxY = Math.max(maxY, b.y + b.h);
2603
+ }
2604
+ const pad = s.unitHeight;
2605
+ const x = minX - pad - s.unitWidth * 2.5;
2606
+ const y = minY - pad;
2607
+ return { x, y, w: maxX - x + pad, h: maxY - y + pad };
2608
+ }
2609
+ destroy() {
2610
+ if (this.raf !== 0) cancelAnimationFrame(this.raf);
2611
+ this.svg.remove();
2612
+ }
2613
+ };
2614
+ var lerp2 = (a, b, t) => a + (b - a) * t;
2615
+ function lerpRect(a, b, t) {
2616
+ return { x: lerp2(a.x, b.x, t), y: lerp2(a.y, b.y, t), w: lerp2(a.w, b.w, t), h: lerp2(a.h, b.h, t) };
2617
+ }
2618
+
2619
+ // src/block/path.ts
2620
+ import { E as E4, ExpressionAtom as ExpressionAtom5 } from "@mettascript/hyperon";
2621
+ function atomAtPath(atom, path) {
2622
+ let cur = atom;
2623
+ for (const k of path) {
2624
+ if (!(cur instanceof ExpressionAtom5)) return null;
2625
+ const child = cur.children()[k];
2626
+ if (child === void 0) return null;
2627
+ cur = child;
2628
+ }
2629
+ return cur;
2630
+ }
2631
+ function replaceAtPath(atom, path, replacement) {
2632
+ if (path.length === 0) return replacement;
2633
+ if (!(atom instanceof ExpressionAtom5)) return atom;
2634
+ const items = atom.children();
2635
+ const [k, ...rest] = path;
2636
+ const child = items[k];
2637
+ if (child === void 0) return atom;
2638
+ const next = [...items];
2639
+ next[k] = replaceAtPath(child, rest, replacement);
2640
+ return E4(...next);
2641
+ }
2642
+ function reduceAtPath(atom, path, metta) {
2643
+ const target2 = atomAtPath(atom, path);
2644
+ if (target2 === null) return null;
2645
+ const reduced = reduceStep(target2, metta);
2646
+ if (reduced === null) return null;
2647
+ return replaceAtPath(atom, path, reduced[0]);
2648
+ }
2649
+
2650
+ // src/svg-gif.ts
2651
+ async function encodeSvgAnimation(animation, rasterize, lib, maxColors = 128) {
2652
+ const { frames, width, height, background } = animation;
2653
+ if (frames.length === 0) throw new Error("no SVG frames to encode");
2654
+ if (!Number.isInteger(width) || width < 1 || !Number.isInteger(height) || height < 1)
2655
+ throw new Error(`invalid GIF dimensions: ${width}x${height}`);
2656
+ if (!Number.isInteger(maxColors) || maxColors < 2 || maxColors > 256)
2657
+ throw new Error(`maxColors must be an integer from 2 to 256, got ${maxColors}`);
2658
+ const expectedBytes = width * height * 4;
2659
+ const gif = lib.GIFEncoder();
2660
+ for (const frame of frames) {
2661
+ const rgba = await rasterize(frame.svg, width, height, background);
2662
+ if (rgba.byteLength !== expectedBytes)
2663
+ throw new Error(
2664
+ `SVG rasterizer returned ${rgba.byteLength} bytes for ${width}x${height}; expected ${expectedBytes}`
2665
+ );
2666
+ const palette = lib.quantize(rgba, maxColors);
2667
+ gif.writeFrame(lib.applyPalette(rgba, palette), width, height, {
2668
+ palette,
2669
+ delay: frame.delay
2670
+ });
2671
+ }
2672
+ gif.finish();
2673
+ return gif.bytes();
2674
+ }
2675
+ function browserSvgRasterizer() {
2676
+ let canvas;
2677
+ let context = null;
2678
+ return async (svg, width, height, background) => {
2679
+ if (canvas === void 0) {
2680
+ canvas = document.createElement("canvas");
2681
+ context = canvas.getContext("2d");
2682
+ if (context === null) throw new Error("no 2d canvas context for GIF export");
2683
+ }
2684
+ if (canvas.width !== width) canvas.width = width;
2685
+ if (canvas.height !== height) canvas.height = height;
2686
+ const image = new Image();
2687
+ image.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
2688
+ await image.decode();
2689
+ context.fillStyle = background;
2690
+ context.fillRect(0, 0, width, height);
2691
+ context.drawImage(image, 0, 0, width, height);
2692
+ return context.getImageData(0, 0, width, height).data;
2693
+ };
2694
+ }
2695
+ async function encodeBrowserSvgAnimation(animation, lib, maxColors = 128) {
2696
+ const bytes = await encodeSvgAnimation(animation, browserSvgRasterizer(), lib, maxColors);
2697
+ return new Blob([bytes], { type: "image/gif" });
2698
+ }
2699
+
2700
+ // src/block/gif.ts
2701
+ function framesPerStepFor(opts, transitions) {
2702
+ const wanted = opts.framesPerStep ?? Math.max(1, Math.round((opts.morphMs ?? DEFAULT_TRACE_MS) / (opts.stepMs ?? 40)));
2703
+ return Math.max(
2704
+ 1,
2705
+ Math.min(wanted, Math.floor((opts.maxFrames ?? 180) / Math.max(1, transitions)))
2706
+ );
2707
+ }
2708
+ function boundsOf(boxes, s) {
2709
+ let minX = Infinity;
2710
+ let minY = Infinity;
2711
+ let maxX = -Infinity;
2712
+ let maxY = -Infinity;
2713
+ const walk = (b) => {
2714
+ minX = Math.min(minX, b.x);
2715
+ minY = Math.min(minY, b.y);
2716
+ maxX = Math.max(maxX, b.x + b.w);
2717
+ maxY = Math.max(maxY, b.y + b.h);
2718
+ if (b.kind === "expr") for (const c of b.children) walk(c);
2719
+ };
2720
+ for (const b of boxes) walk(b);
2721
+ if (!Number.isFinite(minX)) return { x: 0, y: 0, w: 1, h: 1 };
2722
+ const pad = s.unitHeight;
2723
+ return { x: minX - pad, y: minY - pad, w: maxX - minX + 2 * pad, h: maxY - minY + 2 * pad };
2724
+ }
2725
+ function union(a, b) {
2726
+ const x = Math.min(a.x, b.x);
2727
+ const y = Math.min(a.y, b.y);
2728
+ return { x, y, w: Math.max(a.x + a.w, b.x + b.w) - x, h: Math.max(a.y + a.h, b.y + b.h) - y };
2729
+ }
2730
+ function esc(t) {
2731
+ return t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2732
+ }
2733
+ var round = (n) => Math.round(n * 1e3) / 1e3;
2734
+ var FONT = "Iosevka, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
2735
+ function frameSvg(prims, vb, w, h, bg) {
2736
+ const parts = [
2737
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="${round(vb.x)} ${round(vb.y)} ${round(vb.w)} ${round(vb.h)}">`,
2738
+ `<rect x="${round(vb.x)}" y="${round(vb.y)}" width="${round(vb.w)}" height="${round(vb.h)}" fill="${bg}"/>`
2739
+ ];
2740
+ for (const p of prims) {
2741
+ if (p.t === "path") {
2742
+ parts.push(
2743
+ `<path d="${p.d}" transform="translate(${round(p.tx)} ${round(p.ty)})" fill="${p.fill}" stroke="${p.stroke}" stroke-width="1" opacity="${round(p.op)}"/>`
2744
+ );
2745
+ } else if (p.t === "text") {
2746
+ const weight = p.weight ? ' font-weight="600"' : "";
2747
+ parts.push(
2748
+ `<text x="${round(p.x)}" y="${round(p.y)}" font-size="${p.size}" fill="${p.fill}" opacity="${round(p.op)}" text-anchor="middle" dominant-baseline="central" font-family="${FONT}"${weight}>${esc(p.text)}</text>`
2749
+ );
2750
+ }
2751
+ }
2752
+ parts.push("</svg>");
2753
+ return parts.join("");
2754
+ }
2755
+ function blockReductionSvgsWithSettings(states, s, opts = {}) {
2756
+ if (states.length === 0) throw new Error("no reduction states to export");
2757
+ const width = opts.width ?? 720;
2758
+ const holdMs = opts.holdMs ?? 260;
2759
+ const stepMs = opts.stepMs ?? 40;
2760
+ const framePrims = states.map((frontier) => boxesToPrims(placeProgram(frontier, s), s, null));
2761
+ let vb = boundsOf(placeProgram(states[0], s), s);
2762
+ for (const frontier of states) vb = union(vb, boundsOf(placeProgram(frontier, s), s));
2763
+ const height = Math.max(1, Math.round(width * vb.h / vb.w));
2764
+ const background = opts.background ?? s.canvas;
2765
+ const frames = [];
2766
+ const n = states.length;
2767
+ const perStep = framesPerStepFor(opts, n - 1);
2768
+ frames.push({ svg: frameSvg(framePrims[0], vb, width, height, background), delay: holdMs });
2769
+ for (let i = 1; i < n; i++) {
2770
+ for (let k = 1; k <= perStep; k++) {
2771
+ const prims = interpolate(framePrims[i - 1], framePrims[i], ease(k / perStep));
2772
+ frames.push({
2773
+ svg: frameSvg(prims, vb, width, height, background),
2774
+ delay: k === perStep ? holdMs : stepMs
2775
+ });
2776
+ }
2777
+ }
2778
+ return { frames, width, height, background };
2779
+ }
2780
+ async function reductionGif(states, s, lib, opts = {}) {
2781
+ return encodeBrowserSvgAnimation(blockReductionSvgsWithSettings(states, s, opts), lib, 256);
2782
+ }
2783
+
2784
+ // src/sidebyside-gif.ts
2785
+ var VP = { scale: 1, panX: 0, panY: 0 };
2786
+ var round2 = (n) => Math.round(n * 100) / 100;
2787
+ function traceBounds(frame) {
2788
+ let minX = Infinity;
2789
+ let minY = Infinity;
2790
+ let maxX = -Infinity;
2791
+ let maxY = -Infinity;
2792
+ for (const s of frame.slots) {
2793
+ minX = Math.min(minX, s.x - s.radius);
2794
+ minY = Math.min(minY, s.y - s.radius);
2795
+ maxX = Math.max(maxX, s.x + s.radius);
2796
+ maxY = Math.max(maxY, s.y + s.radius);
2797
+ }
2798
+ if (!Number.isFinite(minX)) return { x: 0, y: 0, w: 1, h: 1 };
2799
+ return {
2800
+ x: minX - NODE_H,
2801
+ y: minY - NODE_H,
2802
+ w: maxX - minX + 2 * NODE_H,
2803
+ h: maxY - minY + 2 * NODE_H
2804
+ };
2805
+ }
2806
+ function graphTraceSvg(trace, vb, w, h, bg) {
2807
+ const parts = [
2808
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="${round2(vb.x)} ${round2(vb.y)} ${round2(vb.w)} ${round2(vb.h)}">`,
2809
+ `<rect x="${round2(vb.x)}" y="${round2(vb.y)}" width="${round2(vb.w)}" height="${round2(vb.h)}" fill="${bg}"/>`
2810
+ ];
2811
+ for (const e of trace.edges) {
2812
+ parts.push(
2813
+ `<line x1="${round2(e.x1)}" y1="${round2(e.y1)}" x2="${round2(e.x2)}" y2="${round2(e.y2)}" stroke="${e.colorB || "#6e7681"}" stroke-width="2" stroke-opacity="${round2(e.op)}"/>`
2814
+ );
2815
+ }
2816
+ for (const p of trace.nodes) {
2817
+ const pts = p.points.map(([x, y]) => `${round2(p.x + x)},${round2(p.y + y)}`).join(" ");
2818
+ parts.push(
2819
+ `<polygon points="${pts}" fill="${p.fill}" fill-opacity="${round2(p.fillOp * p.op)}" stroke="${p.fill}" stroke-opacity="${round2((1 - p.fillOp * 0.7) * p.op)}" stroke-width="2"/>`
2820
+ );
2821
+ for (const tx of p.texts) {
2822
+ parts.push(
2823
+ `<text x="${round2(p.x)}" y="${round2(p.y)}" font-size="13" fill="${tx.color}" opacity="${round2(tx.op * p.op)}" text-anchor="middle" dominant-baseline="central" font-family="${FONT}">${esc(tx.text)}</text>`
2824
+ );
2825
+ }
2826
+ }
2827
+ parts.push("</svg>");
2828
+ return parts.join("");
2829
+ }
2830
+ async function sideBySideReductionGif(states, s, lib, opts = {}) {
2831
+ return encodeBrowserSvgAnimation(sideBySideReductionSvgs(states, opts, s), lib, 128);
2832
+ }
2833
+ function sideBySideReductionSvgs(states, opts = {}, s = makeSettings(17, 10)) {
2834
+ if (states.length < 2)
2835
+ throw new Error("need at least two reduction states for a side-by-side GIF");
2836
+ const holdMs = opts.holdMs ?? 260;
2837
+ const stepMs = opts.stepMs ?? 40;
2838
+ const blockPrims = states.map((f2) => boxesToPrims(placeProgram(f2, s), s, null));
2839
+ let blockVb = boundsOf(placeProgram(states[0], s), s);
2840
+ for (const f2 of states) blockVb = union(blockVb, boundsOf(placeProgram(f2, s), s));
2841
+ const graphFrames = states.map((f2) => traceFrame(atomToGraph(f2), VP));
2842
+ let graphVb = traceBounds(graphFrames[0]);
2843
+ for (const gf of graphFrames) graphVb = union(graphVb, traceBounds(gf));
2844
+ const cellAspect = Math.sqrt(graphVb.w / graphVb.h * (blockVb.w / blockVb.h));
2845
+ const naturalPanelH = 340;
2846
+ const naturalGap = 28;
2847
+ const naturalLabelH = 30;
2848
+ const naturalCellW = Math.max(1, Math.round(naturalPanelH * cellAspect));
2849
+ const naturalWidth = naturalCellW * 2 + naturalGap;
2850
+ const scale = (opts.width ?? naturalWidth) / naturalWidth;
2851
+ const panelH = Math.max(1, Math.round(naturalPanelH * scale));
2852
+ const gap = Math.max(1, Math.round(naturalGap * scale));
2853
+ const labelH = Math.max(1, Math.round(naturalLabelH * scale));
2854
+ const cellW = Math.max(1, Math.round(naturalCellW * scale));
2855
+ const totalW = cellW + gap + cellW;
2856
+ const totalH = panelH + labelH;
2857
+ const background = opts.background ?? s.canvas;
2858
+ const fontSize = Math.max(8, Math.round(13 * scale));
2859
+ const frames = [];
2860
+ const addFrame = (bp, gt, delay) => {
2861
+ const graph = graphTraceSvg(gt, graphVb, cellW, panelH, background);
2862
+ const blocks = frameSvg(bp, blockVb, cellW, panelH, background);
2863
+ const graphUri = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(graph);
2864
+ const blocksUri = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(blocks);
2865
+ frames.push({
2866
+ delay,
2867
+ svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${totalH}" viewBox="0 0 ${totalW} ${totalH}"><rect width="${totalW}" height="${totalH}" fill="${background}"/><text x="${cellW / 2}" y="${labelH / 2}" font-size="${fontSize}" font-weight="600" fill="#8b949e" text-anchor="middle" dominant-baseline="central" font-family="${FONT}">graph</text><text x="${cellW + gap + cellW / 2}" y="${labelH / 2}" font-size="${fontSize}" font-weight="600" fill="#8b949e" text-anchor="middle" dominant-baseline="central" font-family="${FONT}">blocks</text><image href="${graphUri}" x="0" y="${labelH}" width="${cellW}" height="${panelH}"/><image href="${blocksUri}" x="${cellW + gap}" y="${labelH}" width="${cellW}" height="${panelH}"/></svg>`
2868
+ });
2869
+ };
2870
+ const n = states.length;
2871
+ const perStep = framesPerStepFor(opts, n - 1);
2872
+ addFrame(blockPrims[0], interpolateTrace(graphFrames[0], graphFrames[0], 0), holdMs);
2873
+ for (let i = 1; i < n; i++) {
2874
+ for (let k = 1; k <= perStep; k++) {
2875
+ const t = ease(k / perStep);
2876
+ const bp = interpolate(blockPrims[i - 1], blockPrims[i], t);
2877
+ const gt = interpolateTrace(graphFrames[i - 1], graphFrames[i], t);
2878
+ addFrame(bp, gt, k === perStep ? holdMs : stepMs);
2879
+ }
2880
+ }
2881
+ return { frames, width: totalW, height: totalH, background };
2882
+ }
2883
+ function graphReductionSvgs(states, opts = {}) {
2884
+ if (states.length < 2) throw new Error("need at least two reduction states for a graph GIF");
2885
+ const bg = opts.background ?? CANVAS_BG;
2886
+ const holdMs = opts.holdMs ?? 260;
2887
+ const stepMs = opts.stepMs ?? 40;
2888
+ const width = opts.width ?? 880;
2889
+ const height = Math.round(width * 0.5);
2890
+ const frames = states.map((f2) => {
2891
+ const fr = traceFrame(atomToGraph(f2), VP);
2892
+ const b = traceBounds(fr);
2893
+ const scale = Math.max(0.68, Math.min(Math.min(width / b.w, height / b.h), 1));
2894
+ const viewport = {
2895
+ scale,
2896
+ panX: width / 2 - (b.x + b.w / 2) * scale,
2897
+ panY: height / 2 - (b.y + b.h / 2) * scale
2898
+ };
2899
+ return { ...fr, viewport };
2900
+ });
2901
+ const out = [];
2902
+ const push2 = (gt, delay) => {
2903
+ const v = gt.viewport;
2904
+ const vb = {
2905
+ x: -v.panX / v.scale,
2906
+ y: -v.panY / v.scale,
2907
+ w: width / v.scale,
2908
+ h: height / v.scale
2909
+ };
2910
+ out.push({ svg: graphTraceSvg(gt, vb, width, height, bg), delay });
2911
+ };
2912
+ const n = states.length;
2913
+ const perStep = framesPerStepFor(opts, n - 1);
2914
+ push2(interpolateTrace(frames[0], frames[0], 0), holdMs);
2915
+ for (let i = 1; i < n; i++) {
2916
+ for (let k = 1; k <= perStep; k++) {
2917
+ const t = ease(k / perStep);
2918
+ push2(interpolateTrace(frames[i - 1], frames[i], t), k === perStep ? holdMs : stepMs);
2919
+ }
2920
+ }
2921
+ return { frames: out, width, height, background: bg };
2922
+ }
2923
+ async function graphReductionGif(states, lib, opts = {}) {
2924
+ return encodeBrowserSvgAnimation(graphReductionSvgs(states, opts), lib, 128);
2925
+ }
2926
+ function blockReductionSvgs(states, opts = {}) {
2927
+ if (states.length < 2) throw new Error("need at least two reduction states for a block GIF");
2928
+ return blockReductionSvgsWithSettings(states, makeSettings(17, 10), opts);
2929
+ }
2930
+
2931
+ // src/block/view.ts
2932
+ var FONT_SIZE = 17;
2933
+ var CARET = "\u258F";
2934
+ function samePath(a, b) {
2935
+ return a.length === b.length && a.every((v, i) => v === b[i]);
2936
+ }
2937
+ var BlockView = class {
2938
+ renderer;
2939
+ getSpace;
2940
+ palette = SITE_PALETTE;
2941
+ settings = null;
2942
+ atoms = [];
2943
+ boxes = [];
2944
+ selectedPath = null;
2945
+ editing = null;
2946
+ readonlyMode = false;
2947
+ history = [];
2948
+ onChange;
2949
+ constructor(container, getSpace, onChange = () => {
2950
+ }) {
2951
+ this.renderer = new BlockRenderer(container);
2952
+ this.getSpace = getSpace;
2953
+ this.onChange = onChange;
2954
+ this.renderer.svg.setAttribute("tabindex", "0");
2955
+ this.renderer.svg.addEventListener("pointerdown", this.onPointerDown);
2956
+ this.renderer.svg.addEventListener("keydown", this.onKeyDown);
2957
+ this.hide();
2958
+ }
2959
+ ensureSettings() {
2960
+ if (this.settings === null)
2961
+ this.settings = makeSettings(FONT_SIZE, measureUnitWidth(FONT_SIZE), this.palette);
2962
+ return this.settings;
2963
+ }
2964
+ /** Recolor the view with a different palette. */
2965
+ setPalette(palette) {
2966
+ this.palette = palette;
2967
+ this.settings = null;
2968
+ this.render();
2969
+ }
2970
+ /** Load a program to edit and reduce interactively. */
2971
+ setAtoms(atoms) {
2972
+ this.atoms = [...atoms];
2973
+ this.history.length = 0;
2974
+ this.selectedPath = null;
2975
+ this.editing = null;
2976
+ this.readonlyMode = false;
2977
+ this.renderer.resetViewport();
2978
+ this.render();
2979
+ }
2980
+ /** Zoom the view in (factor > 1) or out (factor < 1). */
2981
+ zoomBy(factor) {
2982
+ this.renderer.zoomBy(factor);
2983
+ }
2984
+ /** Pan the view by a screen-space delta. */
2985
+ panBy(dx, dy) {
2986
+ this.renderer.panBy(dx, dy);
2987
+ }
2988
+ /** Reset zoom/pan so the whole program is framed again. */
2989
+ fitView() {
2990
+ this.renderer.resetViewport();
2991
+ }
2992
+ /** Frame the given atoms without interaction (used for a step-through playthrough). */
2993
+ showReadonly(atoms) {
2994
+ this.atoms = [...atoms];
2995
+ this.selectedPath = null;
2996
+ this.editing = null;
2997
+ this.readonlyMode = true;
2998
+ this.render(true);
2999
+ }
3000
+ render(animate = false) {
3001
+ const s = this.ensureSettings();
3002
+ let atoms = this.atoms;
3003
+ if (this.editing !== null) {
3004
+ const { path, buffer } = this.editing;
3005
+ const head = path[0];
3006
+ if (head !== void 0 && this.atoms[head] !== void 0)
3007
+ atoms = this.atoms.map(
3008
+ (a, i) => i === head ? replaceAtPath(a, path.slice(1), S4(buffer + CARET)) : a
3009
+ );
3010
+ }
3011
+ this.boxes = placeProgram(atoms, s);
3012
+ this.renderer.render(this.boxes, s, this.editing?.path ?? this.selectedPath, animate);
3013
+ }
3014
+ /** The current program as source, reflecting any edits and reductions. */
3015
+ sourceText() {
3016
+ return this.atoms.map((a) => a.toString()).join("\n");
3017
+ }
3018
+ /** Encode a sequence of reduction states as an animated GIF, using a caller-supplied encoder (gifenc),
3019
+ * so the package needs no GIF dependency of its own. */
3020
+ async exportGif(states, lib, opts) {
3021
+ return reductionGif(states, this.ensureSettings(), lib, opts);
3022
+ }
3023
+ /** Encode a reduction as one GIF that plays it in both the graph and the block views side by side. Shares
3024
+ * the block layout settings so the two panels agree on the canvas color and fonts. */
3025
+ async exportSideBySideGif(states, lib, opts) {
3026
+ return sideBySideReductionGif(states, this.ensureSettings(), lib, opts);
3027
+ }
3028
+ show() {
3029
+ this.renderer.svg.style.display = "block";
3030
+ this.render();
3031
+ }
3032
+ hide() {
3033
+ this.renderer.svg.style.display = "none";
3034
+ }
3035
+ destroy() {
3036
+ this.renderer.svg.removeEventListener("pointerdown", this.onPointerDown);
3037
+ this.renderer.svg.removeEventListener("keydown", this.onKeyDown);
3038
+ this.renderer.destroy();
3039
+ }
3040
+ pathFromEvent(e) {
3041
+ const target2 = e.target;
3042
+ if (!(target2 instanceof Element)) return null;
3043
+ const raw = target2.closest("[data-path]")?.getAttribute("data-path");
3044
+ if (raw === null || raw === void 0) return null;
3045
+ try {
3046
+ const parsed = JSON.parse(raw);
3047
+ return Array.isArray(parsed) ? parsed : null;
3048
+ } catch {
3049
+ return null;
3050
+ }
3051
+ }
3052
+ /** The leaf text at `path`, or null when the term is a form (not editable as text). */
3053
+ leafText(path) {
3054
+ const box = findByPath(this.boxes, path);
3055
+ if (box === null) return null;
3056
+ return box.kind === "atom" || box.kind === "hole" ? box.text : null;
3057
+ }
3058
+ lastPath = null;
3059
+ lastClickTime = 0;
3060
+ onPointerDown = (e) => {
3061
+ if (this.readonlyMode) return;
3062
+ const path = this.pathFromEvent(e);
3063
+ const now = performance.now();
3064
+ const isDouble = path !== null && this.lastPath !== null && samePath(path, this.lastPath) && now - this.lastClickTime < 350;
3065
+ this.lastPath = path;
3066
+ this.lastClickTime = now;
3067
+ if (this.editing !== null) this.commitEdit();
3068
+ this.selectedPath = path;
3069
+ this.renderer.svg.focus({ preventScroll: true });
3070
+ if (isDouble && path !== null) {
3071
+ if (this.leafText(path) !== null) this.startEdit(false);
3072
+ else this.reduceSelected();
3073
+ return;
3074
+ }
3075
+ this.render();
3076
+ };
3077
+ /** Begin editing the selected leaf. When `replace` is set the buffer starts empty (typing replaces the
3078
+ * term); otherwise it starts from the current text (typing extends it). */
3079
+ startEdit(replace, seed = "") {
3080
+ if (this.selectedPath === null) return;
3081
+ const text = this.leafText(this.selectedPath);
3082
+ if (text === null) return;
3083
+ this.editing = { path: this.selectedPath, buffer: replace ? seed : text + seed };
3084
+ this.render();
3085
+ }
3086
+ /** Commit the edit: re-parse the buffer into an atom and splice it back into the program. */
3087
+ commitEdit() {
3088
+ if (this.editing === null) return;
3089
+ const { path, buffer } = this.editing;
3090
+ this.editing = null;
3091
+ const text = buffer.trim();
3092
+ const head = path[0];
3093
+ if (text.length > 0 && head !== void 0 && this.atoms[head] !== void 0) {
3094
+ const parsed = parseLeaf(text) ?? S4(text);
3095
+ this.history.push([...this.atoms]);
3096
+ this.atoms = this.atoms.map(
3097
+ (a, i) => i === head ? replaceAtPath(a, path.slice(1), parsed) : a
3098
+ );
3099
+ this.selectedPath = path;
3100
+ this.onChange();
3101
+ }
3102
+ this.render();
3103
+ }
3104
+ cancelEdit() {
3105
+ this.editing = null;
3106
+ this.render();
3107
+ }
3108
+ /** Reduce the selected term one step in place. Returns whether anything changed. */
3109
+ reduceSelected() {
3110
+ if (this.readonlyMode || this.editing !== null || this.selectedPath === null) return false;
3111
+ const path = this.selectedPath;
3112
+ const head = path[0];
3113
+ if (head === void 0) return false;
3114
+ const target2 = this.atoms[head];
3115
+ if (target2 === void 0) return false;
3116
+ const reduced = reduceAtPath(target2, path.slice(1), this.getSpace());
3117
+ if (reduced === null) return false;
3118
+ this.history.push([...this.atoms]);
3119
+ this.atoms = this.atoms.map((a, i) => i === head ? reduced : a);
3120
+ if (findByPath(placeProgram(this.atoms, this.ensureSettings()), path) === null)
3121
+ this.selectedPath = null;
3122
+ this.render(true);
3123
+ this.onChange();
3124
+ return true;
3125
+ }
3126
+ /** Step back to the program before the last reduction or edit. */
3127
+ back() {
3128
+ const prev = this.history.pop();
3129
+ if (prev === void 0) return false;
3130
+ this.atoms = prev;
3131
+ this.editing = null;
3132
+ this.render(true);
3133
+ this.onChange();
3134
+ return true;
3135
+ }
3136
+ canStepBack() {
3137
+ return this.history.length > 0;
3138
+ }
3139
+ onKeyDown = (e) => {
3140
+ if (this.readonlyMode) return;
3141
+ if (this.editing !== null) {
3142
+ this.onEditKey(e);
3143
+ return;
3144
+ }
3145
+ if (e.key === "Enter") {
3146
+ e.preventDefault();
3147
+ if (this.selectedPath !== null && this.leafText(this.selectedPath) !== null)
3148
+ this.startEdit(false);
3149
+ else this.reduceSelected();
3150
+ return;
3151
+ }
3152
+ if (e.key.startsWith("Arrow")) {
3153
+ e.preventDefault();
3154
+ const next = this.navigate(e.key);
3155
+ if (next !== null) {
3156
+ this.selectedPath = next;
3157
+ this.render();
3158
+ }
3159
+ return;
3160
+ }
3161
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
3162
+ if (this.selectedPath !== null && this.leafText(this.selectedPath) !== null) {
3163
+ e.preventDefault();
3164
+ this.startEdit(true, e.key);
3165
+ }
3166
+ }
3167
+ };
3168
+ onEditKey(e) {
3169
+ if (this.editing === null) return;
3170
+ if (e.key === "Enter") {
3171
+ e.preventDefault();
3172
+ this.commitEdit();
3173
+ } else if (e.key === "Escape") {
3174
+ e.preventDefault();
3175
+ this.cancelEdit();
3176
+ } else if (e.key === "Backspace") {
3177
+ e.preventDefault();
3178
+ this.editing = { path: this.editing.path, buffer: this.editing.buffer.slice(0, -1) };
3179
+ this.render();
3180
+ } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
3181
+ e.preventDefault();
3182
+ this.editing = { path: this.editing.path, buffer: this.editing.buffer + e.key };
3183
+ this.render();
3184
+ }
3185
+ }
3186
+ /** Every box path in preorder (a term before its children). */
3187
+ preorder() {
3188
+ const out = [];
3189
+ const walk = (b) => {
3190
+ out.push(b.path);
3191
+ if (b.kind === "expr") for (const c of b.children) walk(c);
3192
+ };
3193
+ for (const b of this.boxes) walk(b);
3194
+ return out;
3195
+ }
3196
+ /** Move the cursor: right and left step through the tree in preorder, down enters the first child, up
3197
+ * returns to the parent. */
3198
+ navigate(key) {
3199
+ if (this.selectedPath === null) return this.boxes[0]?.path ?? null;
3200
+ const path = this.selectedPath;
3201
+ if (key === "ArrowUp") return path.length > 1 ? path.slice(0, -1) : path;
3202
+ if (key === "ArrowDown") {
3203
+ const box = findByPath(this.boxes, path);
3204
+ return box?.kind === "expr" ? box.children[0]?.path ?? path : path;
3205
+ }
3206
+ const list = this.preorder();
3207
+ const i = list.findIndex((p) => samePath(p, path));
3208
+ if (i < 0) return path;
3209
+ if (key === "ArrowRight") return list[i + 1] ?? path;
3210
+ if (key === "ArrowLeft") return list[i - 1] ?? path;
3211
+ return path;
3212
+ }
3213
+ };
3214
+
3215
+ // src/viz.ts
3216
+ import { ExpressionAtom as ExpressionAtom6, atomIsError as atomIsError2 } from "@mettascript/hyperon";
3217
+ var VIZ_SPACE = "&grapher";
3218
+ function bindVizSpace(space) {
3219
+ space.run(`!(bind! ${VIZ_SPACE} (new-space))`);
3220
+ }
3221
+ function readViz(space) {
3222
+ const results = space.run(`!(get-atoms ${VIZ_SPACE})`)[0] ?? [];
3223
+ const directives = [];
3224
+ const mappers = [];
3225
+ let background = null;
3226
+ for (const r of results) {
3227
+ if (atomIsError2(r)) continue;
3228
+ const bg = parseBackground(r);
3229
+ if (bg !== null) {
3230
+ background = bg;
3231
+ continue;
3232
+ }
3233
+ const mapper = parseMapper(r);
3234
+ if (mapper !== null) {
3235
+ mappers.push(mapper);
3236
+ continue;
3237
+ }
3238
+ const directive = parseDirective(r);
3239
+ if (directive !== null) directives.push(directive);
3240
+ }
3241
+ return { directives, mappers, background };
3242
+ }
3243
+ function parseBackground(atom) {
3244
+ if (!(atom instanceof ExpressionAtom6)) return null;
3245
+ const items = atom.children();
3246
+ return items.length === 2 && items[0]?.toString() === "background" && items[1] !== void 0 ? colorOf(items[1]) : null;
3247
+ }
3248
+ function parseDirective(atom) {
3249
+ if (!(atom instanceof ExpressionAtom6)) return null;
3250
+ const items = atom.children();
3251
+ const head = items[0]?.toString();
3252
+ const target2 = items[1];
3253
+ if (target2 === void 0) return null;
3254
+ if (head === "highlight" && items.length === 2) return { kind: "highlight", target: target2 };
3255
+ if (head === "focus" && items.length === 2) return { kind: "focus", target: target2 };
3256
+ const arg = items[2];
3257
+ if (head === "color" && arg !== void 0) return { kind: "color", target: target2, arg };
3258
+ if (head === "label" && arg !== void 0) return { kind: "label", target: target2, arg };
3259
+ if (head === "size" && arg !== void 0) {
3260
+ const value = numberOf(arg);
3261
+ if (value !== null) return { kind: "size", target: target2, value };
3262
+ }
3263
+ if (head === "shade" && arg !== void 0) {
3264
+ const value = numberOf(arg);
3265
+ if (value !== null) return { kind: "shade", target: target2, value };
3266
+ }
3267
+ return null;
3268
+ }
3269
+ function parseMapper(atom) {
3270
+ if (!(atom instanceof ExpressionAtom6)) return null;
3271
+ const items = atom.children();
3272
+ const func = items[1];
3273
+ if (items.length !== 2 || func === void 0) return null;
3274
+ const head = items[0]?.toString();
3275
+ if (head === "shade-by") return { property: "shade", func };
3276
+ if (head === "size-by") return { property: "size", func };
3277
+ return null;
3278
+ }
3279
+ function numberOf(atom) {
3280
+ const n = Number(atom.toString());
3281
+ return Number.isFinite(n) ? n : null;
3282
+ }
3283
+ var NAMED = {
3284
+ red: "#f85149",
3285
+ green: "#3fb950",
3286
+ blue: "#58a6ff",
3287
+ yellow: "#f2cc60",
3288
+ orange: "#ffa657",
3289
+ purple: "#d2a8ff",
3290
+ cyan: "#39c5cf",
3291
+ pink: "#f778ba",
3292
+ white: "#ffffff",
3293
+ black: "#0d1117",
3294
+ gray: "#8b949e",
3295
+ grey: "#8b949e"
3296
+ };
3297
+ function colorOf(atom) {
3298
+ const s = atom.toString().replace(/^"|"$/g, "");
3299
+ const named = NAMED[s.toLowerCase()];
3300
+ if (named !== void 0) return named;
3301
+ if (/^#[0-9a-fA-F]{3,8}$/.test(s)) return s;
3302
+ return NAMED.yellow;
3303
+ }
3304
+ function textOf(atom) {
3305
+ return atom.toString().replace(/^"|"$/g, "");
3306
+ }
3307
+ function normalizeRange(vals, lo, hi) {
3308
+ if (vals.length === 0) return [];
3309
+ let min = Infinity;
3310
+ let max = -Infinity;
3311
+ for (const [, v] of vals) {
3312
+ if (v < min) min = v;
3313
+ if (v > max) max = v;
3314
+ }
3315
+ const span = max - min;
3316
+ return vals.map(([id, v]) => [
3317
+ id,
3318
+ span === 0 ? (lo + hi) / 2 : lo + (v - min) / span * (hi - lo)
3319
+ ]);
3320
+ }
3321
+
3322
+ // src/editor.ts
3323
+ var NO_VIZ = /* @__PURE__ */ new Map();
3324
+ var NO_SELECTION = /* @__PURE__ */ new Set();
3325
+ var NO_LABELS = /* @__PURE__ */ new Map();
3326
+ function referencesGrapher(a) {
3327
+ if (a.toString() === VIZ_SPACE) return true;
3328
+ return a instanceof ExpressionAtom7 && a.children().some(referencesGrapher);
3329
+ }
3330
+ function styleBlockDirectives(a) {
3331
+ if (!(a instanceof ExpressionAtom7)) return null;
3332
+ const items = a.children();
3333
+ return items[0]?.toString() === "style" ? items.slice(1) : null;
3334
+ }
3335
+ function bareDirective(a) {
3336
+ if (a instanceof ExpressionAtom7) {
3337
+ const items = a.children();
3338
+ if (items.length === 3 && items[0]?.toString() === "add-atom" && items[1]?.toString() === VIZ_SPACE)
3339
+ return items[2];
3340
+ }
3341
+ return a;
3342
+ }
3343
+ function firstNumber(results) {
3344
+ for (const r of results) {
3345
+ const n = numberOf(r);
3346
+ if (n !== null) return n;
3347
+ }
3348
+ return null;
3349
+ }
3350
+ var MeTTaGrapher = class {
3351
+ container;
3352
+ metta;
3353
+ /** Whether a left-drag on empty canvas pans (part of {@link ControllerHost}). */
3354
+ panOnLeftDrag;
3355
+ graph = new Graph();
3356
+ viewport = initialViewport();
3357
+ selection = /* @__PURE__ */ new Set();
3358
+ primaryId = null;
3359
+ labels = /* @__PURE__ */ new Map();
3360
+ /** Per-node overlays (color, highlight, label) driven by MeTTa directives in the `&grapher` space. */
3361
+ viz = /* @__PURE__ */ new Map();
3362
+ vizFocus = /* @__PURE__ */ new Set();
3363
+ vizBound = false;
3364
+ /** The `&grapher` directives from the last loaded source, kept so `toSource` can round-trip them under a
3365
+ * comment even though they are not drawn as nodes. */
3366
+ vizDirectives = [];
3367
+ renderer;
3368
+ controller;
3369
+ changeCbs = /* @__PURE__ */ new Set();
3370
+ sharedSpace;
3371
+ program = null;
3372
+ programDirty = true;
3373
+ // Each state is a frontier: the set of terms currently reducing. A nondeterministic step widens it, so
3374
+ // every branch is shown at once. Deterministic traces stay one term wide.
3375
+ trace = null;
3376
+ traceIndex = 0;
3377
+ traceGraph = null;
3378
+ /** Which view is showing: the node graph, or the nested-block ("block") view. */
3379
+ viewMode = "graph";
3380
+ block;
3381
+ /** The rendered SVG element (part of {@link ControllerHost}). */
3382
+ get svg() {
3383
+ return this.renderer.svg;
3384
+ }
3385
+ constructor(container, opts = {}) {
3386
+ this.container = container;
3387
+ if (container.style.position === "") container.style.position = "relative";
3388
+ this.sharedSpace = opts.metta !== void 0;
3389
+ this.metta = opts.metta ?? new MeTTa2();
3390
+ this.panOnLeftDrag = opts.panOnLeftDrag ?? false;
3391
+ this.renderer = new Renderer(container);
3392
+ this.controller = new Controller(this);
3393
+ this.block = new BlockView(
3394
+ container,
3395
+ () => this.evalSpace(),
3396
+ () => this.notifyBlock()
3397
+ );
3398
+ if (opts.source !== void 0) this.graph = fromSource(opts.source);
3399
+ this.render();
3400
+ }
3401
+ /** The space to evaluate against. A provided engine is used as-is (a shared, persistent space);
3402
+ * otherwise the whole canvas is loaded into a fresh space so its rules and facts are active, rebuilt
3403
+ * lazily when the graph has changed. */
3404
+ evalSpace() {
3405
+ if (this.sharedSpace) {
3406
+ if (!this.vizBound) {
3407
+ bindVizSpace(this.metta);
3408
+ this.vizBound = true;
3409
+ }
3410
+ return this.metta;
3411
+ }
3412
+ if (this.program === null || this.programDirty) {
3413
+ this.program = loadProgram(this.graph);
3414
+ bindVizSpace(this.program);
3415
+ this.programDirty = false;
3416
+ }
3417
+ return this.program;
3418
+ }
3419
+ /** Redraw from the current state. During a playthrough the reduction graph is shown, without the
3420
+ * editing selection or evaluation labels. */
3421
+ render() {
3422
+ if (this.viewMode === "block") {
3423
+ this.block.render();
3424
+ return;
3425
+ }
3426
+ if (this.traceGraph !== null) {
3427
+ this.renderer.showTrace(this.traceState(), false);
3428
+ return;
3429
+ }
3430
+ this.renderer.render({
3431
+ graph: this.graph,
3432
+ viewport: this.viewport,
3433
+ selection: this.selection,
3434
+ labels: this.labels,
3435
+ primaryId: this.primaryId,
3436
+ viz: this.viz
3437
+ });
3438
+ }
3439
+ /** The render state for a playthrough step: the reduction graph, without editing chrome. */
3440
+ traceState() {
3441
+ return {
3442
+ graph: this.traceGraph ?? this.graph,
3443
+ viewport: this.viewport,
3444
+ selection: NO_SELECTION,
3445
+ labels: NO_LABELS,
3446
+ primaryId: null,
3447
+ viz: NO_VIZ
3448
+ };
3449
+ }
3450
+ /** Notify onChange subscribers that the graph changed. */
3451
+ changed() {
3452
+ this.programDirty = true;
3453
+ for (const cb of this.changeCbs) cb(this.graph);
3454
+ }
3455
+ /** Completion candidates for the node-creation input, including the program's own defined symbols. */
3456
+ completions(prefix) {
3457
+ return completionsFor(prefix, this.evalSpace());
3458
+ }
3459
+ /** Evaluate every head the node belongs to and label each with its result. */
3460
+ evaluate(nodeId) {
3461
+ const space = this.evalSpace();
3462
+ for (const head of this.graph.findHeads(nodeId)) this.labelHead(head, space);
3463
+ this.applyViz(space);
3464
+ this.render();
3465
+ this.focusViz();
3466
+ }
3467
+ /** Evaluate every head in the graph. */
3468
+ evaluateAll() {
3469
+ const space = this.evalSpace();
3470
+ for (const head of this.graph.heads()) this.labelHead(head, space);
3471
+ this.applyViz(space);
3472
+ this.render();
3473
+ this.focusViz();
3474
+ }
3475
+ /** Read the `&grapher` space's directives and turn them into per-node overlays and a focus set. */
3476
+ applyViz(space) {
3477
+ this.viz.clear();
3478
+ this.vizFocus.clear();
3479
+ const skip = this.directiveNodeIds();
3480
+ const { directives, mappers, background } = readViz(space);
3481
+ const sizeVals = [];
3482
+ const shadeVals = [];
3483
+ for (const d of directives) {
3484
+ for (const id of this.matchNodes(d.target, skip)) {
3485
+ if (d.kind === "color" && d.arg !== void 0) this.mergeViz(id, { color: colorOf(d.arg) });
3486
+ else if (d.kind === "highlight") this.mergeViz(id, { highlight: true });
3487
+ else if (d.kind === "label" && d.arg !== void 0)
3488
+ this.mergeViz(id, { label: textOf(d.arg) });
3489
+ else if (d.kind === "focus") this.vizFocus.add(id);
3490
+ else if (d.kind === "size" && d.value !== void 0) sizeVals.push([id, d.value]);
3491
+ else if (d.kind === "shade" && d.value !== void 0) shadeVals.push([id, d.value]);
3492
+ }
3493
+ }
3494
+ for (const m of mappers) this.applyMapper(space, m);
3495
+ for (const [id, s] of normalizeRange(sizeVals, 0.8, 2)) this.mergeViz(id, { sizeScale: s });
3496
+ for (const [id, t] of normalizeRange(shadeVals, 0, 1))
3497
+ this.mergeViz(id, { color: heatColor(t) });
3498
+ if ([...this.viz.values()].some((o) => o.sizeScale !== void 0))
3499
+ layout(this.graph, { scaleOf: (node) => this.viz.get(node.id)?.sizeScale ?? 1 });
3500
+ this.renderer.setBackground(background ?? CANVAS_BG);
3501
+ }
3502
+ mergeViz(id, patch) {
3503
+ this.viz.set(id, { ...this.viz.get(id), ...patch });
3504
+ }
3505
+ /** Apply one `(shade-by FUNC)` / `(size-by FUNC)` mapper: value each node by evaluating `(FUNC node)`, then
3506
+ * normalize the values across the graph and colour or size the nodes that produced a number. */
3507
+ applyMapper(space, m) {
3508
+ const vals = [];
3509
+ for (const node of this.graph.nodes.values()) {
3510
+ const atom = composeAtom(this.graph, node.id);
3511
+ if (atom === null) continue;
3512
+ let results;
3513
+ try {
3514
+ results = space.evaluateAtom(E5(m.func, atom));
3515
+ } catch {
3516
+ continue;
3517
+ }
3518
+ const v = firstNumber(results);
3519
+ if (v !== null) vals.push([node.id, v]);
3520
+ }
3521
+ if (m.property === "shade")
3522
+ for (const [id, t] of normalizeRange(vals, 0, 1)) this.mergeViz(id, { color: heatColor(t) });
3523
+ else for (const [id, s] of normalizeRange(vals, 0.8, 2)) this.mergeViz(id, { sizeScale: s });
3524
+ }
3525
+ /** The nodes a directive target names: by node name, else by the atom the node composes to. Nodes that
3526
+ * are part of a `&grapher` directive expression are skipped, so `(color (fact 5) red)` colors the query
3527
+ * node and not the `(fact 5)` written inside the directive itself. */
3528
+ matchNodes(target2, skip) {
3529
+ const t = target2.toString();
3530
+ const out = [];
3531
+ for (const node of this.graph.nodes.values()) {
3532
+ if (skip.has(node.id)) continue;
3533
+ if (node.name === t) {
3534
+ out.push(node.id);
3535
+ continue;
3536
+ }
3537
+ const atom = composeAtom(this.graph, node.id);
3538
+ if (atom !== null && atom.toString() === t) out.push(node.id);
3539
+ }
3540
+ return out;
3541
+ }
3542
+ /** Every node inside a head that operates on `&grapher` (`(add-atom &grapher …)` and friends): the
3543
+ * directives' own plumbing, which should not be matched or colored. */
3544
+ directiveNodeIds() {
3545
+ const skip = /* @__PURE__ */ new Set();
3546
+ const collect = (id) => {
3547
+ if (skip.has(id)) return;
3548
+ skip.add(id);
3549
+ for (const c of this.graph.childrenOf(id)) collect(c);
3550
+ };
3551
+ for (const head of this.graph.heads())
3552
+ if (this.graph.sortedChildren(head.id)[0]?.name === "&grapher") collect(head.id);
3553
+ return skip;
3554
+ }
3555
+ /** Frame the focused nodes (graph view), if any directive asked to focus. */
3556
+ focusViz() {
3557
+ if (this.viewMode !== "graph" || this.vizFocus.size === 0) return;
3558
+ const nodes = [...this.vizFocus].map((id) => this.graph.nodes.get(id)).filter((n) => n !== void 0);
3559
+ if (nodes.length === 0) return;
3560
+ const xs = nodes.map((n) => n.x);
3561
+ const ys = nodes.map((n) => n.y);
3562
+ const minX = Math.min(...xs) - 80;
3563
+ const maxX = Math.max(...xs) + 80;
3564
+ const minY = Math.min(...ys) - 60;
3565
+ const maxY = Math.max(...ys) + 60;
3566
+ const w = this.container.clientWidth || 800;
3567
+ const h = this.container.clientHeight || 440;
3568
+ const pad = 48;
3569
+ const fit = Math.min((w - pad * 2) / (maxX - minX), (h - pad * 2) / (maxY - minY));
3570
+ const scale = Math.min(1.8, Math.max(0.2, fit));
3571
+ this.viewport = {
3572
+ scale,
3573
+ panX: (w - (minX + maxX) * scale) / 2,
3574
+ panY: (h - (minY + maxY) * scale) / 2
3575
+ };
3576
+ this.render();
3577
+ }
3578
+ /** Evaluate one head and store its result label, unless the head is inert: a definition `(= ...)` or any
3579
+ * atom that reduces to itself (a fact, a type declaration). Only genuine queries get a label. */
3580
+ labelHead(head, space) {
3581
+ if (head.name === "=") return;
3582
+ const atom = composeAtom(this.graph, head.id);
3583
+ if (atom === null) return;
3584
+ const result = evaluateHead(this.graph, head.id, space);
3585
+ if (result.atoms.length === 1 && result.atoms[0].equals(atom)) return;
3586
+ this.labels.set(head.id, { text: result.label, error: result.error });
3587
+ }
3588
+ /** Load a graph from JSON. */
3589
+ load(json) {
3590
+ this.graph = fromJson(json);
3591
+ this.afterLoad();
3592
+ }
3593
+ /** Load a graph by parsing MeTTa source. A top-level `(style ...)` block, and any explicit `(add-atom
3594
+ * &grapher ...)`, are the stylesheet, not content: their directives are run into the isolated `&grapher`
3595
+ * space, kept out of the drawn graph, and their overlays painted onto the content. */
3596
+ loadSource(src) {
3597
+ const directives = [];
3598
+ const content = [];
3599
+ for (const a of parseProgram(src)) {
3600
+ const styled = styleBlockDirectives(a);
3601
+ if (styled !== null) directives.push(...styled);
3602
+ else if (referencesGrapher(a)) directives.push(a);
3603
+ else content.push(a);
3604
+ }
3605
+ this.graph = atomToGraph(content);
3606
+ this.afterLoad();
3607
+ if (directives.length === 0) return;
3608
+ this.vizDirectives = directives.map(bareDirective);
3609
+ const space = this.evalSpace();
3610
+ for (const d of directives) {
3611
+ const bare = bareDirective(d);
3612
+ if (bare === d && referencesGrapher(d)) space.run(`!${d.toString()}`);
3613
+ else space.run(`!(add-atom ${VIZ_SPACE} ${bare.toString()})`);
3614
+ }
3615
+ this.applyViz(space);
3616
+ this.render();
3617
+ this.focusViz();
3618
+ }
3619
+ /** Load a program from atoms (for example built with the eDSL), laid out as tidy trees. */
3620
+ loadAtoms(atoms) {
3621
+ this.graph = atomToGraph([...atoms]);
3622
+ this.afterLoad();
3623
+ }
3624
+ /** Recolor the block view with a different palette. */
3625
+ setBlockPalette(palette) {
3626
+ this.block.setPalette(palette);
3627
+ }
3628
+ /** Snapshot the graph to JSON. */
3629
+ save() {
3630
+ return toJson(this.graph);
3631
+ }
3632
+ /** Render the graph as MeTTa source. */
3633
+ toSource() {
3634
+ const program = toSource(this.graph);
3635
+ if (this.vizDirectives.length === 0) return program;
3636
+ const lines = this.vizDirectives.map((d) => ` ${d.toString()}`).join("\n");
3637
+ return `${program}
3638
+
3639
+ (style
3640
+ ${lines})`;
3641
+ }
3642
+ /** Compose the graph's heads into atoms. */
3643
+ atoms() {
3644
+ return graphToAtoms(this.graph);
3645
+ }
3646
+ /** Re-run the tidy tree layout, frame the result, and redraw. */
3647
+ tidy() {
3648
+ layout(this.graph);
3649
+ this.fitView();
3650
+ }
3651
+ /** Zoom around the canvas center by `factor` (above 1 zooms in, below 1 zooms out). Works in both views. */
3652
+ zoomBy(factor) {
3653
+ if (this.viewMode === "block") {
3654
+ this.block.zoomBy(factor);
3655
+ return;
3656
+ }
3657
+ const w = this.container.clientWidth || 800;
3658
+ const h = this.container.clientHeight || 440;
3659
+ this.viewport = zoomAt(this.viewport, w / 2, h / 2, factor);
3660
+ this.render();
3661
+ }
3662
+ /** Pan the view by a screen-space delta (for on-screen pan controls). Works in both views. */
3663
+ panBy(dx, dy) {
3664
+ if (this.viewMode === "block") {
3665
+ this.block.panBy(dx, dy);
3666
+ return;
3667
+ }
3668
+ this.viewport = pan(this.viewport, dx, dy);
3669
+ this.render();
3670
+ }
3671
+ /** Frame the whole program: reset the block view's zoom/pan, or fit the graph. */
3672
+ fitView(padding = 48) {
3673
+ if (this.viewMode === "block") {
3674
+ this.block.fitView();
3675
+ return;
3676
+ }
3677
+ const vp = this.fitViewportFor(this.traceGraph ?? this.graph, padding);
3678
+ if (vp === null) return;
3679
+ this.viewport = vp;
3680
+ this.render();
3681
+ }
3682
+ /** The viewport that frames `graph` in the container, or null when it has no nodes. */
3683
+ fitViewportFor(graph, padding = 48) {
3684
+ const nodes = [...graph.nodes.values()];
3685
+ if (nodes.length === 0) return null;
3686
+ const xs = nodes.map((n) => n.x);
3687
+ const ys = nodes.map((n) => n.y);
3688
+ const minX = Math.min(...xs) - 60;
3689
+ const maxX = Math.max(...xs) + 60;
3690
+ const minY = Math.min(...ys) - 40;
3691
+ const maxY = Math.max(...ys) + 50;
3692
+ const w = this.container.clientWidth || 800;
3693
+ const h = this.container.clientHeight || 440;
3694
+ const fit = Math.min((w - padding * 2) / (maxX - minX), (h - padding * 2) / (maxY - minY));
3695
+ const scale = Math.min(1.5, Math.max(0.2, fit));
3696
+ return {
3697
+ scale,
3698
+ panX: (w - (minX + maxX) * scale) / 2,
3699
+ panY: (h - (minY + maxY) * scale) / 2
3700
+ };
3701
+ }
3702
+ /** Start a step-by-step reduction playthrough of a head (the given node, or the current query target).
3703
+ * While tracing, the canvas shows each reduction state; step with {@link traceForward} /
3704
+ * {@link traceBack} and leave with {@link stopTrace}. */
3705
+ playTrace(nodeId) {
3706
+ const targetId = nodeId ?? this.traceTarget();
3707
+ if (targetId === void 0) return;
3708
+ const head = this.graph.findHeads(targetId)[0];
3709
+ if (head === void 0) return;
3710
+ const atom = composeAtom(this.graph, head.id);
3711
+ if (atom === null) return;
3712
+ const space = this.evalSpace();
3713
+ this.trace = withSilhouettes(reduceTrace(atom, space), space);
3714
+ this.traceIndex = 0;
3715
+ this.showTraceStep();
3716
+ }
3717
+ /** The live morph span, mirrored here so the GIF exporters pace their frames off what the screen plays. */
3718
+ traceMs = DEFAULT_TRACE_MS;
3719
+ /** How long each step's morph takes (ms). Lower it to speed the animation up, raise it to slow it down so
3720
+ * the reduction is easy to follow. The GIF exporters default to the same span. */
3721
+ setTraceDuration(ms) {
3722
+ this.traceMs = Math.max(1, ms);
3723
+ this.renderer.setTraceDuration(ms);
3724
+ }
3725
+ /** Advance the playthrough by one reduction. */
3726
+ traceForward() {
3727
+ if (this.trace === null || this.traceIndex >= this.trace.length - 1) return;
3728
+ this.traceIndex++;
3729
+ this.showTraceStep();
3730
+ }
3731
+ /** Step the playthrough back one reduction. */
3732
+ traceBack() {
3733
+ if (this.trace === null || this.traceIndex <= 0) return;
3734
+ this.traceIndex--;
3735
+ this.showTraceStep();
3736
+ }
3737
+ /** Jump the playthrough back to the first state, to replay it from the start. */
3738
+ traceRestart() {
3739
+ if (this.trace === null) return;
3740
+ this.traceIndex = 0;
3741
+ this.showTraceStep();
3742
+ }
3743
+ /** Leave the playthrough and return to the editable graph (or interactive blocks). */
3744
+ stopTrace() {
3745
+ this.trace = null;
3746
+ this.traceGraph = null;
3747
+ this.renderer.clearTrace();
3748
+ if (this.viewMode === "block") this.block.setAtoms(graphToAtoms(this.graph));
3749
+ else this.fitView();
3750
+ this.notifyView();
3751
+ }
3752
+ /** Whether a playthrough is active (part of {@link ControllerHost}, which pauses editing while true). */
3753
+ isTracing() {
3754
+ return this.trace !== null;
3755
+ }
3756
+ /** The current playthrough position (0-based step and total states), or null when not tracing. */
3757
+ traceInfo() {
3758
+ return this.trace === null ? null : { index: this.traceIndex, total: this.trace.length };
3759
+ }
3760
+ /** A snapshot of the whole view state for the host UI. The editor is the single source of truth: the
3761
+ * host reads this (and subscribes with {@link onViewChange}) instead of tracking which view is showing
3762
+ * or whether a playthrough is active on its own, so the two can never disagree. */
3763
+ uiState() {
3764
+ return {
3765
+ viewMode: this.viewMode,
3766
+ tracing: this.traceInfo(),
3767
+ blockCanBack: this.viewMode === "block" && this.block.canStepBack()
3768
+ };
3769
+ }
3770
+ showTraceStep() {
3771
+ if (this.trace === null) return;
3772
+ const frontier = this.trace[this.traceIndex];
3773
+ if (this.viewMode === "block") this.block.showReadonly(frontier);
3774
+ else {
3775
+ this.traceGraph = atomToGraph(frontier);
3776
+ this.viewport = this.fitViewportFor(this.traceGraph) ?? this.viewport;
3777
+ this.renderer.showTrace(this.traceState(), true);
3778
+ }
3779
+ this.notifyView();
3780
+ }
3781
+ /** Switch between the node-graph view and the nested-block view. */
3782
+ setViewMode(mode) {
3783
+ if (mode === this.viewMode) return;
3784
+ this.trace = null;
3785
+ this.traceGraph = null;
3786
+ this.renderer.clearTrace();
3787
+ this.viewMode = mode;
3788
+ if (mode === "block") {
3789
+ this.renderer.svg.style.display = "none";
3790
+ this.block.setAtoms(graphToAtoms(this.graph));
3791
+ this.block.show();
3792
+ } else {
3793
+ this.block.hide();
3794
+ this.renderer.svg.style.display = "block";
3795
+ this.render();
3796
+ this.fitView();
3797
+ }
3798
+ this.notifyView();
3799
+ }
3800
+ /** Reduce the block view's selected term one step in place. */
3801
+ blockReduce() {
3802
+ return this.block.reduceSelected();
3803
+ }
3804
+ /** Step the block view back to before its last in-place reduction. */
3805
+ blockBack() {
3806
+ return this.block.back();
3807
+ }
3808
+ /** Whether the block view can step back. */
3809
+ blockCanStepBack() {
3810
+ return this.block.canStepBack();
3811
+ }
3812
+ /** The block view's program as source, reflecting its edits and reductions. */
3813
+ blockSource() {
3814
+ return this.block.sourceText();
3815
+ }
3816
+ /** The reduction states to animate for a GIF: the active playthrough's if one is running, else those
3817
+ * computed for the last query. Null when there is nothing (no target, or fewer than two states). */
3818
+ traceStates() {
3819
+ if (this.trace !== null) return this.trace.length < 2 ? null : this.trace;
3820
+ const targetId = this.traceTarget();
3821
+ if (targetId === void 0) return null;
3822
+ const head = this.graph.findHeads(targetId)[0];
3823
+ if (head === void 0) return null;
3824
+ const atom = composeAtom(this.graph, head.id);
3825
+ if (atom === null) return null;
3826
+ const states = reduceTrace(atom, this.evalSpace());
3827
+ return states.length < 2 ? null : states;
3828
+ }
3829
+ /** Encode the current query's reduction as an animated GIF of the block view, using a caller-supplied
3830
+ * encoder (gifenc) so the package carries no GIF dependency. The morph pacing defaults to the live
3831
+ * trace duration, so the GIF plays what the screen plays. Returns null when there is no reduction. */
3832
+ async exportReductionGif(lib, opts) {
3833
+ const states = this.traceStates();
3834
+ return states === null ? null : this.block.exportGif(states, lib, { morphMs: this.traceMs, ...opts });
3835
+ }
3836
+ /** Encode the current reduction as a GIF of the node-graph view alone (companion to the block GIF; stack
3837
+ * the two to show both). Paced like the live view. Returns null when there is nothing to animate. */
3838
+ async exportGraphReductionGif(lib, opts) {
3839
+ const states = this.traceStates();
3840
+ return states === null ? null : graphReductionGif(states, lib, { morphMs: this.traceMs, ...opts });
3841
+ }
3842
+ /** Export the current reduction as one GIF that plays it in the graph and the block views side by side.
3843
+ * Paced like the live view. Returns null when there is nothing to animate. */
3844
+ async exportSideBySideGif(lib, opts) {
3845
+ const states = this.traceStates();
3846
+ return states === null ? null : this.block.exportSideBySideGif(states, lib, { morphMs: this.traceMs, ...opts });
3847
+ }
3848
+ blockCbs = /* @__PURE__ */ new Set();
3849
+ /** Subscribe to block-view changes (an edit or reduction); returns an unsubscribe function. */
3850
+ onBlockChange(cb) {
3851
+ this.blockCbs.add(cb);
3852
+ return () => this.blockCbs.delete(cb);
3853
+ }
3854
+ notifyBlock() {
3855
+ for (const cb of this.blockCbs) cb();
3856
+ }
3857
+ viewCbs = /* @__PURE__ */ new Set();
3858
+ /** Subscribe to view-state changes: a view switch or any playthrough transition. The host reads
3859
+ * {@link uiState} in the callback. Returns an unsubscribe function. */
3860
+ onViewChange(cb) {
3861
+ this.viewCbs.add(cb);
3862
+ return () => this.viewCbs.delete(cb);
3863
+ }
3864
+ notifyView() {
3865
+ for (const cb of this.viewCbs) cb();
3866
+ }
3867
+ /** The head to trace: the selected node's head, else the last query head (not a `=`/`:` definition). */
3868
+ traceTarget() {
3869
+ if (this.primaryId !== null) return this.primaryId;
3870
+ const queries = this.graph.heads().filter((h) => h.name !== "=" && h.name !== ":");
3871
+ return queries[queries.length - 1]?.id;
3872
+ }
3873
+ /** Subscribe to graph changes; returns an unsubscribe function. */
3874
+ onChange(cb) {
3875
+ this.changeCbs.add(cb);
3876
+ return () => this.changeCbs.delete(cb);
3877
+ }
3878
+ /** Detach listeners and remove the SVG. */
3879
+ destroy() {
3880
+ this.controller.destroy();
3881
+ this.block.destroy();
3882
+ this.renderer.svg.remove();
3883
+ }
3884
+ afterLoad() {
3885
+ this.trace = null;
3886
+ this.traceGraph = null;
3887
+ this.renderer.clearTrace();
3888
+ this.selection.clear();
3889
+ this.labels.clear();
3890
+ this.viz.clear();
3891
+ this.vizFocus.clear();
3892
+ this.vizDirectives = [];
3893
+ this.primaryId = null;
3894
+ this.programDirty = true;
3895
+ if (this.viewMode === "block") this.block.setAtoms(graphToAtoms(this.graph));
3896
+ else this.render();
3897
+ this.changed();
3898
+ this.notifyView();
3899
+ }
3900
+ };
3901
+
3902
+ // src/dsl.ts
3903
+ import "@mettascript/hyperon";
3904
+ function toPalette(p) {
3905
+ if (p === "site") return SITE_PALETTE;
3906
+ if (p === "teal") return TEAL_PALETTE;
3907
+ return p;
3908
+ }
3909
+ function grapher(target2, opts = {}) {
3910
+ const el2 = typeof target2 === "string" ? document.querySelector(target2) : target2;
3911
+ if (el2 === null) throw new Error(`grapher: no element matches ${JSON.stringify(target2)}`);
3912
+ const g = new MeTTaGrapher(el2, opts);
3913
+ const handle = {
3914
+ grapher: g,
3915
+ load(source) {
3916
+ g.loadSource(source);
3917
+ return handle;
3918
+ },
3919
+ atoms(atoms) {
3920
+ g.loadAtoms(atoms);
3921
+ return handle;
3922
+ },
3923
+ graph() {
3924
+ g.setViewMode("graph");
3925
+ return handle;
3926
+ },
3927
+ blocks() {
3928
+ g.setViewMode("block");
3929
+ return handle;
3930
+ },
3931
+ palette(palette) {
3932
+ g.setBlockPalette(toPalette(palette));
3933
+ return handle;
3934
+ },
3935
+ fit() {
3936
+ g.tidy();
3937
+ return handle;
3938
+ },
3939
+ evaluate() {
3940
+ g.evaluateAll();
3941
+ return handle;
3942
+ },
3943
+ play() {
3944
+ g.playTrace();
3945
+ return handle;
3946
+ },
3947
+ source() {
3948
+ return g.viewMode === "block" ? g.blockSource() : g.toSource();
3949
+ },
3950
+ gif(encoder, opts2) {
3951
+ return g.exportReductionGif(encoder, opts2);
3952
+ },
3953
+ destroy() {
3954
+ g.destroy();
3955
+ }
3956
+ };
3957
+ return handle;
3958
+ }
3959
+ export {
3960
+ BlockView,
3961
+ CUTOFF,
3962
+ Controller,
3963
+ Graph,
3964
+ MeTTaGrapher,
3965
+ NODE_H,
3966
+ Renderer,
3967
+ SITE_PALETTE,
3968
+ TEAL_PALETTE,
3969
+ VIZ_SPACE,
3970
+ atomToGraph,
3971
+ bindVizSpace,
3972
+ blockReductionSvgs,
3973
+ colorFor,
3974
+ colorOf,
3975
+ completionsFor,
3976
+ composeAtom,
3977
+ displayText,
3978
+ encodeSvgAnimation,
3979
+ evaluateHead,
3980
+ evaluateHeadAsync,
3981
+ fromJson,
3982
+ fromSource,
3983
+ graphReductionGif,
3984
+ graphReductionSvgs,
3985
+ graphToAtoms,
3986
+ grapher,
3987
+ initialViewport,
3988
+ layout,
3989
+ layoutAtom,
3990
+ loadProgram,
3991
+ makeSettings,
3992
+ nodeWidth,
3993
+ pan,
3994
+ parseLeaf,
3995
+ parseProgram,
3996
+ placeProgram,
3997
+ pulledPointsToPath,
3998
+ readViz,
3999
+ reduceStep,
4000
+ reduceTrace,
4001
+ reductionGif,
4002
+ roleOf,
4003
+ roundedBackingPath,
4004
+ roundedRectPath,
4005
+ sideBySideReductionGif,
4006
+ sideBySideReductionSvgs,
4007
+ textOf,
4008
+ toJson,
4009
+ toScreen,
4010
+ toSource,
4011
+ toWorld,
4012
+ variableLinks,
4013
+ zoomAt
4014
+ };