dsh-drawai 0.4.1 → 0.4.2

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.
@@ -1,753 +1,753 @@
1
- /*
2
- * libavoid-routing.js — the draw.io routing core over the libavoid solver.
3
- * Hand-authored; engine-agnostic (takes the `Avoid` namespace as a parameter,
4
- * so it works whether the host loads libavoid as WASM or pure JS) and never
5
- * changes with the libavoid build.
6
- *
7
- * CANONICAL SOURCE: this file (drawio-dev js/libavoid-js/). drawio-mcp vendors
8
- * verbatim copies (mcp-app-server/vendor/libavoid/ and
9
- * mcp-tool-server/vendor/libavoid/) — copy this file over when it changes.
10
- *
11
- * A plain script defining globalThis.AvoidRouting, so the SAME artifact is:
12
- * - concatenated into extensions.min.js here (glue → wasm → loader → this →
13
- * the Closure-compiled LibavoidRouting editor binding),
14
- * - inlined into (or CDN-loaded by) the drawio-mcp app-server viewer,
15
- * - side-effect imported from Node by the drawio-mcp tool server
16
- * (import the file, then read globalThis.AvoidRouting).
17
- * It does not depend on the glue/loader — every entry point takes the Avoid
18
- * namespace as a parameter — and must stay dependency-free (no mxGraph, no
19
- * DOM): anything editor-specific belongs in js/diagramly/LibavoidRouting.js.
20
- *
21
- * libavoid API gotchas baked in here (empirically verified against the
22
- * vendored build, libavoid-js 0.5.0-beta.5):
23
- * - Router flag is an integer: RouterFlag.OrthogonalRouting.value
24
- * - setRoutingParameter takes the enum OBJECT (RoutingParameter.x), not
25
- * .value (the integer silently no-ops and routes run flush against boxes)
26
- * - cleanup is router.delete() (embind), not Avoid.destroy()
27
- * - Point/Rectangle/ConnEnd/Checkpoint(+vector) are embind wrappers COPIED
28
- * into the objects they parameterize and NOT freed by router.delete();
29
- * free each temporary with .delete()
30
- * - a directed ShapeConnectionPin gives the route NO minimum straight
31
- * lead-out (it may turn at the anchor and run flush along the shape); a
32
- * minimum stub needs a routing checkpoint at the stub tip. Checkpoint
33
- * DIRECTION flags have an inverted vertical convention in this build —
34
- * use plain 1-arg checkpoints only.
35
- */
36
- (function()
37
- {
38
- var AvoidRouting = {};
39
-
40
- // libavoid ConnDirFlags (the enum isn't exposed by the JS bindings): a
41
- // bitmask of the directions a connector may approach an endpoint.
42
- AvoidRouting.DIR = {up: 1, down: 2, left: 4, right: 8, all: 15};
43
-
44
- /**
45
- * Clamp to [0,1] — the ShapeConnectionPin proportional-offset domain.
46
- */
47
- AvoidRouting.clamp01 = function(v)
48
- {
49
- return (v < 0) ? 0 : ((v > 1) ? 1 : v);
50
- };
51
-
52
- /**
53
- * ConnDirFlags for a proportional connection point: perpendicular to the
54
- * nearest shape edge (x<=0 left, x>=1 right, y<=0 up, y>=1 down), else any
55
- * direction.
56
- */
57
- AvoidRouting.dirForPoint = function(x, y)
58
- {
59
- var DIR = AvoidRouting.DIR;
60
- var d = 0;
61
-
62
- if (x <= 0) { d |= DIR.left; }
63
- else if (x >= 1) { d |= DIR.right; }
64
-
65
- if (y <= 0) { d |= DIR.up; }
66
- else if (y >= 1) { d |= DIR.down; }
67
-
68
- return (d != 0) ? d : DIR.all;
69
- };
70
-
71
- /**
72
- * A fixed connection point as a routing constraint {x, y, dir}: the
73
- * proportional position clamped to [0,1] (some shapes' connection points
74
- * sit slightly outside, e.g. a hexagon tip at y=-0.017, which
75
- * ShapeConnectionPin rejects) plus the ConnDirFlags direction derived from
76
- * the ORIGINAL values so it still points off the correct edge. Returns
77
- * null when either coordinate is missing/not a number (floating endpoint).
78
- */
79
- AvoidRouting.constraintForPoint = function(x, y)
80
- {
81
- if (x == null || y == null || isNaN(x) || isNaN(y))
82
- {
83
- return null;
84
- }
85
-
86
- return {x: AvoidRouting.clamp01(x), y: AvoidRouting.clamp01(y),
87
- dir: AvoidRouting.dirForPoint(x, y)};
88
- };
89
-
90
- /**
91
- * Proportional pin points for one masked side of bounds b ({x,y,w,h}):
92
- * one pin per ~20px of side length (1..9, spread evenly so a single pin
93
- * sits at the side midpoint), each directed off its side. Side letters
94
- * follow the mxGraph port-constraint vocabulary (N/S/E/W). Used by
95
- * computeRoutes' sourceSides/targetSides handling — the pins of all
96
- * allowed sides share one pin class, so libavoid picks the side AND the
97
- * position that route best.
98
- */
99
- AvoidRouting.maskPinPoints = function(side, b)
100
- {
101
- var DIR = AvoidRouting.DIR;
102
- var horizontal = (side == 'N' || side == 'S');
103
- var len = horizontal ? b.w : b.h;
104
- var n = Math.max(1, Math.min(9, Math.round(len / 20)));
105
- var pts = [];
106
-
107
- for (var i = 0; i < n; i++)
108
- {
109
- var f = (i + 0.5) / n;
110
-
111
- pts.push((side == 'N') ? {x: f, y: 0, dir: DIR.up} :
112
- (side == 'S') ? {x: f, y: 1, dir: DIR.down} :
113
- (side == 'W') ? {x: 0, y: f, dir: DIR.left} :
114
- {x: 1, y: f, dir: DIR.right});
115
- }
116
-
117
- return pts;
118
- };
119
-
120
- /**
121
- * True when the point lies strictly inside any of the obstacles. A pinned
122
- * anchor sits ON its own shape's boundary, so the strict test never
123
- * matches the point's own shape.
124
- */
125
- AvoidRouting.insideAny = function(x, y, obstacles)
126
- {
127
- if (obstacles != null)
128
- {
129
- for (var i = 0; i < obstacles.length; i++)
130
- {
131
- var v = obstacles[i];
132
-
133
- if (v != null && x > v.x && x < v.x + v.w && y > v.y && y < v.y + v.h)
134
- {
135
- return true;
136
- }
137
- }
138
- }
139
-
140
- return false;
141
- };
142
-
143
- /**
144
- * The stub tip a jetty checkpoint pins the route through: the constrained
145
- * anchor on bounds b ({x,y,w,h}), moved jetty px outward along the pin's
146
- * single ConnDirFlags direction. Returns {x, y} or null when there is
147
- * nothing to enforce: no constraint, non-positive jetty, an ambiguous
148
- * direction (corner/interior anchors carry 2+ direction bits), or a tip
149
- * inside an obstacle ({x,y,w,h} array) INFLATED by the solve's buffer —
150
- * libavoid's orthogonal visibility network stays clear of the buffered
151
- * zones, so a checkpoint there is unreachable and libavoid warns
152
- * ("Warning: skipping checkpoint for connector ...") on the console and
153
- * ignores it; not requesting it keeps the route identical without the
154
- * noise. The stub's OWN shape (matched by bounds) is exempt from the
155
- * inflation — the pin's lead-out segment crosses its own buffer zone
156
- * legitimately (verified against the WASM: no warning there).
157
- */
158
- AvoidRouting.jettyStub = function(constraint, jetty, b, obstacles, buffer)
159
- {
160
- if (constraint == null || !(jetty > 0))
161
- {
162
- return null;
163
- }
164
-
165
- var d = constraint.dir;
166
- var DIR = AvoidRouting.DIR;
167
-
168
- if (d != DIR.up && d != DIR.down && d != DIR.left && d != DIR.right)
169
- {
170
- return null;
171
- }
172
-
173
- var x = b.x + constraint.x * b.w +
174
- ((d == DIR.right) ? jetty : ((d == DIR.left) ? -jetty : 0));
175
- var y = b.y + constraint.y * b.h +
176
- ((d == DIR.down) ? jetty : ((d == DIR.up) ? -jetty : 0));
177
-
178
- if (obstacles != null)
179
- {
180
- var inflate = (buffer > 0) ? buffer : 0;
181
-
182
- for (var i = 0; i < obstacles.length; i++)
183
- {
184
- var v = obstacles[i];
185
-
186
- if (v == null ||
187
- (v.x == b.x && v.y == b.y && v.w == b.w && v.h == b.h))
188
- {
189
- continue;
190
- }
191
-
192
- if (x > v.x - inflate && x < v.x + v.w + inflate &&
193
- y > v.y - inflate && y < v.y + v.h + inflate)
194
- {
195
- return null;
196
- }
197
- }
198
- }
199
-
200
- return {x: x, y: y};
201
- };
202
-
203
- /**
204
- * One end's jetty capped to the clearance toward the OTHER terminal along
205
- * the stub's own axis: half the directed gap (stub's base side to the
206
- * facing side of the other shape) minus the 4px routing channel — so two
207
- * facing stubs can never cross and a capped tip lands on the corridor
208
- * channel's boundary; an uncapped stub in a tight gap puts its checkpoint
209
- * past the channel and libavoid answers with a self-overlapping hairpin
210
- * that nudging splays into flat side-loops. No cap when the end floats,
211
- * the anchor is a corner/interior one (2+ direction bits — jettyStub
212
- * skips those ends anyway), the stub points away from the other shape, or
213
- * the shapes overlap along the stub's axis: such a stub never enters the
214
- * pair's corridor, so a narrow gap along the OTHER axis must not shorten
215
- * it (only the center segment squeezes through that gap, e.g. an L around
216
- * a 15px horizontal gap keeps its full vertical lead-out).
217
- */
218
- AvoidRouting.cappedJetty = function(jetty, constraint, from, to)
219
- {
220
- // A dangling (free-point) end has no bounds to measure the gap against.
221
- if (from == null || to == null)
222
- {
223
- return jetty;
224
- }
225
-
226
- var DIR = AvoidRouting.DIR;
227
- var d = (constraint != null) ? constraint.dir : 0;
228
- var dgap = (d == DIR.down) ? to.y - (from.y + from.h) :
229
- ((d == DIR.up) ? from.y - (to.y + to.h) :
230
- ((d == DIR.right) ? to.x - (from.x + from.w) :
231
- ((d == DIR.left) ? from.x - (to.x + to.w) : 0)));
232
-
233
- if (dgap > 0)
234
- {
235
- var half = Math.max(2, Math.floor((dgap - 4) / 2));
236
-
237
- if (jetty > half)
238
- {
239
- return half;
240
- }
241
- }
242
-
243
- return jetty;
244
- };
245
-
246
- /**
247
- * Obstacles minus ENCLOSING shapes: a shape whose bounds fully contain a
248
- * routed edge's terminal (and that is not itself a terminal of one of the
249
- * edges) is the container/pool/group the terminal lives IN, not something
250
- * to route around. Registering it starves the solve of corridors (both
251
- * endpoints sit inside a hard obstacle, so routes escape it or degenerate)
252
- * and suppresses the terminals' jetty stubs (jettyStub refuses tips inside
253
- * any obstacle). Model ancestry is unknown here (this file is model-free),
254
- * so containment is geometric: rect-contains-rect, inclusive. Terminals of
255
- * the routed edges are never dropped (their ShapeRefs carry the connection
256
- * pins). Returns a filtered copy; the input arrays are not modified.
257
- */
258
- AvoidRouting.filterEnclosing = function(vertices, edges)
259
- {
260
- if (vertices == null || edges == null || edges.length === 0)
261
- {
262
- return vertices;
263
- }
264
-
265
- var terminalIds = Object.create(null);
266
- var i;
267
-
268
- for (i = 0; i < edges.length; i++)
269
- {
270
- if (edges[i] != null)
271
- {
272
- terminalIds[edges[i].source] = true;
273
- terminalIds[edges[i].target] = true;
274
- }
275
- }
276
-
277
- var terminals = [];
278
-
279
- for (i = 0; i < vertices.length; i++)
280
- {
281
- var v = vertices[i];
282
-
283
- if (v != null && terminalIds[v.id] === true)
284
- {
285
- terminals.push(v);
286
- }
287
- }
288
-
289
- var out = [];
290
-
291
- for (i = 0; i < vertices.length; i++)
292
- {
293
- var v = vertices[i];
294
-
295
- if (v == null)
296
- {
297
- continue;
298
- }
299
-
300
- var enclosing = false;
301
-
302
- if (terminalIds[v.id] !== true)
303
- {
304
- for (var j = 0; j < terminals.length; j++)
305
- {
306
- var t = terminals[j];
307
-
308
- if (v.x <= t.x && v.y <= t.y &&
309
- v.x + v.w >= t.x + t.w && v.y + v.h >= t.y + t.h)
310
- {
311
- enclosing = true;
312
- break;
313
- }
314
- }
315
- }
316
-
317
- if (!enclosing)
318
- {
319
- out.push(v);
320
- }
321
- }
322
-
323
- return out;
324
- };
325
-
326
- /**
327
- * Compute obstacle-avoiding orthogonal routes for a set of edges.
328
- *
329
- * @param {object} Avoid - the libavoid instance (AvoidLib.getInstance()).
330
- * @param {Array<{id:string,x:number,y:number,w:number,h:number}>} vertices
331
- * Obstacles, in ABSOLUTE coordinates. Shapes enclosing a terminal
332
- * of a routed edge are dropped (filterEnclosing).
333
- * @param {Array<{id,source,target,sourcePoint?,targetPoint?,
334
- * sourceConstraint?,targetConstraint?,
335
- * sourcePoints?,targetPoints?,sourceSides?,targetSides?,
336
- * sourceJetty?,targetJetty?}>} edges
337
- * Edges referencing vertex ids. An end may instead be a DANGLING free
338
- * point — sourcePoint/targetPoint {x,y} in absolute coords, used when
339
- * that terminal has no vertex (an unconnected endpoint): it routes to
340
- * a plain Point ConnEnd (no shape, no direction), matching the
341
- * drag-preview, and takes no constraint/pin/mask/jetty. An edge is
342
- * skipped when an end is neither a vertex nor a free point, and
343
- * when it is a SELF-LOOP (source === target) — loops keep the
344
- * caller's own loop styling and waypoints.
345
- * *Constraint = {x,y,dir} (constraintForPoint): a
346
- * fixed connection point routed via a directed ShapeConnectionPin;
347
- * absent => the endpoint floats at the shape centre. *Points = an
348
- * array of {x,y,dir} candidate anchors (draw.io's snapToPoint —
349
- * the shape's declared connection points): non-exclusive directed
350
- * pins at each candidate under one pin class, libavoid picks the
351
- * anchor that routes best; beaten by a *Constraint on the same end
352
- * (an explicit anchor wins) and beats a *Sides mask (the renderer's
353
- * snapToPoint branch runs before mask enforcement). *Sides = an
354
- * array of allowed attach sides ('N'/'S'/'E'/'W', the legacy
355
- * mxGraph portConstraint masks): non-exclusive pins spread along
356
- * the allowed sides under one pin class, libavoid picks side and
357
- * position. *Jetty = the minimum length of the first/last
358
- * segment in px (draw.io's jettySize), enforced for ends with a
359
- * *Constraint by a routing checkpoint at the stub tip — requested
360
- * lazily, only for edges whose natural route falls short of the
361
- * minimum; snapped and masked ends behave like floating ones here
362
- * (their first bend naturally sits ~shapeBufferDistance out).
363
- * @param {{shapeBufferDistance?:number,idealNudgingDistance?:number}} [opts]
364
- * Defaults: 16 / 14.
365
- * @returns {Object<string, Array<{x:number,y:number}>>} edge id -> interior
366
- * bend points (ABSOLUTE, collinear-filtered). The first/last route
367
- * points (endpoints) are dropped — a floating endpoint connects at
368
- * the shape side midpoint, which is where a floating
369
- * orthogonalEdgeStyle endpoint lands anyway. An edge with a
370
- * straight (bend-free) route maps to [].
371
- */
372
- AvoidRouting.computeRoutes = function(Avoid, vertices, edges, opts)
373
- {
374
- var out = Object.create(null);
375
-
376
- if (Avoid == null || vertices == null || edges == null)
377
- {
378
- return out;
379
- }
380
-
381
- var buffer = (opts && opts.shapeBufferDistance != null) ? opts.shapeBufferDistance : 16;
382
- var nudge = (opts && opts.idealNudgingDistance != null) ? opts.idealNudgingDistance : 14;
383
-
384
- // Containers the terminals live in are not obstacles (also feeds the
385
- // jettyStub checks below, so stubs inside a container are preserved).
386
- vertices = AvoidRouting.filterEnclosing(vertices, edges);
387
-
388
- function collinear(a, b, c)
389
- {
390
- // Zero cross product (1px tolerance) => b lies on segment a..c (redundant).
391
- return Math.abs((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)) < 1;
392
- }
393
-
394
- var router = new Avoid.Router(Avoid.RouterFlag.OrthogonalRouting.value);
395
-
396
- try { router.setRoutingParameter(Avoid.RoutingParameter.shapeBufferDistance, buffer); } catch (e) {}
397
- try { router.setRoutingParameter(Avoid.RoutingParameter.idealNudgingDistance, nudge); } catch (e) {}
398
-
399
- var bounds = Object.create(null);
400
- var shapeRefs = Object.create(null);
401
- var pinClass = 0;
402
- var i;
403
-
404
- for (i = 0; i < vertices.length; i++)
405
- {
406
- var v = vertices[i];
407
-
408
- if (v == null || v.id == null || !(v.w > 0) || !(v.h > 0))
409
- {
410
- continue;
411
- }
412
-
413
- bounds[v.id] = v;
414
- // Point/Rectangle are embind value wrappers that COPY into the
415
- // ShapeRef and are NOT freed by router.delete(); free the temporaries.
416
- var rp1 = new Avoid.Point(v.x, v.y);
417
- var rp2 = new Avoid.Point(v.x + v.w, v.y + v.h);
418
- var rect = new Avoid.Rectangle(rp1, rp2);
419
- shapeRefs[v.id] = new Avoid.ShapeRef(router, rect);
420
- rect.delete();
421
- rp1.delete();
422
- rp2.delete();
423
- }
424
-
425
- // An endpoint is FLOATING (ConnEnd at the shape centre) unless the edge
426
- // gives a fixed connection point — a directed ShapeConnectionPin makes
427
- // the route leave/enter the anchor perpendicular to the shape edge
428
- // instead of cutting through it — a snap-point set (draw.io's
429
- // snapToPoint: the shape's declared connection points as candidate
430
- // anchors, one non-exclusive directed pin each, sharing ONE pin class
431
- // so libavoid picks the anchor that routes best) — or a side MASK (the
432
- // legacy mxGraph portConstraint vocabulary): pins spread along every
433
- // allowed side (maskPinPoints), all sharing ONE pin class, so libavoid
434
- // picks the side and the position that route best. Snap/mask pins are
435
- // non-exclusive: several edges may share a pin and nudging separates
436
- // their corridors, like the render-time router spreading floating
437
- // attaches. Precedence per end mirrors the renderer: a fixed
438
- // constraint (pinned exit/entry) wins over both, and a snap-point set
439
- // wins over a mask (the snapToPoint branch of
440
- // updateFloatingTerminalPoint runs before any mask enforcement).
441
- function makeEnd(vid, b, constraint, sides, points, freePoint)
442
- {
443
- // Dangling end: no vertex bounds, just a free point (the edge's
444
- // unconnected endpoint). Route to a plain Point ConnEnd — no shape,
445
- // no direction — exactly as the warm-session drag preview does, so a
446
- // committed edge with an unconnected end matches its live preview.
447
- if (b == null)
448
- {
449
- var dp = new Avoid.Point(freePoint.x, freePoint.y);
450
- var dce = new Avoid.ConnEnd(dp);
451
- dp.delete();
452
- return dce;
453
- }
454
-
455
- if (constraint != null && constraint.dir != null && shapeRefs[vid] != null)
456
- {
457
- pinClass++;
458
- new Avoid.ShapeConnectionPin(shapeRefs[vid], pinClass,
459
- constraint.x, constraint.y, true, 0, constraint.dir);
460
- return new Avoid.ConnEnd(shapeRefs[vid], pinClass);
461
- }
462
-
463
- if (points != null && points.length > 0 && shapeRefs[vid] != null)
464
- {
465
- pinClass++;
466
-
467
- for (var q = 0; q < points.length; q++)
468
- {
469
- // Registers itself with the ShapeRef (freed with the
470
- // router), like the mask pins below.
471
- var spin = new Avoid.ShapeConnectionPin(shapeRefs[vid],
472
- pinClass, points[q].x, points[q].y, true, 0,
473
- (points[q].dir != null) ? points[q].dir : AvoidRouting.DIR.all);
474
- spin.setExclusive(false);
475
- }
476
-
477
- return new Avoid.ConnEnd(shapeRefs[vid], pinClass);
478
- }
479
-
480
- if (sides != null && sides.length > 0 && shapeRefs[vid] != null)
481
- {
482
- pinClass++;
483
-
484
- for (var s = 0; s < sides.length; s++)
485
- {
486
- var pts = AvoidRouting.maskPinPoints(sides[s], b);
487
-
488
- for (var p = 0; p < pts.length; p++)
489
- {
490
- // The pin registers itself with the ShapeRef (freed
491
- // with the router); the wrapper is only kept long
492
- // enough to make it shareable.
493
- var pin = new Avoid.ShapeConnectionPin(shapeRefs[vid],
494
- pinClass, pts[p].x, pts[p].y, true, 0, pts[p].dir);
495
- pin.setExclusive(false);
496
- }
497
- }
498
-
499
- return new Avoid.ConnEnd(shapeRefs[vid], pinClass);
500
- }
501
-
502
- // Free the temporary centre Point (copied into the ConnEnd); the
503
- // ConnEnd itself is freed by the ConnRef caller below.
504
- var cp = new Avoid.Point(b.x + b.w / 2, b.y + b.h / 2);
505
- var ce = new Avoid.ConnEnd(cp);
506
- cp.delete();
507
-
508
- return ce;
509
- }
510
-
511
- // Endpoint anchor: the free point for a dangling end, else the fixed
512
- // connection point, else the shape centre — the point the ConnEnd routes
513
- // from.
514
- function anchor(b, constraint, freePoint)
515
- {
516
- if (b == null)
517
- {
518
- return freePoint;
519
- }
520
-
521
- return (constraint != null) ?
522
- {x: b.x + constraint.x * b.w, y: b.y + constraint.y * b.h} :
523
- {x: b.x + b.w / 2, y: b.y + b.h / 2};
524
- }
525
-
526
- // Wrap a stub tip as a routing checkpoint. Checkpoint copies the Point
527
- // and the vector copies the Checkpoint — free both embind wrappers.
528
- function addCheckpoint(vec, p)
529
- {
530
- if (p != null)
531
- {
532
- var pt = new Avoid.Point(p.x, p.y);
533
- var cp = new Avoid.Checkpoint(pt);
534
- vec.push_back(cp);
535
- cp.delete();
536
- pt.delete();
537
- }
538
- }
539
-
540
- var conns = [];
541
-
542
- for (i = 0; i < edges.length; i++)
543
- {
544
- var e = edges[i];
545
-
546
- if (e == null)
547
- {
548
- continue;
549
- }
550
-
551
- var sb = bounds[e.source];
552
- var tb = bounds[e.target];
553
-
554
- // An end is either a known vertex (sb/tb) or a dangling free point
555
- // (e.sourcePoint / e.targetPoint, absolute coords) for an unconnected
556
- // endpoint. Skip only when an end is neither.
557
- if ((sb == null && e.sourcePoint == null) ||
558
- (tb == null && e.targetPoint == null))
559
- {
560
- continue;
561
- }
562
-
563
- // Self-loops are out of scope: obstacle avoidance between identical
564
- // endpoints is meaningless and the orthogonal router degenerates to
565
- // a buffer-sized hook beside the shape. Skipping leaves the caller's
566
- // loop styling and waypoints untouched (an edge with no route entry
567
- // is never written), e.g. draw.io's inner-loop-waypoint self-loops.
568
- if (e.source != null && e.source === e.target)
569
- {
570
- continue;
571
- }
572
-
573
- // ConnRef copies the ConnEnds, so free them after construction
574
- // (they are not owned by the router).
575
- var se = makeEnd(e.source, sb, e.sourceConstraint, e.sourceSides, e.sourcePoints, e.sourcePoint);
576
- var de = makeEnd(e.target, tb, e.targetConstraint, e.targetSides, e.targetPoints, e.targetPoint);
577
- var conn = new Avoid.ConnRef(router, se, de);
578
- se.delete();
579
- de.delete();
580
-
581
- // Jetty stubs: force the route through a checkpoint jetty px
582
- // outward of each constrained anchor, so the first/last segment is
583
- // at least that long. Capped per end to the clearance the terminal
584
- // pair's gap allows along the stub's axis (cappedJetty). Skipped
585
- // for ends where there is nothing to enforce (jettyStub), and for
586
- // the whole edge when the anchors are closer than the summed
587
- // stubs — the same too-short guard as mxEdgeStyle.OrthConnector —
588
- // so a short edge isn't forced to double back through its
589
- // checkpoints. The stubs are computed here but requested LAZILY
590
- // after the first solve (see below).
591
- var sourceJetty = AvoidRouting.cappedJetty(e.sourceJetty, e.sourceConstraint, sb, tb);
592
- var targetJetty = AvoidRouting.cappedJetty(e.targetJetty, e.targetConstraint, tb, sb);
593
- var sa = anchor(sb, e.sourceConstraint, e.sourcePoint);
594
- var ta = anchor(tb, e.targetConstraint, e.targetPoint);
595
-
596
- // A pinned anchor buried inside ANOTHER obstacle (a shape dragged
597
- // over the terminal) puts the whole route into libavoid's escape /
598
- // degenerate mode: checkpoints on such a route are unreachable and
599
- // libavoid warns ("skipping checkpoint") for EVERY end — including
600
- // tips in perfectly clear space — before discarding them. Skip BOTH
601
- // stubs for the edge instead (verified against the WASM: same
602
- // route, no console noise).
603
- var buried = (e.sourceConstraint != null &&
604
- AvoidRouting.insideAny(sa.x, sa.y, vertices)) ||
605
- (e.targetConstraint != null &&
606
- AvoidRouting.insideAny(ta.x, ta.y, vertices));
607
-
608
- var scp = buried ? null :
609
- AvoidRouting.jettyStub(e.sourceConstraint, sourceJetty, sb, vertices, buffer);
610
- var tcp = buried ? null :
611
- AvoidRouting.jettyStub(e.targetConstraint, targetJetty, tb, vertices, buffer);
612
-
613
- if (scp != null || tcp != null)
614
- {
615
- var dx = ta.x - sa.x;
616
- var dy = ta.y - sa.y;
617
- var total = ((scp != null) ? sourceJetty : 0) +
618
- ((tcp != null) ? targetJetty : 0);
619
-
620
- if (dx * dx + dy * dy < total * total)
621
- {
622
- scp = null;
623
- tcp = null;
624
- }
625
- }
626
-
627
- conns.push({id: e.id, conn: conn, scp: scp, tcp: tcp,
628
- sourceJetty: sourceJetty, targetJetty: targetJetty});
629
- }
630
-
631
- if (conns.length === 0)
632
- {
633
- router.delete();
634
- return out;
635
- }
636
-
637
- router.processTransaction();
638
-
639
- // LAZY jetty enforcement: the first solve runs WITHOUT the stub
640
- // checkpoints; they are requested only for edges whose natural route
641
- // violates a jetty minimum, and the transaction re-processed once. A
642
- // checkpoint is a hard point the route must touch: when libavoid
643
- // TURNS at one instead of crossing it mid-segment, the adjacent bend
644
- // is pinned at the stub tip and nudging cannot center the segment in
645
- // its channel — lopsided lead-outs (e.g. 30/10 on one edge next to a
646
- // centered 20/20 twin) on routes that met the minimum for free.
647
- // Enforcing lazily keeps the nudged, evenly distributed route
648
- // wherever it already satisfies the jetty, and falls back to the
649
- // checkpointed solve only where it does not.
650
- function endSegment(route, atStart)
651
- {
652
- var n = route.size();
653
-
654
- if (n < 2)
655
- {
656
- // Degenerate route; checkpoints cannot improve it.
657
- return Infinity;
658
- }
659
-
660
- var a = route.at(atStart ? 0 : n - 1);
661
- var b = route.at(atStart ? 1 : n - 2);
662
- var horizontal = Math.abs(b.x - a.x) >= Math.abs(b.y - a.y);
663
- var len = 0;
664
-
665
- // Merge collinear raw points (libavoid may split a straight
666
- // lead-out): the run ends at the first point that leaves the
667
- // terminal's row/column.
668
- for (var k = atStart ? 1 : n - 2; k >= 0 && k < n; k += atStart ? 1 : -1)
669
- {
670
- var p = route.at(k);
671
-
672
- if (Math.abs(horizontal ? p.y - a.y : p.x - a.x) > 0.5)
673
- {
674
- break;
675
- }
676
-
677
- len = Math.abs(horizontal ? p.x - a.x : p.y - a.y);
678
- }
679
-
680
- return len;
681
- }
682
-
683
- var dirty = false;
684
-
685
- for (i = 0; i < conns.length; i++)
686
- {
687
- var c = conns[i];
688
-
689
- if (c.scp == null && c.tcp == null)
690
- {
691
- continue;
692
- }
693
-
694
- // 0.5px tolerance: sub-pixel misses vanish in the output rounding
695
- // and do not warrant pinning the route to the checkpoints.
696
- var r0 = c.conn.displayRoute();
697
-
698
- if ((c.scp != null && endSegment(r0, true) < c.sourceJetty - 0.5) ||
699
- (c.tcp != null && endSegment(r0, false) < c.targetJetty - 0.5))
700
- {
701
- // In route order (source first); setRoutingCheckpoints copies
702
- // the vector, so free the wrapper.
703
- var cps = new Avoid.CheckpointVector();
704
- addCheckpoint(cps, c.scp);
705
- addCheckpoint(cps, c.tcp);
706
- c.conn.setRoutingCheckpoints(cps);
707
- cps.delete();
708
- dirty = true;
709
- }
710
- }
711
-
712
- if (dirty)
713
- {
714
- router.processTransaction();
715
- }
716
-
717
- for (i = 0; i < conns.length; i++)
718
- {
719
- var route = conns[i].conn.displayRoute();
720
- var n = route.size();
721
- var wps = [];
722
-
723
- if (n >= 2)
724
- {
725
- var pts = [];
726
- var k;
727
-
728
- for (k = 0; k < n; k++)
729
- {
730
- var p = route.at(k);
731
- pts.push({x: p.x, y: p.y});
732
- }
733
-
734
- for (k = 1; k < n - 1; k++)
735
- {
736
- if (collinear(pts[k - 1], pts[k], pts[k + 1]))
737
- {
738
- continue;
739
- }
740
-
741
- wps.push({x: Math.round(pts[k].x), y: Math.round(pts[k].y)});
742
- }
743
- }
744
-
745
- out[conns[i].id] = wps;
746
- }
747
-
748
- router.delete();
749
- return out;
750
- };
751
-
752
- globalThis.AvoidRouting = AvoidRouting;
753
- })();
1
+ /*
2
+ * libavoid-routing.js — the draw.io routing core over the libavoid solver.
3
+ * Hand-authored; engine-agnostic (takes the `Avoid` namespace as a parameter,
4
+ * so it works whether the host loads libavoid as WASM or pure JS) and never
5
+ * changes with the libavoid build.
6
+ *
7
+ * CANONICAL SOURCE: this file (drawio-dev js/libavoid-js/). drawio-mcp vendors
8
+ * verbatim copies (mcp-app-server/vendor/libavoid/ and
9
+ * mcp-tool-server/vendor/libavoid/) — copy this file over when it changes.
10
+ *
11
+ * A plain script defining globalThis.AvoidRouting, so the SAME artifact is:
12
+ * - concatenated into extensions.min.js here (glue → wasm → loader → this →
13
+ * the Closure-compiled LibavoidRouting editor binding),
14
+ * - inlined into (or CDN-loaded by) the drawio-mcp app-server viewer,
15
+ * - side-effect imported from Node by the drawio-mcp tool server
16
+ * (import the file, then read globalThis.AvoidRouting).
17
+ * It does not depend on the glue/loader — every entry point takes the Avoid
18
+ * namespace as a parameter — and must stay dependency-free (no mxGraph, no
19
+ * DOM): anything editor-specific belongs in js/diagramly/LibavoidRouting.js.
20
+ *
21
+ * libavoid API gotchas baked in here (empirically verified against the
22
+ * vendored build, libavoid-js 0.5.0-beta.5):
23
+ * - Router flag is an integer: RouterFlag.OrthogonalRouting.value
24
+ * - setRoutingParameter takes the enum OBJECT (RoutingParameter.x), not
25
+ * .value (the integer silently no-ops and routes run flush against boxes)
26
+ * - cleanup is router.delete() (embind), not Avoid.destroy()
27
+ * - Point/Rectangle/ConnEnd/Checkpoint(+vector) are embind wrappers COPIED
28
+ * into the objects they parameterize and NOT freed by router.delete();
29
+ * free each temporary with .delete()
30
+ * - a directed ShapeConnectionPin gives the route NO minimum straight
31
+ * lead-out (it may turn at the anchor and run flush along the shape); a
32
+ * minimum stub needs a routing checkpoint at the stub tip. Checkpoint
33
+ * DIRECTION flags have an inverted vertical convention in this build —
34
+ * use plain 1-arg checkpoints only.
35
+ */
36
+ (function()
37
+ {
38
+ var AvoidRouting = {};
39
+
40
+ // libavoid ConnDirFlags (the enum isn't exposed by the JS bindings): a
41
+ // bitmask of the directions a connector may approach an endpoint.
42
+ AvoidRouting.DIR = {up: 1, down: 2, left: 4, right: 8, all: 15};
43
+
44
+ /**
45
+ * Clamp to [0,1] — the ShapeConnectionPin proportional-offset domain.
46
+ */
47
+ AvoidRouting.clamp01 = function(v)
48
+ {
49
+ return (v < 0) ? 0 : ((v > 1) ? 1 : v);
50
+ };
51
+
52
+ /**
53
+ * ConnDirFlags for a proportional connection point: perpendicular to the
54
+ * nearest shape edge (x<=0 left, x>=1 right, y<=0 up, y>=1 down), else any
55
+ * direction.
56
+ */
57
+ AvoidRouting.dirForPoint = function(x, y)
58
+ {
59
+ var DIR = AvoidRouting.DIR;
60
+ var d = 0;
61
+
62
+ if (x <= 0) { d |= DIR.left; }
63
+ else if (x >= 1) { d |= DIR.right; }
64
+
65
+ if (y <= 0) { d |= DIR.up; }
66
+ else if (y >= 1) { d |= DIR.down; }
67
+
68
+ return (d != 0) ? d : DIR.all;
69
+ };
70
+
71
+ /**
72
+ * A fixed connection point as a routing constraint {x, y, dir}: the
73
+ * proportional position clamped to [0,1] (some shapes' connection points
74
+ * sit slightly outside, e.g. a hexagon tip at y=-0.017, which
75
+ * ShapeConnectionPin rejects) plus the ConnDirFlags direction derived from
76
+ * the ORIGINAL values so it still points off the correct edge. Returns
77
+ * null when either coordinate is missing/not a number (floating endpoint).
78
+ */
79
+ AvoidRouting.constraintForPoint = function(x, y)
80
+ {
81
+ if (x == null || y == null || isNaN(x) || isNaN(y))
82
+ {
83
+ return null;
84
+ }
85
+
86
+ return {x: AvoidRouting.clamp01(x), y: AvoidRouting.clamp01(y),
87
+ dir: AvoidRouting.dirForPoint(x, y)};
88
+ };
89
+
90
+ /**
91
+ * Proportional pin points for one masked side of bounds b ({x,y,w,h}):
92
+ * one pin per ~20px of side length (1..9, spread evenly so a single pin
93
+ * sits at the side midpoint), each directed off its side. Side letters
94
+ * follow the mxGraph port-constraint vocabulary (N/S/E/W). Used by
95
+ * computeRoutes' sourceSides/targetSides handling — the pins of all
96
+ * allowed sides share one pin class, so libavoid picks the side AND the
97
+ * position that route best.
98
+ */
99
+ AvoidRouting.maskPinPoints = function(side, b)
100
+ {
101
+ var DIR = AvoidRouting.DIR;
102
+ var horizontal = (side == 'N' || side == 'S');
103
+ var len = horizontal ? b.w : b.h;
104
+ var n = Math.max(1, Math.min(9, Math.round(len / 20)));
105
+ var pts = [];
106
+
107
+ for (var i = 0; i < n; i++)
108
+ {
109
+ var f = (i + 0.5) / n;
110
+
111
+ pts.push((side == 'N') ? {x: f, y: 0, dir: DIR.up} :
112
+ (side == 'S') ? {x: f, y: 1, dir: DIR.down} :
113
+ (side == 'W') ? {x: 0, y: f, dir: DIR.left} :
114
+ {x: 1, y: f, dir: DIR.right});
115
+ }
116
+
117
+ return pts;
118
+ };
119
+
120
+ /**
121
+ * True when the point lies strictly inside any of the obstacles. A pinned
122
+ * anchor sits ON its own shape's boundary, so the strict test never
123
+ * matches the point's own shape.
124
+ */
125
+ AvoidRouting.insideAny = function(x, y, obstacles)
126
+ {
127
+ if (obstacles != null)
128
+ {
129
+ for (var i = 0; i < obstacles.length; i++)
130
+ {
131
+ var v = obstacles[i];
132
+
133
+ if (v != null && x > v.x && x < v.x + v.w && y > v.y && y < v.y + v.h)
134
+ {
135
+ return true;
136
+ }
137
+ }
138
+ }
139
+
140
+ return false;
141
+ };
142
+
143
+ /**
144
+ * The stub tip a jetty checkpoint pins the route through: the constrained
145
+ * anchor on bounds b ({x,y,w,h}), moved jetty px outward along the pin's
146
+ * single ConnDirFlags direction. Returns {x, y} or null when there is
147
+ * nothing to enforce: no constraint, non-positive jetty, an ambiguous
148
+ * direction (corner/interior anchors carry 2+ direction bits), or a tip
149
+ * inside an obstacle ({x,y,w,h} array) INFLATED by the solve's buffer —
150
+ * libavoid's orthogonal visibility network stays clear of the buffered
151
+ * zones, so a checkpoint there is unreachable and libavoid warns
152
+ * ("Warning: skipping checkpoint for connector ...") on the console and
153
+ * ignores it; not requesting it keeps the route identical without the
154
+ * noise. The stub's OWN shape (matched by bounds) is exempt from the
155
+ * inflation — the pin's lead-out segment crosses its own buffer zone
156
+ * legitimately (verified against the WASM: no warning there).
157
+ */
158
+ AvoidRouting.jettyStub = function(constraint, jetty, b, obstacles, buffer)
159
+ {
160
+ if (constraint == null || !(jetty > 0))
161
+ {
162
+ return null;
163
+ }
164
+
165
+ var d = constraint.dir;
166
+ var DIR = AvoidRouting.DIR;
167
+
168
+ if (d != DIR.up && d != DIR.down && d != DIR.left && d != DIR.right)
169
+ {
170
+ return null;
171
+ }
172
+
173
+ var x = b.x + constraint.x * b.w +
174
+ ((d == DIR.right) ? jetty : ((d == DIR.left) ? -jetty : 0));
175
+ var y = b.y + constraint.y * b.h +
176
+ ((d == DIR.down) ? jetty : ((d == DIR.up) ? -jetty : 0));
177
+
178
+ if (obstacles != null)
179
+ {
180
+ var inflate = (buffer > 0) ? buffer : 0;
181
+
182
+ for (var i = 0; i < obstacles.length; i++)
183
+ {
184
+ var v = obstacles[i];
185
+
186
+ if (v == null ||
187
+ (v.x == b.x && v.y == b.y && v.w == b.w && v.h == b.h))
188
+ {
189
+ continue;
190
+ }
191
+
192
+ if (x > v.x - inflate && x < v.x + v.w + inflate &&
193
+ y > v.y - inflate && y < v.y + v.h + inflate)
194
+ {
195
+ return null;
196
+ }
197
+ }
198
+ }
199
+
200
+ return {x: x, y: y};
201
+ };
202
+
203
+ /**
204
+ * One end's jetty capped to the clearance toward the OTHER terminal along
205
+ * the stub's own axis: half the directed gap (stub's base side to the
206
+ * facing side of the other shape) minus the 4px routing channel — so two
207
+ * facing stubs can never cross and a capped tip lands on the corridor
208
+ * channel's boundary; an uncapped stub in a tight gap puts its checkpoint
209
+ * past the channel and libavoid answers with a self-overlapping hairpin
210
+ * that nudging splays into flat side-loops. No cap when the end floats,
211
+ * the anchor is a corner/interior one (2+ direction bits — jettyStub
212
+ * skips those ends anyway), the stub points away from the other shape, or
213
+ * the shapes overlap along the stub's axis: such a stub never enters the
214
+ * pair's corridor, so a narrow gap along the OTHER axis must not shorten
215
+ * it (only the center segment squeezes through that gap, e.g. an L around
216
+ * a 15px horizontal gap keeps its full vertical lead-out).
217
+ */
218
+ AvoidRouting.cappedJetty = function(jetty, constraint, from, to)
219
+ {
220
+ // A dangling (free-point) end has no bounds to measure the gap against.
221
+ if (from == null || to == null)
222
+ {
223
+ return jetty;
224
+ }
225
+
226
+ var DIR = AvoidRouting.DIR;
227
+ var d = (constraint != null) ? constraint.dir : 0;
228
+ var dgap = (d == DIR.down) ? to.y - (from.y + from.h) :
229
+ ((d == DIR.up) ? from.y - (to.y + to.h) :
230
+ ((d == DIR.right) ? to.x - (from.x + from.w) :
231
+ ((d == DIR.left) ? from.x - (to.x + to.w) : 0)));
232
+
233
+ if (dgap > 0)
234
+ {
235
+ var half = Math.max(2, Math.floor((dgap - 4) / 2));
236
+
237
+ if (jetty > half)
238
+ {
239
+ return half;
240
+ }
241
+ }
242
+
243
+ return jetty;
244
+ };
245
+
246
+ /**
247
+ * Obstacles minus ENCLOSING shapes: a shape whose bounds fully contain a
248
+ * routed edge's terminal (and that is not itself a terminal of one of the
249
+ * edges) is the container/pool/group the terminal lives IN, not something
250
+ * to route around. Registering it starves the solve of corridors (both
251
+ * endpoints sit inside a hard obstacle, so routes escape it or degenerate)
252
+ * and suppresses the terminals' jetty stubs (jettyStub refuses tips inside
253
+ * any obstacle). Model ancestry is unknown here (this file is model-free),
254
+ * so containment is geometric: rect-contains-rect, inclusive. Terminals of
255
+ * the routed edges are never dropped (their ShapeRefs carry the connection
256
+ * pins). Returns a filtered copy; the input arrays are not modified.
257
+ */
258
+ AvoidRouting.filterEnclosing = function(vertices, edges)
259
+ {
260
+ if (vertices == null || edges == null || edges.length === 0)
261
+ {
262
+ return vertices;
263
+ }
264
+
265
+ var terminalIds = Object.create(null);
266
+ var i;
267
+
268
+ for (i = 0; i < edges.length; i++)
269
+ {
270
+ if (edges[i] != null)
271
+ {
272
+ terminalIds[edges[i].source] = true;
273
+ terminalIds[edges[i].target] = true;
274
+ }
275
+ }
276
+
277
+ var terminals = [];
278
+
279
+ for (i = 0; i < vertices.length; i++)
280
+ {
281
+ var v = vertices[i];
282
+
283
+ if (v != null && terminalIds[v.id] === true)
284
+ {
285
+ terminals.push(v);
286
+ }
287
+ }
288
+
289
+ var out = [];
290
+
291
+ for (i = 0; i < vertices.length; i++)
292
+ {
293
+ var v = vertices[i];
294
+
295
+ if (v == null)
296
+ {
297
+ continue;
298
+ }
299
+
300
+ var enclosing = false;
301
+
302
+ if (terminalIds[v.id] !== true)
303
+ {
304
+ for (var j = 0; j < terminals.length; j++)
305
+ {
306
+ var t = terminals[j];
307
+
308
+ if (v.x <= t.x && v.y <= t.y &&
309
+ v.x + v.w >= t.x + t.w && v.y + v.h >= t.y + t.h)
310
+ {
311
+ enclosing = true;
312
+ break;
313
+ }
314
+ }
315
+ }
316
+
317
+ if (!enclosing)
318
+ {
319
+ out.push(v);
320
+ }
321
+ }
322
+
323
+ return out;
324
+ };
325
+
326
+ /**
327
+ * Compute obstacle-avoiding orthogonal routes for a set of edges.
328
+ *
329
+ * @param {object} Avoid - the libavoid instance (AvoidLib.getInstance()).
330
+ * @param {Array<{id:string,x:number,y:number,w:number,h:number}>} vertices
331
+ * Obstacles, in ABSOLUTE coordinates. Shapes enclosing a terminal
332
+ * of a routed edge are dropped (filterEnclosing).
333
+ * @param {Array<{id,source,target,sourcePoint?,targetPoint?,
334
+ * sourceConstraint?,targetConstraint?,
335
+ * sourcePoints?,targetPoints?,sourceSides?,targetSides?,
336
+ * sourceJetty?,targetJetty?}>} edges
337
+ * Edges referencing vertex ids. An end may instead be a DANGLING free
338
+ * point — sourcePoint/targetPoint {x,y} in absolute coords, used when
339
+ * that terminal has no vertex (an unconnected endpoint): it routes to
340
+ * a plain Point ConnEnd (no shape, no direction), matching the
341
+ * drag-preview, and takes no constraint/pin/mask/jetty. An edge is
342
+ * skipped when an end is neither a vertex nor a free point, and
343
+ * when it is a SELF-LOOP (source === target) — loops keep the
344
+ * caller's own loop styling and waypoints.
345
+ * *Constraint = {x,y,dir} (constraintForPoint): a
346
+ * fixed connection point routed via a directed ShapeConnectionPin;
347
+ * absent => the endpoint floats at the shape centre. *Points = an
348
+ * array of {x,y,dir} candidate anchors (draw.io's snapToPoint —
349
+ * the shape's declared connection points): non-exclusive directed
350
+ * pins at each candidate under one pin class, libavoid picks the
351
+ * anchor that routes best; beaten by a *Constraint on the same end
352
+ * (an explicit anchor wins) and beats a *Sides mask (the renderer's
353
+ * snapToPoint branch runs before mask enforcement). *Sides = an
354
+ * array of allowed attach sides ('N'/'S'/'E'/'W', the legacy
355
+ * mxGraph portConstraint masks): non-exclusive pins spread along
356
+ * the allowed sides under one pin class, libavoid picks side and
357
+ * position. *Jetty = the minimum length of the first/last
358
+ * segment in px (draw.io's jettySize), enforced for ends with a
359
+ * *Constraint by a routing checkpoint at the stub tip — requested
360
+ * lazily, only for edges whose natural route falls short of the
361
+ * minimum; snapped and masked ends behave like floating ones here
362
+ * (their first bend naturally sits ~shapeBufferDistance out).
363
+ * @param {{shapeBufferDistance?:number,idealNudgingDistance?:number}} [opts]
364
+ * Defaults: 16 / 14.
365
+ * @returns {Object<string, Array<{x:number,y:number}>>} edge id -> interior
366
+ * bend points (ABSOLUTE, collinear-filtered). The first/last route
367
+ * points (endpoints) are dropped — a floating endpoint connects at
368
+ * the shape side midpoint, which is where a floating
369
+ * orthogonalEdgeStyle endpoint lands anyway. An edge with a
370
+ * straight (bend-free) route maps to [].
371
+ */
372
+ AvoidRouting.computeRoutes = function(Avoid, vertices, edges, opts)
373
+ {
374
+ var out = Object.create(null);
375
+
376
+ if (Avoid == null || vertices == null || edges == null)
377
+ {
378
+ return out;
379
+ }
380
+
381
+ var buffer = (opts && opts.shapeBufferDistance != null) ? opts.shapeBufferDistance : 16;
382
+ var nudge = (opts && opts.idealNudgingDistance != null) ? opts.idealNudgingDistance : 14;
383
+
384
+ // Containers the terminals live in are not obstacles (also feeds the
385
+ // jettyStub checks below, so stubs inside a container are preserved).
386
+ vertices = AvoidRouting.filterEnclosing(vertices, edges);
387
+
388
+ function collinear(a, b, c)
389
+ {
390
+ // Zero cross product (1px tolerance) => b lies on segment a..c (redundant).
391
+ return Math.abs((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)) < 1;
392
+ }
393
+
394
+ var router = new Avoid.Router(Avoid.RouterFlag.OrthogonalRouting.value);
395
+
396
+ try { router.setRoutingParameter(Avoid.RoutingParameter.shapeBufferDistance, buffer); } catch (e) {}
397
+ try { router.setRoutingParameter(Avoid.RoutingParameter.idealNudgingDistance, nudge); } catch (e) {}
398
+
399
+ var bounds = Object.create(null);
400
+ var shapeRefs = Object.create(null);
401
+ var pinClass = 0;
402
+ var i;
403
+
404
+ for (i = 0; i < vertices.length; i++)
405
+ {
406
+ var v = vertices[i];
407
+
408
+ if (v == null || v.id == null || !(v.w > 0) || !(v.h > 0))
409
+ {
410
+ continue;
411
+ }
412
+
413
+ bounds[v.id] = v;
414
+ // Point/Rectangle are embind value wrappers that COPY into the
415
+ // ShapeRef and are NOT freed by router.delete(); free the temporaries.
416
+ var rp1 = new Avoid.Point(v.x, v.y);
417
+ var rp2 = new Avoid.Point(v.x + v.w, v.y + v.h);
418
+ var rect = new Avoid.Rectangle(rp1, rp2);
419
+ shapeRefs[v.id] = new Avoid.ShapeRef(router, rect);
420
+ rect.delete();
421
+ rp1.delete();
422
+ rp2.delete();
423
+ }
424
+
425
+ // An endpoint is FLOATING (ConnEnd at the shape centre) unless the edge
426
+ // gives a fixed connection point — a directed ShapeConnectionPin makes
427
+ // the route leave/enter the anchor perpendicular to the shape edge
428
+ // instead of cutting through it — a snap-point set (draw.io's
429
+ // snapToPoint: the shape's declared connection points as candidate
430
+ // anchors, one non-exclusive directed pin each, sharing ONE pin class
431
+ // so libavoid picks the anchor that routes best) — or a side MASK (the
432
+ // legacy mxGraph portConstraint vocabulary): pins spread along every
433
+ // allowed side (maskPinPoints), all sharing ONE pin class, so libavoid
434
+ // picks the side and the position that route best. Snap/mask pins are
435
+ // non-exclusive: several edges may share a pin and nudging separates
436
+ // their corridors, like the render-time router spreading floating
437
+ // attaches. Precedence per end mirrors the renderer: a fixed
438
+ // constraint (pinned exit/entry) wins over both, and a snap-point set
439
+ // wins over a mask (the snapToPoint branch of
440
+ // updateFloatingTerminalPoint runs before any mask enforcement).
441
+ function makeEnd(vid, b, constraint, sides, points, freePoint)
442
+ {
443
+ // Dangling end: no vertex bounds, just a free point (the edge's
444
+ // unconnected endpoint). Route to a plain Point ConnEnd — no shape,
445
+ // no direction — exactly as the warm-session drag preview does, so a
446
+ // committed edge with an unconnected end matches its live preview.
447
+ if (b == null)
448
+ {
449
+ var dp = new Avoid.Point(freePoint.x, freePoint.y);
450
+ var dce = new Avoid.ConnEnd(dp);
451
+ dp.delete();
452
+ return dce;
453
+ }
454
+
455
+ if (constraint != null && constraint.dir != null && shapeRefs[vid] != null)
456
+ {
457
+ pinClass++;
458
+ new Avoid.ShapeConnectionPin(shapeRefs[vid], pinClass,
459
+ constraint.x, constraint.y, true, 0, constraint.dir);
460
+ return new Avoid.ConnEnd(shapeRefs[vid], pinClass);
461
+ }
462
+
463
+ if (points != null && points.length > 0 && shapeRefs[vid] != null)
464
+ {
465
+ pinClass++;
466
+
467
+ for (var q = 0; q < points.length; q++)
468
+ {
469
+ // Registers itself with the ShapeRef (freed with the
470
+ // router), like the mask pins below.
471
+ var spin = new Avoid.ShapeConnectionPin(shapeRefs[vid],
472
+ pinClass, points[q].x, points[q].y, true, 0,
473
+ (points[q].dir != null) ? points[q].dir : AvoidRouting.DIR.all);
474
+ spin.setExclusive(false);
475
+ }
476
+
477
+ return new Avoid.ConnEnd(shapeRefs[vid], pinClass);
478
+ }
479
+
480
+ if (sides != null && sides.length > 0 && shapeRefs[vid] != null)
481
+ {
482
+ pinClass++;
483
+
484
+ for (var s = 0; s < sides.length; s++)
485
+ {
486
+ var pts = AvoidRouting.maskPinPoints(sides[s], b);
487
+
488
+ for (var p = 0; p < pts.length; p++)
489
+ {
490
+ // The pin registers itself with the ShapeRef (freed
491
+ // with the router); the wrapper is only kept long
492
+ // enough to make it shareable.
493
+ var pin = new Avoid.ShapeConnectionPin(shapeRefs[vid],
494
+ pinClass, pts[p].x, pts[p].y, true, 0, pts[p].dir);
495
+ pin.setExclusive(false);
496
+ }
497
+ }
498
+
499
+ return new Avoid.ConnEnd(shapeRefs[vid], pinClass);
500
+ }
501
+
502
+ // Free the temporary centre Point (copied into the ConnEnd); the
503
+ // ConnEnd itself is freed by the ConnRef caller below.
504
+ var cp = new Avoid.Point(b.x + b.w / 2, b.y + b.h / 2);
505
+ var ce = new Avoid.ConnEnd(cp);
506
+ cp.delete();
507
+
508
+ return ce;
509
+ }
510
+
511
+ // Endpoint anchor: the free point for a dangling end, else the fixed
512
+ // connection point, else the shape centre — the point the ConnEnd routes
513
+ // from.
514
+ function anchor(b, constraint, freePoint)
515
+ {
516
+ if (b == null)
517
+ {
518
+ return freePoint;
519
+ }
520
+
521
+ return (constraint != null) ?
522
+ {x: b.x + constraint.x * b.w, y: b.y + constraint.y * b.h} :
523
+ {x: b.x + b.w / 2, y: b.y + b.h / 2};
524
+ }
525
+
526
+ // Wrap a stub tip as a routing checkpoint. Checkpoint copies the Point
527
+ // and the vector copies the Checkpoint — free both embind wrappers.
528
+ function addCheckpoint(vec, p)
529
+ {
530
+ if (p != null)
531
+ {
532
+ var pt = new Avoid.Point(p.x, p.y);
533
+ var cp = new Avoid.Checkpoint(pt);
534
+ vec.push_back(cp);
535
+ cp.delete();
536
+ pt.delete();
537
+ }
538
+ }
539
+
540
+ var conns = [];
541
+
542
+ for (i = 0; i < edges.length; i++)
543
+ {
544
+ var e = edges[i];
545
+
546
+ if (e == null)
547
+ {
548
+ continue;
549
+ }
550
+
551
+ var sb = bounds[e.source];
552
+ var tb = bounds[e.target];
553
+
554
+ // An end is either a known vertex (sb/tb) or a dangling free point
555
+ // (e.sourcePoint / e.targetPoint, absolute coords) for an unconnected
556
+ // endpoint. Skip only when an end is neither.
557
+ if ((sb == null && e.sourcePoint == null) ||
558
+ (tb == null && e.targetPoint == null))
559
+ {
560
+ continue;
561
+ }
562
+
563
+ // Self-loops are out of scope: obstacle avoidance between identical
564
+ // endpoints is meaningless and the orthogonal router degenerates to
565
+ // a buffer-sized hook beside the shape. Skipping leaves the caller's
566
+ // loop styling and waypoints untouched (an edge with no route entry
567
+ // is never written), e.g. draw.io's inner-loop-waypoint self-loops.
568
+ if (e.source != null && e.source === e.target)
569
+ {
570
+ continue;
571
+ }
572
+
573
+ // ConnRef copies the ConnEnds, so free them after construction
574
+ // (they are not owned by the router).
575
+ var se = makeEnd(e.source, sb, e.sourceConstraint, e.sourceSides, e.sourcePoints, e.sourcePoint);
576
+ var de = makeEnd(e.target, tb, e.targetConstraint, e.targetSides, e.targetPoints, e.targetPoint);
577
+ var conn = new Avoid.ConnRef(router, se, de);
578
+ se.delete();
579
+ de.delete();
580
+
581
+ // Jetty stubs: force the route through a checkpoint jetty px
582
+ // outward of each constrained anchor, so the first/last segment is
583
+ // at least that long. Capped per end to the clearance the terminal
584
+ // pair's gap allows along the stub's axis (cappedJetty). Skipped
585
+ // for ends where there is nothing to enforce (jettyStub), and for
586
+ // the whole edge when the anchors are closer than the summed
587
+ // stubs — the same too-short guard as mxEdgeStyle.OrthConnector —
588
+ // so a short edge isn't forced to double back through its
589
+ // checkpoints. The stubs are computed here but requested LAZILY
590
+ // after the first solve (see below).
591
+ var sourceJetty = AvoidRouting.cappedJetty(e.sourceJetty, e.sourceConstraint, sb, tb);
592
+ var targetJetty = AvoidRouting.cappedJetty(e.targetJetty, e.targetConstraint, tb, sb);
593
+ var sa = anchor(sb, e.sourceConstraint, e.sourcePoint);
594
+ var ta = anchor(tb, e.targetConstraint, e.targetPoint);
595
+
596
+ // A pinned anchor buried inside ANOTHER obstacle (a shape dragged
597
+ // over the terminal) puts the whole route into libavoid's escape /
598
+ // degenerate mode: checkpoints on such a route are unreachable and
599
+ // libavoid warns ("skipping checkpoint") for EVERY end — including
600
+ // tips in perfectly clear space — before discarding them. Skip BOTH
601
+ // stubs for the edge instead (verified against the WASM: same
602
+ // route, no console noise).
603
+ var buried = (e.sourceConstraint != null &&
604
+ AvoidRouting.insideAny(sa.x, sa.y, vertices)) ||
605
+ (e.targetConstraint != null &&
606
+ AvoidRouting.insideAny(ta.x, ta.y, vertices));
607
+
608
+ var scp = buried ? null :
609
+ AvoidRouting.jettyStub(e.sourceConstraint, sourceJetty, sb, vertices, buffer);
610
+ var tcp = buried ? null :
611
+ AvoidRouting.jettyStub(e.targetConstraint, targetJetty, tb, vertices, buffer);
612
+
613
+ if (scp != null || tcp != null)
614
+ {
615
+ var dx = ta.x - sa.x;
616
+ var dy = ta.y - sa.y;
617
+ var total = ((scp != null) ? sourceJetty : 0) +
618
+ ((tcp != null) ? targetJetty : 0);
619
+
620
+ if (dx * dx + dy * dy < total * total)
621
+ {
622
+ scp = null;
623
+ tcp = null;
624
+ }
625
+ }
626
+
627
+ conns.push({id: e.id, conn: conn, scp: scp, tcp: tcp,
628
+ sourceJetty: sourceJetty, targetJetty: targetJetty});
629
+ }
630
+
631
+ if (conns.length === 0)
632
+ {
633
+ router.delete();
634
+ return out;
635
+ }
636
+
637
+ router.processTransaction();
638
+
639
+ // LAZY jetty enforcement: the first solve runs WITHOUT the stub
640
+ // checkpoints; they are requested only for edges whose natural route
641
+ // violates a jetty minimum, and the transaction re-processed once. A
642
+ // checkpoint is a hard point the route must touch: when libavoid
643
+ // TURNS at one instead of crossing it mid-segment, the adjacent bend
644
+ // is pinned at the stub tip and nudging cannot center the segment in
645
+ // its channel — lopsided lead-outs (e.g. 30/10 on one edge next to a
646
+ // centered 20/20 twin) on routes that met the minimum for free.
647
+ // Enforcing lazily keeps the nudged, evenly distributed route
648
+ // wherever it already satisfies the jetty, and falls back to the
649
+ // checkpointed solve only where it does not.
650
+ function endSegment(route, atStart)
651
+ {
652
+ var n = route.size();
653
+
654
+ if (n < 2)
655
+ {
656
+ // Degenerate route; checkpoints cannot improve it.
657
+ return Infinity;
658
+ }
659
+
660
+ var a = route.at(atStart ? 0 : n - 1);
661
+ var b = route.at(atStart ? 1 : n - 2);
662
+ var horizontal = Math.abs(b.x - a.x) >= Math.abs(b.y - a.y);
663
+ var len = 0;
664
+
665
+ // Merge collinear raw points (libavoid may split a straight
666
+ // lead-out): the run ends at the first point that leaves the
667
+ // terminal's row/column.
668
+ for (var k = atStart ? 1 : n - 2; k >= 0 && k < n; k += atStart ? 1 : -1)
669
+ {
670
+ var p = route.at(k);
671
+
672
+ if (Math.abs(horizontal ? p.y - a.y : p.x - a.x) > 0.5)
673
+ {
674
+ break;
675
+ }
676
+
677
+ len = Math.abs(horizontal ? p.x - a.x : p.y - a.y);
678
+ }
679
+
680
+ return len;
681
+ }
682
+
683
+ var dirty = false;
684
+
685
+ for (i = 0; i < conns.length; i++)
686
+ {
687
+ var c = conns[i];
688
+
689
+ if (c.scp == null && c.tcp == null)
690
+ {
691
+ continue;
692
+ }
693
+
694
+ // 0.5px tolerance: sub-pixel misses vanish in the output rounding
695
+ // and do not warrant pinning the route to the checkpoints.
696
+ var r0 = c.conn.displayRoute();
697
+
698
+ if ((c.scp != null && endSegment(r0, true) < c.sourceJetty - 0.5) ||
699
+ (c.tcp != null && endSegment(r0, false) < c.targetJetty - 0.5))
700
+ {
701
+ // In route order (source first); setRoutingCheckpoints copies
702
+ // the vector, so free the wrapper.
703
+ var cps = new Avoid.CheckpointVector();
704
+ addCheckpoint(cps, c.scp);
705
+ addCheckpoint(cps, c.tcp);
706
+ c.conn.setRoutingCheckpoints(cps);
707
+ cps.delete();
708
+ dirty = true;
709
+ }
710
+ }
711
+
712
+ if (dirty)
713
+ {
714
+ router.processTransaction();
715
+ }
716
+
717
+ for (i = 0; i < conns.length; i++)
718
+ {
719
+ var route = conns[i].conn.displayRoute();
720
+ var n = route.size();
721
+ var wps = [];
722
+
723
+ if (n >= 2)
724
+ {
725
+ var pts = [];
726
+ var k;
727
+
728
+ for (k = 0; k < n; k++)
729
+ {
730
+ var p = route.at(k);
731
+ pts.push({x: p.x, y: p.y});
732
+ }
733
+
734
+ for (k = 1; k < n - 1; k++)
735
+ {
736
+ if (collinear(pts[k - 1], pts[k], pts[k + 1]))
737
+ {
738
+ continue;
739
+ }
740
+
741
+ wps.push({x: Math.round(pts[k].x), y: Math.round(pts[k].y)});
742
+ }
743
+ }
744
+
745
+ out[conns[i].id] = wps;
746
+ }
747
+
748
+ router.delete();
749
+ return out;
750
+ };
751
+
752
+ globalThis.AvoidRouting = AvoidRouting;
753
+ })();