@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.
@@ -0,0 +1,3600 @@
1
+ import { createAvoidRouter, placeLabels, edgeHops, DEFAULT_HOP_SIZE, HOPS_DOC_KEY, simplifyStops } from './chunk-T6DQT553.js';
2
+ import { edgeRouter, registerMarkShape, withDiagramRegistry, diagramGroupOf, diagramLabelOf, editEdgeLabelBinding, editEdgeLabelAction, diagramContainerOf, headerBox, labelFollowsLayout, buildGraph, LABEL_DRAG_AFFORDANCE_KIND, EDIT_EDGE_LABEL_ACTION_ID, labelDragLayer, laneOutlineOf, routerOf, labelScratchOf, ROUTERS, avoid, LABEL_PLACEMENT, editableLabels, titleIdOf, orthogonal, diagramAnchorOf, MIN_LANE_SIZE, endAims, resolveEnd, EDGE_DERIVE_PATH, titlePose, regionSpecs, DIAGRAM_THEME_KEY, diagramThemeOf, boxCenter, themeLabelFamily, roleEdgeStyle, roleStyle, themeLabelFont, firstLanes, DEFAULT_LANE_SIZE, bodyBox } from './chunk-43LDZ6L4.js';
3
+ import { diagramEdgeOf, isPromise, dataKeyReader, diagramNodeOf, portsOf, portSitesOf, outlinePath, samePoints, layoutWritesRoute, plannerPlan, planOf, layered, forceRelaxation, center, layoutOwnsPort, chain, force, tree, portOf, labelCenter, labelSpotAt, isPinned, layoutOwnsRoute } from './chunk-IM6DHWZO.js';
4
+ import { bodyPredicate, dragPanContribution, WeaselProvider, useScene, useSelection, useLatest, SceneCanvas, useDepSource, fitViewToBounds, registerNodeShape, reconcileSpecs, nodeGates, effectivePose, pagesBounds, pagesLayer, AUTO_POSE_DESCRIPTOR, usePoseRun, drawnBoundsScene, pointAlongPath, drawnAABBOf, RECT_POSE_DESCRIPTOR, commandFill, commandStroke, translatePoseViaDescriptor, composeAffordanceLayer, createResizeAction, createDeleteAction, polylineFromPoints, SNAP_ROLES, ActionDisabledReason, paintsLayer, viewToTransform, ellipsePath, snapMods, documentDepOf, carriedDescendants, sceneCarrySource, derivedDepOf, PathBuilder, createInsertOp, createSetDataOp, documentPose } from '@weasel-js/core';
5
+ import { AUTO_POSE_DESCRIPTOR as AUTO_POSE_DESCRIPTOR$1, rotatePoint } from '@weasel-js/core/math';
6
+ import { placeRect, pathContainsPoint } from '@weasel-js/geom';
7
+ import { useLatest as useLatest$1, adoptCssOnce } from '@weasel-js/react';
8
+ import { forwardRef, useMemo, useEffect, useState, useRef, useCallback, useImperativeHandle, useLayoutEffect } from 'react';
9
+ import '@weasel-js/animator';
10
+ import { jsx, jsxs } from 'react/jsx-runtime';
11
+
12
+ var PORT_LAYER_ID = "diagram-ports";
13
+ var PORT_AFFORDANCE_KIND = `layer:${PORT_LAYER_ID}`;
14
+ function offersPorts(p, view) {
15
+ return p.hidden !== true && p.locked !== true && paintsLayer(view, p.layer);
16
+ }
17
+ var DEFAULT_HIT_PX = 8;
18
+ var DEFAULT_SIZE_PX = 7;
19
+ function portScratchOf(hit) {
20
+ const payload = hit?.payload;
21
+ return payload !== void 0 && typeof payload.portId === "string" ? payload : null;
22
+ }
23
+ function withPaint(paint) {
24
+ return paint === void 0 ? {} : { paint };
25
+ }
26
+ function createPortAffordance(participants, opts = {}) {
27
+ const {
28
+ id = PORT_LAYER_ID,
29
+ hitRadiusPx = DEFAULT_HIT_PX,
30
+ sizePx = DEFAULT_SIZE_PX,
31
+ strength = "exclusive"
32
+ } = opts;
33
+ const portOpts = {
34
+ ...opts.geometry ? { geometry: opts.geometry } : {},
35
+ ...opts.read ? { read: opts.read } : {}
36
+ };
37
+ const paintOf2 = (port) => {
38
+ if (sizePx <= 0) return void 0;
39
+ const fill = typeof opts.fill === "function" ? opts.fill(port) : opts.fill;
40
+ const look = { ...fill ? { fill } : {}, ...opts.stroke ? { stroke: opts.stroke } : {} };
41
+ if (opts.shape !== "circle") return { kind: "square", sizePx, ...look };
42
+ return {
43
+ kind: "custom",
44
+ // An affordance layer paints in screen pixels, so the mark stays one size at any zoom.
45
+ draw: ({ world, view }) => {
46
+ const t = viewToTransform(view);
47
+ const x = t.panX + world.x * t.zoom.x - sizePx / 2;
48
+ const y = t.panY + world.y * t.zoom.y - sizePx / 2;
49
+ return [{ kind: "path", path: ellipsePath({ x, y, width: sizePx, height: sizePx }), ...look }];
50
+ }
51
+ };
52
+ };
53
+ return {
54
+ id,
55
+ regions(state) {
56
+ const out = [];
57
+ for (const participant of participants()) {
58
+ if (!offersPorts(participant, state)) continue;
59
+ const { node, pose } = participant;
60
+ if (opts.shows !== void 0 && !opts.shows(node, state)) continue;
61
+ for (const port of portsOf(node, pose, portOpts)) {
62
+ if (opts.showsPort !== void 0 && !opts.showsPort(port)) continue;
63
+ out.push({
64
+ id: `${node.id}:${port.id}`,
65
+ targetId: null,
66
+ shape: { kind: "point", x: port.point.x, y: port.point.y, hitRadiusPx },
67
+ ...withPaint(paintOf2(port)),
68
+ ...opts.cursor !== void 0 ? { cursor: opts.cursor } : {},
69
+ strength,
70
+ claimedKinds: ["pointer"],
71
+ bind: () => ({
72
+ initialScratch: {
73
+ targetId: node.id,
74
+ nodeId: node.id,
75
+ portId: port.id,
76
+ port
77
+ }
78
+ })
79
+ });
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+ };
85
+ }
86
+ function isBodyEnd(end) {
87
+ return end.kind === "body";
88
+ }
89
+ function bodyAt(participants, at, opts = {}) {
90
+ const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR$1;
91
+ let hit = null;
92
+ for (const participant of participants()) {
93
+ const { node } = participant;
94
+ if (!offersPorts(participant, opts.view)) continue;
95
+ if (diagramEdgeOf(node) !== null || diagramLabelOf(node) !== null || diagramGroupOf(node) !== null) continue;
96
+ const trait = diagramNodeOf(node, opts.read);
97
+ if (trait === null || trait.group !== void 0) continue;
98
+ const bounds = geometry.getBounds(participant.pose);
99
+ if (bounds.width <= 0 || bounds.height <= 0) continue;
100
+ const rotation = geometry.getRotation?.(participant.pose) ?? 0;
101
+ const local = rotation === 0 ? at : rotatePoint(at.x, at.y, bounds.x + bounds.width / 2, bounds.y + bounds.height / 2, -rotation);
102
+ const inside2 = trait.outline === void 0 ? local.x >= bounds.x && local.x <= bounds.x + bounds.width && local.y >= bounds.y && local.y <= bounds.y + bounds.height : pathContainsPoint(outlinePath(trait.outline, bounds), local.x, local.y);
103
+ if (inside2) hit = { end: { kind: "body", nodeId: node.id, point: { x: at.x, y: at.y } }, participant };
104
+ }
105
+ return hit;
106
+ }
107
+ function createTargets(opts) {
108
+ const portOpts = {
109
+ ...opts.geometry ? { geometry: opts.geometry } : {},
110
+ ...opts.read ? { read: opts.read } : {}
111
+ };
112
+ const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR$1;
113
+ function portTarget(from, at, view) {
114
+ let best = null;
115
+ let bestD2 = opts.snapRadius * opts.snapRadius;
116
+ for (const participant of opts.participants()) {
117
+ if (!offersPorts(participant, view)) continue;
118
+ for (const port of portsOf(participant.node, participant.pose, portOpts)) {
119
+ if (opts.showsPort !== void 0 && !opts.showsPort(port)) continue;
120
+ if (!opts.canConnect(from, port)) continue;
121
+ const d2 = (port.point.x - at.x) ** 2 + (port.point.y - at.y) ** 2;
122
+ if (d2 <= bestD2) {
123
+ bestD2 = d2;
124
+ best = port;
125
+ }
126
+ }
127
+ }
128
+ return best;
129
+ }
130
+ function target(from, at, view) {
131
+ const port = portTarget(from, at, view);
132
+ if (port !== null || !opts.bodies) return port;
133
+ const body = bodyAt(opts.participants, at, { ...portOpts, view });
134
+ if (body === null || portsOf(body.participant.node, body.participant.pose, portOpts).length === 0) return null;
135
+ return opts.canConnect(from, body.end) ? body.end : null;
136
+ }
137
+ function participantOf(nodeId) {
138
+ for (const participant of opts.participants()) {
139
+ if (participant.node.id === nodeId) return participant;
140
+ }
141
+ return void 0;
142
+ }
143
+ function centerOf(end) {
144
+ const participant = participantOf(end.nodeId);
145
+ return participant === void 0 ? end.point : boxCenter(geometry, participant.pose);
146
+ }
147
+ function portFor(end, toward2) {
148
+ if (!isBodyEnd(end)) return end;
149
+ const participant = participantOf(end.nodeId);
150
+ if (participant === void 0) return null;
151
+ const dep = { node: participant.node, pose: participant.pose };
152
+ const all = (d) => portsOf(d.node, d.pose, portOpts);
153
+ return resolveEnd({}, dep, toward2, portOpts, all) ?? null;
154
+ }
155
+ function preview(route, fixed, at, view, moving = "to", waypoints = []) {
156
+ const landed = target(fixed, at, view);
157
+ const [fixedCenter, landedCenter] = [centerOf(fixed), landed === null ? at : centerOf(landed)];
158
+ const aims = moving === "to" ? endAims(fixedCenter, landedCenter, waypoints) : endAims(landedCenter, fixedCenter, waypoints);
159
+ const [fixedAim, landedAim] = moving === "to" ? [aims.from, aims.to] : [aims.to, aims.from];
160
+ const free = landed === null ? { id: "", nodeId: "", point: at, normal: null } : portFor(landed, landedAim);
161
+ const anchored = portFor(fixed, fixedAim);
162
+ if (free === null || anchored === null) return [];
163
+ return moving === "to" ? route({ from: anchored, to: free, waypoints }) : route({ from: free, to: anchored, waypoints });
164
+ }
165
+ return { target, portFor, preview };
166
+ }
167
+ var CONNECT_ACTION_ID = "diagram.connect";
168
+ var defaultCanConnect = (from, to) => {
169
+ if (isBodyEnd(from) || isBodyEnd(to)) return from.nodeId !== to.nodeId;
170
+ if (from.nodeId === to.nodeId && from.id === to.id) return false;
171
+ if (from.type === void 0 || to.type === void 0) return true;
172
+ return from.type === to.type;
173
+ };
174
+ var DEFAULT_SNAP = 16;
175
+ var DEFAULT_EDGE_STROKE = {
176
+ paint: { color: "#7ba7c7" },
177
+ width: 2,
178
+ markerEnd: "arrow"
179
+ };
180
+ var DEFAULT_ROUTER = "straight";
181
+ function connectBinding(actionId = CONNECT_ACTION_ID) {
182
+ return {
183
+ spec: { kind: "drag", target: `affordance:${PORT_AFFORDANCE_KIND}` },
184
+ actionId
185
+ };
186
+ }
187
+ var GRAB_PORT_ACTION_ID = "diagram.grabPort";
188
+ function grabPortAction(id = GRAB_PORT_ACTION_ID) {
189
+ return {
190
+ id,
191
+ label: "Grab port",
192
+ group: "diagram",
193
+ invoker: { timing: "immediate", run: () => {
194
+ } },
195
+ enabled: () => true
196
+ };
197
+ }
198
+ function grabPortBindings(actionId = GRAB_PORT_ACTION_ID, kind = PORT_AFFORDANCE_KIND) {
199
+ return [
200
+ { spec: { kind: "pointerDown", target: `affordance:${kind}` }, actionId },
201
+ { spec: { kind: "click", target: `affordance:${kind}` }, actionId }
202
+ ];
203
+ }
204
+ function commitEdgeToScene(edge, ctx, opts = {}) {
205
+ const scene = ctx.deps.scene;
206
+ if (scene === void 0) return;
207
+ const trait = {
208
+ from: edgeEndOf(edge.from),
209
+ to: edgeEndOf(edge.to),
210
+ router: edge.router
211
+ };
212
+ const source = scene.get(edge.from.nodeId);
213
+ scene.add({
214
+ kind: "leaf",
215
+ layer: source?.layer ?? scene.layers[0].id,
216
+ pose: { x: 0, y: 0, width: 0, height: 0 },
217
+ data: { diagram: trait, stroke: opts.stroke ?? DEFAULT_EDGE_STROKE },
218
+ dependsOn: [edge.from.nodeId, edge.to.nodeId],
219
+ // Without this the edge re-resolves on every dependency move and paints
220
+ // nothing. `EDGE_DERIVE_PATH` rather than a fresh `edgeDerivePath()` so the
221
+ // scene can find its registry key and the edge survives `toJSON`.
222
+ derivePath: EDGE_DERIVE_PATH
223
+ });
224
+ }
225
+ function edgeEndOf(end) {
226
+ return isBodyEnd(end) ? {} : { port: end.id };
227
+ }
228
+ function createConnectAction(opts) {
229
+ const {
230
+ id = CONNECT_ACTION_ID,
231
+ canConnect = defaultCanConnect,
232
+ snapRadius = DEFAULT_SNAP,
233
+ router: routerKey = DEFAULT_ROUTER
234
+ } = opts;
235
+ const stroke = opts.edgeStroke;
236
+ const commit = opts.commit ?? ((edge, ctx) => commitEdgeToScene(edge, ctx, stroke ? { stroke } : {}));
237
+ const routers = opts.routers ?? ROUTERS;
238
+ const route = routers[routerKey] ?? routers["straight"] ?? ROUTERS["straight"];
239
+ const targets = createTargets({ ...opts, canConnect, snapRadius, bodies: opts.bodies ?? false });
240
+ function startEnd(ctx) {
241
+ const pressed = portScratchOf(ctx.drag?.affordance);
242
+ if (pressed !== null) return pressed.port;
243
+ if (opts.startFrom !== "body") return null;
244
+ return bodyAt(opts.participants, ctx.world, { ...opts, view: ctx.deps.view })?.end ?? null;
245
+ }
246
+ return {
247
+ id,
248
+ label: "Connect",
249
+ group: "diagram",
250
+ requires: ["scene", "view"],
251
+ invoker: {
252
+ timing: "ongoing",
253
+ start(ctx) {
254
+ const from = startEnd(ctx);
255
+ if (from === null) return {};
256
+ const scratch = {
257
+ from,
258
+ current: { x: ctx.world.x, y: ctx.world.y },
259
+ open: true,
260
+ deps: ctx.deps
261
+ };
262
+ return {
263
+ kind: "diagram-connect",
264
+ onMove(moveCtx) {
265
+ scratch.current = { x: moveCtx.world.x, y: moveCtx.world.y };
266
+ },
267
+ overlay() {
268
+ if (!scratch.open) return null;
269
+ const points = targets.preview(route, scratch.from, scratch.current, scratch.deps.view);
270
+ if (points.length < 2) return null;
271
+ return { kind: "polyline", points, role: "connector" };
272
+ },
273
+ onEnd(endCtx, reason) {
274
+ scratch.open = false;
275
+ if (reason === "cancel") return;
276
+ const to = targets.target(scratch.from, { x: endCtx.world.x, y: endCtx.world.y }, scratch.deps.view);
277
+ if (to === null) return;
278
+ commit(
279
+ { from: scratch.from, to, router: routerKey },
280
+ { ...endCtx, deps: scratch.deps }
281
+ );
282
+ }
283
+ };
284
+ }
285
+ },
286
+ enabled: () => true
287
+ };
288
+ }
289
+
290
+ // src/containers/canDropInto.ts
291
+ function diagramCanDropInto(scene) {
292
+ const nodeOf = (id) => scene.get(id);
293
+ const roleOf = (id) => {
294
+ const node = nodeOf(id);
295
+ return node === void 0 ? null : diagramContainerOf(node)?.role ?? null;
296
+ };
297
+ const attached = (id) => {
298
+ const node = nodeOf(id);
299
+ return node !== void 0 && (diagramEdgeOf(node) !== null || diagramLabelOf(node) !== null);
300
+ };
301
+ return (parent, ids) => {
302
+ const node = nodeOf(parent);
303
+ if (node === void 0 || ids.some(attached)) return false;
304
+ const role = roleOf(parent);
305
+ if (role === "pool") return ids.every((id) => roleOf(id) === "lane");
306
+ if (ids.some((id) => roleOf(id) === "lane")) return false;
307
+ if (role !== null) return ids.every((id) => roleOf(id) !== "pool");
308
+ return !attached(parent) && dataKeyReader(node) === null;
309
+ };
310
+ }
311
+ function diagramDropParentAt(scene, point, role) {
312
+ if (role === "pool" || role === "lane") return null;
313
+ let best = null;
314
+ const walk = (ids) => {
315
+ for (const id of ids) {
316
+ const node = scene.get(id);
317
+ if (node === void 0 || node.kind !== "container") continue;
318
+ const trait = diagramContainerOf(node);
319
+ const b = bodyBox(node.pose, trait?.header);
320
+ const inside2 = point.x >= b.x && point.x < b.x + b.width && point.y >= b.y && point.y < b.y + b.height;
321
+ if (!inside2) continue;
322
+ if (trait?.role === "container" || trait?.role === "lane") best = id;
323
+ walk(scene.childrenOf(id));
324
+ }
325
+ };
326
+ walk(scene.roots);
327
+ return best;
328
+ }
329
+
330
+ // src/theme/themeWrites.ts
331
+ var traitOf = (data) => {
332
+ const d = data?.diagram;
333
+ return d !== null && typeof d === "object" ? d : null;
334
+ };
335
+ var ownOf = (trait) => new Set(trait.ownStyle ?? []);
336
+ var ownsSize = (trait) => trait.ownFont?.includes("size") === true;
337
+ function containerRole(trait) {
338
+ const c = trait.container;
339
+ return c !== null && typeof c === "object" && typeof c.role === "string" ? c.role : null;
340
+ }
341
+ function laneIndex(scene, node) {
342
+ if (node.parent === null) return 0;
343
+ const lanes = scene.childrenOf(node.parent).filter((id) => {
344
+ const t = traitOf(scene.get(id)?.data);
345
+ return t !== null && containerRole(t) === "lane";
346
+ });
347
+ return Math.max(0, lanes.indexOf(node.id));
348
+ }
349
+ function boxLook(scene, node, theme) {
350
+ const trait = traitOf(node.data);
351
+ if (trait === null || "from" in trait || "label" in trait || "anchor" in trait || "mark" in trait) return null;
352
+ const own = ownOf(trait);
353
+ const ownSize = ownsSize(trait);
354
+ const role = containerRole(trait);
355
+ if (role === "pool") return { style: theme.lane.header, own, ownSize };
356
+ if (role === "lane") {
357
+ const bodies = theme.lane.bodies;
358
+ return { style: bodies[laneIndex(scene, node) % bodies.length] ?? theme.container, own, ownSize };
359
+ }
360
+ if (role !== null || "group" in trait) return { style: theme.container, own, ownSize };
361
+ return { style: roleStyle(theme, trait.style), own, ownSize };
362
+ }
363
+ var recolored = (paint, color) => paint?.paint?.color === color ? void 0 : { ...paint, paint: { ...paint?.paint, color } };
364
+ function paintBox(data, look) {
365
+ const fill = look.own.has("fill") ? void 0 : recolored(data.fill, look.style.fill);
366
+ const stroke = look.own.has("stroke") || data.stroke === void 0 ? void 0 : recolored(data.stroke, look.style.stroke);
367
+ if (fill === void 0 && stroke === void 0) return null;
368
+ return { ...data, ...fill ? { fill } : {}, ...stroke ? { stroke } : {} };
369
+ }
370
+ function paintText(data, color, family, step) {
371
+ const fill = color === void 0 ? void 0 : recolored(data.fill, color);
372
+ const size = data.style?.fontSize;
373
+ const resized = step !== 0 && typeof size === "number" ? { fontSize: Math.max(1, size + step) } : {};
374
+ const face = data.style !== void 0 && (data.style.fontFamily !== family || "fontSize" in resized) ? { ...data.style, fontFamily: family, ...resized } : void 0;
375
+ if (fill === void 0 && face === void 0) return null;
376
+ return { ...data, ...fill ? { fill } : {}, ...face ? { style: face } : {} };
377
+ }
378
+ function paintTag(data, theme, step) {
379
+ const text = paintText(data, theme.label.fill, themeLabelFamily(theme), step);
380
+ const background = recolored(data.background, theme.page);
381
+ if (text === null && background === void 0) return null;
382
+ return { ...text ?? data, ...background ? { background } : {} };
383
+ }
384
+ function edgeLook(trait, theme) {
385
+ const style = roleEdgeStyle(theme, trait.style);
386
+ return { stroke: style.stroke, label: style.label ?? style.stroke, own: ownOf(trait), ownSize: ownsSize(trait) };
387
+ }
388
+ var ownerOf = (node) => Array.isArray(node.dependsOn) && node.dependsOn.length === 1 ? node.dependsOn[0] : node.parent;
389
+ function themeWrites(scene, theme, ids, from) {
390
+ const out = /* @__PURE__ */ new Map();
391
+ const step = from === void 0 ? 0 : theme.font.size - from.font.size;
392
+ const labelStep = from === void 0 ? 0 : themeLabelFont(theme).fontSize - themeLabelFont(from).fontSize;
393
+ const looks = /* @__PURE__ */ new Map();
394
+ const lookOf = (id) => {
395
+ if (!looks.has(id)) {
396
+ const node = scene.get(id);
397
+ looks.set(id, node === void 0 ? null : boxLook(scene, node, theme));
398
+ }
399
+ return looks.get(id);
400
+ };
401
+ const write = (id, next) => {
402
+ if (next !== null) out.set(id, next);
403
+ };
404
+ for (const node of scene.renderOrderNodes()) {
405
+ const data = node.data;
406
+ if (data === null || typeof data !== "object") continue;
407
+ const trait = traitOf(data);
408
+ if (trait !== null && "from" in trait && "to" in trait) {
409
+ if (ids !== void 0 && !ids.has(node.id)) continue;
410
+ const look2 = edgeLook(trait, theme);
411
+ const stroke = look2.own.has("stroke") || data.stroke === void 0 ? void 0 : recolored(data.stroke, look2.stroke);
412
+ if (stroke !== void 0) out.set(node.id, { ...data, stroke });
413
+ continue;
414
+ }
415
+ if (trait !== null && "label" in trait) {
416
+ const edgeId = ownerOf(node);
417
+ const edgeTrait = edgeId === null ? null : traitOf(scene.get(edgeId)?.data);
418
+ if (edgeId === null || edgeTrait === null || ids !== void 0 && !ids.has(edgeId)) continue;
419
+ const look2 = edgeLook(edgeTrait, theme);
420
+ write(node.id, paintText(data, look2.own.has("text") ? void 0 : look2.label, themeLabelFamily(theme), look2.ownSize ? 0 : labelStep));
421
+ continue;
422
+ }
423
+ if (trait !== null && "offPageTag" in trait) {
424
+ const edgeId = ownerOf(node);
425
+ if (ids === void 0 || edgeId !== null && ids.has(edgeId)) write(node.id, paintTag(data, theme, labelStep));
426
+ continue;
427
+ }
428
+ if (trait !== null && "mark" in trait) continue;
429
+ const look = lookOf(node.id);
430
+ if (look !== null) {
431
+ if (ids === void 0 || ids.has(node.id)) write(node.id, paintBox(data, look));
432
+ continue;
433
+ }
434
+ const owner = ownerOf(node);
435
+ const ownerLook = owner === null ? null : lookOf(owner);
436
+ if (ownerLook === null || typeof data.text !== "string" || ids !== void 0 && !ids.has(owner)) continue;
437
+ write(node.id, paintText(data, ownerLook.own.has("text") ? void 0 : ownerLook.style.text, theme.font.family, ownerLook.ownSize ? 0 : step));
438
+ }
439
+ return out;
440
+ }
441
+ var CONTAINER_STENCILS = [
442
+ { id: "container", label: "Container", role: "container", size: { width: 240, height: 160 }, icon: (
443
+ /* @icon */
444
+ "diagram:container"
445
+ ) },
446
+ { id: "pool-rows", label: "Pool (rows)", role: "pool", axis: "y", size: { width: 480, height: 240 }, icon: (
447
+ /* @icon */
448
+ "diagram:poolRows"
449
+ ) },
450
+ { id: "pool-columns", label: "Pool (columns)", role: "pool", axis: "x", size: { width: 480, height: 300 }, icon: (
451
+ /* @icon */
452
+ "diagram:poolColumns"
453
+ ) }
454
+ ];
455
+ var NEW_LANE_SIZE = 120;
456
+ function placedBounds(stencil, b) {
457
+ if (b.width > 0 && b.height > 0) return { ...b };
458
+ const { width, height } = stencil.size;
459
+ return { x: b.x + b.width / 2 - width / 2, y: b.y + b.height / 2 - height / 2, width, height };
460
+ }
461
+ function containerBuild(stencil, bounds, opts) {
462
+ const id = opts.newId();
463
+ const axis = stencil.axis ?? "y";
464
+ const at = placedBounds(stencil, bounds);
465
+ const title = stencil.role === "pool" ? "Pool" : stencil.label;
466
+ const style = stencil.role === "pool" && opts.laneStyles ? opts.laneStyles.header : opts.style;
467
+ const specs = [
468
+ ...regionSpecs(stencil.role, axis, at, title, {
469
+ id,
470
+ layer: opts.layer,
471
+ parent: opts.parent ?? null,
472
+ ...style ? { style } : {}
473
+ }),
474
+ ...stencil.role === "pool" ? firstLanes(id, axis, at, opts) : []
475
+ ];
476
+ const label = `Insert ${stencil.label}`;
477
+ return { id, ops: specs.map((node) => createInsertOp({ node, label })) };
478
+ }
479
+ var poolAxisOf = (pool) => pool.header?.side === "top" ? "x" : "y";
480
+ function lanesOf(scene, poolId) {
481
+ const pool = scene.get(poolId);
482
+ const trait = pool === void 0 ? null : diagramContainerOf(pool);
483
+ if (pool === void 0 || trait?.role !== "pool") return [];
484
+ const axis = poolAxisOf(trait);
485
+ const start = (id) => {
486
+ const p = scene.get(id).pose;
487
+ return axis === "y" ? p.y : p.x;
488
+ };
489
+ return scene.childrenOf(pool.id).filter((id) => diagramContainerOf(scene.get(id))?.role === "lane").sort((a, b) => start(a) - start(b));
490
+ }
491
+ function addLane(scene, poolId, index, style) {
492
+ const pool = scene.get(poolId);
493
+ const trait = pool === void 0 ? null : diagramContainerOf(pool);
494
+ if (pool === void 0 || trait?.role !== "pool") return null;
495
+ const axis = poolAxisOf(trait);
496
+ const lanes = lanesOf(scene, poolId);
497
+ const b = pool.pose;
498
+ const at = index === void 0 || index >= lanes.length ? null : scene.get(lanes[index]).pose;
499
+ const boundary = at !== null ? axis === "y" ? at.y : at.x : axis === "y" ? b.y + b.height : b.x + b.width;
500
+ const start = boundary - NEW_LANE_SIZE / 2;
501
+ const offset = trait.header?.size ?? 0;
502
+ const pose = axis === "y" ? { x: b.x + offset, y: start, width: b.width - offset, height: NEW_LANE_SIZE } : { x: start, y: b.y + offset, width: NEW_LANE_SIZE, height: b.height - offset };
503
+ const id = scene.mintId();
504
+ const specs = regionSpecs("lane", axis, pose, `Lane ${lanes.length + 1}`, {
505
+ id,
506
+ layer: pool.layer,
507
+ parent: poolId,
508
+ ...style ? { style } : {}
509
+ });
510
+ scene.batch("Add lane", () => {
511
+ for (const spec of specs) scene.add(spec);
512
+ if (style === void 0 && scene.doc.get(DIAGRAM_THEME_KEY) !== void 0) {
513
+ const writes = themeWrites(scene, diagramThemeOf(scene), /* @__PURE__ */ new Set([id]));
514
+ for (const [at2, data] of writes) scene.update(at2, { data });
515
+ }
516
+ });
517
+ return id;
518
+ }
519
+ function releasedFrom(scene, id) {
520
+ const parent = scene.get(id).parent;
521
+ const around = parent === null ? void 0 : scene.get(parent);
522
+ return around !== void 0 && diagramContainerOf(around)?.role === "pool" ? around.id : id;
523
+ }
524
+ function releaseMembers(scene, id) {
525
+ const node = scene.get(id);
526
+ const trait = node === void 0 ? null : diagramContainerOf(node);
527
+ if (node === void 0 || trait === null) return;
528
+ const membersOf2 = (c) => scene.childrenOf(c).filter((m) => m !== titleIdOf(c));
529
+ const members2 = trait.role === "pool" ? membersOf2(node.id).flatMap(membersOf2) : membersOf2(node.id);
530
+ const beside = releasedFrom(scene, node.id);
531
+ const parent = scene.get(beside).parent;
532
+ const index = (parent === null ? scene.roots : scene.childrenOf(parent)).indexOf(beside);
533
+ scene.batch("Release members", () => {
534
+ members2.forEach((m, k) => scene.move(m, parent, index + k));
535
+ scene.remove(node.id);
536
+ });
537
+ }
538
+ var LANE_LAYER_ID = "diagram-lanes";
539
+ var LANE_AFFORDANCE_KIND = `layer:${LANE_LAYER_ID}`;
540
+ function laneScratchOf(hit) {
541
+ const payload = hit?.payload;
542
+ return payload !== void 0 && typeof payload.poolId === "string" ? payload : null;
543
+ }
544
+ function poolsOf(scene) {
545
+ const out = [];
546
+ for (const node of scene.renderOrderNodes()) {
547
+ const trait = diagramContainerOf(node);
548
+ if (trait?.role !== "pool" || scene.isHidden(node.id) || scene.isLocked(node.id)) continue;
549
+ out.push({
550
+ id: node.id,
551
+ axis: poolAxisOf(trait),
552
+ pose: node.pose,
553
+ lanes: lanesOf(scene, node.id).map((id) => ({ id, pose: scene.get(id).pose }))
554
+ });
555
+ }
556
+ return out;
557
+ }
558
+ var DEFAULT_FILL = { paint: { kind: "solid", color: "#ffffff" } };
559
+ var DEFAULT_STROKE = { paint: { kind: "solid", color: "#3b6fd4" }, width: 1.5 };
560
+ var ADD_STANDOFF_PX = 14;
561
+ function boundaryOf(axis, lane) {
562
+ const b = new PathBuilder();
563
+ return axis === "y" ? b.moveTo(lane.x, lane.y + lane.height).lineTo(lane.x + lane.width, lane.y + lane.height).build() : b.moveTo(lane.x + lane.width, lane.y).lineTo(lane.x + lane.width, lane.y + lane.height).build();
564
+ }
565
+ function addPaint(sizePx, fill, stroke) {
566
+ return {
567
+ kind: "custom",
568
+ draw: ({ world, view }) => {
569
+ const t = viewToTransform(view);
570
+ const cx = t.panX + world.x * t.zoom.x;
571
+ const cy = t.panY + world.y * t.zoom.y;
572
+ const r = sizePx / 2;
573
+ const arm = r * 0.55;
574
+ return [
575
+ { kind: "path", path: ellipsePath({ x: cx - r, y: cy - r, width: sizePx, height: sizePx }), fill, stroke },
576
+ { kind: "path", path: polylineFromPoints([{ x: cx - arm, y: cy }, { x: cx + arm, y: cy }]), stroke },
577
+ { kind: "path", path: polylineFromPoints([{ x: cx, y: cy - arm }, { x: cx, y: cy + arm }]), stroke }
578
+ ];
579
+ }
580
+ };
581
+ }
582
+ function createLaneAffordance(scene, opts = {}) {
583
+ const { hitRadiusPx = 4, addSizePx = 14 } = opts;
584
+ const fill = opts.fill ?? DEFAULT_FILL;
585
+ const stroke = opts.stroke ?? DEFAULT_STROKE;
586
+ return {
587
+ id: LANE_LAYER_ID,
588
+ regions(state) {
589
+ const out = [];
590
+ for (const pool of poolsOf(scene)) {
591
+ if (!paintsLayer(state, scene.get(pool.id).layer)) continue;
592
+ const cursor = pool.axis === "y" ? "row-resize" : "col-resize";
593
+ for (const lane of pool.lanes) {
594
+ out.push({
595
+ id: `${lane.id}:boundary`,
596
+ targetId: null,
597
+ shape: { kind: "outline", path: boundaryOf(pool.axis, lane.pose), hitRadiusPx },
598
+ cursor,
599
+ strength: "exclusive",
600
+ claimedKinds: ["pointer"],
601
+ bind: () => ({ initialScratch: { targetId: pool.id, poolId: pool.id, laneId: lane.id } })
602
+ });
603
+ }
604
+ if (addSizePx <= 0) continue;
605
+ const p = pool.pose;
606
+ const end = pool.axis === "y" ? { x: p.x + p.width / 2, y: p.y + p.height, normal: { x: 0, y: 1 } } : { x: p.x + p.width, y: p.y + p.height / 2, normal: { x: 1, y: 0 } };
607
+ out.push({
608
+ id: `${pool.id}:add-lane`,
609
+ targetId: null,
610
+ shape: { kind: "point", x: end.x, y: end.y, hitRadiusPx: addSizePx / 2 + 2, standoff: { px: ADD_STANDOFF_PX, normal: end.normal } },
611
+ paint: addPaint(addSizePx, fill, stroke),
612
+ cursor: "pointer",
613
+ strength: "exclusive",
614
+ claimedKinds: ["pointer"],
615
+ bind: () => ({ initialScratch: { targetId: pool.id, poolId: pool.id } })
616
+ });
617
+ }
618
+ return out;
619
+ }
620
+ };
621
+ }
622
+ function laneLayer(scene, opts = {}) {
623
+ return composeAffordanceLayer(LANE_LAYER_ID, "Diagram lanes", [createLaneAffordance(scene, opts)]);
624
+ }
625
+ var LANE_RESIZE_ACTION_ID = "diagram.resizeLane";
626
+ var ADD_LANE_ACTION_ID = "diagram.addLane";
627
+ var RELEASE_MEMBERS_ACTION_ID = "diagram.releaseMembers";
628
+ var GRAB_LANE_ACTION_ID = "diagram.grabLane";
629
+ function commitLaneResizeToScene(resize, ctx) {
630
+ const scene = ctx.deps.scene;
631
+ const pool = scene?.get(resize.poolId);
632
+ const lane = scene?.get(resize.laneId);
633
+ if (scene === void 0 || pool === void 0 || lane === void 0) return;
634
+ const axis = diagramContainerOf(pool)?.header?.side === "top" ? "x" : "y";
635
+ const l = lane.pose;
636
+ const p = pool.pose;
637
+ const d = resize.size - (axis === "y" ? l.height : l.width);
638
+ scene.batch("Resize lane", () => {
639
+ scene.setPose(lane.id, axis === "y" ? { ...l, height: resize.size } : { ...l, width: resize.size });
640
+ scene.setPose(pool.id, axis === "y" ? { ...p, height: p.height + d } : { ...p, width: p.width + d });
641
+ });
642
+ }
643
+ function laneResizeFrame(scene, pool, index, size) {
644
+ const { axis } = pool;
645
+ const along2 = (r) => axis === "y" ? r.height : r.width;
646
+ const grow = (r, by) => axis === "y" ? { ...r, height: r.height + by } : { ...r, width: r.width + by };
647
+ const shift = (r, by) => axis === "y" ? { ...r, y: r.y + by } : { ...r, x: r.x + by };
648
+ const lane = pool.lanes[index];
649
+ const d = size - along2(lane.pose);
650
+ const out = /* @__PURE__ */ new Map();
651
+ const placeTitle = (id, at) => {
652
+ const node = scene.get(id);
653
+ if (node !== void 0 && scene.get(titleIdOf(id)) !== void 0) {
654
+ out.set(titleIdOf(id), titlePose(at, diagramContainerOf(node)?.header));
655
+ }
656
+ };
657
+ const laneAt = grow(lane.pose, d);
658
+ out.set(lane.id, laneAt);
659
+ placeTitle(lane.id, laneAt);
660
+ const poolAt = grow(pool.pose, d);
661
+ out.set(pool.id, poolAt);
662
+ placeTitle(pool.id, poolAt);
663
+ for (const later of pool.lanes.slice(index + 1)) {
664
+ out.set(later.id, shift(later.pose, d));
665
+ const carried = carriedDescendants(sceneCarrySource(scene), later.id, (p) => shift(p, d));
666
+ for (const [id, p] of carried) out.set(id, p);
667
+ }
668
+ return out;
669
+ }
670
+ function createLaneResizeAction(opts) {
671
+ const commit = opts.commitResize ?? commitLaneResizeToScene;
672
+ return {
673
+ id: LANE_RESIZE_ACTION_ID,
674
+ label: "Resize lane",
675
+ group: "diagram",
676
+ requires: ["scene"],
677
+ invoker: {
678
+ timing: "ongoing",
679
+ start(ctx) {
680
+ const pressed = laneScratchOf(ctx.drag?.affordance);
681
+ const pool = poolsOf(opts.scene).find((p) => p.id === pressed?.poolId);
682
+ const index = pool?.lanes.findIndex((l) => l.id === pressed?.laneId) ?? -1;
683
+ if (pressed === null || pool === void 0 || index < 0) return {};
684
+ const lane = pool.lanes[index];
685
+ const start = pool.axis === "y" ? lane.pose.height : lane.pose.width;
686
+ const press = ctx.drag?.start ?? ctx.world;
687
+ const sizeAt = (at) => Math.max(MIN_LANE_SIZE, start + (pool.axis === "y" ? at.y - press.y : at.x - press.x));
688
+ let frame = null;
689
+ let open = true;
690
+ return {
691
+ kind: "diagram-resize-lane",
692
+ onMove(moveCtx) {
693
+ const size = sizeAt(moveCtx.world);
694
+ frame = size === start ? null : laneResizeFrame(opts.scene, pool, index, size);
695
+ },
696
+ previewIds: () => open && frame !== null ? [...frame.keys()] : null,
697
+ previewPose: (id) => open ? frame?.get(id) ?? null : null,
698
+ onEnd(endCtx, reason) {
699
+ open = false;
700
+ if (reason === "cancel") return;
701
+ const size = sizeAt(endCtx.world);
702
+ if (size !== start) commit({ poolId: pool.id, laneId: lane.id, size }, { ...endCtx, deps: ctx.deps });
703
+ }
704
+ };
705
+ }
706
+ },
707
+ enabled: () => true
708
+ };
709
+ }
710
+ var NO_MODIFIERS = { alt: false, shift: false, meta: false, ctrl: false };
711
+ function createAddLaneAction(opts) {
712
+ const commit = opts.commitAddLane ?? ((poolId, ctx) => {
713
+ const scene = ctx.deps.scene;
714
+ if (scene !== void 0) addLane(scene, poolId);
715
+ });
716
+ return {
717
+ id: ADD_LANE_ACTION_ID,
718
+ label: "Add lane",
719
+ group: "diagram",
720
+ requires: ["scene"],
721
+ invoker: {
722
+ timing: "immediate",
723
+ run(deps, params) {
724
+ const pressed = laneScratchOf(params?.affordance);
725
+ if (pressed === null || pressed.laneId !== void 0) return;
726
+ const world = { x: Number(params?.worldX), y: Number(params?.worldY) };
727
+ commit(pressed.poolId, { world, modifiers: NO_MODIFIERS, deps });
728
+ }
729
+ },
730
+ enabled: () => true
731
+ };
732
+ }
733
+ var selectedContainers = (opts) => (opts.selected?.() ?? []).filter((id) => {
734
+ const node = opts.scene.get(id);
735
+ return node !== void 0 && diagramContainerOf(node) !== null;
736
+ });
737
+ function createReleaseMembersAction(opts) {
738
+ return {
739
+ id: RELEASE_MEMBERS_ACTION_ID,
740
+ label: "Release members",
741
+ group: "diagram",
742
+ requires: ["scene"],
743
+ invoker: {
744
+ timing: "immediate",
745
+ run() {
746
+ const ids = selectedContainers(opts);
747
+ if (ids.length === 0) return;
748
+ const commit = opts.commitRelease;
749
+ if (commit !== void 0) {
750
+ for (const id of ids) commit(id);
751
+ return;
752
+ }
753
+ opts.scene.batch("Release members", () => {
754
+ for (const id of ids) releaseMembers(opts.scene, id);
755
+ });
756
+ }
757
+ },
758
+ enabled: () => selectedContainers(opts).length > 0 ? true : ActionDisabledReason.NotApplicable
759
+ };
760
+ }
761
+ function diagramContainers(opts) {
762
+ const target = `affordance:${LANE_AFFORDANCE_KIND}`;
763
+ return {
764
+ layer: laneLayer(opts.scene, opts),
765
+ contribution: {
766
+ id: "weasel-diagram-containers",
767
+ eligibility: { claimed: true },
768
+ actions: [
769
+ createLaneResizeAction(opts),
770
+ createAddLaneAction(opts),
771
+ createReleaseMembersAction(opts),
772
+ grabPortAction(GRAB_LANE_ACTION_ID)
773
+ ],
774
+ bindings: [
775
+ { spec: { kind: "drag", target }, actionId: LANE_RESIZE_ACTION_ID },
776
+ { spec: { kind: "click", target }, actionId: ADD_LANE_ACTION_ID },
777
+ ...grabPortBindings(GRAB_LANE_ACTION_ID, LANE_AFFORDANCE_KIND).filter((b) => b.spec.kind !== "click")
778
+ ]
779
+ }
780
+ };
781
+ }
782
+ var EDIT_CONTAINER_TITLE_ACTION_ID = "diagram.editContainerTitle";
783
+ function regionHeaderAt(deps, at) {
784
+ if (deps === void 0 || at === void 0) return null;
785
+ const scene = deps.scene;
786
+ for (let id = deps.nodeAtPoint?.(at) ?? null; id != null && scene !== void 0; id = scene.get(id)?.parent ?? null) {
787
+ const node = scene.get(id);
788
+ const trait = node === void 0 ? null : diagramContainerOf(node);
789
+ if (node === void 0 || trait === null) continue;
790
+ const h = headerBox(node.pose, trait.header);
791
+ if (h !== null && at.x >= h.x && at.x <= h.x + h.width && at.y >= h.y && at.y <= h.y + h.height) return id;
792
+ }
793
+ return null;
794
+ }
795
+ function editContainerTitleAction(commit) {
796
+ return {
797
+ id: EDIT_CONTAINER_TITLE_ACTION_ID,
798
+ label: "Edit container title",
799
+ group: "diagram",
800
+ requires: ["scene", "nodeAtPoint"],
801
+ eligible: { mode: { in: ["normal", "isolation"] } },
802
+ invoker: {
803
+ timing: "immediate",
804
+ run(deps, params) {
805
+ const id = regionHeaderAt(deps, { x: Number(params?.worldX), y: Number(params?.worldY) });
806
+ if (id !== null) commit(id);
807
+ }
808
+ },
809
+ enabled: (deps, at) => regionHeaderAt(deps, at) !== null ? true : ActionDisabledReason.NotApplicable
810
+ };
811
+ }
812
+ var onBody = bodyPredicate((target) => target === "selected-body" || target === "unselected-body");
813
+ function editContainerTitleBinding() {
814
+ return { spec: { kind: "doubleClick", target: { kindOf: onBody } }, actionId: EDIT_CONTAINER_TITLE_ACTION_ID };
815
+ }
816
+
817
+ // src/hops/hopsAction.ts
818
+ var SET_HOPS_ACTION_ID = "diagram.hops";
819
+ function createSetHopsAction(opts = {}) {
820
+ const size = opts.size ?? DEFAULT_HOP_SIZE;
821
+ return {
822
+ id: SET_HOPS_ACTION_ID,
823
+ label: "Line jumps",
824
+ group: "diagram",
825
+ requires: ["scene"],
826
+ invoker: {
827
+ timing: "immediate",
828
+ run(deps) {
829
+ const doc = deps.scene?.doc;
830
+ doc?.set(HOPS_DOC_KEY, doc.get(HOPS_DOC_KEY) === void 0 ? { size } : void 0);
831
+ }
832
+ },
833
+ active: (deps) => deps?.scene?.doc.get(HOPS_DOC_KEY) !== void 0
834
+ };
835
+ }
836
+ var MOVE_LABEL_ACTION_ID = "diagram.moveLabel";
837
+ var placementAt = (spot) => ({ at: spot.at, offset: spot.offset, dodge: false });
838
+ function placedData(data, placement) {
839
+ const record = data;
840
+ return { ...record, diagram: { ...record.diagram, label: placement } };
841
+ }
842
+ function movedLabelData(data, spot) {
843
+ return placedData(data, placementAt(spot));
844
+ }
845
+ function commitLabelToScene(edit, ctx) {
846
+ const scene = ctx.deps.scene;
847
+ const node = scene?.get(edit.labelId);
848
+ if (scene === void 0 || node === void 0 || diagramLabelOf(node) === null) return;
849
+ const placement = edit.placement ?? LABEL_PLACEMENT;
850
+ scene.batch(edit.placement === null ? "Reset label" : "Move label", () => {
851
+ scene.update(node.id, { data: placedData(node.data, placement) });
852
+ });
853
+ }
854
+ function pressOf(ctx) {
855
+ const pressed = labelScratchOf(ctx.drag?.affordance);
856
+ const scene = ctx.deps.scene;
857
+ const node = pressed === null ? void 0 : scene?.get(pressed.labelId);
858
+ if (pressed === null || scene === void 0 || node === void 0 || diagramLabelOf(node) === null) return null;
859
+ const path = documentDepOf(scene, pressed.edgeId)?.path ?? null;
860
+ if (path === null) return null;
861
+ const pose = effectivePose(scene, node);
862
+ const box = AUTO_POSE_DESCRIPTOR.getBounds(pose);
863
+ const center2 = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
864
+ const press = ctx.drag?.start ?? { x: ctx.world.x, y: ctx.world.y };
865
+ return {
866
+ labelId: pressed.labelId,
867
+ edgeId: pressed.edgeId,
868
+ data: node.data,
869
+ pose,
870
+ path,
871
+ center: center2,
872
+ size: { width: box.width, height: box.height },
873
+ grab: { x: center2.x - press.x, y: center2.y - press.y },
874
+ deps: ctx.deps
875
+ };
876
+ }
877
+ var spotAt = (p, ctx) => labelSpotAt(p.path, { x: ctx.world.x + p.grab.x, y: ctx.world.y + p.grab.y }, p.size);
878
+ function poseAt(p, spot) {
879
+ const c = labelCenter(p.path, placementAt(spot), p.size);
880
+ return c === null ? p.pose : translatePoseViaDescriptor(p.pose, c.x - p.center.x, c.y - p.center.y, AUTO_POSE_DESCRIPTOR);
881
+ }
882
+ function createLabelDragAction(opts = {}) {
883
+ const { commit = commitLabelToScene } = opts;
884
+ return {
885
+ id: opts.id ?? MOVE_LABEL_ACTION_ID,
886
+ label: "Move label",
887
+ group: "diagram",
888
+ requires: ["scene"],
889
+ eligible: { mode: { in: ["normal", "isolation"] } },
890
+ invoker: {
891
+ timing: "ongoing",
892
+ start(ctx) {
893
+ const p = pressOf(ctx);
894
+ if (p === null) return {};
895
+ let spot = null;
896
+ let open = true;
897
+ const proposed = () => open ? spot : null;
898
+ return {
899
+ kind: "diagram-move-label",
900
+ onMove(moveCtx) {
901
+ spot = spotAt(p, moveCtx);
902
+ },
903
+ previewIds: () => proposed() === null ? null : [p.labelId],
904
+ previewPose: (id) => {
905
+ const next = proposed();
906
+ return id === p.labelId && next !== null ? poseAt(p, next) : null;
907
+ },
908
+ previewData: (id) => {
909
+ const next = proposed();
910
+ return id === p.labelId && next !== null ? movedLabelData(p.data, next) : null;
911
+ },
912
+ onEnd(endCtx, reason) {
913
+ open = false;
914
+ const final = reason === "commit" && spot !== null ? spotAt(p, endCtx) ?? spot : null;
915
+ if (final !== null) {
916
+ commit({ labelId: p.labelId, edgeId: p.edgeId, placement: placementAt(final) }, { ...endCtx, deps: p.deps });
917
+ }
918
+ }
919
+ };
920
+ }
921
+ },
922
+ enabled: () => true
923
+ };
924
+ }
925
+ var RESET_LABEL_ACTION_ID = "diagram.resetLabel";
926
+ var NO_MODIFIERS2 = { alt: false, shift: false, meta: false, ctrl: false };
927
+ function draggedLabels(scene, selected) {
928
+ if (scene === void 0 || selected.length === 0) return [];
929
+ const out = [];
930
+ for (const { labelId, edgeId, label } of editableLabels(scene, new Set(selected))) {
931
+ if (label.dodge === false) out.push({ labelId, edgeId, placement: null });
932
+ }
933
+ return out;
934
+ }
935
+ function createResetLabelAction(opts) {
936
+ const { commit = commitLabelToScene } = opts;
937
+ return {
938
+ id: RESET_LABEL_ACTION_ID,
939
+ label: "Reset label",
940
+ group: "diagram",
941
+ requires: ["scene"],
942
+ invoker: {
943
+ timing: "immediate",
944
+ run(deps) {
945
+ const edits = draggedLabels(deps.scene, opts.selected());
946
+ const ctx = { world: { x: 0, y: 0 }, modifiers: NO_MODIFIERS2, deps };
947
+ const all = () => {
948
+ for (const edit of edits) commit(edit, ctx);
949
+ };
950
+ if (deps.scene === void 0) all();
951
+ else deps.scene.batch("Reset label", all);
952
+ }
953
+ },
954
+ enabled: (deps) => draggedLabels(deps?.scene, opts.selected()).length > 0 ? true : ActionDisabledReason.NotApplicable
955
+ };
956
+ }
957
+ function sceneParticipants(scene) {
958
+ return function* () {
959
+ const gates = nodeGates(scene);
960
+ for (const node of scene.renderOrderNodes()) {
961
+ const hidden = gates.hidden(node.id);
962
+ const locked = gates.locked(node.id);
963
+ yield {
964
+ node,
965
+ pose: effectivePose(scene, node),
966
+ ...node.layer !== void 0 ? { layer: node.layer } : {},
967
+ ...hidden ? { hidden } : {},
968
+ ...locked ? { locked } : {}
969
+ };
970
+ }
971
+ };
972
+ }
973
+ function portLayer(participants, opts = {}) {
974
+ return composeAffordanceLayer(
975
+ opts.id ?? PORT_LAYER_ID,
976
+ "Diagram ports",
977
+ [createPortAffordance(participants, opts)]
978
+ );
979
+ }
980
+ var EDGE_END_LAYER_ID = "diagram-edge-ends";
981
+ var EDGE_END_AFFORDANCE_KIND = `layer:${EDGE_END_LAYER_ID}`;
982
+ function edgeEndScratchOf(hit) {
983
+ const payload = hit?.payload;
984
+ return payload !== void 0 && typeof payload.edgeId === "string" && (payload.end === "from" || payload.end === "to") ? payload : null;
985
+ }
986
+ function createEdgeEnds(participants, opts = {}) {
987
+ const derive = edgeRouter({
988
+ ...opts.geometry ? { geometry: opts.geometry } : {},
989
+ ...opts.read ? { read: opts.read } : {},
990
+ ...opts.routers ? { routers: opts.routers } : {}
991
+ });
992
+ return function endsOf(edgeId) {
993
+ const byId = /* @__PURE__ */ new Map();
994
+ for (const p2 of participants()) byId.set(p2.node.id, p2);
995
+ const edge = byId.get(edgeId);
996
+ if (edge === void 0) return null;
997
+ const trait = diagramEdgeOf(edge.node);
998
+ const deps = edge.node.dependsOn;
999
+ if (trait === null || !Array.isArray(deps) || deps.length !== 2) return null;
1000
+ const from = byId.get(deps[0]);
1001
+ const to = byId.get(deps[1]);
1002
+ if (from === void 0 || to === void 0) return null;
1003
+ const dep = (p2) => ({ node: p2.node, pose: p2.pose });
1004
+ const route = derive(edge.node, [dep(from), dep(to)]);
1005
+ const p = route?.points;
1006
+ if (route === null || p === void 0 || p.length < 2) return null;
1007
+ return { edge, trait, from, to, points: { from: p[0], to: p[p.length - 1] }, route };
1008
+ };
1009
+ }
1010
+ var DEFAULT_HIT_PX2 = 8;
1011
+ var DEFAULT_SIZE_PX2 = 8;
1012
+ function createEdgeEndAffordance(participants, selected, opts = {}) {
1013
+ const { id = EDGE_END_LAYER_ID, hitRadiusPx = DEFAULT_HIT_PX2, sizePx = DEFAULT_SIZE_PX2 } = opts;
1014
+ const endsOf = createEdgeEnds(participants, opts);
1015
+ const look = { ...opts.fill ? { fill: opts.fill } : {}, ...opts.stroke ? { stroke: opts.stroke } : {} };
1016
+ const paint = sizePx > 0 ? { paint: { kind: "square", sizePx, ...look } } : {};
1017
+ return {
1018
+ id,
1019
+ regions(state) {
1020
+ const out = [];
1021
+ for (const edgeId of selected()) {
1022
+ const ends = endsOf(edgeId);
1023
+ if (ends === null || !offersPorts(ends.edge, state)) continue;
1024
+ for (const end of ["from", "to"]) {
1025
+ const at = ends.points[end];
1026
+ out.push({
1027
+ id: `${edgeId}:${end}`,
1028
+ targetId: null,
1029
+ shape: { kind: "point", x: at.x, y: at.y, hitRadiusPx },
1030
+ ...paint,
1031
+ ...opts.cursor !== void 0 ? { cursor: opts.cursor } : {},
1032
+ strength: "exclusive",
1033
+ claimedKinds: ["pointer"],
1034
+ bind: () => ({ initialScratch: { targetId: edgeId, edgeId, end } })
1035
+ });
1036
+ }
1037
+ }
1038
+ return out;
1039
+ }
1040
+ };
1041
+ }
1042
+ function edgeEndLayer(participants, selected, opts = {}) {
1043
+ return composeAffordanceLayer(
1044
+ opts.id ?? EDGE_END_LAYER_ID,
1045
+ "Diagram edge ends",
1046
+ [createEdgeEndAffordance(participants, selected, opts)]
1047
+ );
1048
+ }
1049
+
1050
+ // src/waypoints.ts
1051
+ var EPS = 1e-6;
1052
+ var JOG = 16;
1053
+ var same = (a, b) => Math.abs(a.x - b.x) <= EPS && Math.abs(a.y - b.y) <= EPS;
1054
+ function segmentAxis(a, b) {
1055
+ if (same(a, b)) return null;
1056
+ if (Math.abs(a.y - b.y) <= EPS) return "x";
1057
+ if (Math.abs(a.x - b.x) <= EPS) return "y";
1058
+ return null;
1059
+ }
1060
+ function stopIndices(drawn, stops) {
1061
+ const out = [];
1062
+ let from = 0;
1063
+ for (const stop of stops) {
1064
+ let i = from;
1065
+ while (i < drawn.length && !same(drawn[i], stop)) i++;
1066
+ if (i === drawn.length) return null;
1067
+ out.push(i);
1068
+ from = i;
1069
+ }
1070
+ return out;
1071
+ }
1072
+ function middleOf(drawn, a, b) {
1073
+ let total = 0;
1074
+ for (let i = a; i < b; i++) total += Math.hypot(drawn[i + 1].x - drawn[i].x, drawn[i + 1].y - drawn[i].y);
1075
+ let left = total / 2;
1076
+ for (let i = a; i < b; i++) {
1077
+ const p = drawn[i];
1078
+ const q = drawn[i + 1];
1079
+ const len = Math.hypot(q.x - p.x, q.y - p.y);
1080
+ if (len >= left && len > 0) return { x: p.x + (q.x - p.x) * left / len, y: p.y + (q.y - p.y) * left / len };
1081
+ left -= len;
1082
+ }
1083
+ return drawn[a];
1084
+ }
1085
+ function routeHandles(drawn, stops, router) {
1086
+ if (router === orthogonal || router === avoid) {
1087
+ const out2 = [];
1088
+ for (let i = 0; i + 1 < drawn.length; i++) {
1089
+ const a = drawn[i];
1090
+ const b = drawn[i + 1];
1091
+ const axis = segmentAxis(a, b);
1092
+ if (axis !== null) out2.push({ kind: "segment", index: i, axis, at: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 } });
1093
+ }
1094
+ return out2;
1095
+ }
1096
+ const out = [];
1097
+ for (let k = 1; k + 1 < stops.length; k++) out.push({ kind: "vertex", index: k - 1, at: stops[k] });
1098
+ const at = stopIndices(drawn, stops);
1099
+ for (let leg = 0; leg + 1 < stops.length; leg++) {
1100
+ const a = stops[leg];
1101
+ const b = stops[leg + 1];
1102
+ out.push({
1103
+ kind: "insert",
1104
+ leg,
1105
+ at: at === null ? { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 } : middleOf(drawn, at[leg], at[leg + 1])
1106
+ });
1107
+ }
1108
+ return out;
1109
+ }
1110
+ var interior = (stops) => stops.slice(1, -1).map((p) => ({ x: p.x, y: p.y }));
1111
+ function dragVertex(stops, index, to) {
1112
+ const out = interior(stops);
1113
+ out[index] = { x: to.x, y: to.y };
1114
+ return out;
1115
+ }
1116
+ function insertWaypoint(stops, leg, at) {
1117
+ const out = interior(stops);
1118
+ out.splice(leg, 0, { x: at.x, y: at.y });
1119
+ return out;
1120
+ }
1121
+ function removeWaypoint(stops, index) {
1122
+ const out = interior(stops);
1123
+ out.splice(index, 1);
1124
+ return out;
1125
+ }
1126
+ var shifted = (p, axis, offset) => axis === "x" ? { x: p.x, y: p.y + offset } : { x: p.x + offset, y: p.y };
1127
+ function toward(a, b, distance) {
1128
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
1129
+ return len === 0 ? a : { x: a.x + (b.x - a.x) * distance / len, y: a.y + (b.y - a.y) * distance / len };
1130
+ }
1131
+ function dragSegment(drawn, index, offset) {
1132
+ const a = drawn[index];
1133
+ const b = drawn[index + 1];
1134
+ const axis = segmentAxis(a, b);
1135
+ if (axis === null) return interior(drawn);
1136
+ const jog = Math.min(JOG, Math.hypot(b.x - a.x, b.y - a.y) / 2);
1137
+ const start = index === 0 ? toward(a, b, jog) : null;
1138
+ const end = index === drawn.length - 2 ? toward(b, a, jog) : null;
1139
+ const route = [
1140
+ ...drawn.slice(0, index),
1141
+ ...start === null ? [] : [a, start],
1142
+ shifted(start ?? a, axis, offset),
1143
+ shifted(end ?? b, axis, offset),
1144
+ ...end === null ? [] : [end, b],
1145
+ ...drawn.slice(index + 2)
1146
+ ];
1147
+ return interior(simplifyStops(route));
1148
+ }
1149
+ function pinSegment(drawn, stops, index, offset) {
1150
+ const a = drawn[index];
1151
+ const b = drawn[index + 1];
1152
+ const axis = segmentAxis(a, b);
1153
+ const waypoints = interior(stops);
1154
+ if (axis === null) return waypoints;
1155
+ const jog = Math.min(JOG, Math.hypot(b.x - a.x, b.y - a.y) / 2);
1156
+ const start = index === 0 ? toward(a, b, jog) : a;
1157
+ const end = index === drawn.length - 2 ? toward(b, a, jog) : b;
1158
+ const at = waypoints.map((w) => drawn.findIndex((p) => same(p, w)));
1159
+ const before = waypoints.filter((w, k) => at[k] >= 0 && at[k] <= index && !same(w, a));
1160
+ const after = waypoints.filter((w, k) => (at[k] < 0 || at[k] > index) && !same(w, b));
1161
+ return [...before, shifted(start, axis, offset), shifted(end, axis, offset), ...after];
1162
+ }
1163
+ function unpinSegment(drawn, stops, index) {
1164
+ const ends = [drawn[index], drawn[index + 1]];
1165
+ const waypoints = interior(stops);
1166
+ const kept = waypoints.filter((w) => !ends.some((e) => same(e, w)));
1167
+ return kept.length === waypoints.length ? null : kept;
1168
+ }
1169
+ function removeSegment(drawn, index) {
1170
+ const last = drawn.length - 2;
1171
+ if (index <= 0 || index >= last) return null;
1172
+ const axis = segmentAxis(drawn[index], drawn[index + 1]);
1173
+ if (axis === null) return null;
1174
+ const across = (p) => axis === "x" ? p.x : p.y;
1175
+ const prev = index - 1;
1176
+ const next = index + 1;
1177
+ if (next < last) return dragSegment(drawn, next, across(drawn[index]) - across(drawn[index + 1]));
1178
+ if (prev > 0) return dragSegment(drawn, prev, across(drawn[index + 1]) - across(drawn[index]));
1179
+ return null;
1180
+ }
1181
+ var WAYPOINT_LAYER_ID = "diagram-waypoints";
1182
+ var WAYPOINT_AFFORDANCE_KIND = `layer:${WAYPOINT_LAYER_ID}`;
1183
+ function waypointScratchOf(hit) {
1184
+ const payload = hit?.payload;
1185
+ return payload !== void 0 && typeof payload.edgeId === "string" && typeof payload.handle?.kind === "string" ? payload : null;
1186
+ }
1187
+ function routeStateOf(ends, routers) {
1188
+ const { route } = ends;
1189
+ const stops = [route.from.point, ...route.waypoints, route.to.point];
1190
+ const router = routerOf(ends.trait, routers);
1191
+ const handles = route.stubs !== void 0 ? [] : routeHandles(route.points, stops, router);
1192
+ return { from: route.from, to: route.to, stops, drawn: route.points, router, handles };
1193
+ }
1194
+ var WAYPOINT_HIT_PX = 7;
1195
+ var DEFAULT_SIZE_PX3 = 8;
1196
+ var DEFAULT_FILL2 = { paint: { kind: "solid", color: "#ffffff" } };
1197
+ var DEFAULT_STROKE2 = { paint: { kind: "solid", color: "#3b6fd4" }, width: 1.5 };
1198
+ function paintOf(handle, sizePx, fill, stroke) {
1199
+ return {
1200
+ kind: "custom",
1201
+ draw: ({ world, view }) => {
1202
+ const t = viewToTransform(view);
1203
+ const cx = t.panX + world.x * t.zoom.x;
1204
+ const cy = t.panY + world.y * t.zoom.y;
1205
+ if (handle.kind === "segment") {
1206
+ const [w, h] = handle.axis === "x" ? [sizePx * 2, sizePx / 2] : [sizePx / 2, sizePx * 2];
1207
+ return [{ kind: "path", path: { kind: "rect", x: cx - w / 2, y: cy - h / 2, width: w, height: h }, fill, stroke }];
1208
+ }
1209
+ const d = handle.kind === "vertex" ? sizePx : sizePx * 0.75;
1210
+ const path = ellipsePath({ x: cx - d / 2, y: cy - d / 2, width: d, height: d });
1211
+ return [handle.kind === "vertex" ? { kind: "path", path, fill, stroke } : { kind: "path", path, stroke }];
1212
+ }
1213
+ };
1214
+ }
1215
+ function createWaypointAffordance(participants, selected, opts = {}) {
1216
+ const { id = WAYPOINT_LAYER_ID, hitRadiusPx = WAYPOINT_HIT_PX, sizePx = DEFAULT_SIZE_PX3 } = opts;
1217
+ const endsOf = createEdgeEnds(participants, opts);
1218
+ const fill = opts.fill ?? DEFAULT_FILL2;
1219
+ const stroke = opts.stroke ?? DEFAULT_STROKE2;
1220
+ return {
1221
+ id,
1222
+ regions(state) {
1223
+ const out = [];
1224
+ for (const edgeId of selected()) {
1225
+ const ends = endsOf(edgeId);
1226
+ if (ends === null || !offersPorts(ends.edge, state)) continue;
1227
+ for (const handle of routeStateOf(ends, opts.routers).handles) {
1228
+ out.push({
1229
+ id: `${edgeId}:${handle.kind}:${handle.kind === "insert" ? handle.leg : handle.index}`,
1230
+ targetId: null,
1231
+ shape: { kind: "point", x: handle.at.x, y: handle.at.y, hitRadiusPx },
1232
+ ...sizePx > 0 ? { paint: paintOf(handle, sizePx, fill, stroke) } : {},
1233
+ ...opts.cursor !== void 0 ? { cursor: opts.cursor } : {},
1234
+ strength: "exclusive",
1235
+ claimedKinds: ["pointer"],
1236
+ bind: () => ({ initialScratch: { targetId: edgeId, edgeId, handle } })
1237
+ });
1238
+ }
1239
+ }
1240
+ return out;
1241
+ }
1242
+ };
1243
+ }
1244
+ function waypointLayer(participants, selected, opts = {}) {
1245
+ return composeAffordanceLayer(
1246
+ opts.id ?? WAYPOINT_LAYER_ID,
1247
+ "Diagram waypoints",
1248
+ [createWaypointAffordance(participants, selected, opts)]
1249
+ );
1250
+ }
1251
+
1252
+ // src/routeSnapSource.ts
1253
+ var BOTH = ["x", "y"];
1254
+ var ALONG = { x: { x: 0, y: 1 }, y: { x: 1, y: 0 } };
1255
+ function portAxes(port) {
1256
+ const n = port.normal;
1257
+ if (n === null) return BOTH;
1258
+ return Math.abs(n.x) >= Math.abs(n.y) ? ["y"] : ["x"];
1259
+ }
1260
+ function routeSnapSource(opts) {
1261
+ const targets = [
1262
+ ...opts.stops.map((at) => ({ at, axes: BOTH })),
1263
+ ...opts.ports.map((p) => ({ at: p.point, axes: portAxes(p) }))
1264
+ ];
1265
+ return {
1266
+ id: opts.id,
1267
+ tier: "target",
1268
+ reachPx: opts.reachPx,
1269
+ roles: ["pointer"],
1270
+ gestures: ["point"],
1271
+ *collect(q) {
1272
+ const reach = q.reachWorld(q.reachPx);
1273
+ for (const p of q.probes) {
1274
+ for (const t of targets) {
1275
+ for (const axis of t.axes) {
1276
+ if (Math.abs(p.at[axis] - t.at[axis]) > reach) continue;
1277
+ yield {
1278
+ probe: p.id,
1279
+ locus: { at: { ...p.at, [axis]: t.at[axis] }, dirs: [ALONG[axis]] },
1280
+ mark: (delta) => ({
1281
+ kind: "line",
1282
+ from: t.at,
1283
+ to: { x: p.at.x - q.delta0.x + delta.x, y: p.at.y - q.delta0.y + delta.y }
1284
+ })
1285
+ };
1286
+ }
1287
+ }
1288
+ }
1289
+ }
1290
+ };
1291
+ }
1292
+
1293
+ // src/waypointAction.ts
1294
+ var ROUTE_EDIT_ACTION_ID = "diagram.editRoute";
1295
+ var DEFAULT_SNAP_PX = 6;
1296
+ var ROUTE_SNAP_ID = "diagram-route";
1297
+ function routedData(data, trait, waypoints) {
1298
+ const { waypoints: _w, laidOut: _l, ...rest } = trait;
1299
+ return { ...data, diagram: waypoints.length > 0 ? { ...rest, waypoints } : rest };
1300
+ }
1301
+ function commitRouteToScene(edit, ctx) {
1302
+ const scene = ctx.deps.scene;
1303
+ const node = scene?.get(edit.edgeId);
1304
+ const trait = node === void 0 ? null : diagramEdgeOf(node);
1305
+ if (scene === void 0 || node === void 0 || trait === null) return;
1306
+ scene.batch("Edit route", () => {
1307
+ scene.update(node.id, { data: routedData(node.data, trait, edit.waypoints) });
1308
+ });
1309
+ }
1310
+ function beginSnap(ctx, p, reachPx) {
1311
+ const { handle, route } = p;
1312
+ const segment = handle.kind === "segment";
1313
+ const stops = segment ? route.drawn.filter((_, i) => i !== handle.index && i !== handle.index + 1) : neighborsOf(handle, route.stops);
1314
+ const source = routeSnapSource({ id: ROUTE_SNAP_ID, stops, ports: segment ? [] : [route.from, route.to], reachPx });
1315
+ return ctx.deps.snap?.begin({
1316
+ gesture: "point",
1317
+ press: handle.at,
1318
+ ids: [p.edgeId],
1319
+ omit: ["path-snap"],
1320
+ sources: [source],
1321
+ ...segment ? { handle: handle.axis === "x" ? { x: "free", y: "min" } : { x: "min", y: "free" } } : { constrain: "axis" }
1322
+ }) ?? null;
1323
+ }
1324
+ function neighborsOf(handle, stops) {
1325
+ const at = handle.kind === "vertex" ? handle.index + 1 : handle.leg + 1;
1326
+ return [stops[at - 1], stops[handle.kind === "vertex" ? at + 1 : at]];
1327
+ }
1328
+ function moveOf(p, session, ctx) {
1329
+ const d = { x: ctx.world.x - p.press.x, y: ctx.world.y - p.press.y };
1330
+ if (session === null) return d;
1331
+ const at = session.at({ x: p.handle.at.x + d.x, y: p.handle.at.y + d.y }, snapMods(ctx)).point;
1332
+ return { x: at.x - p.handle.at.x, y: at.y - p.handle.at.y };
1333
+ }
1334
+ function waypointsAt(p, d) {
1335
+ if (d.x === 0 && d.y === 0) return null;
1336
+ const { handle, route } = p;
1337
+ let next;
1338
+ if (handle.kind === "segment") {
1339
+ const across = handle.axis === "x" ? "y" : "x";
1340
+ const offset = d[across];
1341
+ next = route.router === avoid ? pinSegment(route.drawn, route.stops, handle.index, offset) : dragSegment(route.drawn, handle.index, offset);
1342
+ } else {
1343
+ const point = { x: handle.at.x + d.x, y: handle.at.y + d.y };
1344
+ next = handle.kind === "vertex" ? dragVertex(route.stops, handle.index, point) : insertWaypoint(route.stops, handle.leg, point);
1345
+ }
1346
+ return samePoints(next, p.trait.waypoints ?? []) ? null : next;
1347
+ }
1348
+ function createRouteEditAction(opts) {
1349
+ const { commit = commitRouteToScene, snapPx = DEFAULT_SNAP_PX } = opts;
1350
+ const endsOf = createEdgeEnds(opts.participants, opts);
1351
+ return {
1352
+ id: opts.id ?? ROUTE_EDIT_ACTION_ID,
1353
+ label: "Edit route",
1354
+ group: "diagram",
1355
+ requires: ["scene", "view", "snap"],
1356
+ modifierRoles: SNAP_ROLES,
1357
+ invoker: {
1358
+ timing: "ongoing",
1359
+ start(ctx) {
1360
+ const pressed = waypointScratchOf(ctx.drag?.affordance);
1361
+ const ends = pressed === null ? null : endsOf(pressed.edgeId);
1362
+ if (pressed === null || ends === null) return {};
1363
+ const p = {
1364
+ edgeId: pressed.edgeId,
1365
+ handle: pressed.handle,
1366
+ route: routeStateOf(ends, opts.routers),
1367
+ trait: ends.trait,
1368
+ data: ends.edge.node.data,
1369
+ pose: ends.edge.pose,
1370
+ press: ctx.drag?.start ?? { x: ctx.world.x, y: ctx.world.y },
1371
+ deps: ctx.deps
1372
+ };
1373
+ const session = beginSnap(ctx, p, snapPx);
1374
+ let waypoints = null;
1375
+ let open = true;
1376
+ const proposed = () => open ? waypoints : null;
1377
+ return {
1378
+ kind: "diagram-edit-route",
1379
+ onMove(moveCtx) {
1380
+ waypoints = waypointsAt(p, moveOf(p, session, moveCtx));
1381
+ },
1382
+ previewIds: () => proposed() === null ? null : [p.edgeId],
1383
+ previewPose: (id) => id === p.edgeId && proposed() !== null ? p.pose : null,
1384
+ previewData: (id) => {
1385
+ const next = proposed();
1386
+ return id === p.edgeId && next !== null ? routedData(p.data, p.trait, next) : null;
1387
+ },
1388
+ onEnd(endCtx, reason) {
1389
+ open = false;
1390
+ const final = reason === "commit" ? waypointsAt(p, moveOf(p, session, endCtx)) : null;
1391
+ session?.end();
1392
+ if (final !== null) commit({ edgeId: p.edgeId, waypoints: final }, { ...endCtx, deps: p.deps });
1393
+ }
1394
+ };
1395
+ }
1396
+ },
1397
+ enabled: () => true
1398
+ };
1399
+ }
1400
+ var REMOVE_WAYPOINT_ACTION_ID = "diagram.removeWaypoint";
1401
+ var RESET_ROUTE_ACTION_ID = "diagram.resetRoute";
1402
+ var NO_MODIFIERS3 = { alt: false, shift: false, meta: false, ctrl: false };
1403
+ function removalOf(route, handle) {
1404
+ if (handle.kind === "vertex") return removeWaypoint(route.stops, handle.index);
1405
+ if (handle.kind === "segment") {
1406
+ return route.router === avoid ? unpinSegment(route.drawn, route.stops, handle.index) : removeSegment(route.drawn, handle.index);
1407
+ }
1408
+ return null;
1409
+ }
1410
+ function createRemoveWaypointAction(opts) {
1411
+ const { commit = commitRouteToScene, hitRadiusPx = WAYPOINT_HIT_PX } = opts;
1412
+ const endsOf = createEdgeEnds(opts.participants, opts);
1413
+ const routeOf3 = (edgeId) => {
1414
+ const ends = endsOf(edgeId);
1415
+ return ends === null ? null : routeStateOf(ends, opts.routers);
1416
+ };
1417
+ const removalAt = (deps, at) => {
1418
+ const reach = hitRadiusPx / Math.abs(deps?.view?.get().scale.x || 1);
1419
+ for (const edgeId of opts.selected()) {
1420
+ const route = routeOf3(edgeId);
1421
+ for (const handle of route?.handles ?? []) {
1422
+ if (Math.max(Math.abs(handle.at.x - at.x), Math.abs(handle.at.y - at.y)) > reach) continue;
1423
+ const waypoints = removalOf(route, handle);
1424
+ if (waypoints !== null) return { edgeId, waypoints };
1425
+ }
1426
+ }
1427
+ return null;
1428
+ };
1429
+ return {
1430
+ id: REMOVE_WAYPOINT_ACTION_ID,
1431
+ label: "Remove waypoint",
1432
+ group: "diagram",
1433
+ requires: ["scene", "view"],
1434
+ invoker: {
1435
+ timing: "immediate",
1436
+ run(deps, params) {
1437
+ const world = { x: Number(params?.worldX), y: Number(params?.worldY) };
1438
+ const pressed = waypointScratchOf(params?.affordance);
1439
+ const route = pressed === null ? null : routeOf3(pressed.edgeId);
1440
+ const waypoints = pressed === null || route === null ? null : removalOf(route, pressed.handle);
1441
+ const edit = pressed !== null && waypoints !== null ? { edgeId: pressed.edgeId, waypoints } : removalAt(deps, world);
1442
+ if (edit !== null) commit(edit, { world, modifiers: NO_MODIFIERS3, deps });
1443
+ }
1444
+ },
1445
+ enabled: (deps, at) => at !== void 0 && removalAt(deps, at) !== null ? true : ActionDisabledReason.NotApplicable
1446
+ };
1447
+ }
1448
+ function routedEdges(opts) {
1449
+ const ids = new Set(opts.selected());
1450
+ if (ids.size === 0) return [];
1451
+ const out = [];
1452
+ for (const { node } of opts.participants()) {
1453
+ if (ids.has(node.id) && (diagramEdgeOf(node)?.waypoints ?? []).length > 0) out.push(node.id);
1454
+ }
1455
+ return out;
1456
+ }
1457
+ function createResetRouteAction(opts) {
1458
+ const { commit = commitRouteToScene } = opts;
1459
+ return {
1460
+ id: RESET_ROUTE_ACTION_ID,
1461
+ label: "Reset route",
1462
+ group: "diagram",
1463
+ requires: ["scene"],
1464
+ invoker: {
1465
+ timing: "immediate",
1466
+ run(deps) {
1467
+ const edges = routedEdges(opts);
1468
+ const ctx = { world: { x: 0, y: 0 }, modifiers: NO_MODIFIERS3, deps };
1469
+ const all = () => {
1470
+ for (const edgeId of edges) commit({ edgeId, waypoints: [] }, ctx);
1471
+ };
1472
+ if (deps.scene === void 0) all();
1473
+ else deps.scene.batch("Reset route", all);
1474
+ }
1475
+ },
1476
+ enabled: () => routedEdges(opts).length > 0 ? true : ActionDisabledReason.NotApplicable
1477
+ };
1478
+ }
1479
+ var SET_ROUTER_ACTION_IDS = {
1480
+ avoid: "diagram.router.avoid",
1481
+ orthogonal: "diagram.router.orthogonal",
1482
+ straight: "diagram.router.straight",
1483
+ bezier: "diagram.router.bezier"
1484
+ };
1485
+ var ROUTER_LABELS = {
1486
+ avoid: "Around shapes",
1487
+ orthogonal: "Orthogonal",
1488
+ straight: "Straight",
1489
+ bezier: "Curved"
1490
+ };
1491
+ function withRouter(data, trait, router) {
1492
+ const { route: _r, ...rest } = trait;
1493
+ return { ...data, diagram: { ...rest, router } };
1494
+ }
1495
+ function commitRouterToScene(edit, ctx) {
1496
+ const scene = ctx.deps.scene;
1497
+ const node = scene?.get(edit.edgeId);
1498
+ const trait = node === void 0 ? null : diagramEdgeOf(node);
1499
+ if (scene === void 0 || node === void 0 || trait === null) return;
1500
+ scene.update(node.id, { data: withRouter(node.data, trait, edit.router) });
1501
+ }
1502
+ function selectedEdges(opts) {
1503
+ const ids = new Set(opts.selected());
1504
+ if (ids.size === 0) return [];
1505
+ const out = [];
1506
+ for (const { node } of opts.participants()) {
1507
+ const trait = ids.has(node.id) ? diagramEdgeOf(node) : null;
1508
+ if (trait !== null) out.push({ id: node.id, router: trait.router ?? "straight" });
1509
+ }
1510
+ return out;
1511
+ }
1512
+ var NO_MODIFIERS4 = { alt: false, shift: false, meta: false, ctrl: false };
1513
+ function createSetRouterAction(router, opts) {
1514
+ const { commit = commitRouterToScene } = opts;
1515
+ return {
1516
+ id: SET_ROUTER_ACTION_IDS[router],
1517
+ label: ROUTER_LABELS[router],
1518
+ group: "diagram",
1519
+ requires: ["scene"],
1520
+ invoker: {
1521
+ timing: "immediate",
1522
+ run(deps) {
1523
+ const edges = selectedEdges(opts).filter((e) => e.router !== router);
1524
+ const ctx = { world: { x: 0, y: 0 }, modifiers: NO_MODIFIERS4, deps };
1525
+ const all = () => {
1526
+ for (const e of edges) commit({ edgeId: e.id, router }, ctx);
1527
+ };
1528
+ if (deps.scene === void 0) all();
1529
+ else deps.scene.batch(`Route ${ROUTER_LABELS[router].toLowerCase()}`, all);
1530
+ }
1531
+ },
1532
+ enabled: () => selectedEdges(opts).length > 0 ? true : ActionDisabledReason.NotApplicable,
1533
+ active: () => {
1534
+ const edges = selectedEdges(opts);
1535
+ return edges.length > 0 && edges.every((e) => e.router === router);
1536
+ }
1537
+ };
1538
+ }
1539
+ function routerActions(opts) {
1540
+ return Object.keys(SET_ROUTER_ACTION_IDS).map((r) => createSetRouterAction(r, opts));
1541
+ }
1542
+ function createDiagramContribution(opts) {
1543
+ const actionId = opts.id ?? CONNECT_ACTION_ID;
1544
+ return {
1545
+ id: "weasel-diagram",
1546
+ eligibility: { claimed: true },
1547
+ actions: [createConnectAction({ ...opts, id: actionId }), grabPortAction()],
1548
+ bindings: [connectBinding(actionId), ...grabPortBindings()]
1549
+ };
1550
+ }
1551
+ function diagramPorts(opts) {
1552
+ return {
1553
+ layer: portLayer(opts.participants, opts),
1554
+ contribution: createDiagramContribution(opts)
1555
+ };
1556
+ }
1557
+ var GRAB_WAYPOINT_ACTION_ID = "diagram.grabWaypoint";
1558
+ function diagramWaypoints(opts) {
1559
+ const actionId = opts.id ?? ROUTE_EDIT_ACTION_ID;
1560
+ const target = `affordance:${WAYPOINT_AFFORDANCE_KIND}`;
1561
+ return {
1562
+ // `opts.id` names the drag action; the layer keeps its own id.
1563
+ layer: waypointLayer(opts.participants, opts.selected, { ...opts, id: WAYPOINT_LAYER_ID }),
1564
+ contribution: {
1565
+ id: "weasel-diagram-waypoints",
1566
+ eligibility: { claimed: true },
1567
+ actions: [
1568
+ createRouteEditAction({ ...opts, id: actionId }),
1569
+ createRemoveWaypointAction(opts),
1570
+ createResetRouteAction(opts),
1571
+ ...routerActions({ participants: opts.participants, selected: opts.selected }),
1572
+ createSetHopsAction(),
1573
+ grabPortAction(GRAB_WAYPOINT_ACTION_ID)
1574
+ ],
1575
+ bindings: [
1576
+ { spec: { kind: "drag", target }, actionId },
1577
+ { spec: { kind: "doubleClick", target }, actionId: REMOVE_WAYPOINT_ACTION_ID },
1578
+ ...grabPortBindings(GRAB_WAYPOINT_ACTION_ID, WAYPOINT_AFFORDANCE_KIND)
1579
+ ]
1580
+ }
1581
+ };
1582
+ }
1583
+ var GRAB_LABEL_ACTION_ID = "diagram.grabLabel";
1584
+ var SELECT_LABEL_EDGE_ACTION_ID = "diagram.selectLabelEdge";
1585
+ var NO_MODIFIERS5 = { shift: false, meta: false, ctrl: false };
1586
+ var ANY_MODIFIERS = { shift: "optional", meta: "optional", ctrl: "optional", alt: "optional" };
1587
+ function selectLabelEdgeAction() {
1588
+ return {
1589
+ id: SELECT_LABEL_EDGE_ACTION_ID,
1590
+ label: "Select label edge",
1591
+ group: "diagram",
1592
+ requires: ["selection"],
1593
+ invoker: {
1594
+ timing: "immediate",
1595
+ run(deps, params) {
1596
+ const p = params;
1597
+ const pressed = labelScratchOf(p?.affordance);
1598
+ if (pressed !== null) deps.selection?.applyClick(pressed.edgeId, p?.mods ?? NO_MODIFIERS5);
1599
+ }
1600
+ },
1601
+ enabled: (deps) => deps?.selection !== void 0 ? true : ActionDisabledReason.NotApplicable
1602
+ };
1603
+ }
1604
+ function diagramLabelDrag(opts) {
1605
+ const actionId = opts.id ?? MOVE_LABEL_ACTION_ID;
1606
+ const target = `affordance:${LABEL_DRAG_AFFORDANCE_KIND}`;
1607
+ return {
1608
+ layer: labelDragLayer(opts.scene, opts.cursor !== void 0 ? { cursor: opts.cursor } : {}),
1609
+ contribution: {
1610
+ id: "weasel-diagram-label-drag",
1611
+ eligibility: { claimed: true },
1612
+ actions: [
1613
+ createLabelDragAction({ ...opts, id: actionId }),
1614
+ createResetLabelAction({ selected: opts.selected, ...opts.commit ? { commit: opts.commit } : {} }),
1615
+ grabPortAction(GRAB_LABEL_ACTION_ID),
1616
+ selectLabelEdgeAction()
1617
+ ],
1618
+ bindings: [
1619
+ { spec: { kind: "drag", target }, actionId },
1620
+ { spec: { kind: "pointerDown", target }, actionId: GRAB_LABEL_ACTION_ID },
1621
+ { spec: { kind: "click", target, mods: ANY_MODIFIERS }, actionId: SELECT_LABEL_EDGE_ACTION_ID },
1622
+ { spec: { kind: "doubleClick", target }, actionId: EDIT_EDGE_LABEL_ACTION_ID }
1623
+ ]
1624
+ }
1625
+ };
1626
+ }
1627
+ var headerIdOf = (id) => `${id}/header`;
1628
+ var facingPorts = (ports) => ports.flatMap((p) => p.normal === null ? [] : [{ point: p.point, normal: p.normal }]);
1629
+ function members(scene, node, into) {
1630
+ const under = Array.isArray(node.dependsOn) && diagramGroupOf(node) !== null ? node.dependsOn : scene.childrenOf(node.id);
1631
+ for (const id of under) {
1632
+ if (into.has(id)) continue;
1633
+ into.add(id);
1634
+ const child = scene.get(id);
1635
+ if (child !== void 0) members(scene, child, into);
1636
+ }
1637
+ return into;
1638
+ }
1639
+ function avoidSceneInput(scene, opts = {}, poseOf) {
1640
+ const sc = scene;
1641
+ const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
1642
+ const portOpts = { geometry, ...opts.read ? { read: opts.read } : {} };
1643
+ const key = opts.routerKey ?? "avoid";
1644
+ const poses = /* @__PURE__ */ new Map();
1645
+ const pose = (node) => {
1646
+ let p = poses.get(node.id);
1647
+ if (p === void 0) {
1648
+ p = poseOf?.(node.id) ?? documentPose(sc, node);
1649
+ poses.set(node.id, p);
1650
+ }
1651
+ return p;
1652
+ };
1653
+ const obstacles = [];
1654
+ const edgeNodes = [];
1655
+ const labels = /* @__PURE__ */ new Map();
1656
+ const labelNodes = /* @__PURE__ */ new Map();
1657
+ const split = [];
1658
+ for (const node of sc.renderOrderNodes()) {
1659
+ const edge = diagramEdgeOf(node);
1660
+ if (edge !== null) {
1661
+ if (edge.router === key && edge.offPage === void 0) edgeNodes.push(node);
1662
+ else if (edge.router === key) split.push(node.id);
1663
+ continue;
1664
+ }
1665
+ const label = diagramLabelOf(node);
1666
+ if (label !== null) {
1667
+ const owner = Array.isArray(node.dependsOn) ? node.dependsOn[0] : void 0;
1668
+ const { width, height } = geometry.getBounds(node.pose);
1669
+ if (owner !== void 0 && !labels.has(owner)) {
1670
+ labels.set(owner, { id: node.id, placement: label, size: { width, height } });
1671
+ labelNodes.set(owner, node);
1672
+ }
1673
+ continue;
1674
+ }
1675
+ const container = diagramContainerOf(node);
1676
+ const isParticipant = diagramNodeOf(node, opts.read) !== null;
1677
+ if (!isParticipant && container?.role !== "lane") continue;
1678
+ const box = geometry.getBounds(pose(node));
1679
+ const header = container === null ? null : headerBox(box, container.header);
1680
+ if (header !== null) obstacles.push({ id: headerIdOf(node.id), kind: "shape", box: header });
1681
+ if (container?.role === "lane") continue;
1682
+ const region = container !== null || diagramGroupOf(node) !== null;
1683
+ obstacles.push(region ? { id: node.id, kind: "region", box, members: members(sc, node, /* @__PURE__ */ new Set()) } : { id: node.id, kind: "shape", box });
1684
+ }
1685
+ for (const id of split) {
1686
+ const node = labelNodes.get(id);
1687
+ if (node !== void 0) obstacles.push({ id: node.id, kind: "label", edge: id, box: geometry.getBounds(pose(node)) });
1688
+ }
1689
+ const edges = [];
1690
+ for (const node of edgeNodes) {
1691
+ const edge = diagramEdgeOf(node);
1692
+ const ends = node.dependsOn;
1693
+ if (!Array.isArray(ends) || ends.length !== 2) continue;
1694
+ if (ends[0] === ends[1] && edge.from.port === void 0 && edge.to.port === void 0) continue;
1695
+ const [a, b] = ends.map((id) => sc.get(id));
1696
+ if (a === void 0 || b === void 0) continue;
1697
+ const portsAt = (n, port) => facingPorts(port === void 0 ? portsOf(n, pose(n), portOpts) : portSitesOf(n, pose(n), port, portOpts));
1698
+ const from = portsAt(a, edge.from.port);
1699
+ const to = portsAt(b, edge.to.port);
1700
+ if (from.length === 0 || to.length === 0) continue;
1701
+ edges.push({
1702
+ id: node.id,
1703
+ from: { node: a.id, ports: from },
1704
+ to: { node: b.id, ports: to },
1705
+ ...edge.waypoints !== void 0 && edge.waypoints.length > 0 ? { waypoints: edge.waypoints } : {},
1706
+ ...edge.route !== void 0 ? { route: edge.route } : {},
1707
+ ...labels.has(node.id) ? { label: labels.get(node.id) } : {}
1708
+ });
1709
+ }
1710
+ return { obstacles, edges };
1711
+ }
1712
+
1713
+ // src/avoid/specRoutes.ts
1714
+ var facing = (ports) => ports.flatMap((p) => p.normal === null ? [] : [{ point: p.point, normal: p.normal }]);
1715
+ function obstaclesOf(specs) {
1716
+ const children = /* @__PURE__ */ new Map();
1717
+ for (const s of specs) {
1718
+ if (s.parent == null) continue;
1719
+ children.set(s.parent, [...children.get(s.parent) ?? [], s.id]);
1720
+ }
1721
+ const inside2 = (id, into) => {
1722
+ for (const child of children.get(id) ?? []) {
1723
+ into.add(child);
1724
+ inside2(child, into);
1725
+ }
1726
+ return into;
1727
+ };
1728
+ const out = [];
1729
+ for (const spec of specs) {
1730
+ const node = { id: spec.id, kind: spec.kind, data: spec.data };
1731
+ if (spec.derivePose !== void 0 || diagramEdgeOf(spec) !== null) continue;
1732
+ const region = diagramContainerOf(spec);
1733
+ if (diagramNodeOf(node) === null && region?.role !== "lane") continue;
1734
+ const header = region === null ? null : headerBox(spec.pose, region.header);
1735
+ if (header !== null) out.push({ id: headerIdOf(node.id), kind: "shape", box: header });
1736
+ if (region?.role === "lane") continue;
1737
+ out.push(region === null ? { id: node.id, kind: "shape", box: spec.pose } : { id: node.id, kind: "region", box: spec.pose, members: inside2(node.id, /* @__PURE__ */ new Set()) });
1738
+ }
1739
+ return out;
1740
+ }
1741
+ function labelsOf(specs) {
1742
+ const out = /* @__PURE__ */ new Map();
1743
+ for (const spec of specs) {
1744
+ const label = diagramLabelOf(spec);
1745
+ const owner = Array.isArray(spec.dependsOn) ? spec.dependsOn[0] : void 0;
1746
+ if (label === null || owner === void 0 || out.has(owner)) continue;
1747
+ const { width, height } = spec.pose;
1748
+ out.set(owner, { id: spec.id, placement: label, size: { width, height } });
1749
+ }
1750
+ return out;
1751
+ }
1752
+ function createSpecRouter(options = {}) {
1753
+ const router = createAvoidRouter(options);
1754
+ let held = /* @__PURE__ */ new Map();
1755
+ return (specs) => {
1756
+ const byId = new Map(specs.map((s) => [s.id, s]));
1757
+ const labels = labelsOf(specs);
1758
+ const obstacles = obstaclesOf(specs);
1759
+ const edges = [];
1760
+ for (const spec of specs) {
1761
+ const edge = diagramEdgeOf(spec);
1762
+ if (edge === null || edge.offPage !== void 0) continue;
1763
+ const ends = Array.isArray(spec.dependsOn) ? spec.dependsOn.map((id) => byId.get(id)) : [];
1764
+ const [a, b] = ends;
1765
+ if (a === void 0 || b === void 0 || a === b) continue;
1766
+ const portsAt = (s, port) => {
1767
+ const n = { id: s.id, kind: s.kind, data: s.data };
1768
+ return facing(port === void 0 ? portsOf(n, s.pose) : portSitesOf(n, s.pose, port));
1769
+ };
1770
+ const route = held.get(spec.id);
1771
+ edges.push({
1772
+ id: spec.id,
1773
+ from: { node: a.id, ports: portsAt(a, edge.from.port) },
1774
+ to: { node: b.id, ports: portsAt(b, edge.to.port) },
1775
+ ...edge.waypoints !== void 0 && edge.waypoints.length > 0 ? { waypoints: edge.waypoints } : {},
1776
+ ...route != null ? { route } : {},
1777
+ ...labels.has(spec.id) ? { label: labels.get(spec.id) } : {}
1778
+ });
1779
+ }
1780
+ if (edges.length === 0) return [...specs];
1781
+ const next = new Map(held);
1782
+ for (const [id, route] of router.update({ obstacles, edges })) next.set(id, route);
1783
+ held = new Map(edges.map((e) => [e.id, next.get(e.id) ?? null]));
1784
+ return specs.map((spec) => {
1785
+ const edge = diagramEdgeOf(spec);
1786
+ if (edge === null || !held.has(spec.id)) return spec;
1787
+ const route = held.get(spec.id);
1788
+ const { route: _r, ...rest } = edge;
1789
+ const diagram = { ...rest, router: "avoid", ...route != null ? { route } : {} };
1790
+ return { ...spec, data: { ...spec.data, diagram } };
1791
+ });
1792
+ };
1793
+ }
1794
+ function sceneEdges(scene) {
1795
+ const out = [];
1796
+ for (const node of scene.renderOrderNodes()) {
1797
+ const trait = diagramEdgeOf(node);
1798
+ const ends = node.dependsOn;
1799
+ if (trait === null || !Array.isArray(ends) || ends.length !== 2) continue;
1800
+ out.push({ node, trait, ends: [ends[0], ends[1]] });
1801
+ }
1802
+ return out;
1803
+ }
1804
+ function edgePolylines(scene, opts = {}) {
1805
+ const route = edgeRouter({ geometry: AUTO_POSE_DESCRIPTOR, ...opts });
1806
+ const dep = (id) => {
1807
+ const node = scene.get(id);
1808
+ return node === void 0 ? void 0 : { node, pose: documentPose(scene, node), path: null };
1809
+ };
1810
+ return (edge) => {
1811
+ const routed = route(edge.node, [dep(edge.ends[0]), dep(edge.ends[1])]);
1812
+ const points = routed?.stubs === void 0 ? routed?.points : void 0;
1813
+ return points === void 0 || points.length < 2 ? null : [...points];
1814
+ };
1815
+ }
1816
+ function inOrUnder(scene, id, ids) {
1817
+ for (let at = id; at != null; at = scene.get(at)?.parent) {
1818
+ if (ids.has(at)) return true;
1819
+ }
1820
+ return false;
1821
+ }
1822
+ function withHopsData(data, trait, hops) {
1823
+ const { hops: _h, ...rest } = trait;
1824
+ return { ...data, diagram: hops.length === 0 ? rest : { ...rest, hops } };
1825
+ }
1826
+ var sameHop = (a, b) => a.at.x === b.at.x && a.at.y === b.at.y && a.r === b.r && a.over.length === b.over.length && a.over.every((o, i) => o === b.over[i]);
1827
+ function hopDataOps(scene, hops) {
1828
+ const ops = [];
1829
+ for (const [id, list] of hops) {
1830
+ const node = scene.get(id);
1831
+ const trait = node === void 0 ? null : diagramEdgeOf(node);
1832
+ if (node === void 0 || trait === null) continue;
1833
+ const had = trait.hops ?? [];
1834
+ if (had.length === list.length && had.every((h, i) => sameHop(h, list[i]))) continue;
1835
+ ops.push(createSetDataOp({ id, from: node.data, to: withHopsData(node.data, trait, list), label: "Line jumps" }));
1836
+ }
1837
+ return ops;
1838
+ }
1839
+
1840
+ // src/hops/specHops.ts
1841
+ function createSpecHops(r) {
1842
+ const route = edgeRouter();
1843
+ return (specs) => {
1844
+ const byId = new Map(specs.map((s) => [s.id, s]));
1845
+ const dep = (id) => {
1846
+ const s = byId.get(id);
1847
+ return s === void 0 ? void 0 : { node: s, pose: s.pose, path: null };
1848
+ };
1849
+ const polylines = /* @__PURE__ */ new Map();
1850
+ const order = [];
1851
+ for (const spec of specs) {
1852
+ const ends = spec.dependsOn;
1853
+ if (diagramEdgeOf(spec) === null || !Array.isArray(ends) || ends.length !== 2) continue;
1854
+ const routed = route(spec, [dep(ends[0]), dep(ends[1])]);
1855
+ const points = routed?.stubs === void 0 ? routed?.points : void 0;
1856
+ if (points === void 0 || points.length < 2) continue;
1857
+ polylines.set(spec.id, [...points]);
1858
+ order.push(spec.id);
1859
+ }
1860
+ const hops = edgeHops(polylines, order, r);
1861
+ return specs.map((spec) => {
1862
+ const trait = diagramEdgeOf(spec);
1863
+ const list = hops.get(spec.id) ?? [];
1864
+ if (trait === null || list.length === 0 && trait.hops === void 0) return spec;
1865
+ return { ...spec, data: withHopsData(spec.data, trait, list) };
1866
+ });
1867
+ };
1868
+ }
1869
+
1870
+ // src/reconnect.ts
1871
+ var RECONNECT_ACTION_ID = "diagram.reconnect";
1872
+ var GRAB_EDGE_END_ACTION_ID = "diagram.grabEdgeEnd";
1873
+ var DEFAULT_SNAP2 = 16;
1874
+ function commitReconnectToScene(change, ctx) {
1875
+ const scene = ctx.deps.scene;
1876
+ if (scene === void 0) return;
1877
+ const node = scene.get(change.edgeId);
1878
+ const trait = node === void 0 ? null : diagramEdgeOf(node);
1879
+ const deps = node?.dependsOn;
1880
+ if (node === void 0 || trait === null || !Array.isArray(deps) || deps.length !== 2) return;
1881
+ const data = node.data;
1882
+ const nextDeps = [...deps];
1883
+ nextDeps[change.end === "from" ? 0 : 1] = change.target.nodeId;
1884
+ scene.batch("Reconnect edge", () => {
1885
+ scene.update(node.id, {
1886
+ data: { ...data, diagram: { ...trait, [change.end]: edgeEndOf(change.target) } }
1887
+ });
1888
+ scene.setDependsOn(node.id, nextDeps);
1889
+ });
1890
+ }
1891
+ function fixedEndOf(ends, moving, opts) {
1892
+ const side = moving === "from" ? "to" : "from";
1893
+ const { node, pose } = ends[side];
1894
+ const named = ends.trait[side].port;
1895
+ const port = named === void 0 ? void 0 : portOf(node, pose, named, {
1896
+ ...opts.geometry ? { geometry: opts.geometry } : {},
1897
+ ...opts.read ? { read: opts.read } : {}
1898
+ });
1899
+ return port ?? { kind: "body", nodeId: node.id, point: ends.points[side] };
1900
+ }
1901
+ function sameEnd(ends, moving, target) {
1902
+ if (ends[moving].node.id !== target.nodeId) return false;
1903
+ const named = ends.trait[moving].port;
1904
+ return isBodyEnd(target) ? named === void 0 : named === target.id;
1905
+ }
1906
+ function createReconnectAction(opts) {
1907
+ const { canConnect = defaultCanConnect, commit = commitReconnectToScene } = opts;
1908
+ const endsOf = createEdgeEnds(opts.participants, opts);
1909
+ const shared = { ...opts, snapRadius: opts.snapRadius ?? DEFAULT_SNAP2, bodies: opts.bodies ?? true };
1910
+ const targetsFor = (moving) => createTargets({
1911
+ ...shared,
1912
+ canConnect: moving.end === "to" ? (fixed, candidate) => canConnect(fixed, candidate, moving) : (fixed, candidate) => canConnect(candidate, fixed, moving)
1913
+ });
1914
+ return {
1915
+ id: opts.id ?? RECONNECT_ACTION_ID,
1916
+ label: "Reconnect",
1917
+ group: "diagram",
1918
+ requires: ["scene", "view"],
1919
+ invoker: {
1920
+ timing: "ongoing",
1921
+ start(ctx) {
1922
+ const pressed = edgeEndScratchOf(ctx.drag?.affordance);
1923
+ const ends = pressed === null ? null : endsOf(pressed.edgeId);
1924
+ if (pressed === null || ends === null) return {};
1925
+ const scratch = {
1926
+ edgeId: pressed.edgeId,
1927
+ moving: pressed.end,
1928
+ fixed: fixedEndOf(ends, pressed.end, opts),
1929
+ ends,
1930
+ route: routerOf(ends.trait, opts.routers),
1931
+ current: { x: ctx.world.x, y: ctx.world.y },
1932
+ open: true,
1933
+ deps: ctx.deps
1934
+ };
1935
+ const resolver = targetsFor({ edge: scratch.edgeId, end: scratch.moving });
1936
+ return {
1937
+ kind: "diagram-reconnect",
1938
+ onMove(moveCtx) {
1939
+ scratch.current = { x: moveCtx.world.x, y: moveCtx.world.y };
1940
+ },
1941
+ overlay() {
1942
+ if (!scratch.open) return null;
1943
+ const points = resolver.preview(scratch.route, scratch.fixed, scratch.current, scratch.deps.view, scratch.moving, scratch.ends.trait.waypoints);
1944
+ return points.length < 2 ? null : { kind: "polyline", points, role: "connector" };
1945
+ },
1946
+ onEnd(endCtx, reason) {
1947
+ scratch.open = false;
1948
+ if (reason === "cancel") return;
1949
+ const target = resolver.target(scratch.fixed, { x: endCtx.world.x, y: endCtx.world.y }, scratch.deps.view);
1950
+ if (target === null || sameEnd(scratch.ends, scratch.moving, target)) return;
1951
+ commit({ edgeId: scratch.edgeId, end: scratch.moving, target }, { ...endCtx, deps: scratch.deps });
1952
+ }
1953
+ };
1954
+ }
1955
+ },
1956
+ enabled: () => true
1957
+ };
1958
+ }
1959
+ function reconnectBinding(actionId = RECONNECT_ACTION_ID) {
1960
+ return { spec: { kind: "drag", target: `affordance:${EDGE_END_AFFORDANCE_KIND}` }, actionId };
1961
+ }
1962
+ function diagramEdgeEnds(opts) {
1963
+ const actionId = opts.id ?? RECONNECT_ACTION_ID;
1964
+ return {
1965
+ // `opts.id` names the action; the layer keeps its own id.
1966
+ layer: edgeEndLayer(opts.participants, opts.selected, { ...opts, id: EDGE_END_LAYER_ID }),
1967
+ contribution: {
1968
+ id: "weasel-diagram-edge-ends",
1969
+ eligibility: { claimed: true },
1970
+ actions: [createReconnectAction({ ...opts, id: actionId }), grabPortAction(GRAB_EDGE_END_ACTION_ID)],
1971
+ bindings: [
1972
+ reconnectBinding(actionId),
1973
+ ...grabPortBindings(GRAB_EDGE_END_ACTION_ID, EDGE_END_AFFORDANCE_KIND)
1974
+ ]
1975
+ }
1976
+ };
1977
+ }
1978
+ function packed(items, spans, order) {
1979
+ const gaps = items.slice(1).map((m, i) => spans.get(m).start - (spans.get(items[i]).start + spans.get(items[i]).size));
1980
+ const out = /* @__PURE__ */ new Map();
1981
+ let at = items.length > 0 ? spans.get(items[0]).start : 0;
1982
+ order.forEach((m, i) => {
1983
+ out.set(m, at);
1984
+ at += spans.get(m).size + (gaps[i] ?? gaps[gaps.length - 1] ?? 0);
1985
+ });
1986
+ return out;
1987
+ }
1988
+ var carriedBy = (item) => item.carries ?? [item.id];
1989
+ var RELEASE_MS = 500;
1990
+ function slideOthers(scene, slots, carried) {
1991
+ const { axis } = slots;
1992
+ const size = axis === "x" ? "width" : "height";
1993
+ const ids = slots.items.map((i) => i.id);
1994
+ const spans = new Map(slots.items.map((i) => [i.id, { start: i.bounds[axis], size: i.bounds[size] }]));
1995
+ const nodes = slots.items.map((item) => ({
1996
+ id: item.id,
1997
+ poses: carriedBy(item).flatMap((id) => {
1998
+ const node = scene.get(id);
1999
+ return node === void 0 || node.derivePose !== void 0 ? [] : [{ id, pose: effectivePose(scene, node) }];
2000
+ })
2001
+ }));
2002
+ const hold = scene.overrides.hold();
2003
+ return {
2004
+ /** The items standing in `order`, or as they were for `null`, with the carried one `by` along. */
2005
+ show(order, by) {
2006
+ const starts = packed(ids, spans, order ?? ids);
2007
+ hold.set(nodes.flatMap(({ id, poses }) => {
2008
+ const d = id === carried ? by : starts.get(id) - spans.get(id).start;
2009
+ return poses.map(({ id: node, pose }) => [node, { ...pose, [axis]: pose[axis] + d }]);
2010
+ }));
2011
+ },
2012
+ drop: () => hold.drop(),
2013
+ /** Held until the host's new specs land, so the items do not jump back for
2014
+ * the frames in between; a host that keeps the old order gets it back shortly after. */
2015
+ keep() {
2016
+ const release = () => {
2017
+ stop();
2018
+ clearTimeout(timer);
2019
+ hold.drop();
2020
+ };
2021
+ const stop = scene.subscribe(release);
2022
+ const timer = setTimeout(release, RELEASE_MS);
2023
+ }
2024
+ };
2025
+ }
2026
+
2027
+ // src/reorder.ts
2028
+ var REORDER_ACTION_ID = "diagram.reorder";
2029
+ var END_GAP = 24;
2030
+ var inside = (p, b) => p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height;
2031
+ function sequenceSlots(axis, items) {
2032
+ const [along2, across, size, depth] = axis === "x" ? ["x", "y", "width", "height"] : ["y", "x", "height", "width"];
2033
+ const lo = (b) => b[along2];
2034
+ const hi = (b) => b[along2] + b[size];
2035
+ const bar = (at, boxes2) => {
2036
+ const from = Math.min(...boxes2.map((b) => b[across]));
2037
+ const to = Math.max(...boxes2.map((b) => b[across] + b[depth]));
2038
+ const point = (c) => axis === "x" ? { x: at, y: c } : { x: c, y: at };
2039
+ return { a: point(from), b: point(to) };
2040
+ };
2041
+ const boxes = items.map((i) => i.bounds);
2042
+ const halves = boxes.slice(1).map((b, k) => (lo(b) - hi(boxes[k])) / 2);
2043
+ const gaps = [];
2044
+ if (boxes.length === 0) return { axis, items, gaps };
2045
+ gaps.push({ index: 0, ...bar(lo(boxes[0]) - (halves[0] ?? END_GAP / 2), [boxes[0]]) });
2046
+ for (let k = 1; k < boxes.length; k++) gaps.push({ index: k, ...bar(hi(boxes[k - 1]) + halves[k - 1], [boxes[k - 1], boxes[k]]) });
2047
+ const last = boxes[boxes.length - 1];
2048
+ gaps.push({ index: boxes.length, ...bar(hi(last) + (halves[halves.length - 1] ?? END_GAP / 2), [last]) });
2049
+ return { axis, items, gaps };
2050
+ }
2051
+ function pickAt(slots, at) {
2052
+ const from = slots?.items.findIndex((i) => i.grip !== null && inside(at, i.grip ?? i.bounds)) ?? -1;
2053
+ return from < 0 ? null : { from, item: slots.items[from] };
2054
+ }
2055
+ function gapAt(held, by) {
2056
+ const { axis, gaps } = held.slots;
2057
+ const size = axis === "x" ? "width" : "height";
2058
+ const mid = held.item.bounds[axis] + held.item.bounds[size] / 2 + by;
2059
+ let best = null;
2060
+ for (const g of gaps) if (best === null || Math.abs(g.a[axis] - mid) < Math.abs(best.a[axis] - mid)) best = g;
2061
+ return best === null || best.index === held.from || best.index === held.from + 1 ? null : best;
2062
+ }
2063
+ function moveOf2(held, gap) {
2064
+ const order = held.slots.items.map((i) => i.id);
2065
+ order.splice(held.from, 1);
2066
+ order.splice(gap.index > held.from ? gap.index - 1 : gap.index, 0, held.item.id);
2067
+ return { id: held.item.id, from: held.from, to: gap.index, order };
2068
+ }
2069
+ function createReorderAction(opts) {
2070
+ const { canDrop = () => true, show = "bar" } = opts;
2071
+ return {
2072
+ id: opts.id ?? REORDER_ACTION_ID,
2073
+ label: opts.label ?? "Reorder",
2074
+ group: "diagram",
2075
+ requires: ["scene", ...opts.requires ?? []],
2076
+ cursor: "grab",
2077
+ activeCursor: "grabbing",
2078
+ enabled: (deps, at) => deps === void 0 || at === void 0 || pickAt(opts.slots(deps, at), at) !== null ? true : ActionDisabledReason.NotApplicable,
2079
+ invoker: {
2080
+ timing: "ongoing",
2081
+ start(ctx) {
2082
+ const start = { x: (ctx.drag?.start ?? ctx.world).x, y: (ctx.drag?.start ?? ctx.world).y };
2083
+ const slots = opts.slots(ctx.deps, start);
2084
+ const picked = pickAt(slots, start);
2085
+ if (slots === null || picked === null) return {};
2086
+ const held = { slots, ...picked };
2087
+ const { axis } = slots;
2088
+ let by = ctx.world[axis] - start[axis];
2089
+ let open = true;
2090
+ const verdicts = /* @__PURE__ */ new Map();
2091
+ const landing = (gap) => {
2092
+ if (gap === null) return null;
2093
+ if (!verdicts.has(gap.index)) {
2094
+ const move = moveOf2(held, gap);
2095
+ verdicts.set(gap.index, canDrop(move) ? move : null);
2096
+ }
2097
+ return verdicts.get(gap.index);
2098
+ };
2099
+ const finish = (endCtx, reason) => {
2100
+ open = false;
2101
+ by = endCtx.world[axis] - start[axis];
2102
+ const move = reason === "commit" ? landing(gapAt(held, by)) : null;
2103
+ if (move !== null) opts.commit(move, { ...endCtx, deps: ctx.deps });
2104
+ return move;
2105
+ };
2106
+ if (show === "slide") {
2107
+ const slide = ctx.deps.scene === void 0 ? null : slideOthers(ctx.deps.scene, held.slots, held.item.id);
2108
+ return {
2109
+ kind: "diagram-reorder",
2110
+ onMove(moveCtx) {
2111
+ by = moveCtx.world[axis] - start[axis];
2112
+ slide?.show(landing(gapAt(held, by))?.order ?? null, by);
2113
+ },
2114
+ onEnd(endCtx, reason) {
2115
+ const move = finish(endCtx, reason);
2116
+ if (move === null) slide?.drop();
2117
+ else slide?.keep();
2118
+ }
2119
+ };
2120
+ }
2121
+ const carried = carriedBy(held.item);
2122
+ const poses = /* @__PURE__ */ new Map();
2123
+ for (const id of carried) {
2124
+ const pose = ctx.deps.scene?.get(id)?.pose;
2125
+ if (pose !== void 0) poses.set(id, pose);
2126
+ }
2127
+ return {
2128
+ kind: "diagram-reorder",
2129
+ onMove(moveCtx) {
2130
+ by = moveCtx.world[axis] - start[axis];
2131
+ },
2132
+ previewIds: () => open && poses.size > 0 ? [...poses.keys()] : null,
2133
+ previewPose: (id) => {
2134
+ const pose = poses.get(id);
2135
+ return pose === void 0 ? null : { ...pose, [axis]: pose[axis] + by };
2136
+ },
2137
+ previewOpaqueIds: () => [...carried],
2138
+ overlay() {
2139
+ const gap = open ? gapAt(held, by) : null;
2140
+ if (gap === null) return null;
2141
+ return { kind: "polyline", points: [gap.a, gap.b], role: landing(gap) !== null ? "insert" : "refused" };
2142
+ },
2143
+ onEnd(endCtx, reason) {
2144
+ finish(endCtx, reason);
2145
+ }
2146
+ };
2147
+ }
2148
+ }
2149
+ };
2150
+ }
2151
+ var ANYWHERE = bodyPredicate(() => true);
2152
+ function reorderBinding(actionId = REORDER_ACTION_ID, on = ANYWHERE) {
2153
+ return { spec: { kind: "drag", target: { kindOf: on } }, actionId };
2154
+ }
2155
+ function reorderContribution(opts) {
2156
+ const actionId = opts.id ?? REORDER_ACTION_ID;
2157
+ return {
2158
+ id: `weasel-diagram-reorder:${actionId}`,
2159
+ eligibility: { always: true },
2160
+ actions: [createReorderAction({ ...opts, id: actionId })],
2161
+ bindings: [reorderBinding(actionId, opts.on)]
2162
+ };
2163
+ }
2164
+ function worldRectOf(scene, id) {
2165
+ const node = scene.get(id);
2166
+ if (node === void 0) return null;
2167
+ const drawn = drawnBoundsScene(scene);
2168
+ const route = diagramEdgeOf(node) === null ? null : drawn.derivedPathOf?.(id) ?? null;
2169
+ const mid = route === null ? null : pointAlongPath(route, 0.5);
2170
+ if (mid != null) return { x: mid.point.x, y: mid.point.y, width: 0, height: 0 };
2171
+ return drawnAABBOf(drawn, id, effectivePose(scene, node), { includeStroke: false });
2172
+ }
2173
+ function screenRectOf(rect, view) {
2174
+ return {
2175
+ x: (rect.x - view.x) * view.scale.x,
2176
+ y: (rect.y - view.y) * view.scale.y,
2177
+ width: rect.width * view.scale.x,
2178
+ height: rect.height * view.scale.y
2179
+ };
2180
+ }
2181
+ var CENTER = "center";
2182
+ function place(el, overlay, anchor, width, height) {
2183
+ const size = { width: el.offsetWidth, height: el.offsetHeight };
2184
+ const placement = overlay.placement ?? CENTER;
2185
+ const at = placement === CENTER ? { x: anchor.x + (anchor.width - size.width) / 2, y: anchor.y + (anchor.height - size.height) / 2 } : placeRect({
2186
+ anchor,
2187
+ overlay: size,
2188
+ boundary: { x: 0, y: 0, width, height },
2189
+ placement,
2190
+ offset: overlay.offset ?? 0
2191
+ }).rect;
2192
+ el.style.transform = `translate(${Math.round(at.x)}px, ${Math.round(at.y)}px)`;
2193
+ }
2194
+ function useOverlayPlacement(canvasRef, scene, overlays) {
2195
+ const elements = useRef(/* @__PURE__ */ new Map());
2196
+ const current = useLatest$1(overlays);
2197
+ const placeAll = useCallback(() => {
2198
+ const canvas = canvasRef.current;
2199
+ const surface = canvas?.element;
2200
+ if (canvas == null || surface == null) return;
2201
+ const view = canvas.getView();
2202
+ for (const overlay of current.current) {
2203
+ const el = elements.current.get(overlay.key ?? overlay.target);
2204
+ if (el === void 0) continue;
2205
+ const world = worldRectOf(scene, overlay.target);
2206
+ el.hidden = world === null;
2207
+ if (world === null) continue;
2208
+ const rect = screenRectOf(world, view);
2209
+ const anchor = { ...rect, x: rect.x + surface.offsetLeft, y: rect.y + surface.offsetTop };
2210
+ place(el, overlay, anchor, surface.offsetWidth, surface.offsetHeight);
2211
+ }
2212
+ }, [canvasRef, scene, current]);
2213
+ useLayoutEffect(() => {
2214
+ placeAll();
2215
+ const canvas = canvasRef.current;
2216
+ if (canvas == null) return;
2217
+ const stop = canvas.subscribeFrame(placeAll);
2218
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(placeAll);
2219
+ for (const el of elements.current.values()) observer?.observe(el);
2220
+ return () => {
2221
+ stop();
2222
+ observer?.disconnect();
2223
+ };
2224
+ }, [canvasRef, placeAll, overlays]);
2225
+ const refs = useRef(/* @__PURE__ */ new Map());
2226
+ return useCallback((key) => {
2227
+ let ref = refs.current.get(key);
2228
+ if (ref === void 0) {
2229
+ ref = (el) => {
2230
+ if (el !== null) {
2231
+ elements.current.set(key, el);
2232
+ return;
2233
+ }
2234
+ elements.current.delete(key);
2235
+ refs.current.delete(key);
2236
+ };
2237
+ refs.current.set(key, ref);
2238
+ }
2239
+ return ref;
2240
+ }, []);
2241
+ }
2242
+
2243
+ // src/reorderModel.ts
2244
+ var depsOf = (node) => Array.isArray(node?.dependsOn) ? node.dependsOn : [];
2245
+ function holders(scene) {
2246
+ const out = /* @__PURE__ */ new Map();
2247
+ for (const node of scene.renderOrderNodes()) {
2248
+ if (diagramGroupOf(node) === null) continue;
2249
+ for (const dep of depsOf(node)) {
2250
+ const member = scene.get(dep);
2251
+ if (member !== void 0 && diagramLabelOf(member) === null) out.set(dep, node.id);
2252
+ }
2253
+ }
2254
+ return out;
2255
+ }
2256
+ function membersOf(scene, group) {
2257
+ return depsOf(scene.get(group)).filter((id) => {
2258
+ const node = scene.get(id);
2259
+ return node !== void 0 && diagramLabelOf(node) === null;
2260
+ });
2261
+ }
2262
+ function reorderTargetOf(scene, id, boxOf) {
2263
+ const held = holders(scene);
2264
+ for (let member = id, group = held.get(id); group !== void 0; member = group, group = held.get(group)) {
2265
+ const axis = diagramGroupOf(scene.get(group))?.reorder;
2266
+ if (axis === void 0) continue;
2267
+ const start = (m) => {
2268
+ const box = boxOf(m);
2269
+ return box === null ? 0 : axis === "x" ? box.x : box.y;
2270
+ };
2271
+ const members2 = membersOf(scene, group).sort((a, b) => start(a) - start(b));
2272
+ return { group, axis, member, members: members2 };
2273
+ }
2274
+ return null;
2275
+ }
2276
+ function carriedWith(scene, member) {
2277
+ const out = /* @__PURE__ */ new Set();
2278
+ const visit = (id) => {
2279
+ if (out.has(id)) return;
2280
+ out.add(id);
2281
+ const node = scene.get(id);
2282
+ if (node !== void 0 && diagramGroupOf(node) !== null) depsOf(node).forEach(visit);
2283
+ scene.childrenOf?.(id).forEach(visit);
2284
+ };
2285
+ visit(member);
2286
+ for (const node of scene.renderOrderNodes()) {
2287
+ const on = depsOf(node)[0];
2288
+ if (on !== void 0 && out.has(on) && diagramAnchorOf(node) !== null) out.add(node.id);
2289
+ }
2290
+ return out;
2291
+ }
2292
+
2293
+ // src/groupReorder.ts
2294
+ var STEP_REORDER_ACTION_ID = "diagram.stepReorder";
2295
+ var sceneOf = (deps) => deps?.scene;
2296
+ function targetAt(deps, id) {
2297
+ const scene = sceneOf(deps);
2298
+ if (scene === void 0 || id == null) return null;
2299
+ return reorderTargetOf(scene, id, (m) => worldRectOf(scene, m));
2300
+ }
2301
+ function groupReorderSlots(deps, at) {
2302
+ const scene = sceneOf(deps);
2303
+ const target = targetAt(deps, deps.nodeAtPoint?.(at));
2304
+ if (scene === void 0 || target === null) return null;
2305
+ return sequenceSlots(target.axis, target.members.map((m) => ({
2306
+ id: m,
2307
+ bounds: worldRectOf(scene, m) ?? { x: 0, y: 0, width: 0, height: 0 },
2308
+ carries: [...carriedWith(scene, m)]
2309
+ })));
2310
+ }
2311
+ var STEPS = [
2312
+ { key: "ArrowUp", axis: "y", step: -1 },
2313
+ { key: "ArrowDown", axis: "y", step: 1 },
2314
+ { key: "ArrowLeft", axis: "x", step: -1 },
2315
+ { key: "ArrowRight", axis: "x", step: 1 }
2316
+ ];
2317
+ function createStepReorderAction(report) {
2318
+ const selectedTarget = (deps) => targetAt(deps, deps?.selection?.get()[0]);
2319
+ return {
2320
+ id: STEP_REORDER_ACTION_ID,
2321
+ label: "Move along",
2322
+ group: "diagram",
2323
+ defaultBinding: STEPS.map(({ key, axis, step }) => ({
2324
+ spec: { kind: "key", key, mods: { alt: true } },
2325
+ opts: { params: { axis, step } }
2326
+ })),
2327
+ requires: ["scene", "selection"],
2328
+ invoker: {
2329
+ timing: "immediate",
2330
+ run(deps, params) {
2331
+ const target = selectedTarget(deps);
2332
+ if (target === null || params?.axis !== target.axis) return;
2333
+ const { members: members2, member } = target;
2334
+ const at = members2.indexOf(member);
2335
+ const to = at + (params.step === -1 ? -1 : 1);
2336
+ if (to < 0 || to >= members2.length) return;
2337
+ const order = [...members2];
2338
+ order.splice(at, 1);
2339
+ order.splice(to, 0, member);
2340
+ report({ group: target.group, order });
2341
+ }
2342
+ },
2343
+ enabled: (deps) => selectedTarget(deps) !== null ? true : ActionDisabledReason.NotApplicable
2344
+ };
2345
+ }
2346
+ var onBody2 = bodyPredicate((target) => target === "selected-body" || target === "unselected-body");
2347
+ function groupReorderContribution(report) {
2348
+ const drag = reorderContribution({
2349
+ slots: groupReorderSlots,
2350
+ requires: ["nodeAtPoint"],
2351
+ show: "slide",
2352
+ on: onBody2,
2353
+ commit: (move, ctx) => {
2354
+ const group = targetAt(ctx.deps, move.id)?.group;
2355
+ if (group !== void 0) report({ group, order: move.order });
2356
+ }
2357
+ });
2358
+ return { ...drag, id: "weasel-diagram-reorder", actions: [...drag.actions ?? [], createStepReorderAction(report)] };
2359
+ }
2360
+
2361
+ // src/labels/specLabels.ts
2362
+ var routeOf = edgeRouter();
2363
+ function inputOf(specs) {
2364
+ const byId = new Map(specs.map((s) => [s.id, s]));
2365
+ const dep = (id) => {
2366
+ const s = byId.get(id);
2367
+ return s === void 0 ? void 0 : { node: { ...s, parent: null }, pose: s.pose, path: null };
2368
+ };
2369
+ const shapes = [];
2370
+ const routes = [];
2371
+ const labels = [];
2372
+ for (const spec of specs) {
2373
+ const id = spec.id;
2374
+ const ends = Array.isArray(spec.dependsOn) ? spec.dependsOn : [];
2375
+ if (diagramEdgeOf(spec) !== null) {
2376
+ const points = routeOf(spec, ends.map(dep))?.points;
2377
+ if (points !== void 0 && points.length >= 2) routes.push({ id, points });
2378
+ continue;
2379
+ }
2380
+ const placement = diagramLabelOf(spec);
2381
+ if (placement !== null) {
2382
+ const { width, height } = spec.pose;
2383
+ if (ends[0] !== void 0) labels.push({ id, edge: ends[0], placement, size: { width, height } });
2384
+ continue;
2385
+ }
2386
+ const node = { id, kind: spec.kind, data: spec.data };
2387
+ if (spec.derivePose !== void 0 || diagramContainerOf(node) !== null || diagramGroupOf(node) !== null || diagramNodeOf(node) === null) continue;
2388
+ shapes.push({ id, box: spec.pose });
2389
+ }
2390
+ return { shapes, routes, labels };
2391
+ }
2392
+ var keyOf = (input) => JSON.stringify([
2393
+ input.shapes.map((s) => [s.id, s.box.x, s.box.y, s.box.width, s.box.height]),
2394
+ input.routes.map((r) => [r.id, r.points.flatMap((p) => [p.x, p.y])]),
2395
+ input.labels.map((l) => [l.id, l.edge, l.placement.at, l.placement.offset, l.placement.dodge, l.size.width, l.size.height])
2396
+ ]);
2397
+ function createSpecLabelPlacer(opts = {}) {
2398
+ let last = null;
2399
+ return (specs) => {
2400
+ const input = inputOf(specs);
2401
+ if (input.labels.length === 0) return [...specs];
2402
+ const key = keyOf(input);
2403
+ if (last?.key !== key) last = { key, placed: placeLabels(input, opts) };
2404
+ const placed = last.placed;
2405
+ return specs.map((spec) => {
2406
+ const spot = placed.get(spec.id);
2407
+ const label = diagramLabelOf(spec);
2408
+ if (spot === void 0 || label === null) return spec;
2409
+ const { placed: _p, ...preferred } = label;
2410
+ const next = spot === null ? preferred : { ...preferred, placed: spot };
2411
+ return { ...spec, data: { ...spec.data, diagram: { ...spec.data.diagram, label: next } } };
2412
+ });
2413
+ };
2414
+ }
2415
+
2416
+ // src/resizable.ts
2417
+ function diagramResizable(scene) {
2418
+ return (id) => {
2419
+ const node = scene.get(id);
2420
+ return node === void 0 || diagramEdgeOf(node) === null && diagramLabelOf(node) === null && diagramGroupOf(node) === null && diagramContainerOf(node)?.role !== "lane";
2421
+ };
2422
+ }
2423
+
2424
+ // src/routeWrites.ts
2425
+ var ROUTE_TOLERANCE = 1e-3;
2426
+ function routesOf(graph, plan) {
2427
+ return {
2428
+ edges: new Set(graph.edges.map((e) => e.origin?.id ?? e.id)),
2429
+ waypoints: plan.waypoints ?? /* @__PURE__ */ new Map(),
2430
+ ...plan.labels !== void 0 ? { labels: plan.labels } : {},
2431
+ ...plan.ports !== void 0 ? { ports: plan.ports } : {}
2432
+ };
2433
+ }
2434
+ function routeWrites(scene, routes) {
2435
+ const out = /* @__PURE__ */ new Map();
2436
+ const owned = /* @__PURE__ */ new Set();
2437
+ for (const id of routes.edges) {
2438
+ const node = scene.get(id);
2439
+ const edge = node === void 0 ? null : diagramEdgeOf(node);
2440
+ if (node === void 0 || edge === null || scene.isLocked(id)) {
2441
+ if (edge !== null && layoutWritesRoute(edge)) owned.add(id);
2442
+ continue;
2443
+ }
2444
+ let next = edge;
2445
+ if (layoutWritesRoute(edge)) {
2446
+ owned.add(id);
2447
+ next = rerouted(edge, routes.waypoints.get(id));
2448
+ }
2449
+ const ports = routes.ports?.get(id);
2450
+ const from = renamed(edge.from, ports?.from);
2451
+ const to = renamed(edge.to, ports?.to);
2452
+ if (from !== edge.from || to !== edge.to) next = { ...next, from, to };
2453
+ if (next !== edge) out.set(id, withTrait(node.data, next));
2454
+ }
2455
+ const stationed = /* @__PURE__ */ new Set();
2456
+ for (const node of scene.renderOrderNodes()) {
2457
+ const label = diagramLabelOf(node);
2458
+ const deps = node.dependsOn;
2459
+ if (label === null || !Array.isArray(deps) || deps.length !== 1 || !owned.has(deps[0])) continue;
2460
+ if (!labelFollowsLayout(label.at) || stationed.has(deps[0])) continue;
2461
+ stationed.add(deps[0]);
2462
+ const near2 = routes.labels?.get(deps[0]);
2463
+ const at = near2 === void 0 ? "mid" : { near: near2 };
2464
+ if (sameAt(label.at, at) || scene.isLocked(node.id)) continue;
2465
+ out.set(node.id, withTrait(node.data, { ...label, at }, "label"));
2466
+ }
2467
+ return out;
2468
+ }
2469
+ function rerouted(edge, route) {
2470
+ if (route === void 0 || route.length === 0) {
2471
+ if ((edge.waypoints ?? []).length === 0 && edge.laidOut === void 0) return edge;
2472
+ const { waypoints: _w, laidOut: _l, ...rest } = edge;
2473
+ return rest;
2474
+ }
2475
+ if (edge.waypoints !== void 0 && near(edge.waypoints, route)) return edge;
2476
+ return { ...edge, waypoints: route, laidOut: route };
2477
+ }
2478
+ function renamed(end, port) {
2479
+ if (!layoutOwnsPort(end) || end.port === port && (port === void 0 || end.laidOut === port)) return end;
2480
+ const { port: _p, laidOut: _l, ...rest } = end;
2481
+ return port === void 0 ? rest : { ...rest, port, laidOut: port };
2482
+ }
2483
+ function near(a, b) {
2484
+ return a.length === b.length && a.every((p, i) => Math.abs(p.x - b[i].x) <= ROUTE_TOLERANCE && Math.abs(p.y - b[i].y) <= ROUTE_TOLERANCE);
2485
+ }
2486
+ function sameAt(a, b) {
2487
+ if (typeof a === "object" && typeof b === "object") return near([a.near], [b.near]);
2488
+ return a === b;
2489
+ }
2490
+ function withTrait(data, value, key) {
2491
+ const own = data;
2492
+ const diagram = key === void 0 ? value : { ...own.diagram, [key]: value };
2493
+ return { ...own, diagram };
2494
+ }
2495
+ var LAYOUTS = Object.freeze({
2496
+ layered,
2497
+ tree,
2498
+ force,
2499
+ chain
2500
+ });
2501
+ function applyLayout(scene, result, opts = {}) {
2502
+ const poses = layoutPoses(scene, result, opts.geometry);
2503
+ const data = opts.routes === void 0 ? /* @__PURE__ */ new Map() : routeWrites(scene, opts.routes);
2504
+ if (poses.size === 0 && data.size === 0) return 0;
2505
+ const open = [...poses].filter(([id]) => !scene.isLocked(id));
2506
+ if (open.length === 0 && data.size === 0) return 0;
2507
+ scene.batch(opts.label ?? "Layout", () => {
2508
+ for (const [id, pose] of open) scene.setPose(id, pose);
2509
+ for (const [id, d] of data) scene.update(id, { data: d });
2510
+ });
2511
+ const wrote = new Set(open.map(([id]) => id));
2512
+ let moved = 0;
2513
+ for (const id of result.keys()) if (wrote.has(id)) moved++;
2514
+ return moved;
2515
+ }
2516
+ function layoutPoses(scene, result, geometry = AUTO_POSE_DESCRIPTOR) {
2517
+ const out = /* @__PURE__ */ new Map();
2518
+ for (const [id, at] of result) {
2519
+ const node = scene.get(id);
2520
+ if (node === void 0) continue;
2521
+ const bounds = geometry.getBounds(node.pose);
2522
+ const dx = at.x - bounds.x;
2523
+ const dy = at.y - bounds.y;
2524
+ if (dx === 0 && dy === 0) continue;
2525
+ out.set(id, translatePoseViaDescriptor(node.pose, dx, dy, geometry));
2526
+ for (const descendant of subtreeOf(scene, id)) {
2527
+ const child = scene.get(descendant);
2528
+ if (child === void 0) continue;
2529
+ out.set(descendant, translatePoseViaDescriptor(child.pose, dx, dy, geometry));
2530
+ }
2531
+ }
2532
+ return out;
2533
+ }
2534
+ function subtreeOf(scene, id) {
2535
+ const out = [];
2536
+ const stack = [...scene.childrenOf(id)];
2537
+ while (stack.length > 0) {
2538
+ const next = stack.pop();
2539
+ out.push(next);
2540
+ stack.push(...scene.childrenOf(next));
2541
+ }
2542
+ return out;
2543
+ }
2544
+ var LAYOUT_ACTION_ID = "diagram.layout";
2545
+ function createLayoutAction(opts) {
2546
+ const algorithm = typeof opts.algorithm === "function" ? opts.algorithm : LAYOUTS[opts.algorithm ?? "layered"] ?? layered;
2547
+ const build = {
2548
+ ...opts.read ? { read: opts.read } : {},
2549
+ ...opts.geometry ? { geometry: opts.geometry } : {}
2550
+ };
2551
+ return {
2552
+ id: opts.id ?? LAYOUT_ACTION_ID,
2553
+ label: opts.label ?? "Lay out diagram",
2554
+ group: opts.group ?? "diagram",
2555
+ requires: ["scene"],
2556
+ invoker: {
2557
+ timing: "immediate",
2558
+ run: (deps) => {
2559
+ const scene = deps["scene"];
2560
+ if (scene === void 0) return;
2561
+ const graph = buildGraph(opts.source, build);
2562
+ const plan = planOf(algorithm, graph, opts.layout);
2563
+ applyLayout(scene, plan.moves, {
2564
+ ...opts.geometry ? { geometry: opts.geometry } : {},
2565
+ ...opts.label ? { label: opts.label } : {},
2566
+ routes: routesOf(graph, plan)
2567
+ });
2568
+ }
2569
+ },
2570
+ enabled: () => true
2571
+ };
2572
+ }
2573
+ var EASE_FRAMES = 24;
2574
+ var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
2575
+ function progress(frame, frames, easing) {
2576
+ const t = Math.min((frame + 1) / frames, 1);
2577
+ return { t, u: easing(t) };
2578
+ }
2579
+ var REST_DISTANCE = 0.1;
2580
+ var REST_TICKS = 5;
2581
+ function forceProducer(graph, opts = {}) {
2582
+ const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
2583
+ const relaxation = forceRelaxation(graph, opts);
2584
+ const dragAlpha = opts.dragAlpha ?? 0.3;
2585
+ const restDistance = opts.restDistance ?? REST_DISTANCE;
2586
+ const restTicks = opts.restTicks ?? REST_TICKS;
2587
+ const was = relaxation.bodies.map((b) => ({ x: b.x, y: b.y }));
2588
+ let still = 0;
2589
+ return ({ pinned }) => {
2590
+ for (const node of graph.nodes) {
2591
+ const body = relaxation.byId.get(node.id);
2592
+ if (body === void 0) continue;
2593
+ const held = pinned.get(node.id);
2594
+ if (held !== void 0) {
2595
+ const bounds = geometry.getBounds(held);
2596
+ body.fx = center(bounds, "x");
2597
+ body.fy = center(bounds, "y");
2598
+ } else if (!relaxation.pinned.has(node.id)) {
2599
+ delete body.fx;
2600
+ delete body.fy;
2601
+ }
2602
+ }
2603
+ relaxation.sim.alphaTarget(pinned.size > 0 ? dragAlpha : 0);
2604
+ relaxation.sim.tick();
2605
+ let moved = 0;
2606
+ relaxation.bodies.forEach((body, i) => {
2607
+ const at = was[i];
2608
+ moved = Math.max(moved, Math.hypot(body.x - at.x, body.y - at.y));
2609
+ at.x = body.x;
2610
+ at.y = body.y;
2611
+ });
2612
+ still = pinned.size === 0 && moved < restDistance ? still + 1 : 0;
2613
+ const done = still >= restTicks || relaxation.sim.isSettled();
2614
+ return { result: relaxation.placed(), done };
2615
+ };
2616
+ }
2617
+ function easedProducer(graph, algorithm, opts = {}) {
2618
+ const frames = opts.frames ?? EASE_FRAMES;
2619
+ const easing = opts.easing ?? easeInOut;
2620
+ const target = algorithm(graph, opts.layout);
2621
+ const from = /* @__PURE__ */ new Map();
2622
+ for (const node of graph.nodes) from.set(node.id, { x: node.bounds.x, y: node.bounds.y });
2623
+ return ({ pinned, frame }) => {
2624
+ const { t, u } = progress(frame, frames, easing);
2625
+ const result = /* @__PURE__ */ new Map();
2626
+ for (const [id, to] of target) {
2627
+ if (pinned.has(id)) continue;
2628
+ const start = from.get(id);
2629
+ if (start === void 0) continue;
2630
+ result.set(id, {
2631
+ x: start.x + (to.x - start.x) * u,
2632
+ y: start.y + (to.y - start.y) * u
2633
+ });
2634
+ }
2635
+ return { result, done: t >= 1 };
2636
+ };
2637
+ }
2638
+ function shapeOf(graph) {
2639
+ const nodes = graph.nodes.map((n) => n.id).sort().join(",");
2640
+ const edges = graph.edges.map((e) => `${e.from}>${e.to}`).sort().join(",");
2641
+ return `${nodes}|${edges}`;
2642
+ }
2643
+ function sameGraph(a, b) {
2644
+ if (shapeOf(a) !== shapeOf(b)) return false;
2645
+ return a.nodes.every((n) => {
2646
+ const m = b.node(n.id)?.bounds;
2647
+ const k = n.bounds;
2648
+ return m !== void 0 && m.x === k.x && m.y === k.y && m.width === k.width && m.height === k.height;
2649
+ });
2650
+ }
2651
+ function usePendingPlan() {
2652
+ const generation = useRef(0);
2653
+ const waiting = useRef(false);
2654
+ const pending = useMemo(() => ({
2655
+ request(answer, ready, failed) {
2656
+ const mine = ++generation.current;
2657
+ waiting.current = true;
2658
+ const current = () => generation.current === mine;
2659
+ void answer.then(
2660
+ (plan) => {
2661
+ if (!current()) return;
2662
+ waiting.current = false;
2663
+ ready(plan);
2664
+ },
2665
+ (error) => {
2666
+ if (!current()) return;
2667
+ waiting.current = false;
2668
+ failed(error);
2669
+ }
2670
+ );
2671
+ },
2672
+ drop() {
2673
+ generation.current++;
2674
+ waiting.current = false;
2675
+ },
2676
+ isWaiting: () => waiting.current
2677
+ }), []);
2678
+ useEffect(() => () => pending.drop(), [pending]);
2679
+ return pending;
2680
+ }
2681
+ var lerp = (a, b, u) => ({ x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u });
2682
+ function along(points, s) {
2683
+ if (points.length === 1) return points[0];
2684
+ const lengths = points.slice(1).map((p, i) => Math.hypot(p.x - points[i].x, p.y - points[i].y));
2685
+ const total = lengths.reduce((a, b) => a + b, 0);
2686
+ let left = s * total;
2687
+ for (const [i, len] of lengths.entries()) {
2688
+ if (left <= len || i === lengths.length - 1) return lerp(points[i], points[i + 1], len === 0 ? 0 : Math.min(left / len, 1));
2689
+ left -= len;
2690
+ }
2691
+ return points[points.length - 1];
2692
+ }
2693
+ var straightRun = (count, start, end) => Array.from({ length: count }, (_, k) => lerp(start, end, (k + 1) / (count + 1)));
2694
+ function respread(points, count, start, end) {
2695
+ if (points.length === count) return [...points];
2696
+ if (points.length === 0) return straightRun(count, start, end);
2697
+ return Array.from({ length: count }, (_, k) => along(points, count === 1 ? 0.5 : k / (count - 1)));
2698
+ }
2699
+ function routeEasings(scene, graph, routes) {
2700
+ const centerOf = (id) => {
2701
+ const ids = graph.node(id) !== void 0 ? [id] : graph.groups?.find((g) => g.id === id)?.members ?? [];
2702
+ const boxes = ids.flatMap((m) => graph.node(m)?.bounds ?? []);
2703
+ if (boxes.length === 0) return null;
2704
+ const x0 = Math.min(...boxes.map((b) => b.x));
2705
+ const y0 = Math.min(...boxes.map((b) => b.y));
2706
+ const x1 = Math.max(...boxes.map((b) => b.x + b.width));
2707
+ const y1 = Math.max(...boxes.map((b) => b.y + b.height));
2708
+ return { x: (x0 + x1) / 2, y: (y0 + y1) / 2 };
2709
+ };
2710
+ const writes = routeWrites(scene, routes);
2711
+ const out = [];
2712
+ for (const id of routes.edges) {
2713
+ const node = scene.get(id);
2714
+ const edge = node === void 0 ? null : diagramEdgeOf(node);
2715
+ const written = writes.get(id);
2716
+ if (node === void 0 || edge === null || written === void 0) continue;
2717
+ const next = diagramEdgeOf({ data: written });
2718
+ if (next === null) continue;
2719
+ const ends = { from: next.from, to: next.to };
2720
+ const was = edge.waypoints ?? [];
2721
+ if (!layoutWritesRoute(edge)) {
2722
+ out.push({ id, from: was, to: was, ends });
2723
+ continue;
2724
+ }
2725
+ const after = routes.waypoints.get(id) ?? [];
2726
+ if (after.length === 0) {
2727
+ out.push({ id, from: was, to: null, ends });
2728
+ continue;
2729
+ }
2730
+ const deps = Array.isArray(node.dependsOn) ? node.dependsOn : [];
2731
+ const [a, b] = deps.length === 2 ? [centerOf(deps[0]), centerOf(deps[1])] : [null, null];
2732
+ if (a === null || b === null) continue;
2733
+ const count = Math.max(was.length, after.length);
2734
+ out.push({ id, from: respread(was, count, a, b), to: respread(after, count, a, b), ends });
2735
+ }
2736
+ return out;
2737
+ }
2738
+ function easedPaths(scene, easings, u, poseOf, geometry = AUTO_POSE_DESCRIPTOR) {
2739
+ const depOf = frameDeps(scene, poseOf);
2740
+ const centerOf = (dep) => {
2741
+ const b = geometry.getBounds(dep.pose);
2742
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
2743
+ };
2744
+ const out = /* @__PURE__ */ new Map();
2745
+ for (const { id, from, to, ends } of easings) {
2746
+ const node = scene.get(id);
2747
+ if (node?.derivePath === void 0 || !Array.isArray(node.dependsOn)) continue;
2748
+ const deps = node.dependsOn.map(depOf);
2749
+ let target = to;
2750
+ if (target === null) {
2751
+ const [a, b] = deps;
2752
+ target = u >= 1 || a === void 0 || b === void 0 ? [] : straightRun(from.length, centerOf(a), centerOf(b));
2753
+ }
2754
+ const waypoints = target.length === 0 && u >= 1 ? [] : from.map((p, k) => lerp(p, target[k], u));
2755
+ const data = node.data;
2756
+ const diagram = { ...data.diagram, ...ends, waypoints };
2757
+ const path = node.derivePath({ ...node, data: { ...data, diagram } }, deps);
2758
+ if (path !== null) out.set(id, path);
2759
+ }
2760
+ return out;
2761
+ }
2762
+ function frameDeps(scene, poseOf) {
2763
+ const memo = /* @__PURE__ */ new Map();
2764
+ const depOf = (id) => {
2765
+ if (memo.has(id)) return memo.get(id);
2766
+ memo.set(id, derivedDepOf(scene, id));
2767
+ const shown = memo.get(id);
2768
+ if (shown === void 0) return void 0;
2769
+ const pose = poseOf(id) ?? derivedFromFrame(scene, shown, depOf) ?? shown.pose;
2770
+ const dep = { node: shown.node, pose, get path() {
2771
+ return shown.path;
2772
+ } };
2773
+ memo.set(id, dep);
2774
+ return dep;
2775
+ };
2776
+ return depOf;
2777
+ }
2778
+ function derivedFromFrame(scene, shown, depOf) {
2779
+ const { node } = shown;
2780
+ if (node.derivePose === void 0 || scene.overrides.read(node.id)?.pose !== void 0) return void 0;
2781
+ const ids = node.dependsOn === "children" ? scene.childrenOf(node.id) : node.dependsOn ?? [];
2782
+ if (ids.length === 0) return void 0;
2783
+ return node.derivePose(node, ids.map(depOf)) ?? void 0;
2784
+ }
2785
+ function pathHold(scene) {
2786
+ const held = /* @__PURE__ */ new Map();
2787
+ const release = (id, entry) => {
2788
+ held.delete(id);
2789
+ if (scene.overrides.get(id) === entry) scene.overrides.clear(id);
2790
+ };
2791
+ return {
2792
+ show(paths) {
2793
+ for (const [id, entry] of held) if (!paths.has(id)) release(id, entry);
2794
+ for (const [id, path] of paths) {
2795
+ const entry = held.get(id);
2796
+ if (entry !== void 0 && scene.overrides.get(id) === entry) {
2797
+ entry.path = path;
2798
+ continue;
2799
+ }
2800
+ const fresh = { path };
2801
+ held.set(id, fresh);
2802
+ scene.overrides.set(id, fresh);
2803
+ }
2804
+ },
2805
+ drop() {
2806
+ for (const [id, entry] of held) release(id, entry);
2807
+ }
2808
+ };
2809
+ }
2810
+ function useEasedRoutes() {
2811
+ const easings = useRef([]);
2812
+ const held = useRef(null);
2813
+ const routes = useMemo(() => ({
2814
+ set(next) {
2815
+ held.current?.hold.drop();
2816
+ easings.current = next;
2817
+ },
2818
+ show(scene, u, poseOf, geometry) {
2819
+ if (u === null) held.current?.hold.drop();
2820
+ if (u === null || easings.current.length === 0) return;
2821
+ if (held.current?.scene !== scene) {
2822
+ held.current?.hold.drop();
2823
+ held.current = { scene, hold: pathHold(scene) };
2824
+ }
2825
+ held.current.hold.show(easedPaths(scene, easings.current, u, poseOf, geometry));
2826
+ },
2827
+ drop() {
2828
+ held.current?.hold.drop();
2829
+ }
2830
+ }), []);
2831
+ useEffect(() => () => routes.drop(), [routes]);
2832
+ return routes;
2833
+ }
2834
+
2835
+ // src/live.ts
2836
+ function graphOf(o) {
2837
+ return buildGraph(o.source, {
2838
+ ...o.read ? { read: o.read } : {},
2839
+ ...o.geometry ? { geometry: o.geometry } : {}
2840
+ });
2841
+ }
2842
+ function planFor(graph, o) {
2843
+ const algorithm = o.algorithm ?? "force";
2844
+ if (algorithm === "force") return null;
2845
+ if (typeof algorithm === "object") return plannerPlan(algorithm, graph, o.layout);
2846
+ return planOf(typeof algorithm === "function" ? algorithm : LAYOUTS[algorithm] ?? layered, graph, o.layout);
2847
+ }
2848
+ function producerFor(graph, plan, o) {
2849
+ if (plan === null) return forceProducer(graph, { ...o.layout, ...o.force, ...o.geometry ? { geometry: o.geometry } : {} });
2850
+ return easedProducer(graph, () => plan.moves, {
2851
+ ...o.frames === void 0 ? {} : { frames: o.frames },
2852
+ ...o.easing ? { easing: o.easing } : {}
2853
+ });
2854
+ }
2855
+ function useLiveLayout(opts) {
2856
+ const optsRef = useLatest(opts);
2857
+ const producer = useRef(null);
2858
+ const producerFrame = useRef(0);
2859
+ const shape = useRef("");
2860
+ const routes = useRef(void 0);
2861
+ const held = useRef(false);
2862
+ const eased = useEasedRoutes();
2863
+ const pending = usePendingPlan();
2864
+ const install = useCallback((graph, plan) => {
2865
+ const o = optsRef.current;
2866
+ producerFrame.current = 0;
2867
+ held.current = false;
2868
+ routes.current = routesOf(graph, plan ?? { });
2869
+ eased.set(routeEasings(o.scene, graph, routes.current));
2870
+ producer.current = producerFor(graph, plan, o);
2871
+ }, [optsRef, eased]);
2872
+ const step = useCallback((ctx) => {
2873
+ const o = optsRef.current;
2874
+ if (producer.current === null) return { poses: [], done: true };
2875
+ if (ctx.pinned.size > 0) held.current = true;
2876
+ const frame = producerFrame.current++;
2877
+ const { result, done } = producer.current({ pinned: ctx.pinned, frame });
2878
+ let placed = result;
2879
+ if (ctx.pinned.size > 0) {
2880
+ const held2 = new Map(result);
2881
+ for (const id of ctx.pinned.keys()) held2.delete(id);
2882
+ placed = held2;
2883
+ }
2884
+ const poses = layoutPoses(o.scene, placed, o.geometry);
2885
+ const { u } = progress(frame, o.frames ?? EASE_FRAMES, o.easing ?? easeInOut);
2886
+ eased.show(o.scene, held.current ? null : u, (id) => poses.get(id) ?? ctx.pinned.get(id), o.geometry);
2887
+ return { poses: [...poses], done };
2888
+ }, [optsRef, eased]);
2889
+ const run = usePoseRun({
2890
+ scene: opts.scene,
2891
+ step,
2892
+ // Eased routes go once the routes are in the document, so no frame shows the old ones.
2893
+ onCommit: (moved) => {
2894
+ eased.drop();
2895
+ optsRef.current.onSettle?.(moved);
2896
+ },
2897
+ // Routes match where the layout was going, so only a run that got there
2898
+ // with nothing held along the way writes them.
2899
+ commitWith: (finished) => {
2900
+ if (!finished || held.current || routes.current === void 0) return;
2901
+ const scene = optsRef.current.scene;
2902
+ for (const [id, data] of routeWrites(scene, routes.current)) scene.update(id, { data });
2903
+ },
2904
+ ...opts.label ? { label: opts.label } : {},
2905
+ ...opts.requestFrame ? { requestFrame: opts.requestFrame } : {},
2906
+ ...opts.cancelFrame ? { cancelFrame: opts.cancelFrame } : {}
2907
+ });
2908
+ const build = useCallback(() => {
2909
+ const graph = graphOf(optsRef.current);
2910
+ shape.current = shapeOf(graph);
2911
+ const plan = planFor(graph, optsRef.current);
2912
+ if (plan === null || !isPromise(plan)) {
2913
+ install(graph, plan);
2914
+ return true;
2915
+ }
2916
+ pending.request(plan, (answer) => {
2917
+ if (!sameGraph(graph, graphOf(optsRef.current))) return;
2918
+ install(graph, answer);
2919
+ run.start();
2920
+ }, (error) => {
2921
+ const onError = optsRef.current.onError;
2922
+ if (onError) onError(error);
2923
+ else console.error(error);
2924
+ });
2925
+ return false;
2926
+ }, [optsRef, install, pending, run]);
2927
+ const restart = useCallback(() => {
2928
+ pending.drop();
2929
+ run.cancel();
2930
+ if (build()) run.start();
2931
+ }, [pending, run, build]);
2932
+ useEffect(() => {
2933
+ if (opts.reheatOnGraphChange === false) return;
2934
+ return opts.scene.subscribe(() => {
2935
+ if (!run.isRunning() && !pending.isWaiting()) return;
2936
+ if (shapeOf(graphOf(optsRef.current)) === shape.current) return;
2937
+ if (typeof optsRef.current.algorithm === "object") restart();
2938
+ else build();
2939
+ });
2940
+ }, [opts.scene, opts.reheatOnGraphChange, run, build, restart, pending, optsRef]);
2941
+ return useMemo(() => ({
2942
+ start() {
2943
+ if (run.isRunning() || pending.isWaiting()) return;
2944
+ if (build()) run.start();
2945
+ },
2946
+ stop: () => {
2947
+ pending.drop();
2948
+ run.stop();
2949
+ },
2950
+ cancel: () => {
2951
+ pending.drop();
2952
+ run.cancel();
2953
+ eased.drop();
2954
+ },
2955
+ restart,
2956
+ isRunning: () => run.isRunning() || pending.isWaiting()
2957
+ }), [run, build, restart, eased, pending]);
2958
+ }
2959
+ var EASED_LAYOUTS = Object.freeze({
2960
+ layered,
2961
+ tree,
2962
+ chain
2963
+ });
2964
+ function diagramShape(opts = {}) {
2965
+ const geometry = opts.geometry ?? RECT_POSE_DESCRIPTOR;
2966
+ const outlineOf = (node) => diagramNodeOf(node, opts.read)?.outline ?? laneOutlineOf(node);
2967
+ return {
2968
+ id: opts.id ?? "diagram:outline",
2969
+ matches: (node) => outlineOf(node) !== void 0,
2970
+ paint: (node, pose) => {
2971
+ const outline = outlineOf(node);
2972
+ if (outline === void 0) return [];
2973
+ const d = node.data;
2974
+ const box = geometry.getBounds(pose);
2975
+ const declaredFill = d.fill === void 0 ? opts.defaultFill : d.fill;
2976
+ const declaredStroke = d.stroke === void 0 ? opts.defaultStroke : d.stroke;
2977
+ const fill = declaredFill && commandFill(declaredFill, box);
2978
+ const stroke = declaredStroke && commandStroke(declaredStroke, box);
2979
+ const header = headerBox(box, diagramContainerOf(node)?.header);
2980
+ const divider = header === null || !stroke ? [] : [{ kind: "path", path: dividerPath(box, header), stroke }];
2981
+ return [{
2982
+ kind: "path",
2983
+ path: outlinePath(outline, box),
2984
+ ...fill ? { fill } : {},
2985
+ ...stroke ? { stroke } : {}
2986
+ }, ...divider];
2987
+ },
2988
+ // The drawn boundary, so picking and clipping follow the diamond rather
2989
+ // than its bounding box.
2990
+ silhouette: (node, pose) => {
2991
+ const outline = outlineOf(node);
2992
+ return outline === void 0 ? null : outlinePath(outline, geometry.getBounds(pose));
2993
+ }
2994
+ };
2995
+ }
2996
+ function dividerPath(box, header) {
2997
+ return header.width < box.width ? polylineFromPoints([{ x: header.x + header.width, y: box.y }, { x: header.x + header.width, y: box.y + box.height }]) : polylineFromPoints([{ x: box.x, y: header.y + header.height }, { x: box.x + box.width, y: header.y + header.height }]);
2998
+ }
2999
+ function registerDiagramShape(opts = {}) {
3000
+ return registerNodeShape(diagramShape(opts), { priority: "high" });
3001
+ }
3002
+ function useMirroredSelection(selection, selected, onSelect) {
3003
+ const want = selected ?? null;
3004
+ const pushed = useRef(void 0);
3005
+ useEffect(() => {
3006
+ if (pushed.current === want) return;
3007
+ pushed.current = want;
3008
+ if ((selection.get()[0] ?? null) !== want) selection.set(want === null ? [] : [want]);
3009
+ }, [want]);
3010
+ const onSelectRef = useLatest(onSelect);
3011
+ return useCallback(() => {
3012
+ onSelectRef.current?.(selection.get()[0] ?? null);
3013
+ }, [selection, onSelectRef]);
3014
+ }
3015
+
3016
+ // src/regionSync.ts
3017
+ function laneList(scene, poolId) {
3018
+ const pool = scene.get(poolId);
3019
+ const trait = pool === void 0 ? null : diagramContainerOf(pool);
3020
+ if (trait === null) return [];
3021
+ const axis = poolAxisOf(trait);
3022
+ return lanesOf(scene, poolId).map((id) => {
3023
+ const p = scene.get(id).pose;
3024
+ return { id, size: axis === "y" ? p.height : p.width };
3025
+ });
3026
+ }
3027
+ function readRegions(scene) {
3028
+ const regions = /* @__PURE__ */ new Map();
3029
+ const pools = /* @__PURE__ */ new Map();
3030
+ for (const n of scene.renderOrderNodes()) {
3031
+ const trait = diagramContainerOf(n);
3032
+ if (trait === null) continue;
3033
+ const { x, y, width, height } = n.pose;
3034
+ regions.set(n.id, { box: { x, y, width, height }, parent: n.parent, fitted: trait.fitted === true });
3035
+ if (trait.role === "pool") pools.set(n.id, laneList(scene, n.id));
3036
+ }
3037
+ return { regions, pools };
3038
+ }
3039
+ var sameBox = (a, b) => a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
3040
+ var sameLanes = (a, b) => a.length === b.length && a.every((l, i) => l.id === b[i].id && l.size === b[i].size);
3041
+ function regionChanges(was, now) {
3042
+ const out = [];
3043
+ const changed = /* @__PURE__ */ new Set();
3044
+ for (const [id, at] of now.regions) {
3045
+ const before = was.regions.get(id);
3046
+ if (before !== void 0 && (!sameBox(before.box, at.box) || before.parent !== at.parent)) changed.add(id);
3047
+ }
3048
+ const carried = (id) => {
3049
+ for (let p = now.regions.get(id)?.parent ?? null; p !== null; p = now.regions.get(p)?.parent ?? null) {
3050
+ if (changed.has(p)) return true;
3051
+ }
3052
+ return false;
3053
+ };
3054
+ for (const id of changed) {
3055
+ const at = now.regions.get(id);
3056
+ const before = was.regions.get(id);
3057
+ if (now.pools.has(at.parent ?? "")) continue;
3058
+ if (at.fitted && before.parent === at.parent && carried(id)) continue;
3059
+ out.push({ kind: "box", id, box: at.box, ...before.parent !== at.parent ? { parent: at.parent } : {} });
3060
+ }
3061
+ for (const [pool, lanes] of now.pools) {
3062
+ const before = was.pools.get(pool);
3063
+ if (before !== void 0 && !sameLanes(before, lanes)) out.push({ kind: "lanes", pool, lanes });
3064
+ }
3065
+ return out;
3066
+ }
3067
+
3068
+ // src/sceneSync.ts
3069
+ var NONE = [];
3070
+ var laidPort = (end) => end.laidOut !== void 0 && end.port === end.laidOut ? end.laidOut : void 0;
3071
+ function portsOf2(from, to) {
3072
+ const f = laidPort(from);
3073
+ const t = laidPort(to);
3074
+ return { ...f !== void 0 ? { from: f } : {}, ...t !== void 0 ? { to: t } : {} };
3075
+ }
3076
+ var samePorts = (a, b) => a.from === b.from && a.to === b.to;
3077
+ var routeOf2 = (edge) => ({ waypoints: edge.waypoints ?? NONE, ports: portsOf2(edge.from, edge.to) });
3078
+ function readScene(scene) {
3079
+ const nodes = /* @__PURE__ */ new Map();
3080
+ const routes = /* @__PURE__ */ new Map();
3081
+ for (const n of scene.renderOrderNodes()) {
3082
+ if (n.kind === "container") {
3083
+ if (diagramContainerOf(n) === null) nodes.set(n.id, { x: n.pose.x, y: n.pose.y, parent: n.parent });
3084
+ continue;
3085
+ }
3086
+ const edge = diagramEdgeOf(n);
3087
+ if (edge !== null) routes.set(n.id, routeOf2(edge));
3088
+ }
3089
+ return { nodes, routes, regions: readRegions(scene) };
3090
+ }
3091
+ function diagramLayoutOf(specs) {
3092
+ const moves = [];
3093
+ const routes = [];
3094
+ const containers = [];
3095
+ for (const s of specs) {
3096
+ const id = s.id;
3097
+ if (id === void 0) continue;
3098
+ if (s.kind === "container") {
3099
+ const region = diagramContainerOf(s);
3100
+ const pinned = isPinned({ id, kind: s.kind, data: s.data });
3101
+ if (region !== null) {
3102
+ if (!pinned && region.role !== "lane" && region.fitted !== true) {
3103
+ const { x, y, width, height } = s.pose;
3104
+ containers.push({ kind: "box", id, box: { x, y, width, height } });
3105
+ }
3106
+ } else if (diagramGroupOf(s) === null && !pinned) moves.push({ id, x: s.pose.x, y: s.pose.y });
3107
+ continue;
3108
+ }
3109
+ const edge = diagramEdgeOf(s);
3110
+ if (edge === null) continue;
3111
+ const route = routeOf2(edge);
3112
+ const laid = edge.laidOut !== void 0 && route.waypoints.length > 0 && layoutOwnsRoute(edge);
3113
+ if (laid || route.ports.from !== void 0 || route.ports.to !== void 0) {
3114
+ routes.push({ id, waypoints: laid ? route.waypoints : NONE, ports: route.ports });
3115
+ }
3116
+ }
3117
+ return { moves, routes, containers };
3118
+ }
3119
+ function changes(was, now) {
3120
+ const moves = [];
3121
+ for (const [id, { x, y, parent }] of now.nodes) {
3122
+ const before = was.nodes.get(id);
3123
+ if (before === void 0) continue;
3124
+ const reparented = before.parent !== parent;
3125
+ if (before.x !== x || before.y !== y || reparented) moves.push({ id, x, y, ...reparented ? { parent } : {} });
3126
+ }
3127
+ const routes = [];
3128
+ for (const [id, after] of now.routes) {
3129
+ const before = was.routes.get(id);
3130
+ if (before === void 0) continue;
3131
+ const moved = before.waypoints !== after.waypoints && !samePoints(before.waypoints, after.waypoints);
3132
+ if (moved || !samePorts(before.ports, after.ports)) routes.push({ id, ...after });
3133
+ }
3134
+ return { moves, routes, containers: regionChanges(was.regions, now.regions) };
3135
+ }
3136
+ function useSceneSync(scene, specs, reports) {
3137
+ const reportsRef = useLatest(reports);
3138
+ const syncing = useRef(false);
3139
+ const applied = useRef(specs);
3140
+ useEffect(() => {
3141
+ let known = readScene(scene);
3142
+ return scene.subscribe(() => {
3143
+ const now = readScene(scene);
3144
+ const found = syncing.current ? null : changes(known, now);
3145
+ known = now;
3146
+ if (found === null) return;
3147
+ if (found.moves.length > 0 || found.routes.length > 0) reportsRef.current.onMove?.(found.moves, found.routes);
3148
+ for (const edit of found.containers) reportsRef.current.onEditContainer?.(edit);
3149
+ });
3150
+ }, [scene, reportsRef]);
3151
+ useEffect(() => {
3152
+ if (applied.current === specs) return;
3153
+ syncing.current = true;
3154
+ try {
3155
+ reconcileSpecs(scene, applied.current, specs);
3156
+ } finally {
3157
+ syncing.current = false;
3158
+ }
3159
+ applied.current = specs;
3160
+ }, [scene, specs]);
3161
+ }
3162
+
3163
+ // src/regionEditing.ts
3164
+ function laneResizeReports(scene, resize) {
3165
+ const pool = poolsOf(scene).find((p) => p.id === resize.poolId);
3166
+ const index = pool?.lanes.findIndex((l) => l.id === resize.laneId) ?? -1;
3167
+ if (pool === void 0 || index < 0) return null;
3168
+ const lanes = laneList(scene, pool.id).map((l) => l.id === resize.laneId ? { ...l, size: resize.size } : l);
3169
+ const moves = [];
3170
+ const boxes = [];
3171
+ for (const [id, pose] of laneResizeFrame(scene, pool, index, resize.size)) {
3172
+ const node = scene.get(id);
3173
+ if (node === void 0) continue;
3174
+ const region = diagramContainerOf(node);
3175
+ const { x, y, width, height } = pose;
3176
+ if (region !== null) {
3177
+ if (region.role === "container" && region.fitted !== true) boxes.push({ kind: "box", id, box: { x, y, width, height } });
3178
+ } else if (node.kind === "container" && dataKeyReader(node) !== null) moves.push({ id, x, y });
3179
+ }
3180
+ return { lanes: { kind: "lanes", pool: pool.id, lanes }, moves, boxes };
3181
+ }
3182
+ function regionEditing(scene, selected, reports) {
3183
+ const edit = (e) => reports.current.onEditContainer?.(e);
3184
+ const containers = diagramContainers({
3185
+ scene,
3186
+ selected,
3187
+ commitResize: (resize) => {
3188
+ const out = laneResizeReports(scene, resize);
3189
+ if (out === null) return;
3190
+ edit(out.lanes);
3191
+ for (const box of out.boxes) edit(box);
3192
+ if (out.moves.length > 0) reports.current.onMove?.(out.moves, []);
3193
+ },
3194
+ commitAddLane: (poolId) => {
3195
+ const lanes = [...laneList(scene, poolId), { id: scene.mintId(), size: DEFAULT_LANE_SIZE }];
3196
+ edit({ kind: "lanes", pool: poolId, lanes });
3197
+ },
3198
+ commitRelease: (id) => edit({ kind: "release", id })
3199
+ });
3200
+ const title = {
3201
+ id: "weasel-diagram-container-title",
3202
+ eligibility: { always: true },
3203
+ actions: [editContainerTitleAction((id) => edit({ kind: "title", id }))],
3204
+ bindings: [editContainerTitleBinding()]
3205
+ };
3206
+ return { layer: containers.layer, contributions: [containers.contribution, title] };
3207
+ }
3208
+
3209
+ // src/diagramEditing.ts
3210
+ var AMBIENT = [dragPanContribution()];
3211
+ var NO_MARQUEE = { areaSelect: null };
3212
+ function regionAndMembers(scene, id, out) {
3213
+ out.add(id);
3214
+ for (const child of scene.childrenOf(id)) {
3215
+ const node = scene.get(child);
3216
+ if (node === void 0 || child === titleIdOf(id)) continue;
3217
+ if (diagramContainerOf(node) !== null) regionAndMembers(scene, child, out);
3218
+ else if (dataKeyReader(node) !== null) out.add(child);
3219
+ }
3220
+ }
3221
+ function removedData(scene, ids) {
3222
+ const nodes = /* @__PURE__ */ new Set();
3223
+ const edges = /* @__PURE__ */ new Set();
3224
+ for (const id of ids) {
3225
+ const node = scene.get(id);
3226
+ if (node === void 0) continue;
3227
+ if (diagramEdgeOf(node) !== null) edges.add(id);
3228
+ else if (diagramContainerOf(node) !== null) regionAndMembers(scene, id, nodes);
3229
+ else if (dataKeyReader(node) !== null) nodes.add(id);
3230
+ }
3231
+ for (const node of scene.renderOrderNodes()) {
3232
+ const ends = node.dependsOn;
3233
+ if (diagramEdgeOf(node) !== null && Array.isArray(ends) && ends.some((end) => nodes.has(end))) edges.add(node.id);
3234
+ }
3235
+ return { nodes: [...nodes], edges: [...edges] };
3236
+ }
3237
+ function reportDeleteAction(report) {
3238
+ return createDeleteAction({
3239
+ commit: (ids, deps) => {
3240
+ if (deps.scene === void 0) return;
3241
+ const removed = removedData(deps.scene, ids);
3242
+ if (removed.nodes.length > 0 || removed.edges.length > 0) report(removed);
3243
+ }
3244
+ });
3245
+ }
3246
+ function reportResizeAction(report, region) {
3247
+ return createResizeAction({
3248
+ commit: (poses, deps) => {
3249
+ const picked = new Set(deps.selection?.get() ?? []);
3250
+ const resizes = [];
3251
+ for (const [id, pose] of poses) {
3252
+ if (!picked.has(id)) continue;
3253
+ const { x, y, width, height } = pose;
3254
+ const node = deps.scene?.get(id);
3255
+ if (node !== void 0 && diagramContainerOf(node) !== null) region?.({ kind: "box", id, box: { x, y, width, height } });
3256
+ else resizes.push({ id, x, y, width, height });
3257
+ }
3258
+ if (resizes.length > 0) report?.(resizes);
3259
+ }
3260
+ });
3261
+ }
3262
+ var reconnectChangeOf = ({ edgeId, end, target }) => ({
3263
+ edge: edgeId,
3264
+ end,
3265
+ node: target.nodeId,
3266
+ ...isBodyEnd(target) ? {} : { port: target.id }
3267
+ });
3268
+ function withPickableEdges(specs) {
3269
+ return specs.map((s) => diagramEdgeOf(s) !== null ? { ...s, pickable: true } : s);
3270
+ }
3271
+ function useDiagramEditing(props) {
3272
+ const { scene, source, selected, mode, canConnect, portOptions, connectBodies } = props;
3273
+ const report = useLatest(props);
3274
+ const reconnects = props.onReconnect !== void 0;
3275
+ const routes = props.onEditRoute !== void 0;
3276
+ const labels = props.onEditLabel !== void 0;
3277
+ const labelMoves = props.onMoveLabel !== void 0;
3278
+ const deletes = props.onDelete !== void 0;
3279
+ const reorders = props.onReorder !== void 0;
3280
+ const resizes = props.onResize !== void 0;
3281
+ const regionEdits = props.onEditContainer !== void 0;
3282
+ const ports = useMemo(() => mode === "connect" ? diagramPorts({
3283
+ ...portOptions,
3284
+ participants: source,
3285
+ bodies: connectBodies === true,
3286
+ ...canConnect ? { canConnect } : {},
3287
+ commit: ({ from, to }) => report.current.onConnect?.({
3288
+ from: from.nodeId,
3289
+ ...isBodyEnd(from) ? {} : { fromPort: from.id },
3290
+ to: to.nodeId,
3291
+ ...isBodyEnd(to) ? {} : { toPort: to.id }
3292
+ })
3293
+ }) : null, [mode, source, canConnect, portOptions, connectBodies, report]);
3294
+ const shown = useMemo(() => {
3295
+ if (mode !== "show") return null;
3296
+ const { hitTest: _hitTest, ...paintOnly } = portLayer(source, portOptions);
3297
+ return paintOnly;
3298
+ }, [mode, source, portOptions]);
3299
+ const ends = useMemo(() => reconnects ? diagramEdgeEnds({
3300
+ participants: source,
3301
+ selected,
3302
+ bodies: connectBodies === true,
3303
+ ...canConnect ? { canConnect } : {},
3304
+ ...portOptions?.snapRadius !== void 0 ? { snapRadius: portOptions.snapRadius } : {},
3305
+ ...portOptions?.showsPort ? { showsPort: portOptions.showsPort } : {},
3306
+ commit: (change) => report.current.onReconnect?.(reconnectChangeOf(change))
3307
+ }) : null, [reconnects, source, selected, connectBodies, canConnect, portOptions, report]);
3308
+ const route = useMemo(() => routes ? diagramWaypoints({
3309
+ participants: source,
3310
+ selected,
3311
+ commit: ({ edgeId, waypoints }) => report.current.onEditRoute?.({ edge: edgeId, waypoints })
3312
+ }) : null, [routes, source, selected, report]);
3313
+ const labelDrag = useMemo(() => labelMoves ? diagramLabelDrag({
3314
+ scene,
3315
+ selected,
3316
+ commit: ({ edgeId, placement }) => report.current.onMoveLabel?.({ edge: edgeId, placement })
3317
+ }) : null, [labelMoves, scene, selected, report]);
3318
+ const label = useMemo(() => labels ? {
3319
+ id: "weasel-diagram-edge-label",
3320
+ eligibility: { always: true },
3321
+ actions: [editEdgeLabelAction({ commit: (edgeId) => report.current.onEditLabel?.(edgeId) })],
3322
+ bindings: [editEdgeLabelBinding()]
3323
+ } : null, [labels, report]);
3324
+ const reorder = useMemo(
3325
+ () => reorders ? groupReorderContribution((change) => report.current.onReorder?.(change)) : null,
3326
+ [reorders, report]
3327
+ );
3328
+ const regions = useMemo(() => regionEdits ? regionEditing(scene, selected, report) : null, [regionEdits, scene, selected, report]);
3329
+ const actions = useMemo(() => ({
3330
+ ...NO_MARQUEE,
3331
+ ...deletes ? { delete: reportDeleteAction((r) => report.current.onDelete?.(r)) } : {},
3332
+ ...resizes || regionEdits ? { resize: reportResizeAction((r) => report.current.onResize?.(r), (e) => report.current.onEditContainer?.(e)) } : {}
3333
+ }), [deletes, resizes, regionEdits, report]);
3334
+ const portsLayer = ports?.layer ?? shown;
3335
+ const layers = useMemo(
3336
+ // Edge ends last: an edge's end sits on a port and beside a route handle, and the last layer is hit first.
3337
+ () => [regions?.layer, labelDrag?.layer, portsLayer, route?.layer, ends?.layer].filter((l) => l != null),
3338
+ [regions, labelDrag, portsLayer, route, ends]
3339
+ );
3340
+ const extra = props.ambient;
3341
+ const ambient = useMemo(() => [
3342
+ ...AMBIENT,
3343
+ ...[ports?.contribution, route?.contribution, ends?.contribution, labelDrag?.contribution, label, reorder].filter((c) => c != null),
3344
+ ...regions?.contributions ?? [],
3345
+ ...extra ?? []
3346
+ ], [ports, route, ends, labelDrag, label, reorder, regions, extra]);
3347
+ return { layers, ambient, actions };
3348
+ }
3349
+ function usePagesView(pages) {
3350
+ const placed = pages?.placed;
3351
+ const fill = pages?.fill;
3352
+ return useMemo(() => {
3353
+ if (placed === void 0 || fill === void 0) return { layers: void 0, box: void 0 };
3354
+ return {
3355
+ layers: { pages: { layer: pagesLayer(placed, { fill }), before: "scene" } },
3356
+ box: pagesBounds(placed) ?? void 0
3357
+ };
3358
+ }, [placed, fill]);
3359
+ }
3360
+ var VIEW_RULE = ':where([data-wzl-diagram]) { position: relative; width: fit-content; } :where([data-wzl-diagram~="fill-x"]) { width: 100%; } :where([data-wzl-diagram~="fill-y"]) { height: 100%; } :where([data-wzl-diagram]) > canvas { display: block; } :where([data-wzl-diagram-overlay]) { position: absolute; left: 0; top: 0; } :where([data-wzl-diagram-overlay][hidden]) { display: none; }';
3361
+ var adoptViewRule = (el) => {
3362
+ if (el !== null) adoptCssOnce(el, VIEW_RULE);
3363
+ };
3364
+ var IDENTITY = { x: 0, y: 0, scale: { x: 1, y: 1 } };
3365
+ var sameView = (a, b) => a.x === b.x && a.y === b.y && a.scale.x === b.scale.x && a.scale.y === b.scale.y;
3366
+ var DiagramView = forwardRef(function DiagramView2(props, ref) {
3367
+ return /* @__PURE__ */ jsx(WeaselProvider, { children: /* @__PURE__ */ jsx(Inner, { ...props, apiRef: ref }) });
3368
+ });
3369
+ function Inner({
3370
+ specs: given,
3371
+ router,
3372
+ labels,
3373
+ hops,
3374
+ width,
3375
+ height,
3376
+ selected,
3377
+ onSelect,
3378
+ onHover,
3379
+ onMove,
3380
+ onResize,
3381
+ onConnect,
3382
+ onReconnect,
3383
+ onEditRoute,
3384
+ onDelete,
3385
+ onEditLabel,
3386
+ onMoveLabel,
3387
+ onReorder,
3388
+ onEditContainer,
3389
+ connectBodies,
3390
+ pickEdges,
3391
+ overlays = NO_OVERLAYS,
3392
+ ambient: extraAmbient,
3393
+ enableKeybindings,
3394
+ canConnect,
3395
+ portOptions,
3396
+ ports: portMode,
3397
+ live: liveOpts,
3398
+ minScale,
3399
+ maxScale,
3400
+ anchor,
3401
+ fitPadding,
3402
+ background,
3403
+ pages,
3404
+ view,
3405
+ onViewChange,
3406
+ className,
3407
+ apiRef
3408
+ }) {
3409
+ const edgesPick = pickEdges ?? (onReconnect !== void 0 || onEditRoute !== void 0 || onDelete !== void 0 || onEditLabel !== void 0 || onMoveLabel !== void 0);
3410
+ const avoid2 = useMemo(() => router === "avoid" ? createSpecRouter() : null, [router]);
3411
+ const placer = useMemo(() => labels === "dodge" ? createSpecLabelPlacer() : null, [labels]);
3412
+ const routed = useMemo(() => {
3413
+ const out = avoid2 === null ? given : avoid2(given);
3414
+ return placer === null ? out : placer(out);
3415
+ }, [avoid2, placer, given]);
3416
+ const hopped = useMemo(() => hops === void 0 ? routed : createSpecHops(hops)(routed), [hops, routed]);
3417
+ const specs = useMemo(() => edgesPick ? withPickableEdges(hopped) : hopped, [edgesPick, hopped]);
3418
+ useEffect(() => {
3419
+ const disposers = [registerDiagramShape(), registerMarkShape()];
3420
+ return () => disposers.forEach((dispose) => dispose());
3421
+ }, []);
3422
+ const registry = useMemo(() => withDiagramRegistry(), []);
3423
+ const scene = useScene({
3424
+ systemLayers: [{ id: "main" }],
3425
+ initial: specs,
3426
+ registry
3427
+ });
3428
+ const selection = useSelection({ scene, mode: "single" });
3429
+ const onClick = useMirroredSelection(selection, selected, onSelect);
3430
+ useSceneSync(scene, specs, { onMove, onEditContainer });
3431
+ const source = useMemo(() => sceneParticipants(scene), [scene]);
3432
+ const [run, setRun] = useState(null);
3433
+ const controlled = view !== void 0;
3434
+ const fills = width === void 0 || height === void 0;
3435
+ const [measured, setMeasured] = useState(null);
3436
+ const boxWidth = width ?? measured?.width ?? 0;
3437
+ const boxHeight = height ?? measured?.height ?? 0;
3438
+ const unsized = boxWidth === 0 || boxHeight === 0;
3439
+ const fitOpts = { minScale, maxScale, anchor, padding: fitPadding };
3440
+ const { layers, box: pagesBox } = usePagesView(pages);
3441
+ const defaultView = useMemo(
3442
+ () => controlled || unsized ? void 0 : fitDiagram(specs, { width: boxWidth, height: boxHeight }, fitOpts, pagesBox),
3443
+ // A later `specs` keeps the view it has.
3444
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3445
+ [controlled, unsized, boxWidth, boxHeight, minScale, maxScale, anchor, fitPadding, pagesBox]
3446
+ );
3447
+ const [settledFit, setSettledFit] = useState(null);
3448
+ const fit = settledFit !== null && settledFit.over === defaultView ? settledFit.view : defaultView;
3449
+ const settled = useLatest((moved) => {
3450
+ liveOpts?.onSettle?.(moved);
3451
+ if (controlled || moved === 0) return;
3452
+ const boxes = Array.from(source(), ({ node, pose }) => ({ ...node, pose }));
3453
+ setSettledFit({ view: fitDiagram(boxes, { width: boxWidth, height: boxHeight }, fitOpts, pagesBox), over: defaultView });
3454
+ });
3455
+ const live = useLiveLayout({
3456
+ ...liveOpts,
3457
+ scene,
3458
+ source,
3459
+ algorithm: run?.algorithm ?? "force",
3460
+ onSettle: (moved) => settled.current(moved)
3461
+ });
3462
+ useEffect(() => {
3463
+ if (run !== null) live.restart();
3464
+ }, [run, live]);
3465
+ const canvasRef = useRef(null);
3466
+ useDiagramApi(apiRef, canvasRef, scene, live, setRun);
3467
+ const selectedIds = useCallback(() => selection.get(), [selection]);
3468
+ const editing = useDiagramEditing({
3469
+ scene,
3470
+ source,
3471
+ selected: selectedIds,
3472
+ mode: portMode ?? (onConnect !== void 0 ? "connect" : "none"),
3473
+ onConnect,
3474
+ onReconnect,
3475
+ onEditRoute,
3476
+ onDelete,
3477
+ onEditLabel,
3478
+ onMoveLabel,
3479
+ onReorder,
3480
+ onResize,
3481
+ onEditContainer,
3482
+ onMove,
3483
+ connectBodies,
3484
+ canConnect,
3485
+ portOptions,
3486
+ ambient: extraAmbient
3487
+ });
3488
+ useEffect(() => {
3489
+ const canvas = canvasRef.current;
3490
+ if (canvas === null) return;
3491
+ const disposers = editing.layers.map((layer) => canvas.registerLayer(layer));
3492
+ return () => disposers.forEach((dispose) => dispose());
3493
+ }, [editing.layers]);
3494
+ const moves = onMove !== void 0 || onEditContainer !== void 0;
3495
+ const resizes = onResize !== void 0 || onEditContainer !== void 0;
3496
+ const edits = moves || onConnect !== void 0 || onReorder !== void 0 || resizes || edgesPick;
3497
+ const features = useMemo(() => ["view", "pick", ...moves ? ["move"] : [], ...resizes ? ["resize"] : []], [moves, resizes]);
3498
+ const selectTool = useMemo(() => ({ resize: { resizable: diagramResizable(scene) } }), [scene]);
3499
+ useRefit(canvasRef, fit);
3500
+ const overlayRef = useOverlayPlacement(canvasRef, scene, overlays);
3501
+ const fillsOn = [width === void 0 ? "fill-x" : "", height === void 0 ? "fill-y" : ""].join(" ").trim();
3502
+ return /* @__PURE__ */ jsxs("div", { ref: adoptViewRule, "data-wzl-diagram": fillsOn, className, children: [
3503
+ /* @__PURE__ */ jsx(
3504
+ SceneCanvas,
3505
+ {
3506
+ ref: canvasRef,
3507
+ scene,
3508
+ selection,
3509
+ features,
3510
+ ...resizes ? { selectTool } : {},
3511
+ enableKeybindings: enableKeybindings ?? edits,
3512
+ ambient: editing.ambient,
3513
+ actions: editing.actions,
3514
+ onClick,
3515
+ ...onHover ? { onHover } : {},
3516
+ width,
3517
+ height,
3518
+ ...fills ? { onResize: setMeasured } : {},
3519
+ ...defaultView ? { defaultView } : {},
3520
+ ...view ? { view } : {},
3521
+ ...onViewChange ? { onViewChange } : {},
3522
+ ...background ? { backgroundFill: background } : {},
3523
+ ...layers ? { layers } : {},
3524
+ children: /* @__PURE__ */ jsx(DropRules, { scene })
3525
+ }
3526
+ ),
3527
+ overlays.map((o) => {
3528
+ const key = o.key ?? o.target;
3529
+ return /* @__PURE__ */ jsx("div", { ref: overlayRef(key), "data-wzl-diagram-overlay": "", children: o.content }, key);
3530
+ })
3531
+ ] });
3532
+ }
3533
+ var NO_OVERLAYS = [];
3534
+ function DropRules({ scene }) {
3535
+ const canDropInto = useMemo(() => diagramCanDropInto(scene), [scene]);
3536
+ useDepSource("canDropInto", () => canDropInto);
3537
+ return null;
3538
+ }
3539
+ function useDiagramApi(apiRef, canvasRef, scene, live, setRun) {
3540
+ useImperativeHandle(apiRef, () => ({
3541
+ layout: (algorithm = "force") => setRun({ algorithm }),
3542
+ live,
3543
+ worldAt: (clientX, clientY) => {
3544
+ const [x, y] = canvasRef.current?.clientToWorld(clientX, clientY) ?? [clientX, clientY];
3545
+ return { x, y };
3546
+ },
3547
+ visible: () => {
3548
+ const canvas = canvasRef.current;
3549
+ const r = canvas?.element?.getBoundingClientRect();
3550
+ if (canvas == null || r === void 0 || r.width === 0 || r.height === 0) return null;
3551
+ const [x0, y0] = canvas.clientToWorld(r.left, r.top);
3552
+ const [x1, y1] = canvas.clientToWorld(r.right, r.bottom);
3553
+ return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
3554
+ },
3555
+ rectOf: (id) => {
3556
+ const canvas = canvasRef.current;
3557
+ const world = worldRectOf(scene, id);
3558
+ const r = canvas?.element?.getBoundingClientRect();
3559
+ if (canvas == null || world === null || r === void 0) return null;
3560
+ const rect = screenRectOf(world, canvas.getView());
3561
+ return { ...rect, x: rect.x + r.left, y: rect.y + r.top };
3562
+ },
3563
+ subscribeFrame: (fn) => canvasRef.current?.subscribeFrame(() => fn()) ?? (() => {
3564
+ })
3565
+ }), [canvasRef, live, scene, setRun]);
3566
+ }
3567
+ function useRefit(canvasRef, fit) {
3568
+ const fitted = useRef(fit);
3569
+ useEffect(() => {
3570
+ const canvas = canvasRef.current;
3571
+ const was = fitted.current;
3572
+ if (canvas === null || fit === void 0 || was === fit) return;
3573
+ if (!sameView(canvas.getView(), was ?? IDENTITY)) return;
3574
+ fitted.current = fit;
3575
+ canvas.setView(fit);
3576
+ }, [canvasRef, fit]);
3577
+ }
3578
+ function fitDiagram(specs, size, fit = {}, extra) {
3579
+ const boxes = specs.filter((s) => s.kind === "container" || diagramGroupOf(s) !== null).map((s) => s.pose);
3580
+ if (extra !== void 0) boxes.push({ ...extra });
3581
+ if (boxes.length === 0) return void 0;
3582
+ const x = Math.min(...boxes.map((b) => b.x));
3583
+ const y = Math.min(...boxes.map((b) => b.y));
3584
+ const bounds = {
3585
+ x,
3586
+ y,
3587
+ width: Math.max(...boxes.map((b) => b.x + b.width)) - x,
3588
+ height: Math.max(...boxes.map((b) => b.y + b.height)) - y
3589
+ };
3590
+ return fitViewToBounds(bounds, size, { x: 0, y: 0, scale: { x: 1, y: 1 } }, {
3591
+ maxScale: fit.maxScale ?? 1,
3592
+ ...fit.minScale !== void 0 ? { minScale: fit.minScale } : {},
3593
+ ...fit.anchor !== void 0 ? { anchor: fit.anchor } : {},
3594
+ ...fit.padding !== void 0 ? { padding: fit.padding } : {}
3595
+ });
3596
+ }
3597
+
3598
+ export { ADD_LANE_ACTION_ID, ANYWHERE, CONNECT_ACTION_ID, CONTAINER_STENCILS, DEFAULT_EDGE_STROKE, DiagramView, EASED_LAYOUTS, EDGE_END_AFFORDANCE_KIND, EDGE_END_LAYER_ID, EDIT_CONTAINER_TITLE_ACTION_ID, GRAB_EDGE_END_ACTION_ID, GRAB_LABEL_ACTION_ID, GRAB_PORT_ACTION_ID, GRAB_WAYPOINT_ACTION_ID, LANE_AFFORDANCE_KIND, LANE_LAYER_ID, LANE_RESIZE_ACTION_ID, LAYOUTS, LAYOUT_ACTION_ID, MOVE_LABEL_ACTION_ID, PORT_AFFORDANCE_KIND, PORT_LAYER_ID, RECONNECT_ACTION_ID, RELEASE_MEMBERS_ACTION_ID, REMOVE_WAYPOINT_ACTION_ID, REORDER_ACTION_ID, RESET_LABEL_ACTION_ID, RESET_ROUTE_ACTION_ID, ROUTER_LABELS, ROUTE_EDIT_ACTION_ID, SELECT_LABEL_EDGE_ACTION_ID, SET_HOPS_ACTION_ID, SET_ROUTER_ACTION_IDS, STEP_REORDER_ACTION_ID, WAYPOINT_AFFORDANCE_KIND, WAYPOINT_HIT_PX, WAYPOINT_LAYER_ID, addLane, applyLayout, avoidSceneInput, bodyAt, commitEdgeToScene, commitLabelToScene, commitLaneResizeToScene, commitReconnectToScene, commitRouteToScene, commitRouterToScene, connectBinding, containerBuild, createAddLaneAction, createConnectAction, createDiagramContribution, createEdgeEndAffordance, createLabelDragAction, createLaneAffordance, createLaneResizeAction, createLayoutAction, createPortAffordance, createReconnectAction, createReleaseMembersAction, createRemoveWaypointAction, createReorderAction, createResetLabelAction, createResetRouteAction, createRouteEditAction, createSetHopsAction, createSetRouterAction, createSpecHops, createSpecLabelPlacer, createSpecRouter, createStepReorderAction, createWaypointAffordance, defaultCanConnect, diagramCanDropInto, diagramContainers, diagramDropParentAt, diagramEdgeEnds, diagramLabelDrag, diagramLayoutOf, diagramPorts, diagramResizable, diagramShape, diagramWaypoints, dragSegment, dragVertex, easedProducer, edgeEndLayer, edgeEndScratchOf, edgePolylines, editContainerTitleAction, editContainerTitleBinding, forceProducer, grabPortAction, grabPortBindings, groupReorderContribution, groupReorderSlots, headerIdOf, hopDataOps, inOrUnder, insertWaypoint, isBodyEnd, laneLayer, laneResizeFrame, laneScratchOf, lanesOf, layoutPoses, movedLabelData, pinSegment, poolAxisOf, poolsOf, portLayer, portScratchOf, reconnectBinding, regionHeaderAt, registerDiagramShape, releaseMembers, removeSegment, removeWaypoint, reorderBinding, reorderContribution, routeHandles, routeStateOf, routeWrites, routedData, routerActions, routesOf, sceneEdges, sceneParticipants, segmentAxis, sequenceSlots, themeWrites, unpinSegment, useLiveLayout, useMirroredSelection, waypointLayer, waypointScratchOf, withHopsData, withRouter };
3599
+ //# sourceMappingURL=chunk-CI3I3I6T.js.map
3600
+ //# sourceMappingURL=chunk-CI3I3I6T.js.map