@weasel-js/diagram 1.11.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-CI3I3I6T.js';
2
+ import './chunk-T6DQT553.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-QKSWZEJH.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-QKSWZEJH.js';
5
+ import { diagramScene, diagramPages, edgeIdOf, titleIdOf, groupLabelIdOf, nestGroups, LABEL_PLACEMENT, groupMinWidth, EDGE_DERIVE_PATH } from './chunk-43LDZ6L4.js';
6
+ import { portOf, DEFAULT_GROUP_INSET, SIDE_TRACK_GAP, widenForMinWidth, portSitesOf, labelCenter, insetOf, rankGaps } from './chunk-IM6DHWZO.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,11 +1381,11 @@ 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;
@@ -910,10 +1432,10 @@ function routeWires(wires, nodes, laid) {
910
1432
  const label = spec.get(`${edgeIdOf(edge)}/label`)?.pose;
911
1433
  const others = keepOut.concat(segments.flatMap((boxes, k) => k === w ? [] : boxes));
912
1434
  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 };
1435
+ const clear2 = path === null || label === void 0 ? void 0 : clearLabel(path, label, offset, others) ?? clearLabel(path, label, offset, keepOut);
1436
+ if (clear2 === void 0) return { edge: routed, style };
1437
+ keepOut.push(clear2.box);
1438
+ return { edge: { ...routed, labelPlacement: { at: { near: clear2.near }, offset: clear2.offset } }, style };
917
1439
  });
918
1440
  return { edges, tracks };
919
1441
  }
@@ -921,11 +1443,8 @@ function routeWires(wires, nodes, laid) {
921
1443
  // src/effect/lanes.ts
922
1444
  var LANE_INSET = 6;
923
1445
  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
1446
  var laneId = (type) => `~lane:${type}`;
1447
+ var laneTypeOf = (id) => id.startsWith("~lane:") ? id.slice("~lane:".length) : void 0;
929
1448
  function laneTypes(stages, first = []) {
930
1449
  const used = [...new Set(stages.flatMap((s) => [...s.inputs ?? [], ...s.outputs ?? []].map((p) => p.type)))];
931
1450
  const lead = [...new Set(first)].filter((t) => used.includes(t));
@@ -935,23 +1454,6 @@ function spanOf(stage, lane) {
935
1454
  const at = [...stage.inputs ?? [], ...stage.outputs ?? []].map((p) => lane.get(p.type));
936
1455
  return at.length === 0 ? [0, 0] : [Math.min(...at), Math.max(...at)];
937
1456
  }
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
1457
  function laneHeights(count, least, needs, insets) {
956
1458
  const heights = Array.from({ length: count }, () => least);
957
1459
  for (const { span: [lo, hi], height } of [...needs].sort((a, b) => a.span[1] - a.span[0] - (b.span[1] - b.span[0]))) {
@@ -978,10 +1480,14 @@ function controlSideOf(stage, pipeline, lane) {
978
1480
  const signal = ports.filter((p) => !isControl(pipeline, p.type));
979
1481
  return control.length > 0 && signal.length > 0 && mean(control) > mean(signal) ? "bottom" : "top";
980
1482
  }
981
- function sizesOf(nodes, measure) {
982
- const specs = diagramScene({ nodes, edges: [] }, { layout: "none", ...measure !== void 0 ? { measure } : {} });
1483
+ function sizesOf(nodes, groups, measure) {
1484
+ const specs = diagramScene({ nodes, edges: [], groups }, { layout: "none", ...measure !== void 0 ? { measure } : {} });
983
1485
  const pose = new Map(specs.map((s) => [s.id, s.pose]));
984
- return new Map(nodes.map((n) => {
1486
+ const titles = new Map(groups.flatMap((g) => {
1487
+ const title2 = pose.get(groupLabelIdOf(g.id));
1488
+ return title2 === void 0 ? [] : [[g.id, title2.width]];
1489
+ }));
1490
+ const size = new Map(nodes.map((n) => {
985
1491
  const box = pose.get(n.id);
986
1492
  const ink = inkOf(n.id, specs);
987
1493
  const over = {
@@ -992,6 +1498,7 @@ function sizesOf(nodes, measure) {
992
1498
  };
993
1499
  return [n.id, { box, over }];
994
1500
  }));
1501
+ return { size, titles };
995
1502
  }
996
1503
  function laneNode(type, box, color) {
997
1504
  return {
@@ -1010,7 +1517,7 @@ function insetsFor(edges, tracks) {
1010
1517
  return LANE_INSET + (k === 0 || k === edges - 1 ? n * TRACK_GAP : Math.max(0, n - 1) * TRACK_GAP / 2);
1011
1518
  });
1012
1519
  }
1013
- function place(plan, insets) {
1520
+ function place2(plan, insets) {
1014
1521
  const { stages, size, spans, column } = plan;
1015
1522
  const inkHeight = (id) => size.get(id).box.height + size.get(id).over.top + size.get(id).over.bottom;
1016
1523
  const heights = laneHeights(plan.rows, plan.least, stages.map((s) => ({ span: spans.get(s.id), height: inkHeight(s.id) })), insets);
@@ -1029,11 +1536,13 @@ function place(plan, insets) {
1029
1536
  const inkWidth = (id) => stood.get(id).width + stood.get(id).over.left + stood.get(id).over.right;
1030
1537
  const widths = [];
1031
1538
  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));
1539
+ const offset = (id) => (widths[column.get(id)] - inkWidth(id)) / 2 + stood.get(id).over.left;
1540
+ const extent = (id) => ({ lo: offset(id), hi: offset(id) + stood.get(id).width });
1541
+ const { lefts, end } = columnLefts(widths, column, plan.blocks, plan.gap, plan.start, extent);
1033
1542
  const nodes = stages.map((stage, k) => {
1034
1543
  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 };
1544
+ const { y, height, width } = stood.get(stage.id);
1545
+ const box = { x: lefts[c] + offset(stage.id), y, width, height };
1037
1546
  return {
1038
1547
  ...plan.drawn[k],
1039
1548
  at: { x: box.x, y: box.y },
@@ -1044,25 +1553,28 @@ function place(plan, insets) {
1044
1553
  });
1045
1554
  const columns = Array.from(widths, (wd, c) => [lefts[c], lefts[c] + (wd ?? 0)]);
1046
1555
  const edges = [...tops, tops.length === 0 ? 0 : tops[tops.length - 1] + heights[heights.length - 1]];
1047
- return { nodes, tops, heights, columns, edges };
1556
+ return { nodes, tops, heights, columns, edges, end };
1048
1557
  }
1049
1558
  var lanes = (pipeline, options = {}) => {
1050
- const color = typeColors(pipeline, options.colors);
1559
+ const theme = options.theme ?? LOOK_THEME;
1560
+ const color = typeColors(pipeline, options.colors, theme.series);
1051
1561
  const stages = uniqueStages(pipeline);
1052
1562
  const types = laneTypes(stages, options.lanes);
1053
1563
  const rows = types.length > 0 ? types : [""];
1054
1564
  const lane = new Map(types.map((t, i) => [t, i]));
1055
1565
  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 }));
1566
+ 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
1567
  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);
1568
+ const groups = uniqueGroups(pipeline);
1569
+ const { size, titles } = sizesOf([...drawn, ...labels], groups, options.measure);
1570
+ const { column, blocks } = laneColumns(stages, drawableWires(pipeline), spans, groups, titles);
1060
1571
  const plan = {
1061
1572
  stages,
1062
1573
  drawn,
1063
1574
  size,
1064
1575
  spans,
1065
1576
  column,
1577
+ blocks,
1066
1578
  lane,
1067
1579
  rows: rows.length,
1068
1580
  least: Math.max(options.laneHeight ?? 48, ...labels.map((l) => size.get(l.id).box.height)),
@@ -1072,23 +1584,24 @@ var lanes = (pipeline, options = {}) => {
1072
1584
  const router = options.router ?? "bezier";
1073
1585
  const wires = wireEdges(pipeline, color);
1074
1586
  let insets = insetsFor(rows.length + 1, /* @__PURE__ */ new Map());
1075
- let laid = place(plan, insets);
1587
+ let laid = place2(plan, insets);
1076
1588
  let routed = routeWires(wires, laid.nodes, { grid: laid, column, router, measure: options.measure });
1077
1589
  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);
1590
+ const need2 = insetsFor(rows.length + 1, routed.tracks);
1591
+ if (need2.every((n, k) => n <= insets[k])) break;
1592
+ insets = need2.map((n, k) => Math.max(n, insets[k]));
1593
+ laid = place2(plan, insets);
1082
1594
  routed = routeWires(wires, laid.nodes, { grid: laid, column, router, measure: options.measure });
1083
1595
  }
1084
1596
  const { columns, tops, heights } = laid;
1085
- const right = columns.length === 0 ? plan.start : columns[columns.length - 1][1] + MARGIN;
1597
+ const right = columns.length === 0 ? plan.start : laid.end + MARGIN;
1086
1598
  const laneNodes = rows.map((t, i) => laneNode(t, { x: 0, y: tops[i], width: right, height: heights[i] }, color));
1087
1599
  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]]));
1600
+ const styles = sceneStyles(stages, edges, theme);
1601
+ const bodies = theme.lane.bodies;
1602
+ const laneStyle = new Map(rows.map((t, i) => [laneId(t), bodies[i % bodies.length]]));
1090
1603
  return {
1091
- data: { nodes: [...laneNodes, ...laid.nodes], edges: edges.map((e) => e.edge), groups: uniqueGroups(pipeline) },
1604
+ data: { nodes: [...laneNodes, ...laid.nodes], edges: edges.map((e) => e.edge), groups },
1092
1605
  scene: {
1093
1606
  layout: "none",
1094
1607
  router,
@@ -1096,61 +1609,17 @@ var lanes = (pipeline, options = {}) => {
1096
1609
  ...styles,
1097
1610
  nodeStyle: (n) => laneStyle.get(n.id) ?? styles.nodeStyle(n)
1098
1611
  },
1099
- color
1612
+ color,
1613
+ order: { axis: "x", items: mainChain(pipeline).map((id) => ({ id })) },
1614
+ ...types.length > 0 ? {
1615
+ laneOrder: {
1616
+ axis: "y",
1617
+ items: types.map((t, i) => ({ id: laneId(t), grip: { x: 0, y: tops[i], width: plan.start, height: heights[i] } }))
1618
+ }
1619
+ } : {}
1100
1620
  };
1101
1621
  };
1102
1622
 
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
1623
  // src/effect/rack.ts
1155
1624
  var SKIP_OUT = "~skip-out";
1156
1625
  var SKIP_IN = "~skip-in";
@@ -1164,7 +1633,14 @@ function rackWires(pipeline, skips) {
1164
1633
  const eaten = new Set(skips.flatMap((s) => [s.into.index, s.through.index, s.skip.index]));
1165
1634
  const wires = drawableWires(pipeline).filter((d) => !eaten.has(d.index)).map((d) => {
1166
1635
  const label = exitLabel(d);
1167
- return { from: d.wire.from, to: d.wire.to, type: d.type, problem: d.problem, ...label !== void 0 ? { label } : {} };
1636
+ return {
1637
+ ...d.wire.id !== void 0 ? { id: d.wire.id } : {},
1638
+ from: d.wire.from,
1639
+ to: d.wire.to,
1640
+ type: d.type,
1641
+ problem: d.problem,
1642
+ ...label !== void 0 ? { label } : {}
1643
+ };
1168
1644
  });
1169
1645
  for (const s of skips) {
1170
1646
  wires.push({
@@ -1186,22 +1662,12 @@ function rackWires(pipeline, skips) {
1186
1662
  }
1187
1663
  return wires;
1188
1664
  }
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 };
1665
+ function lineOf(pipeline, ordered, chain) {
1666
+ const { mainIn, mainOut } = chainLinks(pipeline, ordered);
1667
+ return { at: new Map(chain.map((id, i) => [id, i])), mainIn, mainOut };
1202
1668
  }
1203
- function portsOf(stage, line, wires, near, far) {
1204
- if (!line.at.has(stage.id)) return [...spreadAt(stage.outputs ?? [], far), ...spreadAt(stage.inputs ?? [], near)];
1669
+ function portsOf2(stage, line, wires, near2, far) {
1670
+ if (!line.at.has(stage.id)) return [...spreadAt(stage.outputs ?? [], far), ...spreadAt(stage.inputs ?? [], near2)];
1205
1671
  const typed = (p, anchor) => ({ id: p.id, type: p.type, at: anchor });
1206
1672
  const out = [];
1207
1673
  const side = [];
@@ -1213,7 +1679,7 @@ function portsOf(stage, line, wires, near, far) {
1213
1679
  if (p.id === line.mainOut.get(stage.id)) out.push(typed(p, { u: 1, v: 0.5 }));
1214
1680
  else side.push(p);
1215
1681
  }
1216
- out.push(...spreadAt(side, near));
1682
+ out.push(...spreadAt(side, near2));
1217
1683
  const type = portOn(stage, line.mainOut.get(stage.id) ?? line.mainIn.get(stage.id) ?? "")?.type;
1218
1684
  const arc = (id, u) => ({ id, at: { u, v: far }, ...type !== void 0 ? { type } : {} });
1219
1685
  if (wires.some((w) => w.arc && w.from.stage === stage.id)) out.push(arc(SKIP_OUT, 0.75));
@@ -1221,9 +1687,10 @@ function portsOf(stage, line, wires, near, far) {
1221
1687
  return out;
1222
1688
  }
1223
1689
  var rack = (pipeline, options = {}) => {
1224
- const color = typeColors(pipeline, options.colors);
1690
+ const theme = options.theme ?? LOOK_THEME;
1691
+ const color = typeColors(pipeline, options.colors, theme.series);
1225
1692
  const below = options.side !== "above";
1226
- const near = below ? 1 : 0;
1693
+ const near2 = below ? 1 : 0;
1227
1694
  const far = below ? 0 : 1;
1228
1695
  const stages = uniqueStages(pipeline);
1229
1696
  const given = options.chain?.filter((id) => stages.some((s) => s.id === id));
@@ -1232,12 +1699,12 @@ var rack = (pipeline, options = {}) => {
1232
1699
  const gone = new Set(skips.map((s) => s.decision.id));
1233
1700
  const chain = ordered.filter((id) => !gone.has(id));
1234
1701
  const wires = rackWires(pipeline, skips);
1235
- const line = lineOf(pipeline, chain, wires);
1702
+ const line = lineOf(pipeline, ordered, chain);
1236
1703
  const kept = stages.filter((s) => !gone.has(s.id));
1237
1704
  const controlSide = below ? "bottom" : "top";
1238
1705
  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);
1706
+ if (kindOf(stage) === "decision") return { id: stage.id, ...branchNode(stage, pipeline, options.branch, { controlSide, style: stageStyle(stage, theme) }) };
1707
+ const ports = portsOf2(stage, line, wires, near2, far);
1241
1708
  const node = boxNode(stage, { showParams: options.showParams !== false, rowPorts: false, ports, run: options });
1242
1709
  const room = roomFor(ports);
1243
1710
  return room > 2 * SIDE_PORT_GAP ? { ...node, width: room } : node;
@@ -1248,7 +1715,7 @@ var rack = (pipeline, options = {}) => {
1248
1715
  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
1716
  const label = wire.label ?? (sideInput ? portOn(to, wire.to.port)?.label ?? wire.to.port : void 0);
1250
1717
  return {
1251
- edge: wireEdge(wire.from, wire.to, label),
1718
+ edge: wireEdge(wire.from, wire.to, label, wire.id),
1252
1719
  style: wireStyle(color(wire.type), wire.arc === true || isControl(pipeline, wire.type), wire.problem)
1253
1720
  };
1254
1721
  });
@@ -1268,16 +1735,808 @@ var rack = (pipeline, options = {}) => {
1268
1735
  ...options.rankGap !== void 0 ? { rankGap: options.rankGap } : {}
1269
1736
  },
1270
1737
  router: "bezier",
1271
- ...sceneStyles(kept, edges)
1738
+ ...sceneStyles(kept, edges, theme)
1272
1739
  },
1273
1740
  color,
1741
+ order: { axis: "x", items: chain.map((id) => ({ id })) },
1274
1742
  exits: new Map(wires.flatMap((wire, i) => wire.exit === void 0 ? [] : [[edgeIdOf(edges[i].edge), wire.exit]]))
1275
1743
  };
1276
1744
  };
1277
1745
 
1746
+ // src/effect/editorMapping.ts
1747
+ var endsKey = (from, to) => JSON.stringify([from.stage, from.port, to.stage, to.port]);
1748
+ var edgeEnds = (e) => endsKey({ stage: e.from, port: e.fromPort ?? "" }, { stage: e.to, port: e.toPort ?? "" });
1749
+ function editorScene(doc, look = "nodeGraph", options) {
1750
+ const drawn = drawLook(doc, look, options);
1751
+ const stages = new Set(doc.pipeline.stages.map((s) => s.id));
1752
+ const groups = new Set((doc.pipeline.groups ?? []).map((g) => g.id));
1753
+ const ids = ensureWireIds(doc).pipeline.wires.map((w) => w.id);
1754
+ const byEnds = /* @__PURE__ */ new Map();
1755
+ for (const d of drawableWires(doc.pipeline)) {
1756
+ const key = endsKey(d.wire.from, d.wire.to);
1757
+ if (!byEnds.has(key)) byEnds.set(key, ids[d.index]);
1758
+ }
1759
+ const wires = new Map(drawn.data.edges.flatMap((e) => {
1760
+ const id = byEnds.get(edgeEnds(e));
1761
+ return id === void 0 ? [] : [[edgeIdOf(e), id]];
1762
+ }));
1763
+ const routes = look === "nodeGraph" ? doc.routes : void 0;
1764
+ const edges = routes === void 0 ? drawn.data.edges : drawn.data.edges.map((e) => {
1765
+ const id = wires.get(edgeIdOf(e));
1766
+ const route = id === void 0 ? void 0 : routes[id];
1767
+ return route === void 0 ? e : { ...e, waypoints: route };
1768
+ });
1769
+ return {
1770
+ ...drawn,
1771
+ data: { ...drawn.data, edges },
1772
+ stageOf: (node) => stages.has(node) ? node : void 0,
1773
+ groupOf: (node) => groups.has(node) && !stages.has(node) ? node : void 0,
1774
+ wireOf: (edge) => wires.get(edge)
1775
+ };
1776
+ }
1777
+ function drawLook(doc, look, options) {
1778
+ if (look === "rack") return rack(doc.pipeline, options);
1779
+ if (look === "lanes") {
1780
+ const opts = options;
1781
+ const order = doc.laneOrder ?? opts?.lanes;
1782
+ return lanes(doc.pipeline, { ...opts, ...order !== void 0 ? { lanes: order } : {} });
1783
+ }
1784
+ const drawn = nodeGraph(doc.pipeline, options);
1785
+ const nodes = drawn.data.nodes.map((n) => {
1786
+ const at = doc.places[n.id];
1787
+ return at === void 0 ? n : { ...n, at: { x: at.x, y: at.y }, pinned: true };
1788
+ });
1789
+ const { groups = [], ...rest } = drawn.data;
1790
+ const containers = groups.map((g) => {
1791
+ const box = doc.groupBoxes?.[g.id];
1792
+ return {
1793
+ id: g.id,
1794
+ role: "container",
1795
+ label: g.label ?? "",
1796
+ members: g.members,
1797
+ ...box !== void 0 ? { box: { ...box }, pinned: true } : {}
1798
+ };
1799
+ });
1800
+ return {
1801
+ ...drawn,
1802
+ data: {
1803
+ ...rest,
1804
+ nodes,
1805
+ ...containers.length > 0 ? { containers } : {},
1806
+ ...doc.pages !== void 0 ? { pages: doc.pages } : {}
1807
+ }
1808
+ };
1809
+ }
1810
+
1811
+ // src/effect/groupOrder.ts
1812
+ function groupRuns(pipeline, chain) {
1813
+ const at = new Map(chain.map((id, i) => [id, i]));
1814
+ const doc = { pipeline};
1815
+ const runs = /* @__PURE__ */ new Map();
1816
+ for (const g of pipeline.groups ?? []) {
1817
+ const on = [...groupContents(doc, g.id).stages].flatMap((s) => at.get(s) ?? []);
1818
+ if (on.length === 0) continue;
1819
+ const first = Math.min(...on);
1820
+ const last = Math.max(...on);
1821
+ runs.set(g.id, last - first + 1 === on.length ? { first, last } : null);
1822
+ }
1823
+ return runs;
1824
+ }
1825
+ var holds = (b, p) => p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height;
1826
+ function groupGripOf(doc, group, boxOf) {
1827
+ const box = boxOf(group);
1828
+ const members = doc.pipeline.groups?.find((g) => g.id === group)?.members ?? [];
1829
+ const tops = members.flatMap((m) => boxOf(m)?.y ?? []);
1830
+ if (box === void 0 || tops.length === 0) return void 0;
1831
+ const height = Math.min(...tops) - box.y;
1832
+ return height > 0 ? { x: box.x, y: box.y, width: box.width, height } : void 0;
1833
+ }
1834
+ function unitStages(doc, unit) {
1835
+ return doc.pipeline.groups?.some((g) => g.id === unit) ? groupContents(doc, unit).stages : /* @__PURE__ */ new Set([unit]);
1836
+ }
1837
+ function parentsOf(doc) {
1838
+ const parent = /* @__PURE__ */ new Map();
1839
+ for (const g of doc.pipeline.groups ?? []) for (const m of g.members) parent.set(m, g.id);
1840
+ return parent;
1841
+ }
1842
+ function groupAt(doc, boxOf, at, among = () => true) {
1843
+ const parent = parentsOf(doc);
1844
+ const depth = (id) => parent.has(id) ? 1 + depth(parent.get(id)) : 0;
1845
+ const held = (doc.pipeline.groups ?? []).filter((g) => {
1846
+ const grip = among(g.id) ? groupGripOf(doc, g.id, boxOf) : void 0;
1847
+ return grip !== void 0 && holds(grip, at);
1848
+ });
1849
+ return held.sort((a, b) => depth(b.id) - depth(a.id))[0]?.id;
1850
+ }
1851
+ function groupSlotsAt(source, at) {
1852
+ const { doc, order, boxOf } = source;
1853
+ const groups = doc.pipeline.groups ?? [];
1854
+ if (order === void 0 || groups.length === 0) return null;
1855
+ const parent = parentsOf(doc);
1856
+ const runs = groupRuns(doc.pipeline, mainChain(doc.pipeline));
1857
+ const held = groupAt(doc, boxOf, at, (g) => runs.get(g) != null);
1858
+ if (held === void 0) return null;
1859
+ const up = parent.get(held);
1860
+ const siblings = up !== void 0 ? groups.find((g) => g.id === up).members : [...groups, ...doc.pipeline.stages].flatMap((u) => parent.has(u.id) ? [] : [u.id]);
1861
+ const drawnAt = new Map(order.items.map((i, k) => [i.id, k]));
1862
+ const placed = siblings.flatMap((id) => {
1863
+ const drawn = [...unitStages(doc, id)].flatMap((s) => drawnAt.get(s) ?? []);
1864
+ const bounds = boxOf(id);
1865
+ return drawn.length === 0 || bounds === void 0 ? [] : [{ id, bounds, first: Math.min(...drawn) }];
1866
+ }).sort((a, b) => a.first - b.first);
1867
+ const isGroup = new Set(groups.map((g) => g.id));
1868
+ const items = placed.map(({ id, bounds }) => {
1869
+ if (!isGroup.has(id)) return { id, bounds, grip: null };
1870
+ const inside = groupContents(doc, id);
1871
+ const carries = [...inside.groups, ...inside.stages, ...[...inside.groups].map(groupLabelIdOf)];
1872
+ return { id, bounds, grip: groupGripOf(doc, id, boxOf) ?? null, carries };
1873
+ });
1874
+ return items.length < 2 ? null : sequenceSlots(order.axis, items);
1875
+ }
1876
+
1877
+ // src/effect/editorReorder.ts
1878
+ var REORDER_LANES_ACTION_ID = "diagram.reorderLanes";
1879
+ var REORDER_GROUPS_ACTION_ID = "diagram.reorderGroups";
1880
+ function slotsOf(order, boxOf) {
1881
+ if (order === void 0) return null;
1882
+ const items = order.items.flatMap((i) => {
1883
+ const bounds = boxOf(i.id);
1884
+ return bounds === void 0 ? [] : [{ id: i.id, bounds, ...i.grip !== void 0 ? { grip: i.grip } : {} }];
1885
+ });
1886
+ return items.length < 2 ? null : sequenceSlots(order.axis, items);
1887
+ }
1888
+ function keepsGroups(doc, next) {
1889
+ if (next === null) return null;
1890
+ const after = groupRuns(next.pipeline, mainChain(next.pipeline));
1891
+ for (const [id, run] of groupRuns(doc.pipeline, mainChain(doc.pipeline))) if (run !== null && after.get(id) === null) return null;
1892
+ return next;
1893
+ }
1894
+ function dropInGap(doc, chain, drawn, to, at) {
1895
+ const tries = [to];
1896
+ for (let k = to; k > 0 && !drawn.has(chain[k - 1]); k--) tries.push(k - 1);
1897
+ for (let k = to; k < chain.length && !drawn.has(chain[k]); k++) tries.push(k + 1);
1898
+ for (const t of tries) {
1899
+ const next = keepsGroups(doc, at(t));
1900
+ if (next !== null) return next;
1901
+ }
1902
+ return null;
1903
+ }
1904
+ var drawnOf = (order) => new Set(order.items.map((i) => i.id));
1905
+ function stageMove(doc, order, move) {
1906
+ const chain = mainChain(doc.pipeline);
1907
+ const before = order.items[move.to];
1908
+ const to = before === void 0 ? chain.length : chain.indexOf(before.id);
1909
+ const wired = ensureWireIds(doc);
1910
+ return dropInGap(doc, chain, drawnOf(order), to, (t) => moveInChain(wired, move.id, t));
1911
+ }
1912
+ function groupMove(doc, order, units, move) {
1913
+ const chain = mainChain(doc.pipeline);
1914
+ const drawn = drawnOf(order);
1915
+ const indices = (unit, drawnOnly) => [...unitStages(doc, unit)].flatMap((s) => chain.includes(s) && (!drawnOnly || drawn.has(s)) ? [chain.indexOf(s)] : []);
1916
+ const run = indices(move.id, false);
1917
+ if (run.length === 0) return null;
1918
+ const [i, j] = [Math.min(...run), Math.max(...run)];
1919
+ if (j - i + 1 !== run.length) return null;
1920
+ const before = units[move.to];
1921
+ const edge = before !== void 0 ? indices(before, true) : indices(units[units.length - 1] ?? move.id, true).map((k) => k + 1);
1922
+ if (edge.length === 0) return null;
1923
+ const to = before !== void 0 ? Math.min(...edge) : Math.max(...edge);
1924
+ const wired = ensureWireIds(doc);
1925
+ return dropInGap(doc, chain, drawn, to, (t) => moveRunInChain(wired, chain[i], chain[j], t));
1926
+ }
1927
+ function laneMove(doc, order, move) {
1928
+ const types = order.items.flatMap((i) => laneTypeOf(i.id) ?? []);
1929
+ const moved = types.filter((_, k) => k !== move.from);
1930
+ moved.splice(move.to > move.from ? move.to - 1 : move.to, 0, types[move.from]);
1931
+ return setLaneOrder(doc, moved);
1932
+ }
1933
+ function unitsBefore(move) {
1934
+ const units = move.order.filter((id) => id !== move.id);
1935
+ units.splice(move.from, 0, move.id);
1936
+ return units;
1937
+ }
1938
+ function groupReorder(now) {
1939
+ const moved = (move) => {
1940
+ const { doc, order } = now();
1941
+ return order === void 0 ? null : groupMove(doc, order, unitsBefore(move), move);
1942
+ };
1943
+ return reorderContribution({
1944
+ id: REORDER_GROUPS_ACTION_ID,
1945
+ label: "Reorder group",
1946
+ slots: (_, at) => groupSlotsAt(now(), at),
1947
+ canDrop: (move) => moved(move) !== null,
1948
+ commit: (move) => {
1949
+ const next = moved(move);
1950
+ if (next !== null) now().onChange(next, "Reorder group");
1951
+ }
1952
+ });
1953
+ }
1954
+ function editorReorders(now) {
1955
+ return [
1956
+ reorderContribution({
1957
+ label: "Reorder",
1958
+ slots: () => slotsOf(now().order, now().boxOf),
1959
+ canDrop: (move) => {
1960
+ const { doc, order } = now();
1961
+ return order !== void 0 && stageMove(doc, order, move) !== null;
1962
+ },
1963
+ commit: (move) => {
1964
+ const { doc, order, onChange } = now();
1965
+ const next = order === void 0 ? null : stageMove(doc, order, move);
1966
+ if (next !== null) onChange(next, "Reorder");
1967
+ }
1968
+ }),
1969
+ reorderContribution({
1970
+ id: REORDER_LANES_ACTION_ID,
1971
+ label: "Reorder lanes",
1972
+ slots: () => slotsOf(now().laneOrder, now().boxOf),
1973
+ commit: (move) => {
1974
+ const { doc, laneOrder, onChange } = now();
1975
+ if (laneOrder !== void 0) onChange(laneMove(doc, laneOrder, move), "Reorder lanes");
1976
+ }
1977
+ }),
1978
+ groupReorder(now)
1979
+ ];
1980
+ }
1981
+ var RENAME_GROUP_ACTION_ID = "effect.renameGroup";
1982
+ function renameGroupContribution(now, rename) {
1983
+ const under = (at) => {
1984
+ const { doc, boxOf } = now();
1985
+ return groupAt(doc, boxOf, at);
1986
+ };
1987
+ return {
1988
+ id: `weasel-diagram-effect:${RENAME_GROUP_ACTION_ID}`,
1989
+ eligibility: { always: true },
1990
+ actions: [{
1991
+ id: RENAME_GROUP_ACTION_ID,
1992
+ label: "Rename group",
1993
+ group: "diagram",
1994
+ enabled: (_, at) => at === void 0 || under(at) !== void 0 ? true : ActionDisabledReason.NotApplicable,
1995
+ invoker: {
1996
+ timing: "immediate",
1997
+ run(_, params) {
1998
+ const group = under({ x: Number(params?.worldX), y: Number(params?.worldY) });
1999
+ if (group !== void 0) rename(group);
2000
+ }
2001
+ }
2002
+ }],
2003
+ // Empty space included: an unlabeled group's grip is padding.
2004
+ bindings: [{ spec: { kind: "doubleClick", target: { kindOf: ANYWHERE } }, actionId: RENAME_GROUP_ACTION_ID }]
2005
+ };
2006
+ }
2007
+
2008
+ // src/effect/place.ts
2009
+ var PLACE_GAP = 24;
2010
+ var STEP = 8;
2011
+ 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);
2012
+ function freeSpot(boxes, size, view) {
2013
+ if (view !== null) {
2014
+ const right = view.x + view.width - PLACE_GAP - size.width;
2015
+ const bottom = view.y + view.height - PLACE_GAP - size.height;
2016
+ for (let y = view.y + PLACE_GAP; y <= bottom; y += STEP) {
2017
+ for (let x = view.x + PLACE_GAP; x <= right; x += STEP) {
2018
+ if (clear(x, y, size, boxes)) return { x, y };
2019
+ }
2020
+ }
2021
+ }
2022
+ if (boxes.length === 0) return { x: (view?.x ?? 0) + PLACE_GAP, y: (view?.y ?? 0) + PLACE_GAP };
2023
+ const last = boxes.reduce((a, b) => b.x + b.width > a.x + a.width ? b : a);
2024
+ return { x: last.x + last.width + PLACE_GAP, y: last.y };
2025
+ }
2026
+ var EffectEditor = forwardRef(function EffectEditor2({ doc, onChange, run, selected, onSelect, selectedGroup, onSelectGroup, look = "nodeGraph", options, width, height, background, className }, ref) {
2027
+ const built = useMemo(() => editorScene(doc, look, options), [doc, look, options]);
2028
+ 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]);
2029
+ const painted = useMemo(() => paintRun(doc.pipeline, specs, run, built.exits), [doc.pipeline, specs, run, built]);
2030
+ useReleaseReplaced(run);
2031
+ const portOptions = useRunPortOptions(built.color, doc.pipeline, run);
2032
+ const picks = useEditorPicks(built, { selected, selectedGroup, onSelect, onSelectGroup });
2033
+ const now = useLatest({ doc, built, specs, look, options, onChange });
2034
+ const view = useRef(null);
2035
+ const [renaming, setRenaming] = useState(null);
2036
+ const handlers = useMemo(() => editHandlers(() => now.current, picks.unpick, setRenaming), [now, picks.unpick]);
2037
+ const ambient = useMemo(() => {
2038
+ const source = () => {
2039
+ const { doc: d, built: b, specs: shown, onChange: report } = now.current;
2040
+ const boxes = new Map(shown.map((s) => [s.id, s.pose]));
2041
+ return { doc: d, order: b.order, laneOrder: b.laneOrder, boxOf: (id) => boxes.get(id), onChange: report };
2042
+ };
2043
+ return [...editorReorders(source), renameGroupContribution(source, setRenaming)];
2044
+ }, [now]);
2045
+ useImperativeHandle(ref, () => ({
2046
+ addStage: (type, client) => {
2047
+ const { doc: d, specs: shown, look: l, options: opts, onChange: report } = now.current;
2048
+ const stage = stageFromType(type, nextStageId(d.pipeline, type.type));
2049
+ if (l !== "nodeGraph") {
2050
+ report(addStage(ensureWireIds(d), stage), "Add stage");
2051
+ return stage.id;
2052
+ }
2053
+ const pinned = ensureWireIds(pinShown(d, shown));
2054
+ if (client && view.current) {
2055
+ report(addStage(pinned, stage, view.current.worldAt(client.x, client.y)), "Add stage");
2056
+ } else {
2057
+ const boxes = stageBoxes(d, shown);
2058
+ const alone = { pipeline: { ...d.pipeline, stages: [stage], wires: [], groups: [] }, places: {} };
2059
+ const size = stageBoxes(alone, drawnSpecs(alone, opts)).get(stage.id) ?? { width: 0, height: 0 };
2060
+ const at = freeSpot([...boxes.values()], size, view.current?.visible() ?? null);
2061
+ report(addStage(pinned, stage, at), "Add stage");
2062
+ }
2063
+ return stage.id;
2064
+ },
2065
+ group: (ids) => {
2066
+ const { doc: d, specs: shown, look: l, onChange: report } = now.current;
2067
+ if (ids.length === 0) return null;
2068
+ const base = l === "nodeGraph" ? pinShown(ensureWireIds(d), shown) : d;
2069
+ report(groupStages(base, ids), "Group");
2070
+ return nextGroupId(d.pipeline);
2071
+ },
2072
+ ungroup: (id) => {
2073
+ const { doc: d, onChange: report } = now.current;
2074
+ if (d.pipeline.groups?.some((g) => g.id === id)) report(ungroup(d, id), "Ungroup");
2075
+ },
2076
+ renameGroup: (id) => setRenaming(id)
2077
+ }), [now]);
2078
+ const title2 = useGroupTitle(doc, look, renaming, setRenaming, onChange);
2079
+ const fill = (look === "nodeGraph" ? options?.theme ?? LOOK_THEME : LOOK_THEME).page;
2080
+ const placed = useMemo(() => diagramPages(built.data), [built.data.pages, built.data.pageLayout]);
2081
+ const pages = useMemo(() => placed.length > 0 ? { placed, fill } : void 0, [placed, fill]);
2082
+ return /* @__PURE__ */ jsx(
2083
+ DiagramView,
2084
+ {
2085
+ ref: view,
2086
+ specs: painted,
2087
+ width,
2088
+ height,
2089
+ minScale: EFFECT_MIN_SCALE,
2090
+ anchor: "start",
2091
+ ports: "connect",
2092
+ portOptions,
2093
+ canConnect: handlers.canConnect,
2094
+ onConnect: handlers.onConnect,
2095
+ onReconnect: handlers.onReconnect,
2096
+ onEditRoute: handlers.onEditRoute,
2097
+ onDelete: handlers.onDelete,
2098
+ ...look === "nodeGraph" ? { onMove: handlers.onMove } : { ambient },
2099
+ ...look === "nodeGraph" ? { onEditContainer: handlers.onEditContainer } : {},
2100
+ onSelect: picks.pick,
2101
+ ...picks.shown !== void 0 ? { selected: picks.shown } : {},
2102
+ ...pages !== void 0 ? { pages } : {},
2103
+ ...title2 !== null ? { overlays: title2 } : {},
2104
+ ...background !== void 0 ? { background } : {},
2105
+ ...className !== void 0 ? { className } : {}
2106
+ },
2107
+ look
2108
+ );
2109
+ });
2110
+ function drawnSpecs(doc, options) {
2111
+ const built = editorScene(doc, "nodeGraph", options);
2112
+ return diagramScene(built.data, built.scene);
2113
+ }
2114
+
2115
+ // src/effect/image/evaluators.ts
2116
+ var BLEND = { normal: "source-over", screen: "screen", multiply: "multiply" };
2117
+ function draw(width, height, paint) {
2118
+ const canvas = new OffscreenCanvas(width, height);
2119
+ paint(canvas.getContext("2d"));
2120
+ return canvas.transferToImageBitmap();
2121
+ }
2122
+ function need(ctx, port) {
2123
+ const value = ctx.inputs[port];
2124
+ if (value === void 0) throw new Error(`${port} is not connected`);
2125
+ return value;
2126
+ }
2127
+ function decide(ctx, value) {
2128
+ const outs = ctx.stage.outputs ?? [];
2129
+ const split = ctx.stage.split;
2130
+ const index = split !== void 0 ? split.at.filter((cut) => value >= cut).length : value > 0 ? 0 : 1;
2131
+ return outs[Math.min(index, outs.length - 1)]?.id;
2132
+ }
2133
+ var imageEvaluators = {
2134
+ source: ({ sources }) => {
2135
+ if (sources.image === void 0) throw new Error("no source image");
2136
+ return { outputs: { out: sources.image } };
2137
+ },
2138
+ blur: (ctx) => {
2139
+ const image2 = need(ctx, "in");
2140
+ const radius = Number(ctx.params.radius ?? 0);
2141
+ return {
2142
+ outputs: {
2143
+ out: draw(image2.width, image2.height, (g) => {
2144
+ g.filter = `blur(${radius}px)`;
2145
+ g.drawImage(image2, 0, 0);
2146
+ })
2147
+ }
2148
+ };
2149
+ },
2150
+ composite: (ctx) => {
2151
+ const base = need(ctx, "base");
2152
+ const top = need(ctx, "top");
2153
+ return {
2154
+ outputs: {
2155
+ out: draw(base.width, base.height, (g) => {
2156
+ g.drawImage(base, 0, 0);
2157
+ g.globalCompositeOperation = BLEND[String(ctx.params.blend)] ?? "source-over";
2158
+ g.drawImage(top, 0, 0, base.width, base.height);
2159
+ })
2160
+ }
2161
+ };
2162
+ },
2163
+ mask: (ctx) => {
2164
+ const image2 = need(ctx, "in");
2165
+ const mask = need(ctx, "mask");
2166
+ return {
2167
+ outputs: {
2168
+ out: draw(image2.width, image2.height, (g) => {
2169
+ g.drawImage(image2, 0, 0);
2170
+ g.globalCompositeOperation = "destination-in";
2171
+ g.drawImage(mask, 0, 0, image2.width, image2.height);
2172
+ })
2173
+ }
2174
+ };
2175
+ },
2176
+ oscillator: ({ params, time }) => ({ outputs: { value: Math.sin(2 * Math.PI * Number(params.rate ?? 1) * time) } }),
2177
+ decision: (ctx) => {
2178
+ const tested = ctx.stage.split?.input ?? ctx.stage.inputs?.find((p) => p.type === "number")?.id ?? "test";
2179
+ const taken = decide(ctx, need(ctx, tested));
2180
+ const type = ctx.stage.outputs?.find((p) => p.id === taken)?.type;
2181
+ const from = ctx.stage.inputs?.find((p) => p.type === type && ctx.inputs[p.id] !== void 0);
2182
+ return { outputs: taken !== void 0 && from !== void 0 ? { [taken]: ctx.inputs[from.id] } : {}, ...taken !== void 0 ? { taken } : {} };
2183
+ },
2184
+ out: () => ({ outputs: {} })
2185
+ };
2186
+
2187
+ // src/effect/image/stageTypes.ts
2188
+ var param = (label, control) => ({ label, value: formatParam(control), control });
2189
+ var image = (id, label) => ({ id, type: "image", ...label !== void 0 ? { label } : {} });
2190
+ var IMAGE_STAGE_TYPES = [
2191
+ { type: "source", title: "Source", kind: "source", outputs: [image("out")] },
2192
+ {
2193
+ type: "blur",
2194
+ title: "Blur",
2195
+ inputs: [image("in")],
2196
+ outputs: [image("out")],
2197
+ params: [param("radius", { kind: "number", value: 4, min: 0, max: 50, step: 1, unit: "px" })]
2198
+ },
2199
+ {
2200
+ type: "composite",
2201
+ title: "Composite",
2202
+ inputs: [image("base"), image("top")],
2203
+ outputs: [image("out")],
2204
+ params: [param("blend", {
2205
+ kind: "enum",
2206
+ value: "normal",
2207
+ options: [{ value: "normal", label: "Normal" }, { value: "screen", label: "Screen" }, { value: "multiply", label: "Multiply" }]
2208
+ })]
2209
+ },
2210
+ {
2211
+ type: "mask",
2212
+ title: "Mask",
2213
+ inputs: [image("in"), { id: "mask", type: "mask" }],
2214
+ outputs: [image("out")]
2215
+ },
2216
+ {
2217
+ type: "oscillator",
2218
+ title: "Oscillator",
2219
+ outputs: [{ id: "value", type: "number" }],
2220
+ params: [param("rate", { kind: "number", value: 1, min: 0.1, max: 20, step: 0.1, unit: "Hz" })]
2221
+ },
2222
+ {
2223
+ type: "decision",
2224
+ title: "Decision",
2225
+ kind: "decision",
2226
+ inputs: [image("in"), { id: "test", type: "number" }],
2227
+ outputs: [image("yes", "yes"), image("no", "no")]
2228
+ },
2229
+ { type: "out", title: "Out", kind: "sink", inputs: [image("in")] }
2230
+ ];
2231
+
2232
+ // src/effect/image/testCard.ts
2233
+ var BARS = ["#c0c0c0", "#c0c000", "#00c0c0", "#00c000", "#c000c0", "#c00000", "#0000c0"];
2234
+ function testCard(size) {
2235
+ const { width, height } = typeof size === "number" ? { width: size, height: size } : size;
2236
+ const canvas = new OffscreenCanvas(width, height);
2237
+ const g = canvas.getContext("2d");
2238
+ const barHeight = height * 0.7;
2239
+ BARS.forEach((color, i) => {
2240
+ g.fillStyle = color;
2241
+ const x = Math.floor(i * width / BARS.length);
2242
+ g.fillRect(x, 0, Math.ceil(width / BARS.length) + 1, barHeight);
2243
+ });
2244
+ const ramp = g.createLinearGradient(0, 0, width, 0);
2245
+ ramp.addColorStop(0, "#000");
2246
+ ramp.addColorStop(1, "#fff");
2247
+ g.fillStyle = ramp;
2248
+ g.fillRect(0, barHeight, width, height - barHeight);
2249
+ g.strokeStyle = "#fff";
2250
+ g.lineWidth = Math.max(1, Math.min(width, height) / 32);
2251
+ g.beginPath();
2252
+ g.arc(width / 2, height / 2, Math.min(width, height) * 0.3, 0, Math.PI * 2);
2253
+ g.stroke();
2254
+ return canvas.transferToImageBitmap();
2255
+ }
2256
+
2257
+ // src/effect/image/valueKinds.ts
2258
+ var bitmapKind = {
2259
+ show: (value) => `${value.width}\xD7${value.height}`,
2260
+ preview(value, size) {
2261
+ const width = Math.max(1, Math.round(size.width * PREVIEW_DENSITY));
2262
+ const height = Math.max(1, Math.round(size.height * PREVIEW_DENSITY));
2263
+ const canvas = new OffscreenCanvas(width, height);
2264
+ canvas.getContext("2d").drawImage(value, 0, 0, width, height);
2265
+ return canvas.transferToImageBitmap();
2266
+ },
2267
+ dispose: (value) => value.close()
2268
+ };
2269
+ var imageValueKinds = {
2270
+ image: bitmapKind,
2271
+ mask: bitmapKind,
2272
+ number: { show: (value) => value.toFixed(3) }
2273
+ };
2274
+
1278
2275
  // src/effect/looks.ts
1279
2276
  var EFFECT_LOOKS = Object.freeze({ nodeGraph, rack, lanes, compact });
1280
2277
 
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 };
2278
+ // src/effect/runner/pass.ts
2279
+ function createPass(pipeline, opts) {
2280
+ const stages = new Map(uniqueStages(pipeline).map((s) => [s.id, s]));
2281
+ const queue = runOrder(pipeline).order;
2282
+ const made = /* @__PURE__ */ new Map();
2283
+ const sources = opts.sources ?? {};
2284
+ const size = typeof opts.previewSize === "object" ? opts.previewSize : { width: opts.previewSize ?? PREVIEW_SIZE, height: opts.previewSize ?? PREVIEW_SIZE };
2285
+ const signal = opts.signal ?? new AbortController().signal;
2286
+ let run = { stages: {} };
2287
+ const inputsOf = (stage) => {
2288
+ const inputs2 = {};
2289
+ for (const w of pipeline.wires) {
2290
+ if (w.to.stage !== stage.id || inputs2[w.to.port] !== void 0) continue;
2291
+ const value = made.get(w.from.stage)?.[w.from.port];
2292
+ if (value !== void 0) inputs2[w.to.port] = value;
2293
+ }
2294
+ return inputs2;
2295
+ };
2296
+ const record = async (stage, inputs2, outputs, entry) => {
2297
+ made.set(stage.id, outputs);
2298
+ const values = {};
2299
+ const previews = {};
2300
+ const seen = { ...inputs2, ...outputs };
2301
+ for (const port of shownPorts(stage)) {
2302
+ const value = seen[port.id];
2303
+ if (value === void 0) continue;
2304
+ const kind = opts.kinds?.[port.type];
2305
+ const text = kind?.show !== void 0 ? kind.show(value) : plainText(value);
2306
+ if (text !== void 0) values[port.id] = text;
2307
+ const preview = opts.previews === false ? void 0 : await kind?.preview?.(value, size);
2308
+ if (preview !== void 0) previews[port.id] = preview;
2309
+ }
2310
+ const full = {
2311
+ ...entry,
2312
+ ...Object.keys(values).length > 0 ? { values } : {},
2313
+ ...Object.keys(previews).length > 0 ? { previews } : {}
2314
+ };
2315
+ run = { stages: { ...run.stages, [stage.id]: full } };
2316
+ };
2317
+ const evaluate = async (stage, inputs2) => {
2318
+ if (stage.bypassed === true) return record(stage, inputs2, passThrough(stage, inputs2), {});
2319
+ const evaluator = stage.type === void 0 ? void 0 : opts.evaluators[stage.type];
2320
+ const params = paramValues(stage);
2321
+ const start = performance.now();
2322
+ try {
2323
+ if (evaluator === void 0) throw new Error(`no evaluator for type "${stage.type ?? ""}"`);
2324
+ const result = await evaluator({ stage, inputs: inputs2, params, sources, time: opts.time ?? 0, signal });
2325
+ const cost = performance.now() - start;
2326
+ return record(stage, inputs2, result.outputs, { cost, ...result.taken !== void 0 ? { taken: result.taken } : {} });
2327
+ } catch (err) {
2328
+ signal.throwIfAborted();
2329
+ const cost = performance.now() - start;
2330
+ run = { stages: { ...run.stages, [stage.id]: { cost, error: err instanceof Error ? err.message : String(err) } } };
2331
+ }
2332
+ };
2333
+ return {
2334
+ get run() {
2335
+ return run;
2336
+ },
2337
+ async step() {
2338
+ while (queue.length > 0) {
2339
+ signal.throwIfAborted();
2340
+ const stage = stages.get(queue.shift());
2341
+ const inputs2 = inputsOf(stage);
2342
+ if ((stage.inputs?.length ?? 0) > 0 && Object.keys(inputs2).length === 0) continue;
2343
+ await evaluate(stage, inputs2);
2344
+ signal.throwIfAborted();
2345
+ return stage.id;
2346
+ }
2347
+ return void 0;
2348
+ },
2349
+ dispose() {
2350
+ const given = new Set(Object.values(sources));
2351
+ const done = /* @__PURE__ */ new Set();
2352
+ for (const [id, outputs] of made) {
2353
+ for (const port of stages.get(id)?.outputs ?? []) {
2354
+ const value = outputs[port.id];
2355
+ if (value === void 0 || given.has(value) || done.has(value)) continue;
2356
+ done.add(value);
2357
+ opts.kinds?.[port.type]?.dispose?.(value);
2358
+ }
2359
+ }
2360
+ made.clear();
2361
+ }
2362
+ };
2363
+ }
2364
+ function passThrough(stage, inputs2) {
2365
+ const out = {};
2366
+ for (const port of stage.outputs ?? []) {
2367
+ const from = (stage.inputs ?? []).find((i) => i.type === port.type && inputs2[i.id] !== void 0);
2368
+ if (from !== void 0) out[port.id] = inputs2[from.id];
2369
+ }
2370
+ return out;
2371
+ }
2372
+ function plainText(value) {
2373
+ return typeof value === "number" || typeof value === "string" || typeof value === "boolean" ? String(value) : void 0;
2374
+ }
2375
+
2376
+ // src/effect/runner/runPipeline.ts
2377
+ async function runPipeline(pipeline, opts) {
2378
+ const pass = createPass(pipeline, opts);
2379
+ try {
2380
+ while (await pass.step() !== void 0) ;
2381
+ return pass.run;
2382
+ } finally {
2383
+ pass.dispose();
2384
+ }
2385
+ }
2386
+
2387
+ // src/effect/runner/runner.ts
2388
+ function createEffectRunner(opts) {
2389
+ let { pipeline, ...options } = opts;
2390
+ let state = { run: void 0, playing: false, time: 0 };
2391
+ let stepping = null;
2392
+ let controller = new AbortController();
2393
+ let tail = Promise.resolve();
2394
+ let pending = 0;
2395
+ let published = [];
2396
+ const listeners = /* @__PURE__ */ new Set();
2397
+ const set = (next) => {
2398
+ state = { ...state, ...next };
2399
+ for (const l of [...listeners]) l();
2400
+ };
2401
+ const publish = (run) => {
2402
+ published.push(run);
2403
+ set({ run });
2404
+ };
2405
+ const passOptions = (time) => ({ ...options, time, signal: controller.signal });
2406
+ const dropStepping = () => {
2407
+ stepping?.dispose();
2408
+ stepping = null;
2409
+ };
2410
+ const enqueue = (task) => {
2411
+ const signal = controller.signal;
2412
+ pending++;
2413
+ const done = tail.then(() => signal.aborted ? void 0 : task(signal)).catch((err) => {
2414
+ if (!signal.aborted) throw err;
2415
+ }).finally(() => {
2416
+ pending--;
2417
+ });
2418
+ tail = done.catch(() => {
2419
+ });
2420
+ return done;
2421
+ };
2422
+ const wholeRun = (time) => async (signal) => {
2423
+ dropStepping();
2424
+ const run = await runPipeline(pipeline, passOptions(time));
2425
+ if (signal.aborted) closeBitmaps(previewsOf(run));
2426
+ else publish(run);
2427
+ };
2428
+ const stepOnce = (time) => async () => {
2429
+ let pass = stepping ??= createPass(pipeline, passOptions(time));
2430
+ let at = await pass.step();
2431
+ if (at === void 0) {
2432
+ dropStepping();
2433
+ pass = stepping = createPass(pipeline, passOptions(time));
2434
+ at = await pass.step();
2435
+ }
2436
+ publish(at === void 0 ? pass.run : { ...pass.run, at });
2437
+ };
2438
+ return {
2439
+ get state() {
2440
+ return state;
2441
+ },
2442
+ run: () => enqueue(wholeRun(state.time)),
2443
+ step() {
2444
+ return enqueue(stepOnce(state.time));
2445
+ },
2446
+ play: () => {
2447
+ if (!state.playing) set({ playing: true });
2448
+ },
2449
+ pause: () => {
2450
+ if (state.playing) set({ playing: false });
2451
+ },
2452
+ reset() {
2453
+ controller.abort();
2454
+ controller = new AbortController();
2455
+ dropStepping();
2456
+ set({ run: void 0, playing: false, time: 0 });
2457
+ },
2458
+ advance(seconds) {
2459
+ if (!state.playing) return;
2460
+ state = { ...state, time: state.time + seconds };
2461
+ if (pending === 0) void enqueue(wholeRun(state.time));
2462
+ },
2463
+ setPipeline(next) {
2464
+ if (next === pipeline) return;
2465
+ pipeline = next;
2466
+ dropStepping();
2467
+ },
2468
+ setSources(sources) {
2469
+ if (sources === options.sources) return;
2470
+ options = { ...options, sources };
2471
+ dropStepping();
2472
+ },
2473
+ setOptions(next) {
2474
+ options = { ...options, ...next };
2475
+ },
2476
+ drawn(run) {
2477
+ const at = run === void 0 ? published.length : published.indexOf(run);
2478
+ if (at <= 0) return;
2479
+ const keep = new Set(run === void 0 ? [] : previewsOf(run));
2480
+ closeBitmaps(published.slice(0, at).flatMap(previewsOf).filter((b) => !keep.has(b)));
2481
+ published = published.slice(at);
2482
+ },
2483
+ subscribe(listener) {
2484
+ listeners.add(listener);
2485
+ return () => {
2486
+ listeners.delete(listener);
2487
+ };
2488
+ },
2489
+ dispose() {
2490
+ this.reset();
2491
+ closeBitmaps(published.flatMap(previewsOf));
2492
+ published = [];
2493
+ }
2494
+ };
2495
+ }
2496
+ function useEffectRunner(opts) {
2497
+ const { pipeline, sources, evaluators, kinds, previews, previewSize, target, requestFrame, cancelFrame } = opts;
2498
+ const [runner] = useState(() => createEffectRunner(opts));
2499
+ useEffect(() => {
2500
+ runner.setPipeline(pipeline);
2501
+ }, [runner, pipeline]);
2502
+ useEffect(() => {
2503
+ if (sources !== void 0) runner.setSources(sources);
2504
+ }, [runner, sources]);
2505
+ useEffect(() => {
2506
+ runner.setOptions({
2507
+ evaluators,
2508
+ ...kinds !== void 0 ? { kinds } : {},
2509
+ ...previews !== void 0 ? { previews } : {},
2510
+ ...previewSize !== void 0 ? { previewSize } : {}
2511
+ });
2512
+ }, [runner, evaluators, kinds, previews, previewSize]);
2513
+ const state = useSyncExternalStore(runner.subscribe, () => runner.state);
2514
+ const last = useRef(null);
2515
+ const loop = useVisibleRaf((now, self) => {
2516
+ if (!runner.state.playing) return;
2517
+ runner.advance(last.current === null ? 0 : (now - last.current) / 1e3);
2518
+ last.current = now;
2519
+ self.request();
2520
+ }, {
2521
+ ...target !== void 0 ? { target } : {},
2522
+ ...requestFrame !== void 0 ? { requestFrame } : {},
2523
+ ...cancelFrame !== void 0 ? { cancelFrame } : {},
2524
+ onResume: () => {
2525
+ last.current = null;
2526
+ }
2527
+ });
2528
+ useEffect(() => {
2529
+ last.current = null;
2530
+ if (state.playing) loop.request();
2531
+ else loop.cancel();
2532
+ }, [state.playing, loop]);
2533
+ useEffect(() => {
2534
+ runner.drawn(state.run);
2535
+ }, [runner, state.run]);
2536
+ useEffect(() => () => runner.dispose(), [runner]);
2537
+ return { ...state, runner };
2538
+ }
2539
+
2540
+ 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
2541
  //# sourceMappingURL=effect.js.map
1283
2542
  //# sourceMappingURL=effect.js.map