@itsthecsguy/linework 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +11 -0
  3. package/README.md +164 -2
  4. package/assets/drawio/LICENSE +201 -0
  5. package/assets/drawio/VERSION +1 -0
  6. package/assets/drawio/favicon.ico +0 -0
  7. package/assets/drawio/images/1x1.png +0 -0
  8. package/assets/drawio/images/2x2.png +0 -0
  9. package/assets/drawio/images/3x3.png +0 -0
  10. package/assets/drawio/images/ajax-loader.gif +0 -0
  11. package/assets/drawio/images/android-chrome-512x512.png +0 -0
  12. package/assets/drawio/images/apple-touch-icon.png +0 -0
  13. package/assets/drawio/images/aui-wait.gif +0 -0
  14. package/assets/drawio/images/broken.png +0 -0
  15. package/assets/drawio/images/browserconfig.xml +9 -0
  16. package/assets/drawio/images/close.png +0 -0
  17. package/assets/drawio/images/cloud-connector-atlas.png +0 -0
  18. package/assets/drawio/images/collapsed.gif +0 -0
  19. package/assets/drawio/images/delete.png +0 -0
  20. package/assets/drawio/images/document-google-drive-icon-80.png +0 -0
  21. package/assets/drawio/images/draw-search.png +0 -0
  22. package/assets/drawio/images/drawio.svg +19 -0
  23. package/assets/drawio/images/drawlogo-color.svg +50 -0
  24. package/assets/drawio/images/drawlogo-gray.svg +18 -0
  25. package/assets/drawio/images/drawlogo-text-bottom.svg +75 -0
  26. package/assets/drawio/images/drawlogo.svg +18 -0
  27. package/assets/drawio/images/drawlogo128.png +0 -0
  28. package/assets/drawio/images/drawlogo144.png +0 -0
  29. package/assets/drawio/images/drawlogo256.png +0 -0
  30. package/assets/drawio/images/drawlogo48-gray.png +0 -0
  31. package/assets/drawio/images/drawlogo48.png +0 -0
  32. package/assets/drawio/images/drawlogo80.png +0 -0
  33. package/assets/drawio/images/droptarget.png +0 -0
  34. package/assets/drawio/images/edit.gif +0 -0
  35. package/assets/drawio/images/edit16.png +0 -0
  36. package/assets/drawio/images/edit32.png +0 -0
  37. package/assets/drawio/images/edit80.png +0 -0
  38. package/assets/drawio/images/embed-icon.png +0 -0
  39. package/assets/drawio/images/expanded.gif +0 -0
  40. package/assets/drawio/images/facebook.png +0 -0
  41. package/assets/drawio/images/favicon-16x16.png +0 -0
  42. package/assets/drawio/images/favicon-32x32.png +0 -0
  43. package/assets/drawio/images/file.png +0 -0
  44. package/assets/drawio/images/folder.png +0 -0
  45. package/assets/drawio/images/gadget-screenshot.png +0 -0
  46. package/assets/drawio/images/gadget-thumb.png +0 -0
  47. package/assets/drawio/images/github-logo-white.svg +6 -0
  48. package/assets/drawio/images/github-logo.svg +17 -0
  49. package/assets/drawio/images/gitlab-logo-white.svg +1 -0
  50. package/assets/drawio/images/gitlab-logo.svg +1 -0
  51. package/assets/drawio/images/glyphicons_blogger.png +0 -0
  52. package/assets/drawio/images/glyphicons_facebook.png +0 -0
  53. package/assets/drawio/images/glyphicons_github.png +0 -0
  54. package/assets/drawio/images/glyphicons_google.png +0 -0
  55. package/assets/drawio/images/glyphicons_star.png +0 -0
  56. package/assets/drawio/images/glyphicons_twitter.png +0 -0
  57. package/assets/drawio/images/glyphicons_youtube.png +0 -0
  58. package/assets/drawio/images/google-drive-connector-atlas.png +0 -0
  59. package/assets/drawio/images/google-drive-connector-atlas.svg +121 -0
  60. package/assets/drawio/images/google-drive-logo-white.svg +9 -0
  61. package/assets/drawio/images/google-drive-logo.svg +17 -0
  62. package/assets/drawio/images/google-share.png +0 -0
  63. package/assets/drawio/images/icon-192-maskable.png +0 -0
  64. package/assets/drawio/images/icon-192.png +0 -0
  65. package/assets/drawio/images/icon-512-maskable.png +0 -0
  66. package/assets/drawio/images/icon-512.png +0 -0
  67. package/assets/drawio/images/icon-search.svg +21 -0
  68. package/assets/drawio/images/img-hi-res.png +0 -0
  69. package/assets/drawio/images/img-lo-res.png +0 -0
  70. package/assets/drawio/images/list.svg +16 -0
  71. package/assets/drawio/images/locked.png +0 -0
  72. package/assets/drawio/images/log.png +0 -0
  73. package/assets/drawio/images/logo-confluence.png +0 -0
  74. package/assets/drawio/images/logo-flat-small.png +0 -0
  75. package/assets/drawio/images/logo-flat.png +0 -0
  76. package/assets/drawio/images/logo-jira.png +0 -0
  77. package/assets/drawio/images/logo-small.gif +0 -0
  78. package/assets/drawio/images/logo-white.png +0 -0
  79. package/assets/drawio/images/logo.png +0 -0
  80. package/assets/drawio/images/manifest.json +25 -0
  81. package/assets/drawio/images/move.png +0 -0
  82. package/assets/drawio/images/notion-logo-white.svg +1 -0
  83. package/assets/drawio/images/notion-logo.svg +1 -0
  84. package/assets/drawio/images/onedrive-connector-atlas.png +0 -0
  85. package/assets/drawio/images/onedrive-connector-atlas.svg +248 -0
  86. package/assets/drawio/images/onedrive-logo-white.svg +29 -0
  87. package/assets/drawio/images/onedrive-logo.svg +29 -0
  88. package/assets/drawio/images/osa_database.png +0 -0
  89. package/assets/drawio/images/osa_drive-harddisk.png +0 -0
  90. package/assets/drawio/images/plant-uml-atlas.png +0 -0
  91. package/assets/drawio/images/samepage-icon-color.svg +37 -0
  92. package/assets/drawio/images/save.png +0 -0
  93. package/assets/drawio/images/sidebar-android.png +0 -0
  94. package/assets/drawio/images/sidebar-archimate.png +0 -0
  95. package/assets/drawio/images/sidebar-arrows.png +0 -0
  96. package/assets/drawio/images/sidebar-arrows2.png +0 -0
  97. package/assets/drawio/images/sidebar-basic.png +0 -0
  98. package/assets/drawio/images/sidebar-c4.png +0 -0
  99. package/assets/drawio/images/sidebar-cabinets.png +0 -0
  100. package/assets/drawio/images/sidebar-cumulus.png +0 -0
  101. package/assets/drawio/images/sidebar-dfd.png +0 -0
  102. package/assets/drawio/images/sidebar-dynamics365.png +0 -0
  103. package/assets/drawio/images/sidebar-eip.png +0 -0
  104. package/assets/drawio/images/sidebar-er.png +0 -0
  105. package/assets/drawio/images/sidebar-floorplans.png +0 -0
  106. package/assets/drawio/images/sidebar-flowchart.png +0 -0
  107. package/assets/drawio/images/sidebar-gcp3.png +0 -0
  108. package/assets/drawio/images/sidebar-general.png +0 -0
  109. package/assets/drawio/images/sidebar-kubernetes.png +0 -0
  110. package/assets/drawio/images/sidebar-leanmapping.png +0 -0
  111. package/assets/drawio/images/sidebar-mockups.png +0 -0
  112. package/assets/drawio/images/sidebar-network.png +0 -0
  113. package/assets/drawio/images/sidebar-network2.png +0 -0
  114. package/assets/drawio/images/sidebar-openstack.png +0 -0
  115. package/assets/drawio/images/sidebar-sitemap.png +0 -0
  116. package/assets/drawio/images/sidebar-threatmodeling.png +0 -0
  117. package/assets/drawio/images/sidebar-uml.png +0 -0
  118. package/assets/drawio/images/sketch-logo128.png +0 -0
  119. package/assets/drawio/images/spin.gif +0 -0
  120. package/assets/drawio/images/sprites.png +0 -0
  121. package/assets/drawio/images/stop-flat-icon-80.png +0 -0
  122. package/assets/drawio/images/submenu.gif +0 -0
  123. package/assets/drawio/images/touch-submenu.png +0 -0
  124. package/assets/drawio/images/transparent.gif +0 -0
  125. package/assets/drawio/images/trello-logo-white-orange.svg +6 -0
  126. package/assets/drawio/images/trello-logo-white.svg +6 -0
  127. package/assets/drawio/images/trello-logo.svg +1 -0
  128. package/assets/drawio/images/tweet.png +0 -0
  129. package/assets/drawio/images/update16.png +0 -0
  130. package/assets/drawio/images/update32.png +0 -0
  131. package/assets/drawio/images/update80.png +0 -0
  132. package/assets/drawio/images/youtube.png +0 -0
  133. package/assets/drawio/img/clipart/Battery_0_128x128.png +0 -0
  134. package/assets/drawio/img/clipart/Battery_100_128x128.png +0 -0
  135. package/assets/drawio/img/clipart/Battery_50_128x128.png +0 -0
  136. package/assets/drawio/img/clipart/Battery_75_128x128.png +0 -0
  137. package/assets/drawio/img/clipart/Battery_allstates_128x128.png +0 -0
  138. package/assets/drawio/img/clipart/Bluetooth_128x128.png +0 -0
  139. package/assets/drawio/img/clipart/Earth_globe_128x128.png +0 -0
  140. package/assets/drawio/img/clipart/Empty_Folder_128x128.png +0 -0
  141. package/assets/drawio/img/clipart/Full_Folder_128x128.png +0 -0
  142. package/assets/drawio/img/clipart/Gear_128x128.png +0 -0
  143. package/assets/drawio/img/clipart/Keys_128x128.png +0 -0
  144. package/assets/drawio/img/clipart/Lock_128x128.png +0 -0
  145. package/assets/drawio/img/clipart/Mouse_Pointer_128x128.png +0 -0
  146. package/assets/drawio/img/clipart/Plug_128x128.png +0 -0
  147. package/assets/drawio/img/clipart/Ships_Wheel_128x128.png +0 -0
  148. package/assets/drawio/img/clipart/Star_128x128.png +0 -0
  149. package/assets/drawio/img/clipart/Tire_128x128.png +0 -0
  150. package/assets/drawio/index.html +92 -0
  151. package/assets/drawio/js/PostConfig.js +7 -0
  152. package/assets/drawio/js/PreConfig.js +13 -0
  153. package/assets/drawio/js/app.min.js +18466 -0
  154. package/assets/drawio/js/bootstrap.js +428 -0
  155. package/assets/drawio/js/diagramly/LibavoidRouting.js +4459 -0
  156. package/assets/drawio/js/export-init.js +21 -0
  157. package/assets/drawio/js/extensions.min.js +10478 -0
  158. package/assets/drawio/js/libavoid-js/LICENSE +505 -0
  159. package/assets/drawio/js/libavoid-js/libavoid-routing.js +1633 -0
  160. package/assets/drawio/js/libavoid-js/libavoid.min.js +59 -0
  161. package/assets/drawio/js/main.js +22 -0
  162. package/assets/drawio/js/rough/rough.min.js +1 -0
  163. package/assets/drawio/js/shapes-14-6-5.min.js +3457 -0
  164. package/assets/drawio/js/stencils.min.js +447 -0
  165. package/assets/drawio/mxgraph/css/common.css +147 -0
  166. package/assets/drawio/resources/dia.txt +2901 -0
  167. package/assets/drawio/styles/dark-default.xml +109 -0
  168. package/assets/drawio/styles/default-old.xml +109 -0
  169. package/assets/drawio/styles/default.xml +203 -0
  170. package/assets/drawio/styles/fonts/ArchitectsDaughter-Regular.ttf +0 -0
  171. package/assets/drawio/styles/grapheditor.css +4501 -0
  172. package/assets/drawio/styles/high-contrast.css +128 -0
  173. package/assets/drawio/styles/thumb_horz.png +0 -0
  174. package/assets/drawio/styles/thumb_vertical.png +0 -0
  175. package/assets/fonts/ibm-plex-mono-LICENSE.txt +93 -0
  176. package/assets/fonts/ibm-plex-sans-LICENSE.txt +93 -0
  177. package/assets/fonts/kalam-400.woff2 +0 -0
  178. package/assets/fonts/kalam-700.woff2 +0 -0
  179. package/assets/fonts/kalam-LICENSE.txt +93 -0
  180. package/assets/fonts/plex-mono-400.woff2 +0 -0
  181. package/assets/fonts/plex-mono-600.woff2 +0 -0
  182. package/assets/fonts/plex-sans-400.woff2 +0 -0
  183. package/assets/fonts/plex-sans-600.woff2 +0 -0
  184. package/dist/api.js +181 -0
  185. package/dist/cli.js +262 -0
  186. package/dist/drawio/icons.js +121 -0
  187. package/dist/drawio/merge.js +309 -0
  188. package/dist/drawio/read.js +180 -0
  189. package/dist/drawio/show.js +501 -0
  190. package/dist/drawio/theme.js +425 -0
  191. package/dist/drawio/write.js +300 -0
  192. package/dist/edit/server.js +212 -0
  193. package/dist/git.js +46 -0
  194. package/dist/import/mermaid.js +569 -0
  195. package/dist/layout/layout.js +212 -0
  196. package/dist/layout/sequence.js +333 -0
  197. package/dist/layout/text.js +73 -0
  198. package/dist/lint/check.js +184 -0
  199. package/dist/lint/fixlabels.js +128 -0
  200. package/dist/mcp/server.js +239 -0
  201. package/dist/publish.js +121 -0
  202. package/dist/render/browser.js +210 -0
  203. package/dist/render/fonts.js +66 -0
  204. package/dist/report.js +36 -0
  205. package/dist/spec/model.js +53 -0
  206. package/dist/spec/parse.js +497 -0
  207. package/package.json +71 -4
  208. package/skills/linework/SKILL.md +214 -0
  209. package/skills/linework/examples/billing-er.drawio.svg +4 -0
  210. package/skills/linework/examples/billing-er.yaml +31 -0
  211. package/skills/linework/examples/checkout.drawio.svg +4 -0
  212. package/skills/linework/examples/checkout.yaml +28 -0
  213. package/skills/linework/examples/pay-sequence.drawio.svg +4 -0
  214. package/skills/linework/examples/pay-sequence.yaml +28 -0
  215. package/skills/linework/examples/payment-state.drawio.svg +4 -0
  216. package/skills/linework/examples/payment-state.yaml +22 -0
  217. package/skills/linework/examples/refund-flow.drawio.svg +4 -0
  218. package/skills/linework/examples/refund-flow.yaml +29 -0
  219. package/skills/linework/examples/serverless-aws.drawio.svg +4 -0
  220. package/skills/linework/examples/serverless-aws.yaml +23 -0
  221. package/skills/linework/references/google-docs.md +46 -0
  222. package/skills/linework/references/spec.md +312 -0
  223. package/skills/linework/references/style-guide.md +53 -0
@@ -0,0 +1,1633 @@
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
+ * - displayRoute() and PolyLine.at() return a fresh OWNED copy per call in
31
+ * this build, but an unowned reference in draw.io's own binding
32
+ * (drawio-libavoid) — read routes through readRoute, which frees exactly
33
+ * the copies. A leaked copy changes later routes: libavoid breaks ties
34
+ * by object address, and leaks move where the next solve allocates
35
+ * - a directed ShapeConnectionPin gives the route NO minimum straight
36
+ * lead-out (it may turn at the anchor and run flush along the shape); a
37
+ * minimum stub needs a routing checkpoint at the stub tip. Checkpoint
38
+ * DIRECTION flags have an inverted vertical convention in this build —
39
+ * use plain 1-arg checkpoints only.
40
+ * - setRoutingCheckpoints does not queue the connector for rerouting in
41
+ * draw.io's own build (drawio-libavoid; the libavoid-js WASM build
42
+ * does), so a processTransaction after it keeps the old route: set an
43
+ * end of the connector again (setSourceEndpoint) to queue it
44
+ */
45
+ (function()
46
+ {
47
+ var AvoidRouting = {};
48
+
49
+ // libavoid ConnDirFlags (the enum isn't exposed by the JS bindings): a
50
+ // bitmask of the directions a connector may approach an endpoint.
51
+ AvoidRouting.DIR = {up: 1, down: 2, left: 4, right: 8, all: 15};
52
+
53
+ /**
54
+ * Clamp to [0,1] — the ShapeConnectionPin proportional-offset domain.
55
+ */
56
+ AvoidRouting.clamp01 = function(v)
57
+ {
58
+ return (v < 0) ? 0 : ((v > 1) ? 1 : v);
59
+ };
60
+
61
+ /**
62
+ * ConnDirFlags for a proportional connection point: perpendicular to the
63
+ * nearest shape edge (x<=0 left, x>=1 right, y<=0 up, y>=1 down), else any
64
+ * direction.
65
+ */
66
+ AvoidRouting.dirForPoint = function(x, y)
67
+ {
68
+ var DIR = AvoidRouting.DIR;
69
+ var d = 0;
70
+
71
+ if (x <= 0) { d |= DIR.left; }
72
+ else if (x >= 1) { d |= DIR.right; }
73
+
74
+ if (y <= 0) { d |= DIR.up; }
75
+ else if (y >= 1) { d |= DIR.down; }
76
+
77
+ return (d != 0) ? d : DIR.all;
78
+ };
79
+
80
+ /**
81
+ * A fixed connection point as a routing constraint {x, y, dir}: the
82
+ * proportional position clamped to [0,1] (some shapes' connection points
83
+ * sit slightly outside, e.g. a hexagon tip at y=-0.017, which
84
+ * ShapeConnectionPin rejects) plus the ConnDirFlags direction derived from
85
+ * the ORIGINAL values so it still points off the correct edge. perimeter
86
+ * is the edge's exitPerimeter/entryPerimeter (false only when set to 0;
87
+ * see shapePin). Returns null when either coordinate is missing/not a
88
+ * number (floating endpoint).
89
+ */
90
+ AvoidRouting.constraintForPoint = function(x, y, perimeter)
91
+ {
92
+ if (x == null || y == null || isNaN(x) || isNaN(y))
93
+ {
94
+ return null;
95
+ }
96
+
97
+ var c = {x: AvoidRouting.clamp01(x), y: AvoidRouting.clamp01(y),
98
+ dir: AvoidRouting.dirForPoint(x, y)};
99
+
100
+ // Only false matters (exitPerimeter=0): flips then move the point
101
+ // on a transformed shape (shapePin).
102
+ if (perimeter === false)
103
+ {
104
+ c.perimeter = false;
105
+ }
106
+
107
+ return c;
108
+ };
109
+
110
+ /**
111
+ * Proportional pin points for one masked side of bounds b ({x,y,w,h}):
112
+ * one pin per ~20px of side length (1..9, spread evenly so a single pin
113
+ * sits at the side midpoint), each directed off its side. Side letters
114
+ * follow the mxGraph port-constraint vocabulary (N/S/E/W). Used by
115
+ * computeRoutes' sourceSides/targetSides handling — the pins of all
116
+ * allowed sides share one pin class, so libavoid picks the side AND the
117
+ * position that route best.
118
+ */
119
+ AvoidRouting.maskPinPoints = function(side, b)
120
+ {
121
+ var DIR = AvoidRouting.DIR;
122
+ var horizontal = (side == 'N' || side == 'S');
123
+ var len = horizontal ? b.w : b.h;
124
+ var n = Math.max(1, Math.min(9, Math.round(len / 20)));
125
+ var pts = [];
126
+
127
+ for (var i = 0; i < n; i++)
128
+ {
129
+ var f = (i + 0.5) / n;
130
+
131
+ pts.push((side == 'N') ? {x: f, y: 0, dir: DIR.up} :
132
+ (side == 'S') ? {x: f, y: 1, dir: DIR.down} :
133
+ (side == 'W') ? {x: 0, y: f, dir: DIR.left} :
134
+ {x: 1, y: f, dir: DIR.right});
135
+ }
136
+
137
+ return pts;
138
+ };
139
+
140
+ /**
141
+ * True when the point lies strictly inside any of the obstacles. A pinned
142
+ * anchor sits ON its own shape's boundary, so the strict test never
143
+ * matches the point's own shape.
144
+ */
145
+ AvoidRouting.insideAny = function(x, y, obstacles)
146
+ {
147
+ if (obstacles != null)
148
+ {
149
+ for (var i = 0; i < obstacles.length; i++)
150
+ {
151
+ var v = obstacles[i];
152
+
153
+ if (v != null && x > v.x && x < v.x + v.w && y > v.y && y < v.y + v.h)
154
+ {
155
+ return true;
156
+ }
157
+ }
158
+ }
159
+
160
+ return false;
161
+ };
162
+
163
+ /**
164
+ * The stub tip a jetty checkpoint pins the route through: the constrained
165
+ * anchor on bounds b ({x,y,w,h}), moved jetty px outward along the pin's
166
+ * single ConnDirFlags direction. Returns {x, y} or null when there is
167
+ * nothing to enforce: no constraint, non-positive jetty, an ambiguous
168
+ * direction (corner/interior anchors carry 2+ direction bits), or a tip
169
+ * inside an obstacle ({x,y,w,h} array) INFLATED by the solve's buffer —
170
+ * libavoid's orthogonal visibility network stays clear of the buffered
171
+ * zones, so a checkpoint there is unreachable and libavoid warns
172
+ * ("Warning: skipping checkpoint for connector ...") on the console and
173
+ * ignores it; not requesting it keeps the route identical without the
174
+ * noise. The stub's OWN shape (matched by bounds) is exempt from the
175
+ * inflation — the pin's lead-out segment crosses its own buffer zone
176
+ * legitimately (verified against the WASM: no warning there).
177
+ */
178
+ AvoidRouting.jettyStub = function(constraint, jetty, b, obstacles, buffer)
179
+ {
180
+ if (constraint == null || !(jetty > 0))
181
+ {
182
+ return null;
183
+ }
184
+
185
+ var d = constraint.dir;
186
+ var DIR = AvoidRouting.DIR;
187
+
188
+ if (d != DIR.up && d != DIR.down && d != DIR.left && d != DIR.right)
189
+ {
190
+ return null;
191
+ }
192
+
193
+ var x = b.x + constraint.x * b.w +
194
+ ((d == DIR.right) ? jetty : ((d == DIR.left) ? -jetty : 0));
195
+ var y = b.y + constraint.y * b.h +
196
+ ((d == DIR.down) ? jetty : ((d == DIR.up) ? -jetty : 0));
197
+
198
+ if (obstacles != null)
199
+ {
200
+ var inflate = (buffer > 0) ? buffer : 0;
201
+
202
+ for (var i = 0; i < obstacles.length; i++)
203
+ {
204
+ var v = obstacles[i];
205
+
206
+ if (v == null ||
207
+ (v.x == b.x && v.y == b.y && v.w == b.w && v.h == b.h))
208
+ {
209
+ continue;
210
+ }
211
+
212
+ if (x > v.x - inflate && x < v.x + v.w + inflate &&
213
+ y > v.y - inflate && y < v.y + v.h + inflate)
214
+ {
215
+ return null;
216
+ }
217
+ }
218
+ }
219
+
220
+ return {x: x, y: y};
221
+ };
222
+
223
+ /**
224
+ * One end's jetty capped to the clearance toward the OTHER terminal along
225
+ * the stub's own axis: half the directed gap (stub's base side to the
226
+ * facing side of the other shape) minus the 4px routing channel — so two
227
+ * facing stubs can never cross and a capped tip lands on the corridor
228
+ * channel's boundary; an uncapped stub in a tight gap puts its checkpoint
229
+ * past the channel and libavoid answers with a self-overlapping hairpin
230
+ * that nudging splays into flat side-loops. No cap when the end floats,
231
+ * the anchor is a corner/interior one (2+ direction bits — jettyStub
232
+ * skips those ends anyway), the stub points away from the other shape, or
233
+ * the shapes overlap along the stub's axis: such a stub never enters the
234
+ * pair's corridor, so a narrow gap along the OTHER axis must not shorten
235
+ * it (only the center segment squeezes through that gap, e.g. an L around
236
+ * a 15px horizontal gap keeps its full vertical lead-out).
237
+ */
238
+ AvoidRouting.cappedJetty = function(jetty, constraint, from, to)
239
+ {
240
+ // A dangling (free-point) end has no bounds to measure the gap against.
241
+ if (from == null || to == null)
242
+ {
243
+ return jetty;
244
+ }
245
+
246
+ var DIR = AvoidRouting.DIR;
247
+ var d = (constraint != null) ? constraint.dir : 0;
248
+ var dgap = (d == DIR.down) ? to.y - (from.y + from.h) :
249
+ ((d == DIR.up) ? from.y - (to.y + to.h) :
250
+ ((d == DIR.right) ? to.x - (from.x + from.w) :
251
+ ((d == DIR.left) ? from.x - (to.x + to.w) : 0)));
252
+
253
+ if (dgap > 0)
254
+ {
255
+ var half = Math.max(2, Math.floor((dgap - 4) / 2));
256
+
257
+ if (jetty > half)
258
+ {
259
+ return half;
260
+ }
261
+ }
262
+
263
+ return jetty;
264
+ };
265
+
266
+ /**
267
+ * Obstacles minus ENCLOSING shapes: a shape whose bounds fully contain a
268
+ * routed edge's terminal (and that is not itself a terminal of one of the
269
+ * edges) is the container/pool/group the terminal lives IN, not something
270
+ * to route around. Registering it starves the solve of corridors (both
271
+ * endpoints sit inside a hard obstacle, so routes escape it or degenerate)
272
+ * and suppresses the terminals' jetty stubs (jettyStub refuses tips inside
273
+ * any obstacle). Model ancestry is unknown here (this file is model-free),
274
+ * so containment is geometric: rect-contains-rect, inclusive. Terminals of
275
+ * the routed edges are never dropped (their ShapeRefs carry the connection
276
+ * pins). Returns a filtered copy; the input arrays are not modified.
277
+ */
278
+ AvoidRouting.filterEnclosing = function(vertices, edges)
279
+ {
280
+ if (vertices == null || edges == null || edges.length === 0)
281
+ {
282
+ return vertices;
283
+ }
284
+
285
+ var terminalIds = Object.create(null);
286
+ var i;
287
+
288
+ for (i = 0; i < edges.length; i++)
289
+ {
290
+ if (edges[i] != null)
291
+ {
292
+ terminalIds[edges[i].source] = true;
293
+ terminalIds[edges[i].target] = true;
294
+ }
295
+ }
296
+
297
+ var terminals = [];
298
+
299
+ for (i = 0; i < vertices.length; i++)
300
+ {
301
+ var v = vertices[i];
302
+
303
+ if (v != null && terminalIds[v.id] === true)
304
+ {
305
+ terminals.push(v);
306
+ }
307
+ }
308
+
309
+ var out = [];
310
+
311
+ for (i = 0; i < vertices.length; i++)
312
+ {
313
+ var v = vertices[i];
314
+
315
+ if (v == null)
316
+ {
317
+ continue;
318
+ }
319
+
320
+ var enclosing = false;
321
+
322
+ if (terminalIds[v.id] !== true)
323
+ {
324
+ for (var j = 0; j < terminals.length; j++)
325
+ {
326
+ var t = terminals[j];
327
+
328
+ if (v.x <= t.x && v.y <= t.y &&
329
+ v.x + v.w >= t.x + t.w && v.y + v.h >= t.y + t.h)
330
+ {
331
+ enclosing = true;
332
+ break;
333
+ }
334
+ }
335
+ }
336
+
337
+ if (!enclosing)
338
+ {
339
+ out.push(v);
340
+ }
341
+ }
342
+
343
+ return out;
344
+ };
345
+
346
+ /**
347
+ * The transform a shape's style applies to its connection points, from a
348
+ * plain map of draw.io style values (a parsed style or getCellStyle
349
+ * result): rotation (degrees, clockwise), direction (north/south/west turn
350
+ * the shape inside its bounds), flipH/flipV — plus the legacy
351
+ * stencilFlipH/stencilFlipV when the shape is a stencil (stencil = true),
352
+ * and anchorPointDirection=0, which keeps the connection points still
353
+ * under a direction. Returns null for an untransformed shape. Set it as a
354
+ * vertex's `frame` (computeRoutes, obstacleBounds, shapePin).
355
+ */
356
+ AvoidRouting.shapeFrame = function(style, stencil)
357
+ {
358
+ if (style == null)
359
+ {
360
+ return null;
361
+ }
362
+
363
+ var rotation = parseFloat(style.rotation);
364
+ rotation = isNaN(rotation) ? 0 : ((rotation % 360) + 360) % 360;
365
+ var direction = (style.direction == 'north' || style.direction == 'south' ||
366
+ style.direction == 'west') ? style.direction : null;
367
+ var flipH = style.flipH == 1 || (stencil == true && style.stencilFlipH == 1);
368
+ var flipV = style.flipV == 1 || (stencil == true && style.stencilFlipV == 1);
369
+
370
+ if (rotation == 0 && direction == null && !flipH && !flipV)
371
+ {
372
+ return null;
373
+ }
374
+
375
+ return {rotation: rotation, direction: direction, flipH: flipH, flipV: flipV,
376
+ anchorPointDirection: style.anchorPointDirection == null ||
377
+ style.anchorPointDirection == 1,
378
+ legacy: style.legacyAnchorPoints == null || style.legacyAnchorPoints == 1};
379
+ };
380
+
381
+ // {cos, sin} of a clockwise turn in degrees, EXACT for multiples of 90
382
+ // (the common case) so mapped pins land exactly on the box's sides.
383
+ function turn(degrees)
384
+ {
385
+ var r = ((degrees % 360) + 360) % 360;
386
+
387
+ return (r == 0) ? {cos: 1, sin: 0} : ((r == 90) ? {cos: 0, sin: 1} :
388
+ ((r == 180) ? {cos: -1, sin: 0} : ((r == 270) ? {cos: 0, sin: -1} :
389
+ {cos: Math.cos(r * Math.PI / 180), sin: Math.sin(r * Math.PI / 180)})));
390
+ }
391
+
392
+ /**
393
+ * The obstacle box of vertex v ({x,y,w,h,frame?}): the bounding box of its
394
+ * bounds rotated about their centre by frame.rotation (direction and flips
395
+ * turn the drawing INSIDE the bounds, so they do not move it). v itself
396
+ * when it is not rotated.
397
+ */
398
+ AvoidRouting.obstacleBounds = function(v)
399
+ {
400
+ if (v == null || v.frame == null || !v.frame.rotation)
401
+ {
402
+ return v;
403
+ }
404
+
405
+ var t = turn(v.frame.rotation);
406
+ var w = Math.abs(v.w * t.cos) + Math.abs(v.h * t.sin);
407
+ var h = Math.abs(v.w * t.sin) + Math.abs(v.h * t.cos);
408
+
409
+ return {id: v.id, x: v.x + (v.w - w) / 2, y: v.y + (v.h - h) / 2, w: w, h: h};
410
+ };
411
+
412
+ /**
413
+ * A pin {x, y, dir, perimeter?} given in vertex v's OWN frame (exitX/exitY,
414
+ * a declared connection point — proportional, dir and perimeter from
415
+ * constraintForPoint) mapped to where the shape draws it, as a pin on
416
+ * obstacleBounds(v). Follows the renderer: draw.io's default
417
+ * Graph.getLegacyConnectionPoint (frame.legacy) turns a point by the
418
+ * shape's direction and rotation and applies the flips only to a point
419
+ * NOT projected on the perimeter (perimeter false, i.e.
420
+ * exitPerimeter=0) — the projection undoes them; with
421
+ * legacyAnchorPoints=0, mxGraph.getConnectionPoint flips every point
422
+ * first. Direction north/south place the point in the bounds with width
423
+ * and height swapped (in legacy mode only while anchorPointDirection is
424
+ * on), flips swap with them. The approach direction turns with the
425
+ * point. The projection itself is not applied, as for unrotated shapes.
426
+ * Returns pin unchanged when v has no frame. At angles other than
427
+ * multiples of 90 the point lies inside the box and its direction snaps
428
+ * to the nearest axis (both at 45).
429
+ */
430
+ AvoidRouting.shapePin = function(v, pin)
431
+ {
432
+ var f = (v != null) ? v.frame : null;
433
+
434
+ if (f == null || pin == null)
435
+ {
436
+ return pin;
437
+ }
438
+
439
+ var turned = f.direction == 'north' || f.direction == 'south';
440
+ var swap = turned && (f.anchorPointDirection || !f.legacy);
441
+ var flip = !f.legacy || pin.perimeter === false;
442
+ var flipH = flip && (turned ? f.flipV : f.flipH);
443
+ var flipV = flip && (turned ? f.flipH : f.flipV);
444
+ var dx = (pin.x - 0.5) * (swap ? v.h : v.w);
445
+ var dy = (pin.y - 0.5) * (swap ? v.w : v.h);
446
+ var steps = (f.direction != null && f.anchorPointDirection) ?
447
+ ((f.direction == 'north') ? 270 : ((f.direction == 'west') ? 180 : 90)) : 0;
448
+ var t = turn(f.rotation + steps);
449
+ var b = AvoidRouting.obstacleBounds(v);
450
+
451
+ function clean(value)
452
+ {
453
+ // Float noise off the exact sides, then the pin domain.
454
+ return AvoidRouting.clamp01(Math.round(value * 1e9) / 1e9);
455
+ }
456
+
457
+ dx = flipH ? -dx : dx;
458
+ dy = flipV ? -dy : dy;
459
+
460
+ var DIR = AvoidRouting.DIR;
461
+ var bits = [[DIR.up, 0, -1], [DIR.down, 0, 1], [DIR.left, -1, 0], [DIR.right, 1, 0]];
462
+ var dir = 0;
463
+
464
+ for (var i = 0; i < bits.length; i++)
465
+ {
466
+ if ((pin.dir & bits[i][0]) != 0)
467
+ {
468
+ var vx = flipH ? -bits[i][1] : bits[i][1];
469
+ var vy = flipV ? -bits[i][2] : bits[i][2];
470
+ var rx = vx * t.cos - vy * t.sin;
471
+ var ry = vx * t.sin + vy * t.cos;
472
+
473
+ if (Math.abs(rx) >= Math.abs(ry) - 1e-9)
474
+ {
475
+ dir |= (rx > 0) ? DIR.right : DIR.left;
476
+ }
477
+
478
+ if (Math.abs(ry) >= Math.abs(rx) - 1e-9)
479
+ {
480
+ dir |= (ry > 0) ? DIR.down : DIR.up;
481
+ }
482
+ }
483
+ }
484
+
485
+ return {x: clean((v.w / 2 + dx * t.cos - dy * t.sin + (v.x - b.x)) / b.w),
486
+ y: clean((v.h / 2 + dx * t.sin + dy * t.cos + (v.y - b.y)) / b.h),
487
+ dir: (dir != 0) ? dir : pin.dir};
488
+ };
489
+
490
+ /**
491
+ * vertices and edges in the frame the solver works in: every vertex with
492
+ * a frame becomes its obstacle box, and each end on such a vertex has its
493
+ * constraint and snap points mapped (shapePin). Side masks are taken as
494
+ * already in that frame (the editor resolves them with
495
+ * portConstraintRotation). Returns new arrays; the inputs are not
496
+ * modified, and they come back as they are when nothing is transformed.
497
+ */
498
+ AvoidRouting.toWorldFrame = function(vertices, edges)
499
+ {
500
+ var framed = Object.create(null);
501
+ var boxes = [];
502
+ var any = false;
503
+ var i, k;
504
+
505
+ for (i = 0; i < vertices.length; i++)
506
+ {
507
+ var v = vertices[i];
508
+
509
+ if (v != null && v.frame != null)
510
+ {
511
+ framed[v.id] = v;
512
+ any = true;
513
+ }
514
+
515
+ boxes.push(AvoidRouting.obstacleBounds(v));
516
+ }
517
+
518
+ if (!any)
519
+ {
520
+ return {vertices: vertices, edges: edges};
521
+ }
522
+
523
+ function points(v, pts)
524
+ {
525
+ if (pts == null)
526
+ {
527
+ return pts;
528
+ }
529
+
530
+ var out = [];
531
+
532
+ for (var q = 0; q < pts.length; q++)
533
+ {
534
+ out.push(AvoidRouting.shapePin(v, {x: pts[q].x, y: pts[q].y,
535
+ dir: (pts[q].dir != null) ? pts[q].dir : AvoidRouting.DIR.all,
536
+ perimeter: pts[q].perimeter}));
537
+ }
538
+
539
+ return out;
540
+ }
541
+
542
+ var mapped = [];
543
+
544
+ for (i = 0; i < edges.length; i++)
545
+ {
546
+ var e = edges[i];
547
+ var s = (e != null) ? framed[e.source] : null;
548
+ var t = (e != null) ? framed[e.target] : null;
549
+
550
+ if (s == null && t == null)
551
+ {
552
+ mapped.push(e);
553
+ continue;
554
+ }
555
+
556
+ var c = {};
557
+
558
+ for (k in e)
559
+ {
560
+ c[k] = e[k];
561
+ }
562
+
563
+ if (s != null)
564
+ {
565
+ c.sourceConstraint = AvoidRouting.shapePin(s, e.sourceConstraint);
566
+ c.sourcePoints = points(s, e.sourcePoints);
567
+ }
568
+
569
+ if (t != null)
570
+ {
571
+ c.targetConstraint = AvoidRouting.shapePin(t, e.targetConstraint);
572
+ c.targetPoints = points(t, e.targetPoints);
573
+ }
574
+
575
+ mapped.push(c);
576
+ }
577
+
578
+ return {vertices: boxes, edges: mapped};
579
+ };
580
+
581
+ /**
582
+ * Suffix of the id of the obstacle that libavoidLabels adds for the
583
+ * outside label of a vertex.
584
+ */
585
+ AvoidRouting.LABEL_SUFFIX = '\u0000label';
586
+
587
+ /**
588
+ * Outside labels (labelPerimeter=1, mxGraphView.getLabelPerimeterSide):
589
+ * a vertex may carry label = {side, x, y, w, h}, the box of its label
590
+ * placed outside the shape on side 'N', 'S', 'E' or 'W', in the vertex's
591
+ * coordinates. Such a vertex routes as the shape grown over the label on
592
+ * that side, as wide (or tall) as the shape, plus the label's own box as
593
+ * a second obstacle, so no route crosses the label. An end on the label
594
+ * side ends at the label's far edge, where mxGraphView.getPerimeterPoint
595
+ * ends the edge: a floating end gets one pin at the middle of each side,
596
+ * the label side's at the label's far edge, and the shape's own
597
+ * connection points move with the grown box. Vertices with a frame
598
+ * (rotated, turned or flipped) and labels that don't reach beyond the
599
+ * shape are left as they are. Returns {vertices, edges, any} with new
600
+ * arrays, or the inputs and any false when no vertex has a label.
601
+ */
602
+ AvoidRouting.withOutsideLabels = function(vertices, edges)
603
+ {
604
+ var grown = Object.create(null);
605
+ var out = [];
606
+ var any = false;
607
+ var i, k;
608
+
609
+ for (i = 0; i < vertices.length; i++)
610
+ {
611
+ var v = vertices[i];
612
+ var l = (v != null && v.frame == null) ? v.label : null;
613
+ var d = (l == null) ? 0 : ((l.side == 'S') ? l.y + l.h - (v.y + v.h) :
614
+ ((l.side == 'N') ? v.y - l.y : ((l.side == 'E') ?
615
+ l.x + l.w - (v.x + v.w) : ((l.side == 'W') ? v.x - l.x : 0))));
616
+
617
+ if (!(d > 0) || !(l.w > 0) || !(l.h > 0))
618
+ {
619
+ out.push(v);
620
+ continue;
621
+ }
622
+
623
+ var g = {id: v.id, x: v.x, y: v.y, w: v.w, h: v.h};
624
+
625
+ if (l.side == 'S' || l.side == 'N')
626
+ {
627
+ g.h += d;
628
+ g.y -= (l.side == 'N') ? d : 0;
629
+ }
630
+ else
631
+ {
632
+ g.w += d;
633
+ g.x -= (l.side == 'W') ? d : 0;
634
+ }
635
+
636
+ grown[v.id] = {v: v, g: g, side: l.side};
637
+ out.push(g);
638
+ out.push({id: v.id + AvoidRouting.LABEL_SUFFIX, x: l.x, y: l.y, w: l.w, h: l.h});
639
+ any = true;
640
+ }
641
+
642
+ if (!any)
643
+ {
644
+ return {vertices: vertices, edges: edges, any: false};
645
+ }
646
+
647
+ // A pin of the shape (proportional to v) as a pin of the grown box: a
648
+ // pin on the label side's edge that is projected on the perimeter moves
649
+ // to the label's far edge, like the renderer's perimeter point.
650
+ function move(o, pin)
651
+ {
652
+ if (pin == null)
653
+ {
654
+ return pin;
655
+ }
656
+
657
+ var v = o.v, g = o.g;
658
+ var c = {};
659
+
660
+ for (var key in pin)
661
+ {
662
+ c[key] = pin[key];
663
+ }
664
+
665
+ c.x = (v.x + pin.x * v.w - g.x) / g.w;
666
+ c.y = (v.y + pin.y * v.h - g.y) / g.h;
667
+
668
+ if (pin.perimeter !== false)
669
+ {
670
+ if (o.side == 'S' && pin.y >= 1)
671
+ {
672
+ c.y = 1;
673
+ }
674
+ else if (o.side == 'N' && pin.y <= 0)
675
+ {
676
+ c.y = 0;
677
+ }
678
+ else if (o.side == 'E' && pin.x >= 1)
679
+ {
680
+ c.x = 1;
681
+ }
682
+ else if (o.side == 'W' && pin.x <= 0)
683
+ {
684
+ c.x = 0;
685
+ }
686
+ }
687
+
688
+ return c;
689
+ }
690
+
691
+ function pins(o, pts)
692
+ {
693
+ if (pts == null)
694
+ {
695
+ return pts;
696
+ }
697
+
698
+ var res = [];
699
+
700
+ for (var q = 0; q < pts.length; q++)
701
+ {
702
+ res.push(move(o, pts[q]));
703
+ }
704
+
705
+ return res;
706
+ }
707
+
708
+ // One pin at the middle of each side of the shape, the label side's at
709
+ // the label's far edge: where a floating end meets the shape.
710
+ function floating(o)
711
+ {
712
+ var DIR = AvoidRouting.DIR;
713
+ var v = o.v, g = o.g;
714
+ var cx = (v.x + v.w / 2 - g.x) / g.w;
715
+ var cy = (v.y + v.h / 2 - g.y) / g.h;
716
+
717
+ return [{x: cx, y: 0, dir: DIR.up}, {x: 1, y: cy, dir: DIR.right},
718
+ {x: cx, y: 1, dir: DIR.down}, {x: 0, y: cy, dir: DIR.left}];
719
+ }
720
+
721
+ function end(o, e, prefix, c)
722
+ {
723
+ var constraint = e[prefix + 'Constraint'];
724
+ var points = e[prefix + 'Points'];
725
+ var sides = e[prefix + 'Sides'];
726
+
727
+ if (constraint != null && constraint.dir != null)
728
+ {
729
+ c[prefix + 'Constraint'] = move(o, constraint);
730
+ }
731
+ else if (points != null && points.length > 0)
732
+ {
733
+ c[prefix + 'Points'] = pins(o, points);
734
+ }
735
+ else if (sides == null || sides.length == 0)
736
+ {
737
+ c[prefix + 'Points'] = floating(o);
738
+ }
739
+ }
740
+
741
+ var mapped = [];
742
+
743
+ for (i = 0; i < edges.length; i++)
744
+ {
745
+ var e = edges[i];
746
+ var so = (e != null) ? grown[e.source] : null;
747
+ var to = (e != null) ? grown[e.target] : null;
748
+
749
+ if (so == null && to == null)
750
+ {
751
+ mapped.push(e);
752
+ continue;
753
+ }
754
+
755
+ var c = {};
756
+
757
+ for (k in e)
758
+ {
759
+ c[k] = e[k];
760
+ }
761
+
762
+ if (so != null)
763
+ {
764
+ end(so, e, 'source', c);
765
+ }
766
+
767
+ if (to != null)
768
+ {
769
+ end(to, e, 'target', c);
770
+ }
771
+
772
+ mapped.push(c);
773
+ }
774
+
775
+ return {vertices: out, edges: mapped, any: true};
776
+ };
777
+
778
+ /**
779
+ * The candidate pins of one end on bounds b ({x,y,w,h}), as proportional
780
+ * {x, y, dir}, by the renderer's precedence: a fixed constraint (just its
781
+ * anchor) wins over a snap-point set (the shape's declared connection
782
+ * points, dir defaulting to DIR.all), which wins over a side mask (pins
783
+ * along every allowed side, maskPinPoints). Returns null for a floating
784
+ * end (none of the three).
785
+ */
786
+ AvoidRouting.endPins = function(b, constraint, sides, points)
787
+ {
788
+ if (constraint != null && constraint.dir != null)
789
+ {
790
+ return [constraint];
791
+ }
792
+
793
+ var pins = [];
794
+ var i;
795
+
796
+ if (points != null && points.length > 0)
797
+ {
798
+ for (i = 0; i < points.length; i++)
799
+ {
800
+ pins.push({x: points[i].x, y: points[i].y,
801
+ dir: (points[i].dir != null) ? points[i].dir : AvoidRouting.DIR.all});
802
+ }
803
+
804
+ return pins;
805
+ }
806
+
807
+ if (sides != null && sides.length > 0)
808
+ {
809
+ for (i = 0; i < sides.length; i++)
810
+ {
811
+ pins = pins.concat(AvoidRouting.maskPinPoints(sides[i], b));
812
+ }
813
+
814
+ return pins;
815
+ }
816
+
817
+ return null;
818
+ };
819
+
820
+ /**
821
+ * Length of a route's first (atStart) or last straight run, px: the
822
+ * lead-out a jetty minimum is checked against. route: the route's points
823
+ * ({x, y}, endpoints included). Collinear raw points are merged (libavoid
824
+ * may split a straight lead-out) — the run ends at the first point that
825
+ * leaves the terminal's row/column. Infinity for a degenerate route, which
826
+ * checkpoints cannot improve. Shared by computeRoutes' lazy jetty check and
827
+ * the editor's drag preview, so both enforce the same minimum.
828
+ */
829
+ AvoidRouting.endSegment = function(route, atStart)
830
+ {
831
+ var n = route.length;
832
+
833
+ if (n < 2)
834
+ {
835
+ return Infinity;
836
+ }
837
+
838
+ var a = route[atStart ? 0 : n - 1];
839
+ var b = route[atStart ? 1 : n - 2];
840
+ var horizontal = Math.abs(b.x - a.x) >= Math.abs(b.y - a.y);
841
+ var len = 0;
842
+
843
+ for (var k = atStart ? 1 : n - 2; k >= 0 && k < n; k += atStart ? 1 : -1)
844
+ {
845
+ var p = route[k];
846
+
847
+ if (Math.abs(horizontal ? p.y - a.y : p.x - a.x) > 0.5)
848
+ {
849
+ break;
850
+ }
851
+
852
+ len = Math.abs(horizontal ? p.x - a.x : p.y - a.y);
853
+ }
854
+
855
+ return len;
856
+ };
857
+
858
+ /**
859
+ * True when a route is libavoid's NO-ROUTE fallback instead of a route.
860
+ * When the search finds no path, libavoid returns the straight segment
861
+ * between the connector's two end vertices; for a pinned end that vertex
862
+ * is the pin class's dummy at the shape CENTRE, not a pin. A real
863
+ * orthogonal route is axis-aligned and starts/ends ON one of a pinned
864
+ * end's pins. pts: the route's absolute points. sourcePins/targetPins:
865
+ * the pinned ends' absolute pin positions ({x, y}), null for a floating
866
+ * or dangling end — whose fallback is only recognisable when diagonal.
867
+ */
868
+ AvoidRouting.isFallbackRoute = function(pts, sourcePins, targetPins)
869
+ {
870
+ if (pts == null || pts.length != 2)
871
+ {
872
+ return false;
873
+ }
874
+
875
+ function onPin(p, pins)
876
+ {
877
+ if (pins == null)
878
+ {
879
+ return true;
880
+ }
881
+
882
+ for (var i = 0; i < pins.length; i++)
883
+ {
884
+ if (Math.abs(p.x - pins[i].x) < 0.5 && Math.abs(p.y - pins[i].y) < 0.5)
885
+ {
886
+ return true;
887
+ }
888
+ }
889
+
890
+ return false;
891
+ }
892
+
893
+ return (Math.abs(pts[0].x - pts[1].x) >= 0.5 && Math.abs(pts[0].y - pts[1].y) >= 0.5) ||
894
+ !onPin(pts[0], sourcePins) || !onPin(pts[1], targetPins);
895
+ };
896
+
897
+ /**
898
+ * Initial distance in px between the ends of the routed edges and the
899
+ * border of the region whose obstacles computeRoutes registers. Doubled
900
+ * each time a route comes too close to the border.
901
+ */
902
+ AvoidRouting.regionMargin = 100;
903
+
904
+ /**
905
+ * Largest clearance in px that computeRoutes uses (as the Spacing of an
906
+ * edge in the editor). A connector without a route is retried at half the
907
+ * clearance down to 2px, so a crafted clearance (eg. 1e300) would retry
908
+ * about 1000 times.
909
+ */
910
+ AvoidRouting.maxShapeBufferDistance = 999;
911
+
912
+ /**
913
+ * The given region ({x, y, x2, y2}, null for none) extended by the box
914
+ * b ({x, y, w, h}) or the point b ({x, y}). Returns region when b is null.
915
+ */
916
+ AvoidRouting.addToRegion = function(region, b)
917
+ {
918
+ if (b == null)
919
+ {
920
+ return region;
921
+ }
922
+
923
+ var x2 = b.x + ((b.w > 0) ? b.w : 0);
924
+ var y2 = b.y + ((b.h > 0) ? b.h : 0);
925
+
926
+ return (region == null) ? {x: b.x, y: b.y, x2: x2, y2: y2} :
927
+ {x: Math.min(region.x, b.x), y: Math.min(region.y, b.y),
928
+ x2: Math.max(region.x2, x2), y2: Math.max(region.y2, y2)};
929
+ };
930
+
931
+ /**
932
+ * The given region grown by margin on all sides, or null if the grown
933
+ * region contains the given extent of all obstacles (no region needed).
934
+ */
935
+ AvoidRouting.growRegion = function(region, margin, extent)
936
+ {
937
+ if (region == null || extent == null)
938
+ {
939
+ return null;
940
+ }
941
+
942
+ region = {x: region.x - margin, y: region.y - margin,
943
+ x2: region.x2 + margin, y2: region.y2 + margin};
944
+
945
+ return (region.x <= extent.x && region.y <= extent.y &&
946
+ region.x2 >= extent.x2 && region.y2 >= extent.y2) ? null : region;
947
+ };
948
+
949
+ /**
950
+ * The obstacles ({x, y, w, h}) that intersect the given region.
951
+ */
952
+ AvoidRouting.obstaclesIn = function(obstacles, region)
953
+ {
954
+ var result = [];
955
+
956
+ for (var i = 0; i < obstacles.length; i++)
957
+ {
958
+ var v = obstacles[i];
959
+
960
+ if (v != null && v.x <= region.x2 && v.x + v.w >= region.x &&
961
+ v.y <= region.y2 && v.y + v.h >= region.y)
962
+ {
963
+ result.push(v);
964
+ }
965
+ }
966
+
967
+ return result;
968
+ };
969
+
970
+ /**
971
+ * True if all points of the given routes keep the given clearance from the
972
+ * border of the region, ie. the routes keep the clearance from all
973
+ * obstacles outside of the region.
974
+ */
975
+ AvoidRouting.routesInside = function(routes, region, clearance)
976
+ {
977
+ for (var i = 0; i < routes.length; i++)
978
+ {
979
+ for (var k = 0; k < routes[i].length; k++)
980
+ {
981
+ var p = routes[i][k];
982
+
983
+ if (p.x < region.x + clearance || p.x > region.x2 - clearance ||
984
+ p.y < region.y + clearance || p.y > region.y2 - clearance)
985
+ {
986
+ return false;
987
+ }
988
+ }
989
+ }
990
+
991
+ return true;
992
+ };
993
+
994
+ /**
995
+ * Compute obstacle-avoiding orthogonal routes for a set of edges.
996
+ *
997
+ * @param {object} Avoid - the libavoid instance (AvoidLib.getInstance()).
998
+ * @param {Array<{id:string,x:number,y:number,w:number,h:number,frame?}>} vertices
999
+ * Obstacles, in ABSOLUTE coordinates: the UNROTATED bounds, with
1000
+ * the shape's transform as frame (shapeFrame of its style) — the
1001
+ * obstacle is then its rotated box and its ends' constraints and
1002
+ * snap points, given in the shape's own frame, are mapped to where
1003
+ * it draws them (toWorldFrame). Shapes enclosing a terminal of a
1004
+ * routed edge are dropped (filterEnclosing). label = the box of a
1005
+ * label outside the shape, {side, x, y, w, h}: see
1006
+ * withOutsideLabels.
1007
+ * @param {Array<{id,source,target,sourcePoint?,targetPoint?,
1008
+ * sourceConstraint?,targetConstraint?,
1009
+ * sourcePoints?,targetPoints?,sourceSides?,targetSides?,
1010
+ * sourceJetty?,targetJetty?}>} edges
1011
+ * Edges referencing vertex ids. An end may instead be a DANGLING free
1012
+ * point — sourcePoint/targetPoint {x,y} in absolute coords, used when
1013
+ * that terminal has no vertex (an unconnected endpoint): it routes to
1014
+ * a plain Point ConnEnd (no shape, no direction), matching the
1015
+ * drag-preview, and takes no constraint/pin/mask/jetty. An edge is
1016
+ * skipped when an end is neither a vertex nor a free point, and
1017
+ * when it is a SELF-LOOP (source === target) — loops keep the
1018
+ * caller's own loop styling and waypoints.
1019
+ * *Constraint = {x,y,dir} (constraintForPoint): a
1020
+ * fixed connection point routed via a directed ShapeConnectionPin;
1021
+ * absent => the endpoint floats at the shape centre. *Points = an
1022
+ * array of {x,y,dir} candidate anchors (draw.io's snapToPoint —
1023
+ * the shape's declared connection points): non-exclusive directed
1024
+ * pins at each candidate under one pin class, libavoid picks the
1025
+ * anchor that routes best; beaten by a *Constraint on the same end
1026
+ * (an explicit anchor wins) and beats a *Sides mask (the renderer's
1027
+ * snapToPoint branch runs before mask enforcement). *Sides = an
1028
+ * array of allowed attach sides ('N'/'S'/'E'/'W', the legacy
1029
+ * mxGraph portConstraint masks): non-exclusive pins spread along
1030
+ * the allowed sides under one pin class, libavoid picks side and
1031
+ * position. *Jetty = the minimum length of the first/last
1032
+ * segment in px (draw.io's jettySize), enforced for ends with a
1033
+ * *Constraint by a routing checkpoint at the stub tip — requested
1034
+ * lazily, only for edges whose natural route falls short of the
1035
+ * minimum; snapped and masked ends behave like floating ones here
1036
+ * (their first bend naturally sits ~shapeBufferDistance out).
1037
+ * Ends with the same candidate pins on one shape share them.
1038
+ * @param {{shapeBufferDistance?:number,idealNudgingDistance?:number,
1039
+ * nudgeShapeSegments?:boolean}} [opts]
1040
+ * Defaults: 16 / 14 / false. nudgeShapeSegments nudges apart the
1041
+ * segments that attach to a shape, so that ends sharing a pin
1042
+ * (a side midpoint) don't share their first segment; always on when
1043
+ * a vertex has an outside label. A connector libavoid finds no route for is
1044
+ * retried alone at half the buffer, down to 2px. The buffer is at
1045
+ * most maxShapeBufferDistance, and a buffer that is not a finite
1046
+ * number uses the default.
1047
+ * @returns {Object<string, Array<{x:number,y:number}>>} edge id -> interior
1048
+ * bend points (ABSOLUTE, collinear-filtered). The first/last route
1049
+ * points (endpoints) are dropped — a floating endpoint connects at
1050
+ * the shape side midpoint, which is where a floating
1051
+ * orthogonalEdgeStyle endpoint lands anyway. An edge with a
1052
+ * straight (bend-free) route maps to []; an edge libavoid found no
1053
+ * route for even on retry has NO entry, like a skipped one.
1054
+ */
1055
+ AvoidRouting.computeRoutes = function(Avoid, vertices, edges, opts)
1056
+ {
1057
+ var out = Object.create(null);
1058
+
1059
+ if (Avoid == null || vertices == null || edges == null)
1060
+ {
1061
+ return out;
1062
+ }
1063
+
1064
+ var buffer = (opts && opts.shapeBufferDistance != null) ? opts.shapeBufferDistance : 16;
1065
+ var nudge = (opts && opts.idealNudgingDistance != null) ? opts.idealNudgingDistance : 14;
1066
+
1067
+ // Half of an infinite clearance is infinite, so the retry of the
1068
+ // connectors without a route below would never end
1069
+ if (!isFinite(buffer))
1070
+ {
1071
+ buffer = 16;
1072
+ }
1073
+ else if (buffer > AvoidRouting.maxShapeBufferDistance)
1074
+ {
1075
+ buffer = AvoidRouting.maxShapeBufferDistance;
1076
+ }
1077
+
1078
+ // Outside labels: route around the shape and its label, and end on the
1079
+ // label side at the label's far edge.
1080
+ var labels = AvoidRouting.withOutsideLabels(vertices, edges);
1081
+ vertices = labels.vertices;
1082
+ edges = labels.edges;
1083
+
1084
+ // Opt-in, so that routes without outside labels stay as they are:
1085
+ // nudge apart the segments that attach to a shape, so that ends
1086
+ // sharing a pin don't share their first segment.
1087
+ var nudgeShapeSegments = labels.any || (opts != null && opts.nudgeShapeSegments === true);
1088
+
1089
+ // Rotated, turned and flipped shapes: route around the box they are
1090
+ // drawn in, from the connection points where they are drawn.
1091
+ var world = AvoidRouting.toWorldFrame(vertices, edges);
1092
+ vertices = world.vertices;
1093
+ edges = world.edges;
1094
+
1095
+ // Containers the terminals live in are not obstacles (also feeds the
1096
+ // jettyStub checks below, so stubs inside a container are preserved).
1097
+ vertices = AvoidRouting.filterEnclosing(vertices, edges);
1098
+
1099
+ function collinear(a, b, c)
1100
+ {
1101
+ // Zero cross product (1px tolerance) => b lies on segment a..c (redundant).
1102
+ return Math.abs((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)) < 1;
1103
+ }
1104
+
1105
+ // A connector's display route as plain {x, y} points, freeing exactly
1106
+ // what the binding hands over: displayRoute() and PolyLine.at() are
1107
+ // UNOWNED references in draw.io's own binding (drawio-libavoid: the
1108
+ // editor, the mcp app server) — deleting one destroys the route in
1109
+ // place — and a fresh OWNED copy per call in the upstream libavoid-js
1110
+ // build (the mcp tool server). Two calls alias one object only for
1111
+ // references (isAliasOf), probed once per call. A leaked copy is not
1112
+ // just memory: libavoid breaks ties between equal coordinates by
1113
+ // object address, so it moved the next solve's allocations and a
1114
+ // long-lived module routed identical input differently.
1115
+ var ownedRoutes = null;
1116
+ var ownedPoints = null;
1117
+
1118
+ function owned(first, second)
1119
+ {
1120
+ if (first.isAliasOf(second))
1121
+ {
1122
+ return false;
1123
+ }
1124
+
1125
+ second.delete();
1126
+
1127
+ return true;
1128
+ }
1129
+
1130
+ function readRoute(conn)
1131
+ {
1132
+ var route = conn.displayRoute();
1133
+ var n = route.size();
1134
+ var pts = [];
1135
+
1136
+ if (ownedRoutes == null)
1137
+ {
1138
+ ownedRoutes = owned(route, conn.displayRoute());
1139
+ }
1140
+
1141
+ for (var k = 0; k < n; k++)
1142
+ {
1143
+ var p = route.at(k);
1144
+
1145
+ if (ownedPoints == null)
1146
+ {
1147
+ ownedPoints = owned(p, route.at(k));
1148
+ }
1149
+
1150
+ pts.push({x: p.x, y: p.y});
1151
+
1152
+ if (ownedPoints)
1153
+ {
1154
+ p.delete();
1155
+ }
1156
+ }
1157
+
1158
+ if (ownedRoutes)
1159
+ {
1160
+ route.delete();
1161
+ }
1162
+
1163
+ return pts;
1164
+ }
1165
+
1166
+ // Proportional pins on bounds b as absolute {x, y} (null stays null).
1167
+ function absolutePins(b, pins)
1168
+ {
1169
+ if (pins == null)
1170
+ {
1171
+ return null;
1172
+ }
1173
+
1174
+ var abs = [];
1175
+
1176
+ for (var k = 0; k < pins.length; k++)
1177
+ {
1178
+ abs.push({x: b.x + pins[k].x * b.w, y: b.y + pins[k].y * b.h});
1179
+ }
1180
+
1181
+ return abs;
1182
+ }
1183
+
1184
+ // Endpoint anchor: the free point for a dangling end, else the fixed
1185
+ // connection point, else the shape centre — the point the ConnEnd routes
1186
+ // from.
1187
+ function anchor(b, constraint, freePoint)
1188
+ {
1189
+ if (b == null)
1190
+ {
1191
+ return freePoint;
1192
+ }
1193
+
1194
+ return (constraint != null) ?
1195
+ {x: b.x + constraint.x * b.w, y: b.y + constraint.y * b.h} :
1196
+ {x: b.x + b.w / 2, y: b.y + b.h / 2};
1197
+ }
1198
+
1199
+ // One libavoid solve of batch at the given clearance around the given
1200
+ // obstacles. Returns {routes, points, failed}: the routed edges' bends
1201
+ // by edge id, every route's points (endpoints included) and the edges
1202
+ // libavoid found NO route for (isFallbackRoute), in batch order.
1203
+ function solveWith(batch, clearance, obstacles)
1204
+ {
1205
+ var result = {routes: Object.create(null), points: [], failed: []};
1206
+ var bounds = Object.create(null);
1207
+ var shapes = [];
1208
+ var routable = [];
1209
+ var i;
1210
+
1211
+ for (i = 0; i < obstacles.length; i++)
1212
+ {
1213
+ var v = obstacles[i];
1214
+
1215
+ if (v != null && v.id != null && v.w > 0 && v.h > 0)
1216
+ {
1217
+ bounds[v.id] = v;
1218
+ shapes.push(v);
1219
+ }
1220
+ }
1221
+
1222
+ for (i = 0; i < batch.length; i++)
1223
+ {
1224
+ var e = batch[i];
1225
+
1226
+ // An end is either a known vertex or a dangling free point
1227
+ // (e.sourcePoint / e.targetPoint, absolute coords) for an
1228
+ // unconnected endpoint. Self-loops are out of scope: obstacle
1229
+ // avoidance between identical endpoints is meaningless and the
1230
+ // orthogonal router degenerates to a buffer-sized hook beside the
1231
+ // shape. Skipping leaves the caller's loop styling and waypoints
1232
+ // untouched (an edge with no route entry is never written), e.g.
1233
+ // draw.io's inner-loop-waypoint self-loops.
1234
+ if (e != null && (bounds[e.source] != null || e.sourcePoint != null) &&
1235
+ (bounds[e.target] != null || e.targetPoint != null) &&
1236
+ (e.source == null || e.source !== e.target))
1237
+ {
1238
+ routable.push(e);
1239
+ }
1240
+ }
1241
+
1242
+ // No router without connectors: a ShapeRef only queues its ShapeAdd,
1243
+ // it becomes one of the obstacles that ~Router deletes in
1244
+ // processTransaction, so a router deleted before a transaction
1245
+ // leaked every shape with its vertices and pins (eg. a batch whose
1246
+ // terminals are all on edges). The leak moved the next solves'
1247
+ // allocations, and libavoid breaks ties by address.
1248
+ if (routable.length === 0)
1249
+ {
1250
+ return result;
1251
+ }
1252
+
1253
+ var router = new Avoid.Router(Avoid.RouterFlag.OrthogonalRouting.value);
1254
+
1255
+ try { router.setRoutingParameter(Avoid.RoutingParameter.shapeBufferDistance, clearance); } catch (e) {}
1256
+ try { router.setRoutingParameter(Avoid.RoutingParameter.idealNudgingDistance, nudge); } catch (e) {}
1257
+
1258
+ if (nudgeShapeSegments)
1259
+ {
1260
+ try { router.setRoutingOption(Avoid.RoutingOption.nudgeOrthogonalSegmentsConnectedToShapes, true); } catch (e) {}
1261
+ try { router.setRoutingOption(Avoid.RoutingOption.penaliseOrthogonalSharedPathsAtConnEnds, true); } catch (e) {}
1262
+ }
1263
+
1264
+ var shapeRefs = Object.create(null);
1265
+ var pinClasses = Object.create(null);
1266
+ var pinClass = 0;
1267
+
1268
+ for (i = 0; i < shapes.length; i++)
1269
+ {
1270
+ v = shapes[i];
1271
+
1272
+ // Point/Rectangle are embind value wrappers that COPY into the
1273
+ // ShapeRef and are NOT freed by router.delete(); free the temporaries.
1274
+ var rp1 = new Avoid.Point(v.x, v.y);
1275
+ var rp2 = new Avoid.Point(v.x + v.w, v.y + v.h);
1276
+ var rect = new Avoid.Rectangle(rp1, rp2);
1277
+ shapeRefs[v.id] = new Avoid.ShapeRef(router, rect);
1278
+ rect.delete();
1279
+ rp1.delete();
1280
+ rp2.delete();
1281
+ }
1282
+
1283
+ // ONE pin class per distinct candidate set on a shape, with
1284
+ // non-exclusive pins, shared by every end that attaches there.
1285
+ // Two pins at the SAME point in different classes break libavoid's
1286
+ // orthogonal visibility sweep, which assumes no two breakpoints
1287
+ // coincide (an assert in orthogonal.cpp, compiled out in release
1288
+ // builds): the connector registered second got no route at all —
1289
+ // every fan-out/fan-in on one fixed connection point, snap-point
1290
+ // set or side mask. The key is order-free; the pins are registered
1291
+ // in the first end's order.
1292
+ function pinClassFor(vid, pins)
1293
+ {
1294
+ var parts = [];
1295
+
1296
+ for (var k = 0; k < pins.length; k++)
1297
+ {
1298
+ parts.push(pins[k].x + ',' + pins[k].y + ',' + pins[k].dir);
1299
+ }
1300
+
1301
+ var key = vid + '\n' + parts.sort().join(' ');
1302
+
1303
+ if (pinClasses[key] == null)
1304
+ {
1305
+ pinClasses[key] = ++pinClass;
1306
+
1307
+ for (k = 0; k < pins.length; k++)
1308
+ {
1309
+ // Registers itself with the ShapeRef (freed with the
1310
+ // router); the wrapper is only kept to make it shareable.
1311
+ var pin = new Avoid.ShapeConnectionPin(shapeRefs[vid], pinClass,
1312
+ pins[k].x, pins[k].y, true, 0, pins[k].dir);
1313
+ pin.setExclusive(false);
1314
+ }
1315
+ }
1316
+
1317
+ return pinClasses[key];
1318
+ }
1319
+
1320
+ // An endpoint is FLOATING (ConnEnd at the shape centre) unless it
1321
+ // has candidate pins (endPins): a fixed connection point — a
1322
+ // directed pin makes the route leave/enter the anchor
1323
+ // perpendicular to the shape edge instead of cutting through it —
1324
+ // a snap-point set (draw.io's snapToPoint: the shape's declared
1325
+ // connection points) or a side MASK (the legacy mxGraph
1326
+ // portConstraint vocabulary). A set's pins share one class, so
1327
+ // libavoid picks the anchor (side and position) that routes best;
1328
+ // ends sharing a class share its pins and nudging separates their
1329
+ // corridors, like the render-time router spreading floating
1330
+ // attaches.
1331
+ function makeEnd(vid, b, pins, freePoint)
1332
+ {
1333
+ // Dangling end: no vertex bounds, just a free point (the edge's
1334
+ // unconnected endpoint). Route to a plain Point ConnEnd — no shape,
1335
+ // no direction — exactly as the warm-session drag preview does, so a
1336
+ // committed edge with an unconnected end matches its live preview.
1337
+ if (b == null)
1338
+ {
1339
+ var dp = new Avoid.Point(freePoint.x, freePoint.y);
1340
+ var dce = new Avoid.ConnEnd(dp);
1341
+ dp.delete();
1342
+ return dce;
1343
+ }
1344
+
1345
+ if (pins != null && shapeRefs[vid] != null)
1346
+ {
1347
+ return new Avoid.ConnEnd(shapeRefs[vid], pinClassFor(vid, pins));
1348
+ }
1349
+
1350
+ // Free the temporary centre Point (copied into the ConnEnd); the
1351
+ // ConnEnd itself is freed by the ConnRef caller below.
1352
+ var cp = new Avoid.Point(b.x + b.w / 2, b.y + b.h / 2);
1353
+ var ce = new Avoid.ConnEnd(cp);
1354
+ cp.delete();
1355
+
1356
+ return ce;
1357
+ }
1358
+
1359
+ // Wrap a stub tip as a routing checkpoint. Checkpoint copies the Point
1360
+ // and the vector copies the Checkpoint — free both embind wrappers.
1361
+ function addCheckpoint(vec, p)
1362
+ {
1363
+ if (p != null)
1364
+ {
1365
+ var pt = new Avoid.Point(p.x, p.y);
1366
+ var cp = new Avoid.Checkpoint(pt);
1367
+ vec.push_back(cp);
1368
+ cp.delete();
1369
+ pt.delete();
1370
+ }
1371
+ }
1372
+
1373
+ var conns = [];
1374
+
1375
+ for (i = 0; i < routable.length; i++)
1376
+ {
1377
+ e = routable[i];
1378
+ var sb = bounds[e.source];
1379
+ var tb = bounds[e.target];
1380
+ var sPins = (sb != null) ? AvoidRouting.endPins(sb,
1381
+ e.sourceConstraint, e.sourceSides, e.sourcePoints) : null;
1382
+ var tPins = (tb != null) ? AvoidRouting.endPins(tb,
1383
+ e.targetConstraint, e.targetSides, e.targetPoints) : null;
1384
+
1385
+ // ConnRef copies the ConnEnds, so free them after construction
1386
+ // (they are not owned by the router).
1387
+ var se = makeEnd(e.source, sb, sPins, e.sourcePoint);
1388
+ var de = makeEnd(e.target, tb, tPins, e.targetPoint);
1389
+ var conn = new Avoid.ConnRef(router, se, de);
1390
+ se.delete();
1391
+ de.delete();
1392
+
1393
+ // Jetty stubs: force the route through a checkpoint jetty px
1394
+ // outward of each constrained anchor, so the first/last segment is
1395
+ // at least that long. Capped per end to the clearance the terminal
1396
+ // pair's gap allows along the stub's axis (cappedJetty). Skipped
1397
+ // for ends where there is nothing to enforce (jettyStub), and for
1398
+ // the whole edge when the anchors are closer than the summed
1399
+ // stubs — the same too-short guard as mxEdgeStyle.OrthConnector —
1400
+ // so a short edge isn't forced to double back through its
1401
+ // checkpoints. The stubs are computed here but requested LAZILY
1402
+ // after the first solve (see below).
1403
+ var sourceJetty = AvoidRouting.cappedJetty(e.sourceJetty, e.sourceConstraint, sb, tb);
1404
+ var targetJetty = AvoidRouting.cappedJetty(e.targetJetty, e.targetConstraint, tb, sb);
1405
+ var sa = anchor(sb, e.sourceConstraint, e.sourcePoint);
1406
+ var ta = anchor(tb, e.targetConstraint, e.targetPoint);
1407
+
1408
+ // A pinned anchor buried inside ANOTHER obstacle (a shape dragged
1409
+ // over the terminal) puts the whole route into libavoid's escape /
1410
+ // degenerate mode: checkpoints on such a route are unreachable and
1411
+ // libavoid warns ("skipping checkpoint") for EVERY end — including
1412
+ // tips in perfectly clear space — before discarding them. Skip BOTH
1413
+ // stubs for the edge instead (verified against the WASM: same
1414
+ // route, no console noise).
1415
+ var buried = (e.sourceConstraint != null &&
1416
+ AvoidRouting.insideAny(sa.x, sa.y, vertices)) ||
1417
+ (e.targetConstraint != null &&
1418
+ AvoidRouting.insideAny(ta.x, ta.y, vertices));
1419
+
1420
+ var scp = buried ? null :
1421
+ AvoidRouting.jettyStub(e.sourceConstraint, sourceJetty, sb, vertices, clearance);
1422
+ var tcp = buried ? null :
1423
+ AvoidRouting.jettyStub(e.targetConstraint, targetJetty, tb, vertices, clearance);
1424
+
1425
+ if (scp != null || tcp != null)
1426
+ {
1427
+ var dx = ta.x - sa.x;
1428
+ var dy = ta.y - sa.y;
1429
+ var total = ((scp != null) ? sourceJetty : 0) +
1430
+ ((tcp != null) ? targetJetty : 0);
1431
+
1432
+ if (dx * dx + dy * dy < total * total)
1433
+ {
1434
+ scp = null;
1435
+ tcp = null;
1436
+ }
1437
+ }
1438
+
1439
+ conns.push({edge: e, conn: conn, scp: scp, tcp: tcp,
1440
+ sourceJetty: sourceJetty, targetJetty: targetJetty,
1441
+ sourcePins: absolutePins(sb, sPins),
1442
+ targetPins: absolutePins(tb, tPins),
1443
+ sourceEnd: [e.source, sb, sPins, e.sourcePoint]});
1444
+ }
1445
+
1446
+ router.processTransaction();
1447
+
1448
+ // LAZY jetty enforcement: the first solve runs WITHOUT the stub
1449
+ // checkpoints; they are requested only for edges whose natural route
1450
+ // violates a jetty minimum, and the transaction re-processed once. A
1451
+ // checkpoint is a hard point the route must touch: when libavoid
1452
+ // TURNS at one instead of crossing it mid-segment, the adjacent bend
1453
+ // is pinned at the stub tip and nudging cannot center the segment in
1454
+ // its channel — lopsided lead-outs (e.g. 30/10 on one edge next to a
1455
+ // centered 20/20 twin) on routes that met the minimum for free.
1456
+ // Enforcing lazily keeps the nudged, evenly distributed route
1457
+ // wherever it already satisfies the jetty, and falls back to the
1458
+ // checkpointed solve only where it does not. A connector without a
1459
+ // route gets no checkpoints: they cannot give it one, only
1460
+ // "skipping checkpoint" warnings.
1461
+ var dirty = false;
1462
+
1463
+ for (i = 0; i < conns.length; i++)
1464
+ {
1465
+ var c = conns[i];
1466
+
1467
+ if (c.scp == null && c.tcp == null)
1468
+ {
1469
+ continue;
1470
+ }
1471
+
1472
+ // 0.5px tolerance: sub-pixel misses vanish in the output rounding
1473
+ // and do not warrant pinning the route to the checkpoints.
1474
+ var r0 = readRoute(c.conn);
1475
+
1476
+ if (!AvoidRouting.isFallbackRoute(r0, c.sourcePins, c.targetPins) &&
1477
+ ((c.scp != null && AvoidRouting.endSegment(r0, true) < c.sourceJetty - 0.5) ||
1478
+ (c.tcp != null && AvoidRouting.endSegment(r0, false) < c.targetJetty - 0.5)))
1479
+ {
1480
+ // In route order (source first); setRoutingCheckpoints copies
1481
+ // the vector, so free the wrapper.
1482
+ var cps = new Avoid.CheckpointVector();
1483
+ addCheckpoint(cps, c.scp);
1484
+ addCheckpoint(cps, c.tcp);
1485
+ c.conn.setRoutingCheckpoints(cps);
1486
+ cps.delete();
1487
+
1488
+ // Queues the reroute: see the setRoutingCheckpoints gotcha
1489
+ // in the header
1490
+ var se2 = makeEnd.apply(null, c.sourceEnd);
1491
+ c.conn.setSourceEndpoint(se2);
1492
+ se2.delete();
1493
+ dirty = true;
1494
+ }
1495
+ }
1496
+
1497
+ if (dirty)
1498
+ {
1499
+ router.processTransaction();
1500
+ }
1501
+
1502
+ for (i = 0; i < conns.length; i++)
1503
+ {
1504
+ var pts = readRoute(conns[i].conn);
1505
+
1506
+ if (AvoidRouting.isFallbackRoute(pts, conns[i].sourcePins, conns[i].targetPins))
1507
+ {
1508
+ result.failed.push(conns[i].edge);
1509
+ continue;
1510
+ }
1511
+
1512
+ var wps = [];
1513
+
1514
+ for (var k = 1; k < pts.length - 1; k++)
1515
+ {
1516
+ if (collinear(pts[k - 1], pts[k], pts[k + 1]))
1517
+ {
1518
+ continue;
1519
+ }
1520
+
1521
+ wps.push({x: Math.round(pts[k].x), y: Math.round(pts[k].y)});
1522
+ }
1523
+
1524
+ result.routes[conns[i].edge.id] = wps;
1525
+ result.points.push(pts);
1526
+ }
1527
+
1528
+ router.delete();
1529
+ return result;
1530
+ }
1531
+
1532
+ // Terminal bounds by id for the regions, matching the bounds that
1533
+ // solveWith registers, and the extent of all obstacles
1534
+ var terminals = Object.create(null);
1535
+ var extent = null;
1536
+
1537
+ for (var i = 0; i < vertices.length; i++)
1538
+ {
1539
+ var v = vertices[i];
1540
+
1541
+ if (v != null)
1542
+ {
1543
+ extent = AvoidRouting.addToRegion(extent, v);
1544
+
1545
+ if (v.id != null && v.w > 0 && v.h > 0)
1546
+ {
1547
+ terminals[v.id] = v;
1548
+ }
1549
+ }
1550
+ }
1551
+
1552
+ // One solve of batch at the given clearance. Writes the routed edges
1553
+ // into out; returns the edges libavoid found NO route for, in batch
1554
+ // order, for the retry below. The obstacles are limited to a region
1555
+ // around the batch's ends: the cost of a solve grows with the number
1556
+ // of obstacles (~400ms for one edge among 2600 shapes), not with the
1557
+ // distance the routes cover. A route that keeps the clearance from
1558
+ // the region's border is also valid with all obstacles and, as fewer
1559
+ // obstacles never make the best route worse, also a best route with
1560
+ // all obstacles. Routes closer to the border are solved again in a
1561
+ // larger region. A connector without a route in the region has none
1562
+ // with all obstacles either, as more obstacles never open up a route.
1563
+ function solve(batch, clearance)
1564
+ {
1565
+ var margin = AvoidRouting.regionMargin;
1566
+ var region = null;
1567
+
1568
+ for (var i = 0; i < batch.length; i++)
1569
+ {
1570
+ var e = batch[i];
1571
+
1572
+ if (e != null)
1573
+ {
1574
+ var sb = (e.source != null) ? terminals[e.source] : null;
1575
+ var tb = (e.target != null) ? terminals[e.target] : null;
1576
+ region = AvoidRouting.addToRegion(region, (sb != null) ? sb : e.sourcePoint);
1577
+ region = AvoidRouting.addToRegion(region, (tb != null) ? tb : e.targetPoint);
1578
+ }
1579
+ }
1580
+
1581
+ region = AvoidRouting.growRegion(region, margin, extent);
1582
+
1583
+ while (true)
1584
+ {
1585
+ var result = solveWith(batch, clearance, (region != null) ?
1586
+ AvoidRouting.obstaclesIn(vertices, region) : vertices);
1587
+
1588
+ if (region == null || AvoidRouting.routesInside(
1589
+ result.points, region, clearance))
1590
+ {
1591
+ for (var id in result.routes)
1592
+ {
1593
+ out[id] = result.routes[id];
1594
+ }
1595
+
1596
+ return result.failed;
1597
+ }
1598
+
1599
+ for (i = 0; i < result.points.length; i++)
1600
+ {
1601
+ for (var k = 0; k < result.points[i].length; k++)
1602
+ {
1603
+ region = AvoidRouting.addToRegion(region, result.points[i][k]);
1604
+ }
1605
+ }
1606
+
1607
+ margin *= 2;
1608
+ region = AvoidRouting.growRegion(region, margin, extent);
1609
+ }
1610
+ }
1611
+
1612
+ // A connector libavoid found NO route for is retried on its own with
1613
+ // the clearance halved, down to 2px: typically a pinned end faces a
1614
+ // gap narrower than twice the buffer (a label or marker placed a few
1615
+ // px from the shape), which the buffered obstacles close. Only the
1616
+ // failed connectors are re-solved — the others keep the configured
1617
+ // clearance — so a retried route is not nudged against the rest,
1618
+ // like the editor's per-buffer buckets. Whatever still fails has no
1619
+ // entry: callers leave such an edge as authored instead of writing
1620
+ // the fallback's straight line over its waypoints.
1621
+ var failed = solve(edges, buffer);
1622
+
1623
+ for (var retry = Math.floor(buffer / 2); failed.length > 0 && retry >= 2;
1624
+ retry = Math.floor(retry / 2))
1625
+ {
1626
+ failed = solve(failed, retry);
1627
+ }
1628
+
1629
+ return out;
1630
+ };
1631
+
1632
+ globalThis.AvoidRouting = AvoidRouting;
1633
+ })();