@weasel-js/diagram 1.11.0 → 1.12.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.
Files changed (37) hide show
  1. package/README.md +696 -4
  2. package/dist/chunk-2SYY4W3B.js +2055 -0
  3. package/dist/chunk-2SYY4W3B.js.map +1 -0
  4. package/dist/chunk-5GIP2LA6.js +312 -0
  5. package/dist/chunk-5GIP2LA6.js.map +1 -0
  6. package/dist/chunk-DYDX6EJC.js +3653 -0
  7. package/dist/chunk-DYDX6EJC.js.map +1 -0
  8. package/dist/chunk-GA4BX7SH.js +1431 -0
  9. package/dist/chunk-GA4BX7SH.js.map +1 -0
  10. package/dist/{chunk-NUMMW5BI.js → chunk-R2XB6D32.js} +416 -36
  11. package/dist/chunk-R2XB6D32.js.map +1 -0
  12. package/dist/chunk-WDIE35AK.js +3 -0
  13. package/dist/chunk-WDIE35AK.js.map +1 -0
  14. package/dist/{fromData-GKqRgPFs.d.ts → dataTypes-DHdkzCL8.d.ts} +238 -59
  15. package/dist/effect-audio.d.ts +139 -0
  16. package/dist/effect-audio.js +533 -0
  17. package/dist/effect-audio.js.map +1 -0
  18. package/dist/effect.d.ts +486 -121
  19. package/dist/effect.js +1632 -347
  20. package/dist/effect.js.map +1 -1
  21. package/dist/icons.table.d.ts +6 -0
  22. package/dist/icons.table.js +32 -0
  23. package/dist/icons.table.js.map +1 -0
  24. package/dist/icons.table.json +24 -0
  25. package/dist/index.d.ts +2159 -92
  26. package/dist/index.js +1056 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/layout.d.ts +3 -3
  29. package/dist/layout.entry-B-jJMc50.d.ts +617 -0
  30. package/dist/layout.js +3 -1
  31. package/dist/stageTypes-DvnR8cKQ.d.ts +189 -0
  32. package/dist/{body-2lC7IiIA.d.ts → types-M_ccNAlk.d.ts} +221 -182
  33. package/package.json +28 -7
  34. package/dist/chunk-C4NAI6NV.js +0 -2041
  35. package/dist/chunk-C4NAI6NV.js.map +0 -1
  36. package/dist/chunk-NUMMW5BI.js.map +0 -1
  37. package/dist/layout.entry-BoWFcR4G.d.ts +0 -285
package/dist/effect.js CHANGED
@@ -1,200 +1,398 @@
1
- import { diagramScene, DiagramView, edgeIdOf, LABEL_PLACEMENT, EDGE_DERIVE_PATH, labelCenter } from './chunk-C4NAI6NV.js';
2
- import { SIDE_TRACK_GAP, portSitesOf } from './chunk-NUMMW5BI.js';
1
+ import { DiagramView, reorderContribution, ANYWHERE, isBodyEnd, sequenceSlots } from './chunk-DYDX6EJC.js';
2
+ import './chunk-GA4BX7SH.js';
3
+ import { stageFromType, LOOK_THEME, drawableWires, uniqueStages, shownPorts, valueRowId, NO_VALUE, previewRowId, formatParam, stageStyle, PROBLEM_COLOR, typeColors, kindOf, boxNode, portOn, wireStyle, isControl, wireEdge, sceneStyles, uniqueGroups, wireEdges, exitLabel, splitProblem, checkPipeline, PREVIEW_DENSITY, runOrder, PREVIEW_SIZE, closeBitmaps, previewsOf, paramValues } from './chunk-5GIP2LA6.js';
4
+ export { LOOK_THEME, NO_VALUE, PREVIEW_SIZE, PROBLEM_COLOR, checkPipeline, drawableWires, exitLabel, formatParam, isControl, kindOf, portOn, previewRowId, runOrder, shownPorts, stageFromType, stageRows, stageStyle, typeColors, uniqueGroups, uniqueStages, valueRowId, wireEdges, wireStyle } from './chunk-5GIP2LA6.js';
5
+ import { diagramScene, diagramPages, edgeIdOf, titleIdOf, groupLabelIdOf, nestGroups, groupMinWidth, LABEL_PLACEMENT, EDGE_DERIVE_PATH } from './chunk-2SYY4W3B.js';
6
+ import { portOf, insetOf, diagramNodeOf, DEFAULT_GROUP_INSET, SIDE_TRACK_GAP, widenForMinWidth, rankGaps, portSitesOf, labelCenter } from './chunk-R2XB6D32.js';
3
7
  import { polygonFromPoints } from '@weasel-js/core/math';
4
- import { useMemo, useEffect, useState, useRef } from 'react';
5
- import { bitmapSrc, useLatest, pointAlongPath } from '@weasel-js/core';
8
+ import { forwardRef, useMemo, useRef, useState, useImperativeHandle, useEffect, useCallback, useSyncExternalStore } from 'react';
9
+ import { useLatest, bitmapSrc, ActionDisabledReason, pointAlongPath } from '@weasel-js/core';
6
10
  import { releaseImageSource } from '@weasel-js/core/renderer';
7
11
  import { jsx } from 'react/jsx-runtime';
12
+ import { useLatest as useLatest$1, adoptCssOnce, useVisibleRaf } from '@weasel-js/react';
8
13
 
9
- // src/effect/check.ts
10
- function splitProblem(stage) {
11
- const split = stage.split;
12
- if (split === void 0) return null;
13
- if (!(stage.inputs ?? []).some((p) => p.id === split.input)) return `splits on "${split.input}", which is not one of its inputs`;
14
- if (split.at.length === 0) return "splits with no cuts";
15
- if (!split.at.every(Number.isFinite)) return "has a cut that is not a finite number";
16
- if (split.at.some((cut, i) => i > 0 && cut <= split.at[i - 1])) return "has cuts that are not in ascending order";
17
- const exits = (stage.outputs ?? []).length;
18
- if (exits !== split.at.length + 1) return `has ${split.at.length} cuts and ${exits} outputs; it needs ${split.at.length + 1}`;
19
- return null;
20
- }
21
- function checkPipeline(pipeline) {
22
- const out = [];
23
- const stages = /* @__PURE__ */ new Map();
24
- for (const stage of pipeline.stages) {
25
- if (stages.has(stage.id)) {
26
- out.push({ kind: "duplicate-stage", stage: stage.id, message: `Stage "${stage.id}" appears more than once.` });
27
- continue;
28
- }
29
- stages.set(stage.id, stage);
30
- const seen = /* @__PURE__ */ new Set();
31
- for (const port of [...stage.inputs ?? [], ...stage.outputs ?? []]) {
32
- if (seen.has(port.id)) {
33
- out.push({ kind: "duplicate-port", stage: stage.id, port: port.id, message: `Stage "${stage.id}" has two ports called "${port.id}".` });
34
- }
35
- seen.add(port.id);
36
- }
37
- const split = splitProblem(stage);
38
- if (split !== null) out.push({ kind: "bad-split", stage: stage.id, message: `Stage "${stage.id}" ${split}.` });
14
+ // src/effect/groupEdit.ts
15
+ var groupsOf = (doc) => doc.pipeline.groups ?? [];
16
+ function withGroups(doc, groups, boxes = doc.groupBoxes) {
17
+ const ids = new Set(groups.map((g) => g.id));
18
+ const kept = Object.entries(boxes ?? {}).filter(([id]) => ids.has(id));
19
+ const { groupBoxes: _was, ...rest } = doc;
20
+ return { ...rest, pipeline: { ...doc.pipeline, groups }, ...kept.length > 0 ? { groupBoxes: Object.fromEntries(kept) } : {} };
21
+ }
22
+ function pruneGroups(groups, gone, keep = () => false) {
23
+ const out = new Set(gone);
24
+ let left = [...groups];
25
+ for (; ; ) {
26
+ const emptied = left.filter((g) => g.members.length > 0 && g.members.every((m) => out.has(m)) && !keep(g.id));
27
+ if (emptied.length === 0) return left.map((g) => ({ ...g, members: g.members.filter((m) => !out.has(m)) }));
28
+ for (const g of emptied) out.add(g.id);
29
+ left = left.filter((g) => !emptied.includes(g));
39
30
  }
40
- pipeline.wires.forEach((wire, index) => {
41
- const ends = [["from", "outputs", "inputs"], ["to", "inputs", "outputs"]].map(([end, own, other]) => {
42
- const ref = wire[end];
43
- const stage = stages.get(ref.stage);
44
- if (stage === void 0) {
45
- out.push({ kind: "missing-stage", wire: index, stage: ref.stage, message: `Wire ${index} names stage "${ref.stage}", which does not exist.` });
46
- return null;
47
- }
48
- const port = (stage[own] ?? []).find((p) => p.id === ref.port);
49
- if (port !== void 0) return port;
50
- const wrong = (stage[other] ?? []).some((p) => p.id === ref.port);
51
- out.push(wrong ? { kind: "wrong-direction", wire: index, stage: ref.stage, port: ref.port, message: `Wire ${index} ${end === "from" ? "starts at an input" : "ends at an output"}, "${ref.stage}.${ref.port}".` } : { kind: "missing-port", wire: index, stage: ref.stage, port: ref.port, message: `Wire ${index} names port "${ref.stage}.${ref.port}", which does not exist.` });
52
- return null;
53
- });
54
- const [from, to] = ends;
55
- if (from && to && from.type !== to.type) {
56
- out.push({ kind: "type-mismatch", wire: index, message: `Wire ${index} joins a "${from.type}" output to a "${to.type}" input.` });
57
- }
58
- });
59
- const groupIds = /* @__PURE__ */ new Set();
60
- const groups = (pipeline.groups ?? []).filter((group) => {
61
- const repeat = groupIds.has(group.id);
62
- if (repeat || stages.has(group.id)) {
63
- out.push({
64
- kind: "duplicate-group",
65
- group: group.id,
66
- message: repeat ? `Group "${group.id}" appears more than once.` : `Group "${group.id}" has the id of a stage.`
67
- });
68
- return false;
69
- }
70
- groupIds.add(group.id);
71
- return true;
31
+ }
32
+ var parentOf = (groups, id) => groups.find((g) => g.members.includes(id));
33
+ function nextGroupId(p) {
34
+ const used = /* @__PURE__ */ new Set([...p.stages.map((s) => s.id), ...(p.groups ?? []).map((g) => g.id)]);
35
+ if (!used.has("group")) return "group";
36
+ let n = 2;
37
+ while (used.has(`group-${n}`)) n++;
38
+ return `group-${n}`;
39
+ }
40
+ function groupStages(doc, ids, label) {
41
+ if (ids.length === 0) return doc;
42
+ const id = nextGroupId(doc.pipeline);
43
+ const groups = groupsOf(doc);
44
+ const parents = new Set(ids.map((m) => parentOf(groups, m)?.id));
45
+ const shared = parents.size === 1 ? [...parents][0] : void 0;
46
+ const taken = new Set(ids);
47
+ const made = { id, ...label ? { label } : {}, members: [...ids] };
48
+ const rest = shared === void 0 ? pruneGroups(groups, taken) : groups.map((g) => {
49
+ if (g.id !== shared) return g;
50
+ const at = g.members.findIndex((m) => taken.has(m));
51
+ const members = g.members.filter((m) => !taken.has(m));
52
+ members.splice(at, 0, id);
53
+ return { ...g, members };
72
54
  });
73
- for (const group of groups) {
74
- for (const member of group.members) {
75
- if (stages.has(member) || groupIds.has(member)) continue;
76
- out.push({ kind: "missing-member", group: group.id, message: `Group "${group.id}" names "${member}", which is neither a stage nor a group.` });
55
+ return withGroups(doc, [...rest, made]);
56
+ }
57
+ function ungroup(doc, id) {
58
+ const groups = groupsOf(doc);
59
+ const gone = groups.find((g) => g.id === id);
60
+ if (gone === void 0) return doc;
61
+ const kept = groups.filter((g) => g.id !== id).map((g) => g.members.includes(id) ? { ...g, members: g.members.flatMap((m) => m === id ? gone.members : [m]) } : g);
62
+ return withGroups(doc, kept);
63
+ }
64
+ function renameGroup(doc, id, label) {
65
+ return withGroups(doc, groupsOf(doc).map((g) => {
66
+ if (g.id !== id) return g;
67
+ const { label: _was, ...rest } = g;
68
+ return label === "" ? rest : { ...rest, label };
69
+ }));
70
+ }
71
+ function within(groups, inner, outer) {
72
+ for (let at = inner; at !== void 0; at = parentOf(groups, at)?.id) if (at === outer) return true;
73
+ return false;
74
+ }
75
+ function moveToGroup(doc, ids, group) {
76
+ const groups = groupsOf(doc);
77
+ if (group !== null && (!groups.some((g) => g.id === group) || ids.some((id) => within(groups, group, id)))) return doc;
78
+ const moving = new Set(ids);
79
+ const boxed = (id) => id === group || doc.groupBoxes?.[id] !== void 0;
80
+ const left = pruneGroups(groups, moving, boxed);
81
+ const placed = group === null ? left : left.map((g) => g.id === group ? { ...g, members: [...g.members, ...ids] } : g);
82
+ return withGroups(doc, placed);
83
+ }
84
+ function setGroupBox(doc, id, box) {
85
+ const { [id]: _was, ...rest } = doc.groupBoxes ?? {};
86
+ return withGroups(doc, groupsOf(doc), box === null ? rest : { ...rest, [id]: { x: box.x, y: box.y, width: box.width, height: box.height } });
87
+ }
88
+ function groupContents(doc, id) {
89
+ const groups = new Map(groupsOf(doc).map((g) => [g.id, g]));
90
+ const out = { stages: /* @__PURE__ */ new Set(), groups: /* @__PURE__ */ new Set() };
91
+ const visit = (g) => {
92
+ if (out.groups.has(g)) return;
93
+ out.groups.add(g);
94
+ for (const m of groups.get(g)?.members ?? []) {
95
+ if (groups.has(m)) visit(m);
96
+ else out.stages.add(m);
77
97
  }
78
- }
98
+ };
99
+ if (groups.has(id)) visit(id);
79
100
  return out;
80
101
  }
102
+ function moveGroup(doc, id, offset) {
103
+ const { stages, groups } = groupContents(doc, id);
104
+ const by = (p) => ({ x: p.x + offset.x, y: p.y + offset.y });
105
+ const places = Object.fromEntries(Object.entries(doc.places).map(([s, p]) => [s, stages.has(s) ? by(p) : p]));
106
+ const inside = new Set(doc.pipeline.wires.flatMap((w) => w.id !== void 0 && stages.has(w.from.stage) && stages.has(w.to.stage) ? [w.id] : []));
107
+ const routes = doc.routes === void 0 ? void 0 : Object.fromEntries(Object.entries(doc.routes).map(([w, r]) => [w, inside.has(w) ? r.map(by) : r]));
108
+ const boxes = doc.groupBoxes === void 0 ? void 0 : Object.fromEntries(Object.entries(doc.groupBoxes).map(([g, b]) => [g, groups.has(g) ? { ...b, ...by(b) } : b]));
109
+ return {
110
+ ...doc,
111
+ places,
112
+ ...routes !== void 0 ? { routes } : {},
113
+ ...boxes !== void 0 ? { groupBoxes: boxes } : {}
114
+ };
115
+ }
81
116
 
82
- // src/effect/draw.ts
83
- var PALETTE = ["#6fb3ff", "#c58bff", "#f2c14e", "#5ad07f", "#ff8a6b", "#4fd1c5", "#e58fb8", "#b0bec5"];
84
- var UNTYPED = "#8aa2b5";
85
- var PROBLEM_COLOR = "#ff5d5d";
86
- var NORMAL = { fill: "#16222c", stroke: "#3d566b", text: "#dbe7f2", strokeWidth: 1.5 };
87
- var BYPASSED = { fill: "#121a21", stroke: "#3d566b", text: "#6d8496", strokeWidth: 1.5, dash: [4, 3] };
88
- function typeColors(pipeline, given = {}) {
89
- const colors = /* @__PURE__ */ new Map();
90
- for (const stage of pipeline.stages) {
91
- for (const port of [...stage.inputs ?? [], ...stage.outputs ?? []]) {
92
- if (colors.has(port.type)) continue;
93
- colors.set(port.type, given[port.type] ?? PALETTE[colors.size % PALETTE.length]);
94
- }
95
- }
96
- return (type) => type === void 0 ? UNTYPED : colors.get(type) ?? given[type] ?? UNTYPED;
117
+ // src/effect/lookRoutes.ts
118
+ var EPS = 1e-3;
119
+ var near = (a, b) => Math.abs(a.x - b.x) <= EPS && Math.abs(a.y - b.y) <= EPS;
120
+ var copy = (p) => ({ x: p.x, y: p.y });
121
+ function withLookRoutes(doc, routes) {
122
+ const kept = Object.fromEntries(Object.entries(routes).filter(([, r]) => r !== void 0 && Object.keys(r).length > 0));
123
+ const { lookRoutes: _was, ...rest } = doc;
124
+ return Object.keys(kept).length > 0 ? { ...rest, lookRoutes: kept } : rest;
125
+ }
126
+ function setLookRoute(doc, look, wire, waypoints, ends) {
127
+ const { [wire]: _was, ...rest } = doc.lookRoutes?.[look] ?? {};
128
+ const route = { waypoints: waypoints.map(copy), ends: [copy(ends[0]), copy(ends[1])] };
129
+ return withLookRoutes(doc, { ...doc.lookRoutes, [look]: waypoints.length > 0 ? { ...rest, [wire]: route } : rest });
130
+ }
131
+ function pruneLookRoutes(doc, rewired = []) {
132
+ if (doc.lookRoutes === void 0) return doc;
133
+ const gone = new Set(rewired);
134
+ const ids = new Set(doc.pipeline.wires.flatMap((w) => w.id === void 0 || gone.has(w.id) ? [] : [w.id]));
135
+ const keep = (r) => r === void 0 ? void 0 : Object.fromEntries(Object.entries(r).filter(([id]) => ids.has(id)));
136
+ return withLookRoutes(doc, { rack: keep(doc.lookRoutes.rack), lanes: keep(doc.lookRoutes.lanes) });
137
+ }
138
+ function wireEnds(specs, edge) {
139
+ const spec = specs.find((s) => s.id === edge);
140
+ const trait = spec?.data?.diagram;
141
+ const [from, to] = spec?.dependsOn ?? [];
142
+ const point = (node, port) => {
143
+ const n = node === void 0 || port === void 0 ? void 0 : specs.find((s) => s.id === node);
144
+ return n === void 0 ? void 0 : portOf({ id: node, kind: n.kind ?? "leaf", data: n.data }, n.pose, port)?.point;
145
+ };
146
+ const a = point(from, trait?.from?.port);
147
+ const b = point(to, trait?.to?.port);
148
+ return a === void 0 || b === void 0 ? void 0 : [a, b];
149
+ }
150
+ function applyLookRoutes(specs, doc, look, built) {
151
+ const routes = look === "nodeGraph" ? void 0 : doc.lookRoutes?.[look];
152
+ if (routes === void 0) return specs;
153
+ let held = false;
154
+ const edges = built.data.edges.map((e) => {
155
+ const id = edgeIdOf(e);
156
+ const wire = built.wireOf(id);
157
+ const route = wire === void 0 ? void 0 : routes[wire];
158
+ const ends = route === void 0 ? void 0 : wireEnds(specs, id);
159
+ if (route === void 0 || ends === void 0 || !near(ends[0], route.ends[0]) || !near(ends[1], route.ends[1])) return e;
160
+ held = true;
161
+ return { ...e, waypoints: route.waypoints };
162
+ });
163
+ return held ? diagramScene({ ...built.data, edges }, built.scene) : specs;
97
164
  }
98
- function isControl(pipeline, type) {
99
- return type !== void 0 && pipeline.types?.[type]?.control === true;
165
+
166
+ // src/effect/edit.ts
167
+ var withPipeline = (doc, pipeline) => ({
168
+ ...doc,
169
+ pipeline: { ...doc.pipeline, ...pipeline }
170
+ });
171
+ function nextStageId(p, base) {
172
+ const used = new Set(p.stages.map((s) => s.id));
173
+ if (!used.has(base)) return base;
174
+ let n = 2;
175
+ while (used.has(`${base}-${n}`)) n++;
176
+ return `${base}-${n}`;
177
+ }
178
+ function nextWireId(p) {
179
+ const used = new Set(p.wires.map((w) => w.id));
180
+ let n = 1;
181
+ while (used.has(`w${n}`)) n++;
182
+ return `w${n}`;
183
+ }
184
+ function addStage(doc, stage, at) {
185
+ const next = withPipeline(doc, { stages: [...doc.pipeline.stages, stage] });
186
+ return at ? { ...next, places: { ...doc.places, [stage.id]: { x: at.x, y: at.y } } } : next;
187
+ }
188
+ function removeStages(doc, ids) {
189
+ const gone = new Set(ids);
190
+ const { groups } = doc.pipeline;
191
+ const wires = doc.pipeline.wires.filter((w) => !gone.has(w.from.stage) && !gone.has(w.to.stage));
192
+ const kept = groups ? pruneGroups(groups, gone) : void 0;
193
+ const left = new Set(kept?.map((g) => g.id));
194
+ const boxes = doc.groupBoxes === void 0 ? void 0 : Object.entries(doc.groupBoxes).filter(([id]) => left.has(id));
195
+ const { groupBoxes: _was, ...rest } = doc;
196
+ return pruneRoutes({
197
+ ...rest,
198
+ pipeline: {
199
+ ...doc.pipeline,
200
+ stages: doc.pipeline.stages.filter((s) => !gone.has(s.id)),
201
+ wires,
202
+ ...kept ? { groups: kept } : {}
203
+ },
204
+ places: Object.fromEntries(Object.entries(doc.places).filter(([id]) => !gone.has(id))),
205
+ ...boxes !== void 0 && boxes.length > 0 ? { groupBoxes: Object.fromEntries(boxes) } : {}
206
+ });
100
207
  }
101
- function stageStyle(stage) {
102
- return stage.bypassed === true ? BYPASSED : NORMAL;
208
+ function pruneRoutes(doc, rewired = []) {
209
+ const pruned = pruneLookRoutes(doc, rewired);
210
+ if (pruned.routes === void 0) return pruned;
211
+ const ids = new Set(doc.pipeline.wires.flatMap((w) => w.id === void 0 || rewired.includes(w.id) ? [] : [w.id]));
212
+ return { ...pruned, routes: Object.fromEntries(Object.entries(pruned.routes).filter(([id]) => ids.has(id))) };
103
213
  }
104
- function wireStyle(color, control, problem) {
105
- return {
106
- stroke: problem ? PROBLEM_COLOR : color,
107
- width: 2,
108
- markerEnd: null,
109
- ...control ? { dash: [5, 4] } : {}
110
- };
214
+ function setWireRoute(doc, wire, points) {
215
+ const { [wire]: _was, ...rest } = doc.routes ?? {};
216
+ return { ...doc, routes: points.length > 0 ? { ...rest, [wire]: points.map((p) => ({ x: p.x, y: p.y })) } : rest };
111
217
  }
112
- function uniqueStages(pipeline) {
113
- const seen = /* @__PURE__ */ new Set();
114
- return pipeline.stages.filter((s) => seen.has(s.id) ? false : (seen.add(s.id), true));
218
+ function addWire(doc, wire) {
219
+ const w = wire.id === void 0 ? { ...wire, id: nextWireId(doc.pipeline) } : wire;
220
+ return withPipeline(doc, { wires: [...doc.pipeline.wires, w] });
115
221
  }
116
- function uniqueGroups(pipeline) {
117
- const seen = new Set(pipeline.stages.map((s) => s.id));
118
- return (pipeline.groups ?? []).filter((g) => seen.has(g.id) ? false : (seen.add(g.id), true));
222
+ function removeWires(doc, ids) {
223
+ const gone = new Set(ids);
224
+ return pruneRoutes(withPipeline(doc, { wires: doc.pipeline.wires.filter((w) => w.id === void 0 || !gone.has(w.id)) }));
119
225
  }
120
- function portOn(stage, id) {
121
- return [...stage?.inputs ?? [], ...stage?.outputs ?? []].find((p) => p.id === id);
226
+ function reconnectWire(doc, id, end, to) {
227
+ return withPipeline(doc, {
228
+ wires: doc.pipeline.wires.map((w) => w.id === id ? { ...w, [end]: { stage: to.stage, port: to.port } } : w)
229
+ });
122
230
  }
123
- function kindOf(stage) {
124
- return stage.kind === "decision" || stage.kind === "source" || stage.kind === "sink" ? stage.kind : "stage";
231
+ var addressed = (p, key) => (p.id ?? p.label) === key;
232
+ function setParam(doc, stage, param2, control) {
233
+ return withPipeline(doc, {
234
+ stages: doc.pipeline.stages.map(
235
+ (s) => s.id !== stage || !s.params ? s : { ...s, params: s.params.map((p) => addressed(p, param2) ? { ...p, control, value: formatParam(control) } : p) }
236
+ )
237
+ });
125
238
  }
126
- function drawableWires(pipeline) {
127
- const stages = new Map(uniqueStages(pipeline).map((s) => [s.id, s]));
128
- const flagged = new Set(checkPipeline(pipeline).flatMap((p) => p.wire === void 0 ? [] : [p.wire]));
129
- return pipeline.wires.flatMap((wire, index) => {
130
- const fromStage = stages.get(wire.from.stage);
131
- const toStage = stages.get(wire.to.stage);
132
- const from = portOn(fromStage, wire.from.port);
133
- if (fromStage === void 0 || toStage === void 0 || from === void 0 || portOn(toStage, wire.to.port) === void 0) return [];
134
- return [{ wire, index, type: from.type, problem: flagged.has(index), fromStage, toStage }];
239
+ function moveStages(doc, moves) {
240
+ const places = { ...doc.places };
241
+ for (const m of moves) places[m.id] = { x: m.x, y: m.y };
242
+ return { ...doc, places };
243
+ }
244
+ function setLaneOrder(doc, order) {
245
+ return { ...doc, laneOrder: [...order] };
246
+ }
247
+ function ensureWireIds(doc) {
248
+ if (doc.pipeline.wires.every((w) => w.id !== void 0)) return doc;
249
+ const used = new Set(doc.pipeline.wires.map((w) => w.id));
250
+ let n = 1;
251
+ const wires = doc.pipeline.wires.map((w) => {
252
+ if (w.id !== void 0) return w;
253
+ while (used.has(`w${n}`)) n++;
254
+ const id = `w${n}`;
255
+ used.add(id);
256
+ return { ...w, id };
135
257
  });
258
+ return withPipeline(doc, { wires });
136
259
  }
137
- var NO_VALUE = "\u2013";
138
- var VALUE_ROOM = "00000.000";
139
- var valueRowId = (port) => `value:${port}`;
140
- var previewRowId = (port) => `preview:${port}`;
141
- var PREVIEW_SIZE = 48;
142
- function runRowsOf(o) {
143
- return {
144
- ...o.showValues !== void 0 ? { showValues: o.showValues } : {},
145
- ...o.showPreviews !== void 0 ? { showPreviews: o.showPreviews } : {},
146
- ...o.previewSize !== void 0 ? { previewSize: o.previewSize } : {}
147
- };
260
+ function canWire(p, from, to) {
261
+ if (from.stage === to.stage) return false;
262
+ const out = p.stages.find((s) => s.id === from.stage)?.outputs?.find((x) => x.id === from.port);
263
+ const inn = p.stages.find((s) => s.id === to.stage)?.inputs?.find((x) => x.id === to.port);
264
+ return out !== void 0 && inn !== void 0 && out.type === inn.type;
148
265
  }
149
- function stageRows(stage, opts) {
150
- const rows = [{ kind: "label", text: stage.title, style: { bold: true } }];
151
- const side = (ports) => (ports ?? []).map((p) => ({ id: p.id, label: p.label ?? p.id, type: p.type }));
152
- if (opts.withPorts && ((stage.inputs?.length ?? 0) > 0 || (stage.outputs?.length ?? 0) > 0)) {
153
- rows.push({ kind: "ports", left: side(stage.inputs), right: side(stage.outputs) });
266
+
267
+ // src/effect/mainChain.ts
268
+ function mainChain(pipeline) {
269
+ const next = /* @__PURE__ */ new Map();
270
+ for (const d of drawableWires(pipeline)) {
271
+ if (isControl(pipeline, d.type)) continue;
272
+ next.set(d.wire.from.stage, [...next.get(d.wire.from.stage) ?? [], d.wire.to.stage]);
154
273
  }
155
- const size = opts.previewSize ?? PREVIEW_SIZE;
156
- const preview = typeof size === "number" ? { width: size, height: size } : size;
157
- const seen = /* @__PURE__ */ new Set();
158
- for (const p of stage.outputs ?? []) {
159
- if (seen.has(p.id)) continue;
160
- seen.add(p.id);
161
- if (opts.showValues === true) {
162
- rows.push({ kind: "field", id: valueRowId(p.id), label: p.label ?? p.id, value: NO_VALUE, room: VALUE_ROOM });
274
+ const best = /* @__PURE__ */ new Map();
275
+ const walking = /* @__PURE__ */ new Set();
276
+ const from = (id) => {
277
+ const known = best.get(id);
278
+ if (known !== void 0) return known;
279
+ if (walking.has(id)) return [];
280
+ walking.add(id);
281
+ let tail = [];
282
+ for (const to of next.get(id) ?? []) {
283
+ const path2 = from(to);
284
+ if (path2.length > tail.length && !path2.includes(id)) tail = path2;
163
285
  }
164
- if (opts.showPreviews === true) rows.push({ kind: "slot", id: previewRowId(p.id), ...preview });
286
+ walking.delete(id);
287
+ const path = [id, ...tail];
288
+ best.set(id, path);
289
+ return path;
290
+ };
291
+ let longest = [];
292
+ for (const stage of uniqueStages(pipeline)) {
293
+ const path = from(stage.id);
294
+ if (path.length > longest.length) longest = path;
165
295
  }
166
- if (opts.showParams) for (const p of stage.params ?? []) rows.push({ kind: "field", label: p.label, value: p.value });
167
- return rows;
296
+ return longest;
168
297
  }
169
- function exitLabel(d) {
170
- if (d.wire.label !== void 0) return d.wire.label;
171
- return kindOf(d.fromStage) === "decision" ? portOn(d.fromStage, d.wire.from.port)?.label ?? d.wire.from.port : void 0;
298
+ function skipDecisions(pipeline, line) {
299
+ const wires = drawableWires(pipeline);
300
+ const stages = new Map(uniqueStages(pipeline).map((s) => [s.id, s]));
301
+ const out = [];
302
+ line.forEach((id, i) => {
303
+ const decision = stages.get(id);
304
+ if (decision === void 0 || kindOf(decision) !== "decision" || i === 0) return;
305
+ if ((decision.inputs ?? []).length !== 1 || (decision.outputs ?? []).length !== 2) return;
306
+ const entering = wires.filter((d) => d.wire.to.stage === id);
307
+ const leaving = wires.filter((d) => d.wire.from.stage === id);
308
+ if (entering.length !== 1 || leaving.length !== 2 || entering[0].wire.from.stage !== line[i - 1]) return;
309
+ const through = leaving.find((d) => d.wire.to.stage === line[i + 1]);
310
+ const skip = leaving.find((d) => d !== through && line.indexOf(d.wire.to.stage) > i + 1);
311
+ const ordinary = (s) => s !== void 0 && kindOf(s) !== "decision";
312
+ if (!ordinary(stages.get(line[i - 1])) || !ordinary(through?.toStage) || !ordinary(skip?.toStage)) return;
313
+ if (through !== void 0 && skip !== void 0) out.push({ decision, into: entering[0], through, skip });
314
+ });
315
+ return out;
172
316
  }
173
- function boxNode(stage, opts) {
174
- return {
175
- id: stage.id,
176
- outline: kindOf(stage) === "stage" ? "rect" : "stadium",
177
- rows: stageRows(stage, { showParams: opts.showParams, withPorts: opts.rowPorts, ...runRowsOf(opts.run ?? {}) }),
178
- // Always set: left off, a stage port named n, e, s or w would share its id with a compass default.
179
- ports: opts.ports
180
- };
317
+
318
+ // src/effect/chainEdit.ts
319
+ function chainLinks(pipeline, chain) {
320
+ const at = new Map(chain.map((id, i) => [id, i]));
321
+ const mainIn = /* @__PURE__ */ new Map();
322
+ const mainOut = /* @__PURE__ */ new Map();
323
+ const between2 = Array.from({ length: Math.max(0, chain.length - 1) }, () => void 0);
324
+ for (const d of drawableWires(pipeline)) {
325
+ if (isControl(pipeline, d.type)) continue;
326
+ const a = at.get(d.wire.from.stage);
327
+ if (a === void 0 || at.get(d.wire.to.stage) !== a + 1 || between2[a] !== void 0) continue;
328
+ between2[a] = d.index;
329
+ mainOut.set(d.wire.from.stage, d.wire.from.port);
330
+ mainIn.set(d.wire.to.stage, d.wire.to.port);
331
+ }
332
+ return { mainIn, mainOut, between: between2 };
181
333
  }
182
- function wireEdge(from, to, label) {
183
- return { from: from.stage, fromPort: from.port, to: to.stage, toPort: to.port, ...label !== void 0 ? { label } : {} };
334
+ var portsOf = (p, stage, side) => p.stages.find((s) => s.id === stage)?.[side] ?? [];
335
+ function movedChain(chain, i, j, to) {
336
+ const run = chain.slice(i, j + 1);
337
+ const out = [...chain.slice(0, i), ...chain.slice(j + 1)];
338
+ out.splice(to > j ? to - run.length : to, 0, ...run);
339
+ return out;
184
340
  }
185
- function wireEdges(pipeline, color) {
186
- return drawableWires(pipeline).map((d) => ({
187
- edge: wireEdge(d.wire.from, d.wire.to, exitLabel(d)),
188
- style: wireStyle(color(d.type), isControl(pipeline, d.type), d.problem)
189
- }));
341
+ function moveInChain(doc, stage, to) {
342
+ return moveRunInChain(doc, stage, stage, to);
343
+ }
344
+ function moveRunInChain(doc, first, last, to) {
345
+ const p = doc.pipeline;
346
+ const chain = mainChain(p);
347
+ const i = chain.indexOf(first);
348
+ const j = chain.indexOf(last);
349
+ if (i < 0 || j < i || !Number.isInteger(to) || to < 0 || to > chain.length || to >= i && to <= j + 1) return null;
350
+ const { mainIn, mainOut, between: between2 } = chainLinks(p, chain);
351
+ const wIn = i > 0 ? between2[i - 1] : void 0;
352
+ const wOut = j < chain.length - 1 ? between2[j] : void 0;
353
+ const wAB = to > 0 && to < chain.length ? between2[to - 1] : void 0;
354
+ const gone = new Set([wIn, wOut, wAB].filter((k) => k !== void 0));
355
+ const kept = p.wires.filter((_, k) => !gone.has(k));
356
+ const free = (ref) => !kept.some((w) => w.to.stage === ref.stage && w.to.port === ref.port);
357
+ const pick = (id, side, type) => portsOf(p, id, side).find((port) => (type === void 0 || port.type === type) && (side === "outputs" || free({ stage: id, port: port.id })))?.id;
358
+ const typeOf = (id, port) => [...portsOf(p, id, "inputs"), ...portsOf(p, id, "outputs")].find((x) => x.id === port)?.type;
359
+ const made = [];
360
+ const prev = chain[i - 1];
361
+ const next = chain[j + 1];
362
+ if (prev !== void 0 && next !== void 0) made.push({ reuse: wIn, from: p.wires[wIn].from, to: p.wires[wOut].to });
363
+ const a = chain[to - 1];
364
+ const b = chain[to];
365
+ const sIn = mainIn.get(first) ?? pick(first, "inputs");
366
+ const sOut = mainOut.get(last) ?? pick(last, "outputs");
367
+ if (a !== void 0) {
368
+ const aOut = wAB !== void 0 ? p.wires[wAB].from.port : mainOut.get(a) ?? pick(a, "outputs", typeOf(first, sIn));
369
+ if (aOut === void 0 || sIn === void 0) return null;
370
+ made.push({ reuse: wAB, from: { stage: a, port: aOut }, to: { stage: first, port: sIn } });
371
+ }
372
+ if (b !== void 0) {
373
+ const bIn = wAB !== void 0 ? p.wires[wAB].to.port : mainIn.get(b) ?? pick(b, "inputs", typeOf(last, sOut));
374
+ if (bIn === void 0 || sOut === void 0) return null;
375
+ made.push({ reuse: wOut, from: { stage: last, port: sOut }, to: { stage: b, port: bIn } });
376
+ }
377
+ if (made.some((m) => !canWire(p, m.from, m.to) || !free(m.to))) return null;
378
+ if (new Set(made.map((m) => `${m.to.stage}\0${m.to.port}`)).size !== made.length) return null;
379
+ const wires = p.wires.flatMap((w, k) => {
380
+ if (!gone.has(k)) return [w];
381
+ const m = made.find((x) => x.reuse === k);
382
+ return m === void 0 ? [] : [{ ...w, from: m.from, to: m.to }];
383
+ });
384
+ for (const m of made) {
385
+ if (m.reuse !== void 0) continue;
386
+ wires.push({ id: nextWireId({ ...p, wires }), from: m.from, to: m.to });
387
+ }
388
+ const pipeline = { ...p, wires };
389
+ const want = movedChain(chain, i, j, to);
390
+ const got = mainChain(pipeline);
391
+ if (got.length !== want.length || got.some((id, k) => id !== want[k])) return null;
392
+ return pruneRoutes({ ...doc, pipeline }, [...gone].flatMap((k) => p.wires[k].id ?? []));
190
393
  }
191
- function sceneStyles(stages, edges) {
192
- const nodes = new Map(stages.map((s) => [s.id, stageStyle(s)]));
193
- const wires = new Map(edges.map((e) => [edgeIdOf(e.edge), e.style]));
194
- return {
195
- nodeStyle: (n) => nodes.get(n.id) ?? NORMAL,
196
- edgeStyle: (e) => wires.get(edgeIdOf(e)) ?? wireStyle(UNTYPED, false, false)
197
- };
394
+ function canMoveInChain(doc, stage, to) {
395
+ return moveInChain(doc, stage, to) !== null;
198
396
  }
199
397
 
200
398
  // src/effect/symbolParts.ts
@@ -219,11 +417,11 @@ function alongLine(points, t) {
219
417
  return { u: points[0][0], v: points[0][1] };
220
418
  }
221
419
  var placeAlong = (sides) => (ports, side) => ports.map((p, i) => ({ id: p.id, type: p.type, at: alongLine(sides[side], (i + 1) / (ports.length + 1)) }));
222
- function inputs(stage, pipeline, options, steer, place2 = spread) {
420
+ function inputs(stage, pipeline, options, steer, place3 = spread) {
223
421
  const all = stage.inputs ?? [];
224
422
  const steering = all.filter((p) => isControl(pipeline, p.type) || p.id === steer && all.length > 1);
225
423
  const side = options?.controlSide === "bottom" ? "bottom" : "top";
226
- return [...place2(all.filter((p) => !steering.includes(p)), "left"), ...place2(steering, side)];
424
+ return [...place3(all.filter((p) => !steering.includes(p)), "left"), ...place3(steering, side)];
227
425
  }
228
426
  var title = (stage, options, gap = 4) => options?.controlSide === "bottom" ? { text: stage.title, at: { u: 0.5, v: 0 }, self: { u: 0.5, v: 1 }, offset: { x: 0, y: -gap } } : { text: stage.title, at: { u: 0.5, v: 1 }, self: { u: 0.5, v: 0 }, offset: { x: 0, y: gap } };
229
427
  var DIAMOND_SIDES = placeAlong({
@@ -482,11 +680,11 @@ function sidePorts(stage) {
482
680
  ];
483
681
  }
484
682
  var untyped = (ports) => (ports ?? []).map(({ type: _type, ...port }) => port);
485
- function nodeOf(stage, pipeline, branch) {
683
+ function nodeOf(stage, pipeline, branch, theme) {
486
684
  if (kindOf(stage) !== "decision") {
487
685
  return { ...boxNode(stage, { showParams: false, rowPorts: false, ports: sidePorts(stage) }), padding: COMPACT_PADDING };
488
686
  }
489
- const symbol = branchNode(stage, pipeline, branch);
687
+ const symbol = branchNode(stage, pipeline, branch, { style: stageStyle(stage, theme) });
490
688
  return { id: stage.id, ...symbol, ports: untyped(symbol.ports), padding: symbol.padding ?? COMPACT_PADDING };
491
689
  }
492
690
  function lines(pipeline) {
@@ -499,9 +697,10 @@ function lines(pipeline) {
499
697
  return [...by.values()];
500
698
  }
501
699
  var compact = (pipeline, options = {}) => {
502
- const color = typeColors(pipeline, options.colors);
700
+ const theme = options.theme ?? LOOK_THEME;
701
+ const color = typeColors(pipeline, options.colors, theme.series);
503
702
  const stages = uniqueStages(pipeline);
504
- const nodes = stages.map((stage) => nodeOf(stage, pipeline, options.branch ?? "hexagon"));
703
+ const nodes = stages.map((stage) => nodeOf(stage, pipeline, options.branch ?? "hexagon", theme));
505
704
  const edges = lines(pipeline).map((line) => {
506
705
  const first = line.find((d) => !isControl(pipeline, d.type)) ?? line[0];
507
706
  return {
@@ -515,7 +714,7 @@ var compact = (pipeline, options = {}) => {
515
714
  layout: "layered",
516
715
  layoutOptions: { direction: "right", nodeGap: options.nodeGap ?? 16, rankGap: options.rankGap ?? 48 },
517
716
  router: options.router ?? "bezier",
518
- ...sceneStyles(stages, edges)
717
+ ...sceneStyles(stages, edges, theme)
519
718
  },
520
719
  color
521
720
  };
@@ -523,9 +722,10 @@ var compact = (pipeline, options = {}) => {
523
722
 
524
723
  // src/effect/nodeGraph.ts
525
724
  var nodeGraph = (pipeline, options = {}) => {
526
- const color = typeColors(pipeline, options.colors);
725
+ const theme = options.theme ?? LOOK_THEME;
726
+ const color = typeColors(pipeline, options.colors, theme.series);
527
727
  const stages = uniqueStages(pipeline);
528
- const nodes = stages.map((stage) => kindOf(stage) === "decision" ? { id: stage.id, ...branchNode(stage, pipeline, options.branch) } : boxNode(stage, { showParams: options.showParams !== false, rowPorts: true, ports: [], run: options }));
728
+ const nodes = stages.map((stage) => kindOf(stage) === "decision" ? { id: stage.id, ...branchNode(stage, pipeline, options.branch, { style: stageStyle(stage, theme) }) } : boxNode(stage, { showParams: options.showParams !== false, rowPorts: true, ports: [], run: options }));
529
729
  const edges = wireEdges(pipeline, color);
530
730
  return {
531
731
  data: { nodes, edges: edges.map((e) => e.edge), groups: uniqueGroups(pipeline) },
@@ -537,12 +737,26 @@ var nodeGraph = (pipeline, options = {}) => {
537
737
  ...options.rankGap !== void 0 ? { rankGap: options.rankGap } : {}
538
738
  },
539
739
  router: options.router ?? "bezier",
540
- ...sceneStyles(stages, edges)
740
+ ...sceneStyles(stages, edges, theme)
541
741
  },
542
742
  color
543
743
  };
544
744
  };
745
+ function useReleaseReplaced(run) {
746
+ const shown = useRef(/* @__PURE__ */ new Set());
747
+ useEffect(() => {
748
+ const now = new Set(Object.values(run?.stages ?? {}).flatMap((s) => Object.values(s.previews ?? {})));
749
+ for (const old of shown.current) if (!now.has(old)) releaseImageSource(old);
750
+ shown.current = now;
751
+ }, [run]);
752
+ useEffect(() => () => {
753
+ for (const old of shown.current) releaseImageSource(old);
754
+ shown.current = /* @__PURE__ */ new Set();
755
+ }, []);
756
+ }
545
757
  var DIM = 0.3;
758
+ var AT_COLOR = "#ffc94d";
759
+ var AT_WIDTH = 3;
546
760
  var COST_GAP = 2;
547
761
  var COST_SCALE = 0.8;
548
762
  function paintRun(pipeline, specs, run, exits = /* @__PURE__ */ new Map()) {
@@ -552,7 +766,7 @@ function paintRun(pipeline, specs, run, exits = /* @__PURE__ */ new Map()) {
552
766
  const owner = ownerOf(stages);
553
767
  const passed = /* @__PURE__ */ new Set();
554
768
  for (const spec of specs) {
555
- const ends = wireEnds(spec);
769
+ const ends = wireEnds2(spec);
556
770
  if (ends === null) continue;
557
771
  const exit = exits.get(spec.id) ?? { stage: ends.from, port: ends.fromPort };
558
772
  const taken = run.stages[exit.stage]?.taken;
@@ -563,7 +777,7 @@ function paintRun(pipeline, specs, run, exits = /* @__PURE__ */ new Map()) {
563
777
  const values = /* @__PURE__ */ new Map();
564
778
  const previews = /* @__PURE__ */ new Map();
565
779
  for (const [id, stage] of stages) {
566
- for (const p of stage.outputs ?? []) {
780
+ for (const p of shownPorts(stage)) {
567
781
  values.set(`${id}/${valueRowId(p.id)}`, `${p.label ?? p.id}: ${run.stages[id]?.values?.[p.id] ?? NO_VALUE}`);
568
782
  const preview = run.stages[id]?.previews?.[p.id];
569
783
  if (preview !== void 0) previews.set(`${id}/${previewRowId(p.id)}`, preview);
@@ -585,12 +799,16 @@ function paintRun(pipeline, specs, run, exits = /* @__PURE__ */ new Map()) {
585
799
  const dependsOn = spec.dependsOn;
586
800
  const onWire = Array.isArray(dependsOn) && dependsOn.length === 1 && passed.has(dependsOn[0]);
587
801
  if (stage !== void 0 && unreached.has(stage) || passed.has(id) || onWire) next = { ...next, data: dimmed(next.data) };
802
+ if (id === run.at && next.data.stroke !== void 0) {
803
+ next = { ...next, data: { ...next.data, stroke: { paint: { color: AT_COLOR }, width: AT_WIDTH } } };
804
+ }
588
805
  return next;
589
806
  });
590
807
  for (const spec of specs) {
591
808
  const id = spec.id;
592
- const cost = stages.has(id) ? run.stages[id]?.cost : void 0;
593
- if (cost !== void 0) out.push(costSpec(spec, specs.filter((s) => owner(s) === id), stages.get(id), cost));
809
+ const entry = stages.has(id) ? run.stages[id] : void 0;
810
+ const note = entry?.error ?? (entry?.cost !== void 0 ? costText(entry.cost) : void 0);
811
+ if (note !== void 0) out.push(costSpec(spec, specs.filter((s) => owner(s) === id), stages.get(id), note, entry?.error !== void 0));
594
812
  }
595
813
  return out;
596
814
  }
@@ -609,7 +827,7 @@ function ownerOf(stages) {
609
827
  return anchor !== void 0 && stages.has(anchor) ? anchor : void 0;
610
828
  };
611
829
  }
612
- function wireEnds(spec) {
830
+ function wireEnds2(spec) {
613
831
  const trait = spec.data?.diagram;
614
832
  const deps = spec.dependsOn;
615
833
  if (trait?.from === void 0 || trait.to === void 0 || !Array.isArray(deps) || deps.length !== 2) return null;
@@ -633,13 +851,12 @@ function dimmed(data) {
633
851
  ...d.stroke !== void 0 ? { stroke: fade(d.stroke) } : {}
634
852
  };
635
853
  }
636
- function costSpec(stage, own, of, cost) {
854
+ function costSpec(stage, own, of, text, problem) {
637
855
  const title2 = own.find((s) => s !== stage && s.data.text !== void 0);
638
856
  const titleData = title2?.data ?? {};
639
857
  const fontSize = Math.round((titleData.style?.fontSize ?? 12) * COST_SCALE);
640
858
  const style = { fontFamily: titleData.style?.fontFamily ?? "sans-serif", fontSize };
641
- const fill = titleData.fill ?? { paint: { color: stageStyle(of).text } };
642
- const text = costText(cost);
859
+ const fill = problem ? { paint: { color: PROBLEM_COLOR } } : titleData.fill ?? { paint: { color: stageStyle(of).text } };
643
860
  const width = text.length * fontSize * 0.6;
644
861
  const height = Math.ceil(fontSize * 1.2);
645
862
  const box = stage.pose;
@@ -660,6 +877,19 @@ function costText(ms) {
660
877
  if (ms >= 100) return `${Math.round(ms)} ms`;
661
878
  return `${Number(ms.toPrecision(3))} ms`;
662
879
  }
880
+ function useRunPortOptions(color, pipeline, run) {
881
+ const missed = JSON.stringify([...unreachedStages(pipeline, run)].sort());
882
+ return useMemo(() => {
883
+ const faded = new Set(JSON.parse(missed));
884
+ return {
885
+ shape: "circle",
886
+ sizePx: 8,
887
+ // Every port a look declares has a value type; the compass defaults have none.
888
+ showsPort: (port) => port.type !== void 0,
889
+ fill: (port) => ({ paint: { color: color(port.type), ...faded.has(port.nodeId) ? { opacity: DIM } : {} } })
890
+ };
891
+ }, [color, missed]);
892
+ }
663
893
  var EFFECT_MIN_SCALE = 0.5;
664
894
  var keys = /* @__PURE__ */ new WeakMap();
665
895
  var nextKey = 0;
@@ -686,18 +916,6 @@ function useSameContent(value) {
686
916
  setKept({ value, content });
687
917
  return value;
688
918
  }
689
- function useReleaseReplaced(run) {
690
- const shown = useRef(/* @__PURE__ */ new Set());
691
- useEffect(() => {
692
- const now = new Set(Object.values(run?.stages ?? {}).flatMap((s) => Object.values(s.previews ?? {})));
693
- for (const old of shown.current) if (!now.has(old)) releaseImageSource(old);
694
- shown.current = now;
695
- }, [run]);
696
- useEffect(() => () => {
697
- for (const old of shown.current) releaseImageSource(old);
698
- shown.current = /* @__PURE__ */ new Set();
699
- }, []);
700
- }
701
919
  function EffectChart({
702
920
  pipeline: given,
703
921
  run,
@@ -724,17 +942,7 @@ function EffectChart({
724
942
  const specs = diagramScene(drawn.data, drawn.scene).map((s) => s.pickable === false || stageIds.has(s.id) ? s : { ...s, pickable: false });
725
943
  return { specs, color: drawn.color, exits: drawn.exits };
726
944
  }, [pipeline, look, options, stageIds]);
727
- const missed = JSON.stringify([...unreachedStages(pipeline, run)].sort());
728
- const portOptions = useMemo(() => {
729
- const faded = new Set(JSON.parse(missed));
730
- return {
731
- shape: "circle",
732
- sizePx: 8,
733
- // Every port a look declares has a value type; the compass defaults have none.
734
- showsPort: (port) => port.type !== void 0,
735
- fill: (port) => ({ paint: { color: built.color(port.type), ...faded.has(port.nodeId) ? { opacity: DIM } : {} } })
736
- };
737
- }, [built, missed]);
945
+ const portOptions = useRunPortOptions(built.color, pipeline, run);
738
946
  const painted = useMemo(() => paintRun(pipeline, built.specs, run, built.exits), [pipeline, built, run]);
739
947
  useReleaseReplaced(run);
740
948
  return /* @__PURE__ */ jsx(
@@ -755,17 +963,331 @@ function EffectChart({
755
963
  keyOf(built)
756
964
  );
757
965
  }
966
+
967
+ // src/effect/groupHandlers.ts
968
+ var parentGroup = (doc, id) => doc.pipeline.groups?.find((g) => g.members.includes(id))?.id;
969
+ function dragEdit(doc, built, reports, boxOf) {
970
+ let next = doc;
971
+ let resized = false;
972
+ let touched = false;
973
+ const carried = /* @__PURE__ */ new Set();
974
+ const reparent = (id, parent) => {
975
+ if (parent === void 0) return;
976
+ if (parent === null || built.groupOf(parent) !== void 0) next = moveToGroup(next, [id], parent);
977
+ };
978
+ const depth = (id) => {
979
+ let n = 0;
980
+ for (let at = parentGroup(doc, id); at !== void 0; at = parentGroup(doc, at)) n++;
981
+ return n;
982
+ };
983
+ for (const edit of [...reports.boxes].sort((a, b) => depth(a.id) - depth(b.id))) {
984
+ const group = built.groupOf(edit.id);
985
+ const was = boxOf(edit.id);
986
+ if (group === void 0 || was === void 0 || carried.has(group)) continue;
987
+ touched = true;
988
+ reparent(group, edit.parent);
989
+ if (edit.box.width === was.width && edit.box.height === was.height) {
990
+ next = moveGroup(next, group, { x: edit.box.x - was.x, y: edit.box.y - was.y });
991
+ const inside = groupContents(doc, group);
992
+ for (const id of [...inside.stages, ...inside.groups]) carried.add(id);
993
+ } else {
994
+ next = setGroupBox(next, group, edit.box);
995
+ resized = true;
996
+ }
997
+ }
998
+ const moves = reports.moves.filter((m) => built.stageOf(m.id) !== void 0 && !carried.has(m.id));
999
+ for (const m of moves) reparent(m.id, m.parent);
1000
+ if (moves.length > 0) {
1001
+ next = moveStages(next, moves.map(({ id, x, y }) => ({ id, x, y })));
1002
+ touched = true;
1003
+ }
1004
+ return touched ? { doc: next, label: resized && moves.length === 0 ? "Resize group" : "Move" } : null;
1005
+ }
1006
+
1007
+ // src/effect/editorHandlers.ts
1008
+ function orient(p, a, b) {
1009
+ if (canWire(p, a, b)) return { from: a, to: b };
1010
+ if (canWire(p, b, a)) return { from: b, to: a };
1011
+ return null;
1012
+ }
1013
+ var fed = (p, to, except) => p.wires.some((w) => w.to.stage === to.stage && w.to.port === to.port && (except === void 0 || w.id !== except));
1014
+ function wireFor(p, a, b, except) {
1015
+ const wire = orient(p, a, b);
1016
+ return wire !== null && !fed(p, wire.to, except) ? wire : null;
1017
+ }
1018
+ function pinShown(doc, specs) {
1019
+ const shown = Object.fromEntries([...stageBoxes(doc, specs)].map(([id, b]) => [id, { x: b.x, y: b.y }]));
1020
+ return { ...doc, places: { ...shown, ...doc.places } };
1021
+ }
1022
+ function stageBoxes(doc, specs) {
1023
+ const ids = new Set(doc.pipeline.stages.map((s) => s.id));
1024
+ return new Map(specs.filter((s) => ids.has(s.id)).map((s) => [s.id, s.pose]));
1025
+ }
1026
+ function editHandlers(now, unpick, rename = () => {
1027
+ }) {
1028
+ const commit = (label, edit) => {
1029
+ const { doc, built, onChange } = now();
1030
+ const d = ensureWireIds(doc);
1031
+ const next = edit(d, built);
1032
+ if (next !== null) onChange(next, label);
1033
+ };
1034
+ const drag = { moves: [], boxes: [] };
1035
+ let held = false;
1036
+ const hold = () => {
1037
+ if (held) return;
1038
+ held = true;
1039
+ queueMicrotask(() => {
1040
+ held = false;
1041
+ const reports = { moves: drag.moves.splice(0), boxes: drag.boxes.splice(0) };
1042
+ const { doc, built, specs, onChange } = now();
1043
+ const boxes = new Map(specs.map((s) => [s.id, s.pose]));
1044
+ const out = dragEdit(pinShown(ensureWireIds(doc), specs), built, reports, (id) => boxes.get(id));
1045
+ if (out !== null) onChange(out.doc, out.label);
1046
+ });
1047
+ };
1048
+ const canConnect = (from, to, moving) => {
1049
+ if (isBodyEnd(from) || isBodyEnd(to)) return false;
1050
+ const { doc, built } = now();
1051
+ const except = moving === void 0 ? void 0 : built.wireOf(moving.edge);
1052
+ return wireFor(doc.pipeline, { stage: from.nodeId, port: from.id }, { stage: to.nodeId, port: to.id }, except) !== null;
1053
+ };
1054
+ return {
1055
+ canConnect,
1056
+ onConnect: (e) => commit("Connect", (d) => {
1057
+ if (e.fromPort === void 0 || e.toPort === void 0) return null;
1058
+ const wire = wireFor(d.pipeline, { stage: e.from, port: e.fromPort }, { stage: e.to, port: e.toPort });
1059
+ return wire === null ? null : addWire(d, wire);
1060
+ }),
1061
+ onReconnect: (c) => commit("Reconnect", (d, b) => {
1062
+ const id = b.wireOf(c.edge);
1063
+ const wire = d.pipeline.wires.find((w) => w.id === id);
1064
+ if (id === void 0 || wire === void 0 || c.port === void 0) return null;
1065
+ const moved = { stage: c.node, port: c.port };
1066
+ const [from, to] = c.end === "from" ? [moved, wire.to] : [wire.from, moved];
1067
+ return canWire(d.pipeline, from, to) && !fed(d.pipeline, to, id) ? reconnectWire(d, id, c.end, moved) : null;
1068
+ }),
1069
+ onEditRoute: (c) => commit("Edit route", (d, b) => {
1070
+ const id = b.wireOf(c.edge);
1071
+ const { look, specs } = now();
1072
+ if (id === void 0) return null;
1073
+ if (look === "nodeGraph") return setWireRoute(d, id, c.waypoints);
1074
+ if (c.waypoints.length === 0) {
1075
+ const was = d.lookRoutes?.[look]?.[id];
1076
+ return was === void 0 ? null : setLookRoute(d, look, id, [], was.ends);
1077
+ }
1078
+ const ends = wireEnds(specs, c.edge);
1079
+ return ends === void 0 ? null : setLookRoute(d, look, id, c.waypoints, ends);
1080
+ }),
1081
+ onDelete: (r) => commit("Delete", (d, b) => {
1082
+ unpick();
1083
+ const stages = r.nodes.flatMap((n) => b.stageOf(n) ?? []);
1084
+ const wires = r.edges.flatMap((e) => b.wireOf(e) ?? []);
1085
+ return stages.length === 0 && wires.length === 0 ? null : removeWires(removeStages(d, stages), wires);
1086
+ }),
1087
+ onMove: (moves) => {
1088
+ drag.moves.push(...moves);
1089
+ hold();
1090
+ },
1091
+ onEditContainer: (edit) => {
1092
+ if (edit.kind === "title") {
1093
+ const group = now().built.groupOf(edit.id);
1094
+ if (group !== void 0) rename(group);
1095
+ } else if (edit.kind === "box") {
1096
+ drag.boxes.push(edit);
1097
+ hold();
1098
+ }
1099
+ }
1100
+ };
1101
+ }
1102
+ var isPart = (built, id) => built.stageOf(id) !== void 0 || built.groupOf(id) !== void 0;
1103
+ function useEditorPicks(built, given) {
1104
+ const [stage, setStage] = useState(null);
1105
+ const [group, setGroup] = useState(null);
1106
+ const [edge, setEdge] = useState(null);
1107
+ const latest = useLatest$1({ built, given });
1108
+ const pick = useCallback((id) => {
1109
+ const { built: b, given: g } = latest.current;
1110
+ const s = id === null ? void 0 : b.stageOf(id);
1111
+ const k = id === null ? void 0 : b.groupOf(id);
1112
+ setStage(s ?? null);
1113
+ setGroup(k ?? null);
1114
+ setEdge(id !== null && s === void 0 && k === void 0 && b.wireOf(id) !== void 0 ? id : null);
1115
+ g.onSelect?.(s ?? null);
1116
+ g.onSelectGroup?.(k ?? null);
1117
+ }, [latest]);
1118
+ const unpick = useCallback(() => setEdge(null), []);
1119
+ const shownStage = given.selected !== void 0 ? given.selected : stage;
1120
+ const shownGroup = given.selectedGroup !== void 0 ? given.selectedGroup : group;
1121
+ const wire = edge === null || built.wireOf(edge) === void 0 ? null : edge;
1122
+ const controlled = given.selected !== void 0 || given.selectedGroup !== void 0;
1123
+ const live = (id) => id !== null && isPart(built, id) ? id : null;
1124
+ const shown = controlled ? live(shownStage) ?? live(shownGroup) ?? wire : void 0;
1125
+ return { pick, unpick, shown };
1126
+ }
1127
+ var FIELD_RULE = ":where([data-wzl-group-title]) { width: 16ch; font: inherit; font-weight: 700; text-align: center; }";
1128
+ var TOP = { side: "top", align: "center" };
1129
+ var adoptFieldRule = (el) => {
1130
+ if (el === null) return;
1131
+ adoptCssOnce(el, FIELD_RULE);
1132
+ el.focus();
1133
+ el.select();
1134
+ };
1135
+ function useGroupTitle(doc, look, renaming, setRenaming, onChange) {
1136
+ const latest = useLatest$1({ doc, onChange });
1137
+ const group = renaming === null ? void 0 : doc.pipeline.groups?.find((g) => g.id === renaming);
1138
+ const id = group?.id;
1139
+ const was = group?.label ?? "";
1140
+ const over = id === void 0 ? void 0 : look === "nodeGraph" ? titleIdOf(id) : was ? groupLabelIdOf(id) : id;
1141
+ return useMemo(() => {
1142
+ if (id === void 0 || over === void 0) return null;
1143
+ const done = (text) => {
1144
+ setRenaming(null);
1145
+ if (text !== null && text !== was) latest.current.onChange(renameGroup(latest.current.doc, id, text), "Rename group");
1146
+ };
1147
+ const placement = over === id ? TOP : "center";
1148
+ return [{ target: over, placement, content: /* @__PURE__ */ jsx(GroupTitleField, { initial: was, onDone: done }, id) }];
1149
+ }, [id, was, over, setRenaming, latest]);
1150
+ }
1151
+ function GroupTitleField({ initial, onDone }) {
1152
+ const [text, setText] = useState(initial);
1153
+ const [done, setDone] = useState(false);
1154
+ const finish = (value) => {
1155
+ if (done) return;
1156
+ setDone(true);
1157
+ onDone(value);
1158
+ };
1159
+ return /* @__PURE__ */ jsx(
1160
+ "input",
1161
+ {
1162
+ ref: adoptFieldRule,
1163
+ "data-wzl-group-title": "",
1164
+ "aria-label": "Group name",
1165
+ value: text,
1166
+ onChange: (e) => setText(e.target.value),
1167
+ onKeyDown: (e) => {
1168
+ e.stopPropagation();
1169
+ if (e.key === "Enter") finish(text.trim());
1170
+ else if (e.key === "Escape") finish(null);
1171
+ },
1172
+ onBlur: () => finish(text.trim())
1173
+ }
1174
+ );
1175
+ }
1176
+
1177
+ // src/effect/laneColumns.ts
1178
+ function laneColumns(stages, wires, spans, groups, titles = /* @__PURE__ */ new Map()) {
1179
+ const order = new Map(stages.map((s, i) => [s.id, i]));
1180
+ const nested = nestGroups(groups, (id) => order.has(id));
1181
+ const byId = new Map(nested.map((g) => [g.id, g]));
1182
+ const grouped = new Set(nested.flatMap((g) => g.members));
1183
+ const first = (id) => Math.min(...(byId.get(id)?.members ?? [id]).map((m) => order.get(m)));
1184
+ const itemsOf = (ids, depth) => [...ids].sort((a, b) => first(a) - first(b)).map((id) => {
1185
+ const g = byId.get(id);
1186
+ if (g === void 0) return { stages: [id] };
1187
+ const inside = place(itemsOf(g.direct, depth + 1), wires, spans);
1188
+ const minWidth = titles.has(g.id) ? groupMinWidth(titles.get(g.id), DEFAULT_GROUP_INSET) : 0;
1189
+ const own = { group: g.id, first: 0, last: inside.width - 1, depth, minWidth };
1190
+ return { stages: g.members, inner: { ...inside, blocks: [own, ...inside.blocks] } };
1191
+ });
1192
+ const top = [...stages.map((s) => s.id).filter((id) => !grouped.has(id)), ...nested.filter((g) => g.parent === void 0).map((g) => g.id)];
1193
+ const { column, blocks } = place(itemsOf(top, 0), wires, spans);
1194
+ return { column, blocks };
1195
+ }
1196
+ function place(items, wires, spans) {
1197
+ const owner = /* @__PURE__ */ new Map();
1198
+ items.forEach((item, k) => item.stages.forEach((id) => owner.set(id, k)));
1199
+ const feeds = items.map(() => /* @__PURE__ */ new Set());
1200
+ for (const { wire: { from, to } } of wires) {
1201
+ const [a, b] = [owner.get(from.stage), owner.get(to.stage)];
1202
+ if (a !== void 0 && b !== void 0 && a !== b) feeds[b].add(from.stage);
1203
+ }
1204
+ const column = /* @__PURE__ */ new Map();
1205
+ const blocks = [];
1206
+ const held = [];
1207
+ const whole = /* @__PURE__ */ new Set();
1208
+ const fits = (item, c) => {
1209
+ if (item.inner === void 0) {
1210
+ const span = spans.get(item.stages[0]);
1211
+ return !whole.has(c) && !held[c]?.some(([lo, hi]) => lo <= span[1] && span[0] <= hi);
1212
+ }
1213
+ for (let j = c; j < c + item.inner.width; j++) if (whole.has(j) || (held[j]?.length ?? 0) > 0) return false;
1214
+ return true;
1215
+ };
1216
+ const left = items.map((_, k) => k);
1217
+ while (left.length > 0) {
1218
+ const i = Math.max(0, left.findIndex((k2) => [...feeds[k2]].every((f) => column.has(f))));
1219
+ const k = left.splice(i, 1)[0];
1220
+ const item = items[k];
1221
+ let c = Math.max(0, ...[...feeds[k]].flatMap((f) => column.has(f) ? [column.get(f) + 1] : []));
1222
+ while (!fits(item, c)) c++;
1223
+ if (item.inner === void 0) {
1224
+ (held[c] ??= []).push(spans.get(item.stages[0]));
1225
+ column.set(item.stages[0], c);
1226
+ continue;
1227
+ }
1228
+ for (let j = c; j < c + item.inner.width; j++) whole.add(j);
1229
+ for (const [id, at] of item.inner.column) column.set(id, c + at);
1230
+ for (const b of item.inner.blocks) blocks.push({ ...b, first: b.first + c, last: b.last + c });
1231
+ }
1232
+ return { width: Math.max(0, ...column.values()) + (column.size > 0 ? 1 : 0), column, blocks };
1233
+ }
1234
+ function columnLefts(widths, column, blocks, gap, start, extent) {
1235
+ const count = widths.length;
1236
+ const room = blockRoom(widths.length, column, blocks, gap);
1237
+ const leftsNow = (now) => {
1238
+ let x = start;
1239
+ return Array.from(widths, (wd, c) => {
1240
+ x += now[c];
1241
+ const at = x;
1242
+ x += wd ?? 0;
1243
+ return at;
1244
+ });
1245
+ };
1246
+ const byId = new Map(blocks.map((b) => [b.group, b]));
1247
+ const span = (group, now) => {
1248
+ const b = byId.get(group);
1249
+ const lefts2 = leftsNow(now);
1250
+ let [lo, hi] = [Infinity, -Infinity];
1251
+ for (const [id, c] of column) {
1252
+ if (c >= b.first && c <= b.last) [lo, hi] = [Math.min(lo, lefts2[c] + extent(id).lo), Math.max(hi, lefts2[c] + extent(id).hi)];
1253
+ }
1254
+ return { lo, hi };
1255
+ };
1256
+ const inset = { lo: DEFAULT_GROUP_INSET, hi: DEFAULT_GROUP_INSET };
1257
+ widenForMinWidth([...blocks].sort((p, q) => q.depth - p.depth).map((b) => ({
1258
+ id: b.group,
1259
+ minWidth: b.minWidth,
1260
+ inset,
1261
+ children: blocks.filter((k) => k.depth === b.depth + 1 && k.first >= b.first && k.last <= b.last).map((k) => k.group),
1262
+ before: b.first,
1263
+ after: b.last + 1
1264
+ })), room, span);
1265
+ const lefts = leftsNow(room);
1266
+ return { lefts, end: count === 0 ? start : lefts[count - 1] + (widths[count - 1] ?? 0) + room[count] };
1267
+ }
1268
+ function blockRoom(count, column, blocks, gap) {
1269
+ if (blocks.length === 0) return Array.from({ length: count + 1 }, (_, c) => c === 0 || c === count ? 0 : gap);
1270
+ const rows = Array.from({ length: count + 2 }, () => []);
1271
+ for (const [id, c] of column) rows[c + 1].push({ id });
1272
+ const inside = (b) => [...column].filter(([, c]) => c >= b.first && c <= b.last).map(([id]) => id);
1273
+ const groups = blocks.map((b) => {
1274
+ const parent = blocks.find((p) => p.depth === b.depth - 1 && p.first <= b.first && b.last <= p.last);
1275
+ return { id: b.group, members: inside(b), inset: insetOf(DEFAULT_GROUP_INSET), ...parent !== void 0 ? { parent: parent.group } : {} };
1276
+ });
1277
+ const gaps = rankGaps({ groups }, rows, { rank: "x", cross: "y", sign: 1 }, gap);
1278
+ return gaps.map((g, c) => c === 0 || c === count ? g - gap : g);
1279
+ }
758
1280
  var upright = (n) => n !== null && Math.abs(n.y) > Math.abs(n.x);
759
1281
  var facingY = (end, y) => [end, ...end.sites ?? []].find((s) => s.normal !== null && Math.sign(s.normal.y) === Math.sign(y - s.point.y) && upright(s.normal)) ?? end;
760
1282
  var TRACK_GAP = SIDE_TRACK_GAP;
761
1283
  var CLEAR = 3;
762
1284
  var covers = (boxes, y, to = y) => boxes.some((b) => b.y - CLEAR < Math.max(y, to) && Math.min(y, to) < b.y + b.height + CLEAR);
763
1285
  var laneAt = (grid, y) => Math.max(0, grid.edges.filter((e) => e <= y).length - 1);
764
- function corridor(grid, columns, near) {
1286
+ function corridor(grid, columns, near2) {
765
1287
  const ink = columns.flatMap((c) => grid.ink[c] ?? []);
766
- const middle = laneAt(grid, near) + 0.5;
767
- const clear = grid.edges.flatMap((y, k) => covers(ink, y) ? [] : [k]);
768
- return clear.reduce((best, k) => Math.abs(k - middle) < Math.abs(best - middle) ? k : best, clear[0] ?? 0);
1288
+ const middle = laneAt(grid, near2) + 0.5;
1289
+ const clear2 = grid.edges.flatMap((y, k) => covers(ink, y) ? [] : [k]);
1290
+ return clear2.reduce((best, k) => Math.abs(k - middle) < Math.abs(best - middle) ? k : best, clear2[0] ?? 0);
769
1291
  }
770
1292
  var between = (a, b) => Array.from({ length: Math.abs(b - a) + 1 }, (_, i) => a + Math.sign(b - a) * i);
771
1293
  function laneCourse(grid, from, to) {
@@ -859,18 +1381,30 @@ var overlaps = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y
859
1381
  function clearLabel(path, size, offset, keepOut) {
860
1382
  for (const side of [offset, -offset]) {
861
1383
  for (const t of [0.5, 0.4, 0.6, 0.3, 0.7, 0.2, 0.8, 0.1, 0.9]) {
862
- const near = pointAlongPath(path, t)?.point;
863
- const center = near === void 0 ? null : labelCenter(path, { at: { near }, offset: side }, size);
864
- if (near === void 0 || center === null) continue;
1384
+ const near2 = pointAlongPath(path, t)?.point;
1385
+ const center = near2 === void 0 ? null : labelCenter(path, { at: { near: near2 }, offset: side }, size);
1386
+ if (near2 === void 0 || center === null) continue;
865
1387
  const box = { x: center.x - size.width / 2, y: center.y - size.height / 2, width: size.width, height: size.height };
866
- if (!keepOut.some((k) => overlaps(box, k))) return { near, offset: side, box };
1388
+ if (!keepOut.some((k) => overlaps(box, k))) return { near: near2, offset: side, box };
867
1389
  }
868
1390
  }
869
1391
  return void 0;
870
1392
  }
871
1393
  var inkOf = (id, specs) => specs.filter((s) => s.id === id || s.parent === id || String(s.id).startsWith(`${id}/mark:`)).map((s) => s.pose);
1394
+ function walledIn(grid, walls) {
1395
+ if (walls.length === 0) return grid;
1396
+ const over = (c) => walls.filter((w) => w.first <= c && c <= w.last);
1397
+ return {
1398
+ edges: grid.edges,
1399
+ ink: grid.ink.map((boxes, c) => boxes.concat(over(c).map((w) => w.box))),
1400
+ columns: grid.columns.map(([lo, hi], c) => over(c).reduce(([l, h], { box, first, last }) => [
1401
+ c === first ? Math.min(l, box.x - CLEAR) : l,
1402
+ c === last ? Math.max(h, box.x + box.width + CLEAR) : h
1403
+ ], [lo, hi]))
1404
+ };
1405
+ }
872
1406
  function routeWires(wires, nodes, laid) {
873
- const specs = diagramScene({ nodes, edges: wires.map((w) => w.edge) }, {
1407
+ const specs = diagramScene({ nodes, edges: wires.map((w) => w.edge), ...laid.groups !== void 0 ? { groups: laid.groups } : {} }, {
874
1408
  layout: "none",
875
1409
  router: laid.router,
876
1410
  ...laid.measure !== void 0 ? { measure: laid.measure } : {}
@@ -883,7 +1417,13 @@ function routeWires(wires, nodes, laid) {
883
1417
  const [site, ...sites] = portSitesOf({ id: stage, data: s.data }, s.pose, port);
884
1418
  return { column: laid.column.get(stage), point: site?.point ?? { x: 0, y: 0 }, normal: site?.normal ?? null, sites };
885
1419
  };
886
- const courses = wires.map(({ edge }) => edge.from === edge.to ? void 0 : laneCourse(grid, end(edge.from, edge.fromPort), end(edge.to, edge.toPort)));
1420
+ const walls = nestGroups(laid.groups ?? [], (id) => ink.has(id)).flatMap((g) => {
1421
+ const box = spec.get(g.id)?.pose;
1422
+ const at = g.members.flatMap((m) => laid.column.has(m) ? [laid.column.get(m)] : []);
1423
+ return box === void 0 || at.length === 0 ? [] : [{ box, first: Math.min(...at), last: Math.max(...at), holds: new Set(g.members) }];
1424
+ });
1425
+ const grids = wires.map(({ edge }) => walledIn(grid, walls.filter((w) => !w.holds.has(edge.from) && !w.holds.has(edge.to))));
1426
+ const courses = wires.map(({ edge }, w) => edge.from === edge.to ? void 0 : laneCourse(grids[w], end(edge.from, edge.fromPort), end(edge.to, edge.toPort)));
887
1427
  const { stretches, tracks } = assignTracks(courses);
888
1428
  const [top, bottom] = [grid.edges[0] ?? 0, grid.edges[grid.edges.length - 1] ?? 0];
889
1429
  const keepOut = [...ink.values()].flat().concat({ x: -1e9, y: top - 1e9, width: 2e9, height: 1e9 }, { x: -1e9, y: bottom, width: 2e9, height: 1e9 });
@@ -894,7 +1434,7 @@ function routeWires(wires, nodes, laid) {
894
1434
  const s = stretches[w].find((t) => t.edge === at.edge && t.lo <= column && column <= t.hi);
895
1435
  return trackY(grid, at.edge, s.track, tracks.get(at.edge));
896
1436
  });
897
- const waypoints = course === void 0 ? void 0 : waypointsOf(grid, course, ys);
1437
+ const waypoints = course === void 0 ? void 0 : waypointsOf(grids[w], course, ys);
898
1438
  return { waypoints, path: routeOf(spec.get(edge.from), edge.fromPort, spec.get(edge.to), edge.toPort, laid.router, waypoints) };
899
1439
  });
900
1440
  const segments = routes.map(({ path }) => {
@@ -910,10 +1450,10 @@ function routeWires(wires, nodes, laid) {
910
1450
  const label = spec.get(`${edgeIdOf(edge)}/label`)?.pose;
911
1451
  const others = keepOut.concat(segments.flatMap((boxes, k) => k === w ? [] : boxes));
912
1452
  const offset = LABEL_PLACEMENT.offset ?? 0;
913
- const clear = path === null || label === void 0 ? void 0 : clearLabel(path, label, offset, others) ?? clearLabel(path, label, offset, keepOut);
914
- if (clear === void 0) return { edge: routed, style };
915
- keepOut.push(clear.box);
916
- return { edge: { ...routed, labelPlacement: { at: { near: clear.near }, offset: clear.offset } }, style };
1453
+ const clear2 = path === null || label === void 0 ? void 0 : clearLabel(path, label, offset, others) ?? clearLabel(path, label, offset, keepOut);
1454
+ if (clear2 === void 0) return { edge: routed, style };
1455
+ keepOut.push(clear2.box);
1456
+ return { edge: { ...routed, labelPlacement: { at: { near: clear2.near }, offset: clear2.offset } }, style };
917
1457
  });
918
1458
  return { edges, tracks };
919
1459
  }
@@ -921,11 +1461,8 @@ function routeWires(wires, nodes, laid) {
921
1461
  // src/effect/lanes.ts
922
1462
  var LANE_INSET = 6;
923
1463
  var MARGIN = 16;
924
- var LANE_STYLES = [
925
- { fill: "rgba(255, 255, 255, 0.035)", stroke: "rgba(255, 255, 255, 0.06)", text: "#9fbdd6", strokeWidth: 1 },
926
- { fill: "rgba(255, 255, 255, 0)", stroke: "rgba(255, 255, 255, 0.06)", text: "#9fbdd6", strokeWidth: 1 }
927
- ];
928
1464
  var laneId = (type) => `~lane:${type}`;
1465
+ var laneTypeOf = (id) => id.startsWith("~lane:") ? id.slice("~lane:".length) : void 0;
929
1466
  function laneTypes(stages, first = []) {
930
1467
  const used = [...new Set(stages.flatMap((s) => [...s.inputs ?? [], ...s.outputs ?? []].map((p) => p.type)))];
931
1468
  const lead = [...new Set(first)].filter((t) => used.includes(t));
@@ -935,23 +1472,6 @@ function spanOf(stage, lane) {
935
1472
  const at = [...stage.inputs ?? [], ...stage.outputs ?? []].map((p) => lane.get(p.type));
936
1473
  return at.length === 0 ? [0, 0] : [Math.min(...at), Math.max(...at)];
937
1474
  }
938
- function columnsOf(stages, wires, spans) {
939
- const feeds = new Map(stages.map((s) => [s.id, /* @__PURE__ */ new Set()]));
940
- for (const d of wires) if (d.wire.from.stage !== d.wire.to.stage) feeds.get(d.wire.to.stage).add(d.wire.from.stage);
941
- const column = /* @__PURE__ */ new Map();
942
- const held = [];
943
- const left = [...stages];
944
- while (left.length > 0) {
945
- const i = Math.max(0, left.findIndex((s) => [...feeds.get(s.id)].every((f) => column.has(f))));
946
- const stage = left.splice(i, 1)[0];
947
- const span = spans.get(stage.id);
948
- let c = Math.max(0, ...[...feeds.get(stage.id)].flatMap((f) => column.has(f) ? [column.get(f) + 1] : []));
949
- while (held[c]?.some(([lo, hi]) => lo <= span[1] && span[0] <= hi)) c++;
950
- (held[c] ??= []).push(span);
951
- column.set(stage.id, c);
952
- }
953
- return column;
954
- }
955
1475
  function laneHeights(count, least, needs, insets) {
956
1476
  const heights = Array.from({ length: count }, () => least);
957
1477
  for (const { span: [lo, hi], height } of [...needs].sort((a, b) => a.span[1] - a.span[0] - (b.span[1] - b.span[0]))) {
@@ -978,20 +1498,28 @@ function controlSideOf(stage, pipeline, lane) {
978
1498
  const signal = ports.filter((p) => !isControl(pipeline, p.type));
979
1499
  return control.length > 0 && signal.length > 0 && mean(control) > mean(signal) ? "bottom" : "top";
980
1500
  }
981
- function sizesOf(nodes, measure) {
982
- const specs = diagramScene({ nodes, edges: [] }, { layout: "none", ...measure !== void 0 ? { measure } : {} });
1501
+ function sizesOf(nodes, groups, measure) {
1502
+ const specs = diagramScene({ nodes, edges: [], groups }, { layout: "none", ...measure !== void 0 ? { measure } : {} });
983
1503
  const pose = new Map(specs.map((s) => [s.id, s.pose]));
984
- return new Map(nodes.map((n) => {
1504
+ const spec = new Map(specs.map((s) => [s.id, s]));
1505
+ const nested = nestGroups(groups, (id) => nodes.some((n) => n.id === id)).map((g) => ({ members: g.members, inset: insetOf(diagramNodeOf({ id: g.id, kind: "leaf", data: spec.get(g.id)?.data })?.group?.inset) }));
1506
+ const boxed = (id, side) => nested.reduce((sum, g) => sum + (g.members.includes(id) ? g.inset[side] : 0), 0);
1507
+ const titles = new Map(groups.flatMap((g) => {
1508
+ const title2 = pose.get(groupLabelIdOf(g.id));
1509
+ return title2 === void 0 ? [] : [[g.id, title2.width]];
1510
+ }));
1511
+ const size = new Map(nodes.map((n) => {
985
1512
  const box = pose.get(n.id);
986
1513
  const ink = inkOf(n.id, specs);
987
1514
  const over = {
988
1515
  left: box.x - Math.min(...ink.map((b) => b.x)),
989
- top: box.y - Math.min(...ink.map((b) => b.y)),
1516
+ top: box.y - Math.min(...ink.map((b) => b.y)) + boxed(n.id, "top"),
990
1517
  right: Math.max(...ink.map((b) => b.x + b.width)) - box.x - box.width,
991
- bottom: Math.max(...ink.map((b) => b.y + b.height)) - box.y - box.height
1518
+ bottom: Math.max(...ink.map((b) => b.y + b.height)) - box.y - box.height + boxed(n.id, "bottom")
992
1519
  };
993
1520
  return [n.id, { box, over }];
994
1521
  }));
1522
+ return { size, titles };
995
1523
  }
996
1524
  function laneNode(type, box, color) {
997
1525
  return {
@@ -1010,7 +1538,7 @@ function insetsFor(edges, tracks) {
1010
1538
  return LANE_INSET + (k === 0 || k === edges - 1 ? n * TRACK_GAP : Math.max(0, n - 1) * TRACK_GAP / 2);
1011
1539
  });
1012
1540
  }
1013
- function place(plan, insets) {
1541
+ function place2(plan, insets) {
1014
1542
  const { stages, size, spans, column } = plan;
1015
1543
  const inkHeight = (id) => size.get(id).box.height + size.get(id).over.top + size.get(id).over.bottom;
1016
1544
  const heights = laneHeights(plan.rows, plan.least, stages.map((s) => ({ span: spans.get(s.id), height: inkHeight(s.id) })), insets);
@@ -1029,11 +1557,13 @@ function place(plan, insets) {
1029
1557
  const inkWidth = (id) => stood.get(id).width + stood.get(id).over.left + stood.get(id).over.right;
1030
1558
  const widths = [];
1031
1559
  for (const s of stages) widths[column.get(s.id)] = Math.max(widths[column.get(s.id)] ?? 0, inkWidth(s.id));
1032
- const lefts = Array.from(widths, (_, c) => plan.start + Array.from(widths.slice(0, c), (wd) => (wd ?? 0) + plan.gap).reduce((a, b) => a + b, 0));
1560
+ const offset = (id) => (widths[column.get(id)] - inkWidth(id)) / 2 + stood.get(id).over.left;
1561
+ const extent = (id) => ({ lo: offset(id), hi: offset(id) + stood.get(id).width });
1562
+ const { lefts, end } = columnLefts(widths, column, plan.blocks, plan.gap, plan.start, extent);
1033
1563
  const nodes = stages.map((stage, k) => {
1034
1564
  const c = column.get(stage.id);
1035
- const { y, height, width, over } = stood.get(stage.id);
1036
- const box = { x: lefts[c] + (widths[c] - inkWidth(stage.id)) / 2 + over.left, y, width, height };
1565
+ const { y, height, width } = stood.get(stage.id);
1566
+ const box = { x: lefts[c] + offset(stage.id), y, width, height };
1037
1567
  return {
1038
1568
  ...plan.drawn[k],
1039
1569
  at: { x: box.x, y: box.y },
@@ -1044,25 +1574,28 @@ function place(plan, insets) {
1044
1574
  });
1045
1575
  const columns = Array.from(widths, (wd, c) => [lefts[c], lefts[c] + (wd ?? 0)]);
1046
1576
  const edges = [...tops, tops.length === 0 ? 0 : tops[tops.length - 1] + heights[heights.length - 1]];
1047
- return { nodes, tops, heights, columns, edges };
1577
+ return { nodes, tops, heights, columns, edges, end };
1048
1578
  }
1049
1579
  var lanes = (pipeline, options = {}) => {
1050
- const color = typeColors(pipeline, options.colors);
1580
+ const theme = options.theme ?? LOOK_THEME;
1581
+ const color = typeColors(pipeline, options.colors, theme.series);
1051
1582
  const stages = uniqueStages(pipeline);
1052
1583
  const types = laneTypes(stages, options.lanes);
1053
1584
  const rows = types.length > 0 ? types : [""];
1054
1585
  const lane = new Map(types.map((t, i) => [t, i]));
1055
1586
  const spans = new Map(stages.map((s) => [s.id, spanOf(s, lane)]));
1056
- const drawn = stages.map((stage) => kindOf(stage) === "decision" ? { id: stage.id, ...branchNode(stage, pipeline, options.branch, { controlSide: controlSideOf(stage, pipeline, lane) }) } : boxNode(stage, { showParams: options.showParams !== false, rowPorts: false, ports: [], run: options }));
1587
+ const drawn = stages.map((stage) => kindOf(stage) === "decision" ? { id: stage.id, ...branchNode(stage, pipeline, options.branch, { controlSide: controlSideOf(stage, pipeline, lane), style: stageStyle(stage, theme) }) } : boxNode(stage, { showParams: options.showParams !== false, rowPorts: false, ports: [], run: options }));
1057
1588
  const labels = rows.map((t) => ({ id: laneId(t), lines: [t] }));
1058
- const size = sizesOf([...drawn, ...labels], options.measure);
1059
- const column = columnsOf(stages, drawableWires(pipeline), spans);
1589
+ const groups = uniqueGroups(pipeline);
1590
+ const { size, titles } = sizesOf([...drawn, ...labels], groups, options.measure);
1591
+ const { column, blocks } = laneColumns(stages, drawableWires(pipeline), spans, groups, titles);
1060
1592
  const plan = {
1061
1593
  stages,
1062
1594
  drawn,
1063
1595
  size,
1064
1596
  spans,
1065
1597
  column,
1598
+ blocks,
1066
1599
  lane,
1067
1600
  rows: rows.length,
1068
1601
  least: Math.max(options.laneHeight ?? 48, ...labels.map((l) => size.get(l.id).box.height)),
@@ -1072,23 +1605,25 @@ var lanes = (pipeline, options = {}) => {
1072
1605
  const router = options.router ?? "bezier";
1073
1606
  const wires = wireEdges(pipeline, color);
1074
1607
  let insets = insetsFor(rows.length + 1, /* @__PURE__ */ new Map());
1075
- let laid = place(plan, insets);
1076
- let routed = routeWires(wires, laid.nodes, { grid: laid, column, router, measure: options.measure });
1608
+ let laid = place2(plan, insets);
1609
+ const route = (at) => routeWires(wires, at.nodes, { grid: at, column, router, measure: options.measure, groups });
1610
+ let routed = route(laid);
1077
1611
  for (let round = 0; round < 4; round++) {
1078
- const need = insetsFor(rows.length + 1, routed.tracks);
1079
- if (need.every((n, k) => n <= insets[k])) break;
1080
- insets = need.map((n, k) => Math.max(n, insets[k]));
1081
- laid = place(plan, insets);
1082
- routed = routeWires(wires, laid.nodes, { grid: laid, column, router, measure: options.measure });
1612
+ const need2 = insetsFor(rows.length + 1, routed.tracks);
1613
+ if (need2.every((n, k) => n <= insets[k])) break;
1614
+ insets = need2.map((n, k) => Math.max(n, insets[k]));
1615
+ laid = place2(plan, insets);
1616
+ routed = route(laid);
1083
1617
  }
1084
1618
  const { columns, tops, heights } = laid;
1085
- const right = columns.length === 0 ? plan.start : columns[columns.length - 1][1] + MARGIN;
1619
+ const right = columns.length === 0 ? plan.start : laid.end + MARGIN;
1086
1620
  const laneNodes = rows.map((t, i) => laneNode(t, { x: 0, y: tops[i], width: right, height: heights[i] }, color));
1087
1621
  const edges = routed.edges;
1088
- const styles = sceneStyles(stages, edges);
1089
- const laneStyle = new Map(rows.map((t, i) => [laneId(t), LANE_STYLES[i % LANE_STYLES.length]]));
1622
+ const styles = sceneStyles(stages, edges, theme);
1623
+ const bodies = theme.lane.bodies;
1624
+ const laneStyle = new Map(rows.map((t, i) => [laneId(t), bodies[i % bodies.length]]));
1090
1625
  return {
1091
- data: { nodes: [...laneNodes, ...laid.nodes], edges: edges.map((e) => e.edge), groups: uniqueGroups(pipeline) },
1626
+ data: { nodes: [...laneNodes, ...laid.nodes], edges: edges.map((e) => e.edge), groups },
1092
1627
  scene: {
1093
1628
  layout: "none",
1094
1629
  router,
@@ -1096,61 +1631,17 @@ var lanes = (pipeline, options = {}) => {
1096
1631
  ...styles,
1097
1632
  nodeStyle: (n) => laneStyle.get(n.id) ?? styles.nodeStyle(n)
1098
1633
  },
1099
- color
1634
+ color,
1635
+ order: { axis: "x", items: mainChain(pipeline).map((id) => ({ id })) },
1636
+ ...types.length > 0 ? {
1637
+ laneOrder: {
1638
+ axis: "y",
1639
+ items: types.map((t, i) => ({ id: laneId(t), grip: { x: 0, y: tops[i], width: plan.start, height: heights[i] } }))
1640
+ }
1641
+ } : {}
1100
1642
  };
1101
1643
  };
1102
1644
 
1103
- // src/effect/mainChain.ts
1104
- function mainChain(pipeline) {
1105
- const next = /* @__PURE__ */ new Map();
1106
- for (const d of drawableWires(pipeline)) {
1107
- if (isControl(pipeline, d.type)) continue;
1108
- next.set(d.wire.from.stage, [...next.get(d.wire.from.stage) ?? [], d.wire.to.stage]);
1109
- }
1110
- const best = /* @__PURE__ */ new Map();
1111
- const walking = /* @__PURE__ */ new Set();
1112
- const from = (id) => {
1113
- const known = best.get(id);
1114
- if (known !== void 0) return known;
1115
- if (walking.has(id)) return [];
1116
- walking.add(id);
1117
- let tail = [];
1118
- for (const to of next.get(id) ?? []) {
1119
- const path2 = from(to);
1120
- if (path2.length > tail.length && !path2.includes(id)) tail = path2;
1121
- }
1122
- walking.delete(id);
1123
- const path = [id, ...tail];
1124
- best.set(id, path);
1125
- return path;
1126
- };
1127
- let longest = [];
1128
- for (const stage of uniqueStages(pipeline)) {
1129
- const path = from(stage.id);
1130
- if (path.length > longest.length) longest = path;
1131
- }
1132
- return longest;
1133
- }
1134
- function skipDecisions(pipeline, line) {
1135
- const wires = drawableWires(pipeline);
1136
- const stages = new Map(uniqueStages(pipeline).map((s) => [s.id, s]));
1137
- const out = [];
1138
- line.forEach((id, i) => {
1139
- const decision = stages.get(id);
1140
- if (decision === void 0 || kindOf(decision) !== "decision" || i === 0) return;
1141
- if ((decision.inputs ?? []).length !== 1 || (decision.outputs ?? []).length !== 2) return;
1142
- const entering = wires.filter((d) => d.wire.to.stage === id);
1143
- const leaving = wires.filter((d) => d.wire.from.stage === id);
1144
- if (entering.length !== 1 || leaving.length !== 2 || entering[0].wire.from.stage !== line[i - 1]) return;
1145
- const through = leaving.find((d) => d.wire.to.stage === line[i + 1]);
1146
- const skip = leaving.find((d) => d !== through && line.indexOf(d.wire.to.stage) > i + 1);
1147
- const ordinary = (s) => s !== void 0 && kindOf(s) !== "decision";
1148
- if (!ordinary(stages.get(line[i - 1])) || !ordinary(through?.toStage) || !ordinary(skip?.toStage)) return;
1149
- if (through !== void 0 && skip !== void 0) out.push({ decision, into: entering[0], through, skip });
1150
- });
1151
- return out;
1152
- }
1153
-
1154
1645
  // src/effect/rack.ts
1155
1646
  var SKIP_OUT = "~skip-out";
1156
1647
  var SKIP_IN = "~skip-in";
@@ -1164,7 +1655,14 @@ function rackWires(pipeline, skips) {
1164
1655
  const eaten = new Set(skips.flatMap((s) => [s.into.index, s.through.index, s.skip.index]));
1165
1656
  const wires = drawableWires(pipeline).filter((d) => !eaten.has(d.index)).map((d) => {
1166
1657
  const label = exitLabel(d);
1167
- return { from: d.wire.from, to: d.wire.to, type: d.type, problem: d.problem, ...label !== void 0 ? { label } : {} };
1658
+ return {
1659
+ ...d.wire.id !== void 0 ? { id: d.wire.id } : {},
1660
+ from: d.wire.from,
1661
+ to: d.wire.to,
1662
+ type: d.type,
1663
+ problem: d.problem,
1664
+ ...label !== void 0 ? { label } : {}
1665
+ };
1168
1666
  });
1169
1667
  for (const s of skips) {
1170
1668
  wires.push({
@@ -1186,22 +1684,12 @@ function rackWires(pipeline, skips) {
1186
1684
  }
1187
1685
  return wires;
1188
1686
  }
1189
- function lineOf(pipeline, chain, wires) {
1190
- const at = new Map(chain.map((id, i) => [id, i]));
1191
- const mainIn = /* @__PURE__ */ new Map();
1192
- const mainOut = /* @__PURE__ */ new Map();
1193
- for (const wire of wires) {
1194
- if (wire.arc || isControl(pipeline, wire.type)) continue;
1195
- const a = at.get(wire.from.stage);
1196
- const b = at.get(wire.to.stage);
1197
- if (a === void 0 || b !== a + 1) continue;
1198
- if (!mainOut.has(wire.from.stage)) mainOut.set(wire.from.stage, wire.from.port);
1199
- if (!mainIn.has(wire.to.stage)) mainIn.set(wire.to.stage, wire.to.port);
1200
- }
1201
- return { at, mainIn, mainOut };
1687
+ function lineOf(pipeline, ordered, chain) {
1688
+ const { mainIn, mainOut } = chainLinks(pipeline, ordered);
1689
+ return { at: new Map(chain.map((id, i) => [id, i])), mainIn, mainOut };
1202
1690
  }
1203
- function portsOf(stage, line, wires, near, far) {
1204
- if (!line.at.has(stage.id)) return [...spreadAt(stage.outputs ?? [], far), ...spreadAt(stage.inputs ?? [], near)];
1691
+ function portsOf2(stage, line, wires, near2, far) {
1692
+ if (!line.at.has(stage.id)) return [...spreadAt(stage.outputs ?? [], far), ...spreadAt(stage.inputs ?? [], near2)];
1205
1693
  const typed = (p, anchor) => ({ id: p.id, type: p.type, at: anchor });
1206
1694
  const out = [];
1207
1695
  const side = [];
@@ -1213,7 +1701,7 @@ function portsOf(stage, line, wires, near, far) {
1213
1701
  if (p.id === line.mainOut.get(stage.id)) out.push(typed(p, { u: 1, v: 0.5 }));
1214
1702
  else side.push(p);
1215
1703
  }
1216
- out.push(...spreadAt(side, near));
1704
+ out.push(...spreadAt(side, near2));
1217
1705
  const type = portOn(stage, line.mainOut.get(stage.id) ?? line.mainIn.get(stage.id) ?? "")?.type;
1218
1706
  const arc = (id, u) => ({ id, at: { u, v: far }, ...type !== void 0 ? { type } : {} });
1219
1707
  if (wires.some((w) => w.arc && w.from.stage === stage.id)) out.push(arc(SKIP_OUT, 0.75));
@@ -1221,9 +1709,10 @@ function portsOf(stage, line, wires, near, far) {
1221
1709
  return out;
1222
1710
  }
1223
1711
  var rack = (pipeline, options = {}) => {
1224
- const color = typeColors(pipeline, options.colors);
1712
+ const theme = options.theme ?? LOOK_THEME;
1713
+ const color = typeColors(pipeline, options.colors, theme.series);
1225
1714
  const below = options.side !== "above";
1226
- const near = below ? 1 : 0;
1715
+ const near2 = below ? 1 : 0;
1227
1716
  const far = below ? 0 : 1;
1228
1717
  const stages = uniqueStages(pipeline);
1229
1718
  const given = options.chain?.filter((id) => stages.some((s) => s.id === id));
@@ -1232,12 +1721,12 @@ var rack = (pipeline, options = {}) => {
1232
1721
  const gone = new Set(skips.map((s) => s.decision.id));
1233
1722
  const chain = ordered.filter((id) => !gone.has(id));
1234
1723
  const wires = rackWires(pipeline, skips);
1235
- const line = lineOf(pipeline, chain, wires);
1724
+ const line = lineOf(pipeline, ordered, chain);
1236
1725
  const kept = stages.filter((s) => !gone.has(s.id));
1237
1726
  const controlSide = below ? "bottom" : "top";
1238
1727
  const nodes = kept.map((stage) => {
1239
- if (kindOf(stage) === "decision") return { id: stage.id, ...branchNode(stage, pipeline, options.branch, { controlSide }) };
1240
- const ports = portsOf(stage, line, wires, near, far);
1728
+ if (kindOf(stage) === "decision") return { id: stage.id, ...branchNode(stage, pipeline, options.branch, { controlSide, style: stageStyle(stage, theme) }) };
1729
+ const ports = portsOf2(stage, line, wires, near2, far);
1241
1730
  const node = boxNode(stage, { showParams: options.showParams !== false, rowPorts: false, ports, run: options });
1242
1731
  const room = roomFor(ports);
1243
1732
  return room > 2 * SIDE_PORT_GAP ? { ...node, width: room } : node;
@@ -1248,7 +1737,7 @@ var rack = (pipeline, options = {}) => {
1248
1737
  const sideInput = to !== void 0 && kindOf(to) !== "decision" && !wire.arc && wire.to.port !== line.mainIn.get(wire.to.stage) && line.at.has(wire.to.stage);
1249
1738
  const label = wire.label ?? (sideInput ? portOn(to, wire.to.port)?.label ?? wire.to.port : void 0);
1250
1739
  return {
1251
- edge: wireEdge(wire.from, wire.to, label),
1740
+ edge: wireEdge(wire.from, wire.to, label, wire.id),
1252
1741
  style: wireStyle(color(wire.type), wire.arc === true || isControl(pipeline, wire.type), wire.problem)
1253
1742
  };
1254
1743
  });
@@ -1268,16 +1757,812 @@ var rack = (pipeline, options = {}) => {
1268
1757
  ...options.rankGap !== void 0 ? { rankGap: options.rankGap } : {}
1269
1758
  },
1270
1759
  router: "bezier",
1271
- ...sceneStyles(kept, edges)
1760
+ ...sceneStyles(kept, edges, theme)
1272
1761
  },
1273
1762
  color,
1763
+ order: { axis: "x", items: chain.map((id) => ({ id })) },
1274
1764
  exits: new Map(wires.flatMap((wire, i) => wire.exit === void 0 ? [] : [[edgeIdOf(edges[i].edge), wire.exit]]))
1275
1765
  };
1276
1766
  };
1277
1767
 
1768
+ // src/effect/editorMapping.ts
1769
+ var endsKey = (from, to) => JSON.stringify([from.stage, from.port, to.stage, to.port]);
1770
+ var edgeEnds = (e) => endsKey({ stage: e.from, port: e.fromPort ?? "" }, { stage: e.to, port: e.toPort ?? "" });
1771
+ function editorScene(doc, look = "nodeGraph", options) {
1772
+ const drawn = drawLook(doc, look, options);
1773
+ const stages = new Set(doc.pipeline.stages.map((s) => s.id));
1774
+ const groups = new Set((doc.pipeline.groups ?? []).map((g) => g.id));
1775
+ const ids = ensureWireIds(doc).pipeline.wires.map((w) => w.id);
1776
+ const byEnds = /* @__PURE__ */ new Map();
1777
+ for (const d of drawableWires(doc.pipeline)) {
1778
+ const key = endsKey(d.wire.from, d.wire.to);
1779
+ if (!byEnds.has(key)) byEnds.set(key, ids[d.index]);
1780
+ }
1781
+ const wires = new Map(drawn.data.edges.flatMap((e) => {
1782
+ const id = byEnds.get(edgeEnds(e));
1783
+ return id === void 0 ? [] : [[edgeIdOf(e), id]];
1784
+ }));
1785
+ const routes = look === "nodeGraph" ? doc.routes : void 0;
1786
+ const edges = routes === void 0 ? drawn.data.edges : drawn.data.edges.map((e) => {
1787
+ const id = wires.get(edgeIdOf(e));
1788
+ const route = id === void 0 ? void 0 : routes[id];
1789
+ return route === void 0 ? e : { ...e, waypoints: route };
1790
+ });
1791
+ return {
1792
+ ...drawn,
1793
+ data: { ...drawn.data, edges },
1794
+ stageOf: (node) => stages.has(node) ? node : void 0,
1795
+ groupOf: (node) => groups.has(node) && !stages.has(node) ? node : void 0,
1796
+ wireOf: (edge) => wires.get(edge)
1797
+ };
1798
+ }
1799
+ function drawLook(doc, look, options) {
1800
+ if (look === "rack") return rack(doc.pipeline, options);
1801
+ if (look === "lanes") {
1802
+ const opts = options;
1803
+ const order = doc.laneOrder ?? opts?.lanes;
1804
+ return lanes(doc.pipeline, { ...opts, ...order !== void 0 ? { lanes: order } : {} });
1805
+ }
1806
+ const drawn = nodeGraph(doc.pipeline, options);
1807
+ const nodes = drawn.data.nodes.map((n) => {
1808
+ const at = doc.places[n.id];
1809
+ return at === void 0 ? n : { ...n, at: { x: at.x, y: at.y }, pinned: true };
1810
+ });
1811
+ const { groups = [], ...rest } = drawn.data;
1812
+ const containers = groups.map((g) => {
1813
+ const box = doc.groupBoxes?.[g.id];
1814
+ return {
1815
+ id: g.id,
1816
+ role: "container",
1817
+ label: g.label ?? "",
1818
+ members: g.members,
1819
+ ...box !== void 0 ? { box: { ...box }, pinned: true } : {}
1820
+ };
1821
+ });
1822
+ return {
1823
+ ...drawn,
1824
+ data: {
1825
+ ...rest,
1826
+ nodes,
1827
+ ...containers.length > 0 ? { containers } : {},
1828
+ ...doc.pages !== void 0 ? { pages: doc.pages } : {}
1829
+ }
1830
+ };
1831
+ }
1832
+
1833
+ // src/effect/groupOrder.ts
1834
+ function groupRuns(pipeline, chain) {
1835
+ const at = new Map(chain.map((id, i) => [id, i]));
1836
+ const doc = { pipeline};
1837
+ const runs = /* @__PURE__ */ new Map();
1838
+ for (const g of pipeline.groups ?? []) {
1839
+ const on = [...groupContents(doc, g.id).stages].flatMap((s) => at.get(s) ?? []);
1840
+ if (on.length === 0) continue;
1841
+ const first = Math.min(...on);
1842
+ const last = Math.max(...on);
1843
+ runs.set(g.id, last - first + 1 === on.length ? { first, last } : null);
1844
+ }
1845
+ return runs;
1846
+ }
1847
+ var holds = (b, p) => p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height;
1848
+ function groupGripOf(doc, group, boxOf) {
1849
+ const box = boxOf(group);
1850
+ const members = doc.pipeline.groups?.find((g) => g.id === group)?.members ?? [];
1851
+ const tops = members.flatMap((m) => boxOf(m)?.y ?? []);
1852
+ if (box === void 0 || tops.length === 0) return void 0;
1853
+ const height = Math.min(...tops) - box.y;
1854
+ return height > 0 ? { x: box.x, y: box.y, width: box.width, height } : void 0;
1855
+ }
1856
+ function unitStages(doc, unit) {
1857
+ return doc.pipeline.groups?.some((g) => g.id === unit) ? groupContents(doc, unit).stages : /* @__PURE__ */ new Set([unit]);
1858
+ }
1859
+ function parentsOf(doc) {
1860
+ const parent = /* @__PURE__ */ new Map();
1861
+ for (const g of doc.pipeline.groups ?? []) for (const m of g.members) parent.set(m, g.id);
1862
+ return parent;
1863
+ }
1864
+ function groupAt(doc, boxOf, at, among = () => true) {
1865
+ const parent = parentsOf(doc);
1866
+ const depth = (id) => parent.has(id) ? 1 + depth(parent.get(id)) : 0;
1867
+ const held = (doc.pipeline.groups ?? []).filter((g) => {
1868
+ const grip = among(g.id) ? groupGripOf(doc, g.id, boxOf) : void 0;
1869
+ return grip !== void 0 && holds(grip, at);
1870
+ });
1871
+ return held.sort((a, b) => depth(b.id) - depth(a.id))[0]?.id;
1872
+ }
1873
+ function groupSlotsAt(source, at) {
1874
+ const { doc, order, boxOf } = source;
1875
+ const groups = doc.pipeline.groups ?? [];
1876
+ if (order === void 0 || groups.length === 0) return null;
1877
+ const parent = parentsOf(doc);
1878
+ const runs = groupRuns(doc.pipeline, mainChain(doc.pipeline));
1879
+ const held = groupAt(doc, boxOf, at, (g) => runs.get(g) != null);
1880
+ if (held === void 0) return null;
1881
+ const up = parent.get(held);
1882
+ const siblings = up !== void 0 ? groups.find((g) => g.id === up).members : [...groups, ...doc.pipeline.stages].flatMap((u) => parent.has(u.id) ? [] : [u.id]);
1883
+ const drawnAt = new Map(order.items.map((i, k) => [i.id, k]));
1884
+ const placed = siblings.flatMap((id) => {
1885
+ const drawn = [...unitStages(doc, id)].flatMap((s) => drawnAt.get(s) ?? []);
1886
+ const bounds = boxOf(id);
1887
+ return drawn.length === 0 || bounds === void 0 ? [] : [{ id, bounds, first: Math.min(...drawn) }];
1888
+ }).sort((a, b) => a.first - b.first);
1889
+ const isGroup = new Set(groups.map((g) => g.id));
1890
+ const items = placed.map(({ id, bounds }) => {
1891
+ if (!isGroup.has(id)) return { id, bounds, grip: null };
1892
+ const inside = groupContents(doc, id);
1893
+ const carries = [...inside.groups, ...inside.stages, ...[...inside.groups].map(groupLabelIdOf)];
1894
+ return { id, bounds, grip: groupGripOf(doc, id, boxOf) ?? null, carries };
1895
+ });
1896
+ return items.length < 2 ? null : sequenceSlots(order.axis, items);
1897
+ }
1898
+
1899
+ // src/effect/editorReorder.ts
1900
+ var REORDER_LANES_ACTION_ID = "diagram.reorderLanes";
1901
+ var REORDER_GROUPS_ACTION_ID = "diagram.reorderGroups";
1902
+ function slotsOf(order, boxOf) {
1903
+ if (order === void 0) return null;
1904
+ const items = order.items.flatMap((i) => {
1905
+ const bounds = boxOf(i.id);
1906
+ return bounds === void 0 ? [] : [{ id: i.id, bounds, ...i.grip !== void 0 ? { grip: i.grip } : {} }];
1907
+ });
1908
+ return items.length < 2 ? null : sequenceSlots(order.axis, items);
1909
+ }
1910
+ function keepsGroups(doc, next) {
1911
+ if (next === null) return null;
1912
+ const after = groupRuns(next.pipeline, mainChain(next.pipeline));
1913
+ for (const [id, run] of groupRuns(doc.pipeline, mainChain(doc.pipeline))) if (run !== null && after.get(id) === null) return null;
1914
+ return next;
1915
+ }
1916
+ function dropInGap(doc, chain, drawn, to, at) {
1917
+ const tries = [to];
1918
+ for (let k = to; k > 0 && !drawn.has(chain[k - 1]); k--) tries.push(k - 1);
1919
+ for (let k = to; k < chain.length && !drawn.has(chain[k]); k++) tries.push(k + 1);
1920
+ for (const t of tries) {
1921
+ const next = keepsGroups(doc, at(t));
1922
+ if (next !== null) return next;
1923
+ }
1924
+ return null;
1925
+ }
1926
+ var drawnOf = (order) => new Set(order.items.map((i) => i.id));
1927
+ function stageMove(doc, order, move) {
1928
+ const chain = mainChain(doc.pipeline);
1929
+ const before = order.items[move.to];
1930
+ const to = before === void 0 ? chain.length : chain.indexOf(before.id);
1931
+ const wired = ensureWireIds(doc);
1932
+ return dropInGap(doc, chain, drawnOf(order), to, (t) => moveInChain(wired, move.id, t));
1933
+ }
1934
+ function groupMove(doc, order, units, move) {
1935
+ const chain = mainChain(doc.pipeline);
1936
+ const drawn = drawnOf(order);
1937
+ const indices = (unit, drawnOnly) => [...unitStages(doc, unit)].flatMap((s) => chain.includes(s) && (!drawnOnly || drawn.has(s)) ? [chain.indexOf(s)] : []);
1938
+ const run = indices(move.id, false);
1939
+ if (run.length === 0) return null;
1940
+ const [i, j] = [Math.min(...run), Math.max(...run)];
1941
+ if (j - i + 1 !== run.length) return null;
1942
+ const before = units[move.to];
1943
+ const edge = before !== void 0 ? indices(before, true) : indices(units[units.length - 1] ?? move.id, true).map((k) => k + 1);
1944
+ if (edge.length === 0) return null;
1945
+ const to = before !== void 0 ? Math.min(...edge) : Math.max(...edge);
1946
+ const wired = ensureWireIds(doc);
1947
+ return dropInGap(doc, chain, drawn, to, (t) => moveRunInChain(wired, chain[i], chain[j], t));
1948
+ }
1949
+ function laneMove(doc, order, move) {
1950
+ const types = order.items.flatMap((i) => laneTypeOf(i.id) ?? []);
1951
+ const moved = types.filter((_, k) => k !== move.from);
1952
+ moved.splice(move.to > move.from ? move.to - 1 : move.to, 0, types[move.from]);
1953
+ return setLaneOrder(doc, moved);
1954
+ }
1955
+ function unitsBefore(move) {
1956
+ const units = move.order.filter((id) => id !== move.id);
1957
+ units.splice(move.from, 0, move.id);
1958
+ return units;
1959
+ }
1960
+ function groupReorder(now) {
1961
+ const moved = (move) => {
1962
+ const { doc, order } = now();
1963
+ return order === void 0 ? null : groupMove(doc, order, unitsBefore(move), move);
1964
+ };
1965
+ return reorderContribution({
1966
+ id: REORDER_GROUPS_ACTION_ID,
1967
+ label: "Reorder group",
1968
+ slots: (_, at) => groupSlotsAt(now(), at),
1969
+ canDrop: (move) => moved(move) !== null,
1970
+ commit: (move) => {
1971
+ const next = moved(move);
1972
+ if (next !== null) now().onChange(next, "Reorder group");
1973
+ }
1974
+ });
1975
+ }
1976
+ function editorReorders(now) {
1977
+ return [
1978
+ reorderContribution({
1979
+ label: "Reorder",
1980
+ slots: () => slotsOf(now().order, now().boxOf),
1981
+ canDrop: (move) => {
1982
+ const { doc, order } = now();
1983
+ return order !== void 0 && stageMove(doc, order, move) !== null;
1984
+ },
1985
+ commit: (move) => {
1986
+ const { doc, order, onChange } = now();
1987
+ const next = order === void 0 ? null : stageMove(doc, order, move);
1988
+ if (next !== null) onChange(next, "Reorder");
1989
+ }
1990
+ }),
1991
+ reorderContribution({
1992
+ id: REORDER_LANES_ACTION_ID,
1993
+ label: "Reorder lanes",
1994
+ slots: () => slotsOf(now().laneOrder, now().boxOf),
1995
+ commit: (move) => {
1996
+ const { doc, laneOrder, onChange } = now();
1997
+ if (laneOrder !== void 0) onChange(laneMove(doc, laneOrder, move), "Reorder lanes");
1998
+ }
1999
+ }),
2000
+ groupReorder(now)
2001
+ ];
2002
+ }
2003
+ var RENAME_GROUP_ACTION_ID = "effect.renameGroup";
2004
+ function renameGroupContribution(now, rename) {
2005
+ const under = (at) => {
2006
+ const { doc, boxOf } = now();
2007
+ return groupAt(doc, boxOf, at);
2008
+ };
2009
+ return {
2010
+ id: `weasel-diagram-effect:${RENAME_GROUP_ACTION_ID}`,
2011
+ eligibility: { always: true },
2012
+ actions: [{
2013
+ id: RENAME_GROUP_ACTION_ID,
2014
+ label: "Rename group",
2015
+ group: "diagram",
2016
+ enabled: (_, at) => at === void 0 || under(at) !== void 0 ? true : ActionDisabledReason.NotApplicable,
2017
+ invoker: {
2018
+ timing: "immediate",
2019
+ run(_, params) {
2020
+ const group = under({ x: Number(params?.worldX), y: Number(params?.worldY) });
2021
+ if (group !== void 0) rename(group);
2022
+ }
2023
+ }
2024
+ }],
2025
+ // Empty space included: an unlabeled group's grip is padding.
2026
+ bindings: [{
2027
+ spec: { kind: "doubleClick", target: { kindOf: ANYWHERE } },
2028
+ actionId: RENAME_GROUP_ACTION_ID,
2029
+ opts: { id: `${RENAME_GROUP_ACTION_ID}.doubleClick` }
2030
+ }]
2031
+ };
2032
+ }
2033
+
2034
+ // src/effect/place.ts
2035
+ var PLACE_GAP = 24;
2036
+ var STEP = 8;
2037
+ var clear = (x, y, size, boxes) => boxes.every((b) => x >= b.x + b.width + PLACE_GAP || x + size.width + PLACE_GAP <= b.x || y >= b.y + b.height + PLACE_GAP || y + size.height + PLACE_GAP <= b.y);
2038
+ function freeSpot(boxes, size, view) {
2039
+ if (view !== null) {
2040
+ const right = view.x + view.width - PLACE_GAP - size.width;
2041
+ const bottom = view.y + view.height - PLACE_GAP - size.height;
2042
+ for (let y = view.y + PLACE_GAP; y <= bottom; y += STEP) {
2043
+ for (let x = view.x + PLACE_GAP; x <= right; x += STEP) {
2044
+ if (clear(x, y, size, boxes)) return { x, y };
2045
+ }
2046
+ }
2047
+ }
2048
+ if (boxes.length === 0) return { x: (view?.x ?? 0) + PLACE_GAP, y: (view?.y ?? 0) + PLACE_GAP };
2049
+ const last = boxes.reduce((a, b) => b.x + b.width > a.x + a.width ? b : a);
2050
+ return { x: last.x + last.width + PLACE_GAP, y: last.y };
2051
+ }
2052
+ var EffectEditor = forwardRef(function EffectEditor2({ doc, onChange, run, selected, onSelect, selectedGroup, onSelectGroup, look = "nodeGraph", options, width, height, background, className }, ref) {
2053
+ const built = useMemo(() => editorScene(doc, look, options), [doc, look, options]);
2054
+ const specs = useMemo(() => applyLookRoutes(diagramScene(built.data, built.scene), doc, look, built).map((s) => s.pickable === false || isPart(built, s.id) ? s : { ...s, pickable: false }), [built, doc, look]);
2055
+ const painted = useMemo(() => paintRun(doc.pipeline, specs, run, built.exits), [doc.pipeline, specs, run, built]);
2056
+ useReleaseReplaced(run);
2057
+ const portOptions = useRunPortOptions(built.color, doc.pipeline, run);
2058
+ const picks = useEditorPicks(built, { selected, selectedGroup, onSelect, onSelectGroup });
2059
+ const now = useLatest({ doc, built, specs, look, options, onChange });
2060
+ const view = useRef(null);
2061
+ const [renaming, setRenaming] = useState(null);
2062
+ const handlers = useMemo(() => editHandlers(() => now.current, picks.unpick, setRenaming), [now, picks.unpick]);
2063
+ const ambient = useMemo(() => {
2064
+ const source = () => {
2065
+ const { doc: d, built: b, specs: shown, onChange: report } = now.current;
2066
+ const boxes = new Map(shown.map((s) => [s.id, s.pose]));
2067
+ return { doc: d, order: b.order, laneOrder: b.laneOrder, boxOf: (id) => boxes.get(id), onChange: report };
2068
+ };
2069
+ return [...editorReorders(source), renameGroupContribution(source, setRenaming)];
2070
+ }, [now]);
2071
+ useImperativeHandle(ref, () => ({
2072
+ addStage: (type, client) => {
2073
+ const { doc: d, specs: shown, look: l, options: opts, onChange: report } = now.current;
2074
+ const stage = stageFromType(type, nextStageId(d.pipeline, type.type));
2075
+ if (l !== "nodeGraph") {
2076
+ report(addStage(ensureWireIds(d), stage), "Add stage");
2077
+ return stage.id;
2078
+ }
2079
+ const pinned = ensureWireIds(pinShown(d, shown));
2080
+ if (client && view.current) {
2081
+ report(addStage(pinned, stage, view.current.worldAt(client.x, client.y)), "Add stage");
2082
+ } else {
2083
+ const boxes = stageBoxes(d, shown);
2084
+ const alone = { pipeline: { ...d.pipeline, stages: [stage], wires: [], groups: [] }, places: {} };
2085
+ const size = stageBoxes(alone, drawnSpecs(alone, opts)).get(stage.id) ?? { width: 0, height: 0 };
2086
+ const at = freeSpot([...boxes.values()], size, view.current?.visible() ?? null);
2087
+ report(addStage(pinned, stage, at), "Add stage");
2088
+ }
2089
+ return stage.id;
2090
+ },
2091
+ group: (ids) => {
2092
+ const { doc: d, specs: shown, look: l, onChange: report } = now.current;
2093
+ if (ids.length === 0) return null;
2094
+ const base = l === "nodeGraph" ? pinShown(ensureWireIds(d), shown) : d;
2095
+ report(groupStages(base, ids), "Group");
2096
+ return nextGroupId(d.pipeline);
2097
+ },
2098
+ ungroup: (id) => {
2099
+ const { doc: d, onChange: report } = now.current;
2100
+ if (d.pipeline.groups?.some((g) => g.id === id)) report(ungroup(d, id), "Ungroup");
2101
+ },
2102
+ renameGroup: (id) => setRenaming(id)
2103
+ }), [now]);
2104
+ const title2 = useGroupTitle(doc, look, renaming, setRenaming, onChange);
2105
+ const fill = (look === "nodeGraph" ? options?.theme ?? LOOK_THEME : LOOK_THEME).page;
2106
+ const placed = useMemo(() => diagramPages(built.data), [built.data.pages, built.data.pageLayout]);
2107
+ const pages = useMemo(() => placed.length > 0 ? { placed, fill } : void 0, [placed, fill]);
2108
+ return /* @__PURE__ */ jsx(
2109
+ DiagramView,
2110
+ {
2111
+ ref: view,
2112
+ specs: painted,
2113
+ width,
2114
+ height,
2115
+ minScale: EFFECT_MIN_SCALE,
2116
+ anchor: "start",
2117
+ ports: "connect",
2118
+ portOptions,
2119
+ canConnect: handlers.canConnect,
2120
+ onConnect: handlers.onConnect,
2121
+ onReconnect: handlers.onReconnect,
2122
+ onEditRoute: handlers.onEditRoute,
2123
+ onDelete: handlers.onDelete,
2124
+ ...look === "nodeGraph" ? { onMove: handlers.onMove } : { ambient },
2125
+ ...look === "nodeGraph" ? { onEditContainer: handlers.onEditContainer } : {},
2126
+ onSelect: picks.pick,
2127
+ ...picks.shown !== void 0 ? { selected: picks.shown } : {},
2128
+ ...pages !== void 0 ? { pages } : {},
2129
+ ...title2 !== null ? { overlays: title2 } : {},
2130
+ ...background !== void 0 ? { background } : {},
2131
+ ...className !== void 0 ? { className } : {}
2132
+ },
2133
+ look
2134
+ );
2135
+ });
2136
+ function drawnSpecs(doc, options) {
2137
+ const built = editorScene(doc, "nodeGraph", options);
2138
+ return diagramScene(built.data, built.scene);
2139
+ }
2140
+
2141
+ // src/effect/image/evaluators.ts
2142
+ var BLEND = { normal: "source-over", screen: "screen", multiply: "multiply" };
2143
+ function draw(width, height, paint) {
2144
+ const canvas = new OffscreenCanvas(width, height);
2145
+ paint(canvas.getContext("2d"));
2146
+ return canvas.transferToImageBitmap();
2147
+ }
2148
+ function need(ctx, port) {
2149
+ const value = ctx.inputs[port];
2150
+ if (value === void 0) throw new Error(`${port} is not connected`);
2151
+ return value;
2152
+ }
2153
+ function decide(ctx, value) {
2154
+ const outs = ctx.stage.outputs ?? [];
2155
+ const split = ctx.stage.split;
2156
+ const index = split !== void 0 ? split.at.filter((cut) => value >= cut).length : value > 0 ? 0 : 1;
2157
+ return outs[Math.min(index, outs.length - 1)]?.id;
2158
+ }
2159
+ var imageEvaluators = {
2160
+ source: ({ sources }) => {
2161
+ if (sources.image === void 0) throw new Error("no source image");
2162
+ return { outputs: { out: sources.image } };
2163
+ },
2164
+ blur: (ctx) => {
2165
+ const image2 = need(ctx, "in");
2166
+ const radius = Number(ctx.params.radius ?? 0);
2167
+ return {
2168
+ outputs: {
2169
+ out: draw(image2.width, image2.height, (g) => {
2170
+ g.filter = `blur(${radius}px)`;
2171
+ g.drawImage(image2, 0, 0);
2172
+ })
2173
+ }
2174
+ };
2175
+ },
2176
+ composite: (ctx) => {
2177
+ const base = need(ctx, "base");
2178
+ const top = need(ctx, "top");
2179
+ return {
2180
+ outputs: {
2181
+ out: draw(base.width, base.height, (g) => {
2182
+ g.drawImage(base, 0, 0);
2183
+ g.globalCompositeOperation = BLEND[String(ctx.params.blend)] ?? "source-over";
2184
+ g.drawImage(top, 0, 0, base.width, base.height);
2185
+ })
2186
+ }
2187
+ };
2188
+ },
2189
+ mask: (ctx) => {
2190
+ const image2 = need(ctx, "in");
2191
+ const mask = need(ctx, "mask");
2192
+ return {
2193
+ outputs: {
2194
+ out: draw(image2.width, image2.height, (g) => {
2195
+ g.drawImage(image2, 0, 0);
2196
+ g.globalCompositeOperation = "destination-in";
2197
+ g.drawImage(mask, 0, 0, image2.width, image2.height);
2198
+ })
2199
+ }
2200
+ };
2201
+ },
2202
+ oscillator: ({ params, time }) => ({ outputs: { value: Math.sin(2 * Math.PI * Number(params.rate ?? 1) * time) } }),
2203
+ decision: (ctx) => {
2204
+ const tested = ctx.stage.split?.input ?? ctx.stage.inputs?.find((p) => p.type === "number")?.id ?? "test";
2205
+ const taken = decide(ctx, need(ctx, tested));
2206
+ const type = ctx.stage.outputs?.find((p) => p.id === taken)?.type;
2207
+ const from = ctx.stage.inputs?.find((p) => p.type === type && ctx.inputs[p.id] !== void 0);
2208
+ return { outputs: taken !== void 0 && from !== void 0 ? { [taken]: ctx.inputs[from.id] } : {}, ...taken !== void 0 ? { taken } : {} };
2209
+ },
2210
+ out: () => ({ outputs: {} })
2211
+ };
2212
+
2213
+ // src/effect/image/stageTypes.ts
2214
+ var param = (label, control) => ({ label, value: formatParam(control), control });
2215
+ var image = (id, label) => ({ id, type: "image", ...label !== void 0 ? { label } : {} });
2216
+ var IMAGE_STAGE_TYPES = [
2217
+ { type: "source", title: "Source", kind: "source", outputs: [image("out")] },
2218
+ {
2219
+ type: "blur",
2220
+ title: "Blur",
2221
+ inputs: [image("in")],
2222
+ outputs: [image("out")],
2223
+ params: [param("radius", { kind: "number", value: 4, min: 0, max: 50, step: 1, unit: "px" })]
2224
+ },
2225
+ {
2226
+ type: "composite",
2227
+ title: "Composite",
2228
+ inputs: [image("base"), image("top")],
2229
+ outputs: [image("out")],
2230
+ params: [param("blend", {
2231
+ kind: "enum",
2232
+ value: "normal",
2233
+ options: [{ value: "normal", label: "Normal" }, { value: "screen", label: "Screen" }, { value: "multiply", label: "Multiply" }]
2234
+ })]
2235
+ },
2236
+ {
2237
+ type: "mask",
2238
+ title: "Mask",
2239
+ inputs: [image("in"), { id: "mask", type: "mask" }],
2240
+ outputs: [image("out")]
2241
+ },
2242
+ {
2243
+ type: "oscillator",
2244
+ title: "Oscillator",
2245
+ outputs: [{ id: "value", type: "number" }],
2246
+ params: [param("rate", { kind: "number", value: 1, min: 0.1, max: 20, step: 0.1, unit: "Hz" })]
2247
+ },
2248
+ {
2249
+ type: "decision",
2250
+ title: "Decision",
2251
+ kind: "decision",
2252
+ inputs: [image("in"), { id: "test", type: "number" }],
2253
+ outputs: [image("yes", "yes"), image("no", "no")]
2254
+ },
2255
+ { type: "out", title: "Out", kind: "sink", inputs: [image("in")] }
2256
+ ];
2257
+
2258
+ // src/effect/image/testCard.ts
2259
+ var BARS = ["#c0c0c0", "#c0c000", "#00c0c0", "#00c000", "#c000c0", "#c00000", "#0000c0"];
2260
+ function testCard(size) {
2261
+ const { width, height } = typeof size === "number" ? { width: size, height: size } : size;
2262
+ const canvas = new OffscreenCanvas(width, height);
2263
+ const g = canvas.getContext("2d");
2264
+ const barHeight = height * 0.7;
2265
+ BARS.forEach((color, i) => {
2266
+ g.fillStyle = color;
2267
+ const x = Math.floor(i * width / BARS.length);
2268
+ g.fillRect(x, 0, Math.ceil(width / BARS.length) + 1, barHeight);
2269
+ });
2270
+ const ramp = g.createLinearGradient(0, 0, width, 0);
2271
+ ramp.addColorStop(0, "#000");
2272
+ ramp.addColorStop(1, "#fff");
2273
+ g.fillStyle = ramp;
2274
+ g.fillRect(0, barHeight, width, height - barHeight);
2275
+ g.strokeStyle = "#fff";
2276
+ g.lineWidth = Math.max(1, Math.min(width, height) / 32);
2277
+ g.beginPath();
2278
+ g.arc(width / 2, height / 2, Math.min(width, height) * 0.3, 0, Math.PI * 2);
2279
+ g.stroke();
2280
+ return canvas.transferToImageBitmap();
2281
+ }
2282
+
2283
+ // src/effect/image/valueKinds.ts
2284
+ var bitmapKind = {
2285
+ show: (value) => `${value.width}\xD7${value.height}`,
2286
+ preview(value, size) {
2287
+ const width = Math.max(1, Math.round(size.width * PREVIEW_DENSITY));
2288
+ const height = Math.max(1, Math.round(size.height * PREVIEW_DENSITY));
2289
+ const canvas = new OffscreenCanvas(width, height);
2290
+ canvas.getContext("2d").drawImage(value, 0, 0, width, height);
2291
+ return canvas.transferToImageBitmap();
2292
+ },
2293
+ dispose: (value) => value.close()
2294
+ };
2295
+ var imageValueKinds = {
2296
+ image: bitmapKind,
2297
+ mask: bitmapKind,
2298
+ number: { show: (value) => value.toFixed(3) }
2299
+ };
2300
+
1278
2301
  // src/effect/looks.ts
1279
2302
  var EFFECT_LOOKS = Object.freeze({ nodeGraph, rack, lanes, compact });
1280
2303
 
1281
- export { BRANCH_SYMBOLS, COMPACT_PADDING, EFFECT_LOOKS, EFFECT_MIN_SCALE, EffectChart, LANE_INSET, NO_VALUE, PREVIEW_SIZE, PROBLEM_COLOR, branchNode, checkPipeline, compact, costText, drawableWires, exitLabel, isControl, kindOf, laneId, lanes, mainChain, nodeGraph, paintRun, portOn, previewRowId, rack, stageRows, stageStyle, symbolNameFor, typeColors, uniqueGroups, uniqueStages, valueRowId, wireEdges, wireStyle };
2304
+ // src/effect/runner/pass.ts
2305
+ function createPass(pipeline, opts) {
2306
+ const stages = new Map(uniqueStages(pipeline).map((s) => [s.id, s]));
2307
+ const queue = runOrder(pipeline).order;
2308
+ const made = /* @__PURE__ */ new Map();
2309
+ const sources = opts.sources ?? {};
2310
+ const size = typeof opts.previewSize === "object" ? opts.previewSize : { width: opts.previewSize ?? PREVIEW_SIZE, height: opts.previewSize ?? PREVIEW_SIZE };
2311
+ const signal = opts.signal ?? new AbortController().signal;
2312
+ let run = { stages: {} };
2313
+ const inputsOf = (stage) => {
2314
+ const inputs2 = {};
2315
+ for (const w of pipeline.wires) {
2316
+ if (w.to.stage !== stage.id || inputs2[w.to.port] !== void 0) continue;
2317
+ const value = made.get(w.from.stage)?.[w.from.port];
2318
+ if (value !== void 0) inputs2[w.to.port] = value;
2319
+ }
2320
+ return inputs2;
2321
+ };
2322
+ const record = async (stage, inputs2, outputs, entry) => {
2323
+ made.set(stage.id, outputs);
2324
+ const values = {};
2325
+ const previews = {};
2326
+ const seen = { ...inputs2, ...outputs };
2327
+ for (const port of shownPorts(stage)) {
2328
+ const value = seen[port.id];
2329
+ if (value === void 0) continue;
2330
+ const kind = opts.kinds?.[port.type];
2331
+ const text = kind?.show !== void 0 ? kind.show(value) : plainText(value);
2332
+ if (text !== void 0) values[port.id] = text;
2333
+ const preview = opts.previews === false ? void 0 : await kind?.preview?.(value, size);
2334
+ if (preview !== void 0) previews[port.id] = preview;
2335
+ }
2336
+ const full = {
2337
+ ...entry,
2338
+ ...Object.keys(values).length > 0 ? { values } : {},
2339
+ ...Object.keys(previews).length > 0 ? { previews } : {}
2340
+ };
2341
+ run = { stages: { ...run.stages, [stage.id]: full } };
2342
+ };
2343
+ const evaluate = async (stage, inputs2) => {
2344
+ if (stage.bypassed === true) return record(stage, inputs2, passThrough(stage, inputs2), {});
2345
+ const evaluator = stage.type === void 0 ? void 0 : opts.evaluators[stage.type];
2346
+ const params = paramValues(stage);
2347
+ const start = performance.now();
2348
+ try {
2349
+ if (evaluator === void 0) throw new Error(`no evaluator for type "${stage.type ?? ""}"`);
2350
+ const result = await evaluator({ stage, inputs: inputs2, params, sources, time: opts.time ?? 0, signal });
2351
+ const cost = performance.now() - start;
2352
+ return record(stage, inputs2, result.outputs, { cost, ...result.taken !== void 0 ? { taken: result.taken } : {} });
2353
+ } catch (err) {
2354
+ signal.throwIfAborted();
2355
+ const cost = performance.now() - start;
2356
+ run = { stages: { ...run.stages, [stage.id]: { cost, error: err instanceof Error ? err.message : String(err) } } };
2357
+ }
2358
+ };
2359
+ return {
2360
+ get run() {
2361
+ return run;
2362
+ },
2363
+ async step() {
2364
+ while (queue.length > 0) {
2365
+ signal.throwIfAborted();
2366
+ const stage = stages.get(queue.shift());
2367
+ const inputs2 = inputsOf(stage);
2368
+ if ((stage.inputs?.length ?? 0) > 0 && Object.keys(inputs2).length === 0) continue;
2369
+ await evaluate(stage, inputs2);
2370
+ signal.throwIfAborted();
2371
+ return stage.id;
2372
+ }
2373
+ return void 0;
2374
+ },
2375
+ dispose() {
2376
+ const given = new Set(Object.values(sources));
2377
+ const done = /* @__PURE__ */ new Set();
2378
+ for (const [id, outputs] of made) {
2379
+ for (const port of stages.get(id)?.outputs ?? []) {
2380
+ const value = outputs[port.id];
2381
+ if (value === void 0 || given.has(value) || done.has(value)) continue;
2382
+ done.add(value);
2383
+ opts.kinds?.[port.type]?.dispose?.(value);
2384
+ }
2385
+ }
2386
+ made.clear();
2387
+ }
2388
+ };
2389
+ }
2390
+ function passThrough(stage, inputs2) {
2391
+ const out = {};
2392
+ for (const port of stage.outputs ?? []) {
2393
+ const from = (stage.inputs ?? []).find((i) => i.type === port.type && inputs2[i.id] !== void 0);
2394
+ if (from !== void 0) out[port.id] = inputs2[from.id];
2395
+ }
2396
+ return out;
2397
+ }
2398
+ function plainText(value) {
2399
+ return typeof value === "number" || typeof value === "string" || typeof value === "boolean" ? String(value) : void 0;
2400
+ }
2401
+
2402
+ // src/effect/runner/runPipeline.ts
2403
+ async function runPipeline(pipeline, opts) {
2404
+ const pass = createPass(pipeline, opts);
2405
+ try {
2406
+ while (await pass.step() !== void 0) ;
2407
+ return pass.run;
2408
+ } finally {
2409
+ pass.dispose();
2410
+ }
2411
+ }
2412
+
2413
+ // src/effect/runner/runner.ts
2414
+ function createEffectRunner(opts) {
2415
+ let { pipeline, ...options } = opts;
2416
+ let state = { run: void 0, playing: false, time: 0 };
2417
+ let stepping = null;
2418
+ let controller = new AbortController();
2419
+ let tail = Promise.resolve();
2420
+ let pending = 0;
2421
+ let published = [];
2422
+ const listeners = /* @__PURE__ */ new Set();
2423
+ const set = (next) => {
2424
+ state = { ...state, ...next };
2425
+ for (const l of [...listeners]) l();
2426
+ };
2427
+ const publish = (run) => {
2428
+ published.push(run);
2429
+ set({ run });
2430
+ };
2431
+ const passOptions = (time) => ({ ...options, time, signal: controller.signal });
2432
+ const dropStepping = () => {
2433
+ stepping?.dispose();
2434
+ stepping = null;
2435
+ };
2436
+ const enqueue = (task) => {
2437
+ const signal = controller.signal;
2438
+ pending++;
2439
+ const done = tail.then(() => signal.aborted ? void 0 : task(signal)).catch((err) => {
2440
+ if (!signal.aborted) throw err;
2441
+ }).finally(() => {
2442
+ pending--;
2443
+ });
2444
+ tail = done.catch(() => {
2445
+ });
2446
+ return done;
2447
+ };
2448
+ const wholeRun = (time) => async (signal) => {
2449
+ dropStepping();
2450
+ const run = await runPipeline(pipeline, passOptions(time));
2451
+ if (signal.aborted) closeBitmaps(previewsOf(run));
2452
+ else publish(run);
2453
+ };
2454
+ const stepOnce = (time) => async () => {
2455
+ let pass = stepping ??= createPass(pipeline, passOptions(time));
2456
+ let at = await pass.step();
2457
+ if (at === void 0) {
2458
+ dropStepping();
2459
+ pass = stepping = createPass(pipeline, passOptions(time));
2460
+ at = await pass.step();
2461
+ }
2462
+ publish(at === void 0 ? pass.run : { ...pass.run, at });
2463
+ };
2464
+ return {
2465
+ get state() {
2466
+ return state;
2467
+ },
2468
+ run: () => enqueue(wholeRun(state.time)),
2469
+ step() {
2470
+ return enqueue(stepOnce(state.time));
2471
+ },
2472
+ play: () => {
2473
+ if (!state.playing) set({ playing: true });
2474
+ },
2475
+ pause: () => {
2476
+ if (state.playing) set({ playing: false });
2477
+ },
2478
+ reset() {
2479
+ controller.abort();
2480
+ controller = new AbortController();
2481
+ dropStepping();
2482
+ set({ run: void 0, playing: false, time: 0 });
2483
+ },
2484
+ advance(seconds) {
2485
+ if (!state.playing) return;
2486
+ state = { ...state, time: state.time + seconds };
2487
+ if (pending === 0) void enqueue(wholeRun(state.time));
2488
+ },
2489
+ setPipeline(next) {
2490
+ if (next === pipeline) return;
2491
+ pipeline = next;
2492
+ dropStepping();
2493
+ },
2494
+ setSources(sources) {
2495
+ if (sources === options.sources) return;
2496
+ options = { ...options, sources };
2497
+ dropStepping();
2498
+ },
2499
+ setOptions(next) {
2500
+ options = { ...options, ...next };
2501
+ },
2502
+ drawn(run) {
2503
+ const at = run === void 0 ? published.length : published.indexOf(run);
2504
+ if (at <= 0) return;
2505
+ const keep = new Set(run === void 0 ? [] : previewsOf(run));
2506
+ closeBitmaps(published.slice(0, at).flatMap(previewsOf).filter((b) => !keep.has(b)));
2507
+ published = published.slice(at);
2508
+ },
2509
+ subscribe(listener) {
2510
+ listeners.add(listener);
2511
+ return () => {
2512
+ listeners.delete(listener);
2513
+ };
2514
+ },
2515
+ dispose() {
2516
+ this.reset();
2517
+ closeBitmaps(published.flatMap(previewsOf));
2518
+ published = [];
2519
+ }
2520
+ };
2521
+ }
2522
+ function useEffectRunner(opts) {
2523
+ const { pipeline, sources, evaluators, kinds, previews, previewSize, target, requestFrame, cancelFrame } = opts;
2524
+ const [runner] = useState(() => createEffectRunner(opts));
2525
+ useEffect(() => {
2526
+ runner.setPipeline(pipeline);
2527
+ }, [runner, pipeline]);
2528
+ useEffect(() => {
2529
+ if (sources !== void 0) runner.setSources(sources);
2530
+ }, [runner, sources]);
2531
+ useEffect(() => {
2532
+ runner.setOptions({
2533
+ evaluators,
2534
+ ...kinds !== void 0 ? { kinds } : {},
2535
+ ...previews !== void 0 ? { previews } : {},
2536
+ ...previewSize !== void 0 ? { previewSize } : {}
2537
+ });
2538
+ }, [runner, evaluators, kinds, previews, previewSize]);
2539
+ const state = useSyncExternalStore(runner.subscribe, () => runner.state);
2540
+ const last = useRef(null);
2541
+ const loop = useVisibleRaf((now, self) => {
2542
+ if (!runner.state.playing) return;
2543
+ runner.advance(last.current === null ? 0 : (now - last.current) / 1e3);
2544
+ last.current = now;
2545
+ self.request();
2546
+ }, {
2547
+ ...target !== void 0 ? { target } : {},
2548
+ ...requestFrame !== void 0 ? { requestFrame } : {},
2549
+ ...cancelFrame !== void 0 ? { cancelFrame } : {},
2550
+ onResume: () => {
2551
+ last.current = null;
2552
+ }
2553
+ });
2554
+ useEffect(() => {
2555
+ last.current = null;
2556
+ if (state.playing) loop.request();
2557
+ else loop.cancel();
2558
+ }, [state.playing, loop]);
2559
+ useEffect(() => {
2560
+ runner.drawn(state.run);
2561
+ }, [runner, state.run]);
2562
+ useEffect(() => () => runner.dispose(), [runner]);
2563
+ return { ...state, runner };
2564
+ }
2565
+
2566
+ export { AT_COLOR, BRANCH_SYMBOLS, COMPACT_PADDING, EFFECT_LOOKS, EFFECT_MIN_SCALE, EffectChart, EffectEditor, IMAGE_STAGE_TYPES, LANE_INSET, REORDER_LANES_ACTION_ID, addStage, addWire, applyLookRoutes, branchNode, canMoveInChain, canWire, chainLinks, compact, costText, createEffectRunner, createPass, editorScene, ensureWireIds, groupContents, groupStages, imageEvaluators, imageValueKinds, laneId, laneTypeOf, lanes, mainChain, moveGroup, moveInChain, moveStages, moveToGroup, nextGroupId, nextStageId, nextWireId, nodeGraph, paintRun, pruneLookRoutes, rack, reconnectWire, removeStages, removeWires, renameGroup, runPipeline, setGroupBox, setLaneOrder, setLookRoute, setParam, setWireRoute, symbolNameFor, testCard, ungroup, useEffectRunner, wireEnds };
1282
2567
  //# sourceMappingURL=effect.js.map
1283
2568
  //# sourceMappingURL=effect.js.map