@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,4459 @@
1
+ /**
2
+ * LibavoidRouting — draw.io editor binding for the vendored libavoid-js WASM
3
+ * obstacle-avoiding orthogonal edge router (js/libavoid-js/). It ROUTES edges
4
+ * rather than placing nodes: every vertex becomes an obstacle, the target edges
5
+ * are re-routed around them, and the bend points are written back as edge
6
+ * waypoints. Vertices are never moved.
7
+ *
8
+ * Two entry points:
9
+ * - run() — one-shot Arrange > Layout > Orthogonal Routing menu action
10
+ * (diagramly/Menus.js): routes the selected edges (or all edges) once,
11
+ * converting them to orthogonalEdgeStyle.
12
+ * - installAutoRouting() — live auto-routing for edges carrying the
13
+ * `libavoidRouting=1` style flag: re-routes them when they are inserted,
14
+ * reconnected, or a connected shape is moved/resized, as long as the user
15
+ * hasn't taken manual control (a manual waypoint edit clears the flag — see
16
+ * mxEdgeHandler.changePoints in grapheditor/Graph.js) and the edge lives
17
+ * outside any live layout container (a childLayout ancestor owns its
18
+ * children's edge geometry and would overwrite every route — see
19
+ * isAutoEdge/layoutContainerOf; the flag stays on the style, inert). Wired
20
+ * from the diagramly EditorUi.init (guarded by `typeof LibavoidRouting`).
21
+ *
22
+ * Auto-routing listens on the GRAPH events (CELLS_ADDED / CELL_CONNECTED /
23
+ * CELLS_MOVED / CELLS_RESIZED / cellsArranged, plus the rotation handle of
24
+ * its vertex handlers), not the model CHANGE event:
25
+ * those fire only for forward user actions (never on undo/redo replay or for
26
+ * remote changes), and the route is written via
27
+ * model.setGeometry (a model-level change that does NOT re-fire these graph
28
+ * events) — so there is no re-entry and no fighting with undo. The events only
29
+ * COLLECT the affected cells; the solve is PARKED until the model's BEFORE_UNDO
30
+ * (see autoReroute), which fires after the layout manager has run the edit's
31
+ * synchronous childLayouts — so a terminal dropped into a stack/table/tree
32
+ * container is routed against its final laid-out slot, not the drop point
33
+ * (layout writes are model-level and would never re-trigger an event-time
34
+ * solve). BEFORE_UNDO fires before the edit closes and the endingUpdate latch
35
+ * folds the handler's writes in, so a warm-WASM route still merges into the
36
+ * same undoable edit as the move (one Ctrl+Z reverts both).
37
+ *
38
+ * The routing core (AvoidRouting: computeRoutes + the pure geometry helpers)
39
+ * lives in js/libavoid-js/libavoid-routing.js — the canonical shared artifact
40
+ * that drawio-mcp vendors verbatim for its app-server viewer and node tool
41
+ * server. This file is the EDITOR binding only (mxGraph model access, events,
42
+ * previews, styles); anything editor-independent belongs in the core.
43
+ */
44
+ var LibavoidRouting =
45
+ {
46
+ // Default shape buffer (obstacle clearance) and nudging distance, in model
47
+ // px. shapeBufferDistance is overridable via the menu's spacing prompt.
48
+ shapeBufferDistance: 16,
49
+ idealNudgingDistance: 14,
50
+
51
+ // Routing strategy switch (configurable):
52
+ // false (default) — one multi-edge computeRoutes: libavoid nudging separates
53
+ // shared paths (nicer for parallel edges), and it's faster (a single
54
+ // solve), but competing edges between the same shape pair route
55
+ // non-deterministically and can oscillate during the live preview.
56
+ // true — route each edge in its OWN single-connector solve
57
+ // (computeRoutesIndependent): deterministic, so competing edges don't
58
+ // oscillate, but no edge-edge nudging (shared paths can overlap).
59
+ // Toggle live from the console (LibavoidRouting.independentRouting = true) or per
60
+ // session with the ?libavoid-independent=0|1 URL param.
61
+ independentRouting: false,
62
+
63
+ // Edge style flag: '1' means "auto-route this edge via libavoid". Paired with
64
+ // edgeStyle=orthogonalEdgeStyle (set together by the Format checkbox and the
65
+ // default-edge-style). Cleared on manual waypoint edits so the user's points
66
+ // win. Listed in Graph.edgeStyles + Graph.createCurrentEdgeStyle so it
67
+ // propagates to new edges and copies as a default like any edge style.
68
+ STYLE: 'libavoidRouting',
69
+
70
+ // Style keys of an edge and its terminals that are routing input besides
71
+ // the geometry: the port constraints (maskSides) and the transform of a
72
+ // shape (AvoidRouting.shapeFrame). Style edits of these re-route the
73
+ // affected edges (see installAutoRouting).
74
+ constraintStyles: ['portConstraint', 'portConstraintRotation',
75
+ 'sourcePortConstraint', 'targetPortConstraint'],
76
+ frameStyles: ['rotation', 'direction', 'flipH', 'flipV', 'stencilFlipH',
77
+ 'stencilFlipV', 'anchorPointDirection', 'legacyAnchorPoints'],
78
+
79
+ // Style keys of a vertex that place a label outside the shape that edges
80
+ // end at (labelPerimeter, see getOutsideLabel): routing input of the
81
+ // vertex like its frame.
82
+ labelStyles: ['labelPerimeter', 'labelPosition', 'verticalLabelPosition'],
83
+
84
+ // Style keys of a vertex that change the size or the place of such a
85
+ // label: routing input of a vertex with a label outside the shape.
86
+ labelBoxStyles: ['fontSize', 'fontFamily', 'fontStyle', 'whiteSpace', 'html',
87
+ 'labelWidth', 'align', 'verticalAlign', 'spacing', 'spacingTop',
88
+ 'spacingBottom', 'spacingLeft', 'spacingRight'],
89
+
90
+ // The other routing input of the edge ends: the fixed connection points
91
+ // (fixedConstraint), the snapToPoint anchors (snapPoints, from the points
92
+ // or the shape of the terminal) and the jetty sizes (jettyFor), plus the
93
+ // routing flag itself, so that turning it on routes the edge, and the
94
+ // spacing of the edge (spacingFor).
95
+ endStyles: ['exitX', 'exitY', 'exitPerimeter', 'entryX', 'entryY',
96
+ 'entryPerimeter', 'snapToPoint', 'points', 'shape', 'jettySize',
97
+ 'sourceJettySize', 'targetJettySize', 'libavoidRouting', 'libavoidSpacing'],
98
+
99
+ // The arrows and their sizes, routing input of an edge only for an end
100
+ // with jettySize=auto (jettyFor).
101
+ arrowStyles: ['startArrow', 'endArrow', 'startSize', 'endSize'],
102
+
103
+ // JSON layout-spec name / shorthand (see Graph.createLayouts). Routes the page's
104
+ // edges via libavoid through the generic layout pipeline — custom-layout dialog,
105
+ // embed 'layout' action, executeLayoutSpec (URL / desktop --layout) and headless
106
+ // export — like elk*/mxParallelEdgeLayout. Display label is the 'orthogonalRouting'
107
+ // resource; this is the technical name used in the JSON {layout:...} field.
108
+ LAYOUT_NAME: 'orthogonalEdge',
109
+
110
+ // User-facing shorthand accepted wherever a layout spec names a layout
111
+ // without JSON (EditorUi.resolveLayoutList): the CSV `# layout:` line, the
112
+ // embed 'layout' action / load option, the #create hash and the desktop
113
+ // --layout flag. Resolves to [{layout: LAYOUT_NAME}].
114
+ SHORTHAND: 'libavoid'
115
+ };
116
+
117
+ /**
118
+ * One-shot menu action. Awaits WASM readiness, then routes the selected edges
119
+ * (or all edges if none selected), converting them to orthogonalEdgeStyle.
120
+ *
121
+ * @param {EditorUi} editorUi
122
+ * @param {{shapeBufferDistance?:number, idealNudgingDistance?:number}} [opts]
123
+ * @param {function(boolean)} [done]
124
+ */
125
+ LibavoidRouting.run = function(editorUi, opts, done)
126
+ {
127
+ var finish = function(applied)
128
+ {
129
+ if (typeof done === 'function')
130
+ {
131
+ done(applied);
132
+ }
133
+ };
134
+
135
+ var ready = (typeof window !== 'undefined') ? window.__libavoidReady : null;
136
+
137
+ if (ready == null)
138
+ {
139
+ editorUi.handleError(new Error(mxResources.get('libavoidUnavailable')));
140
+ finish(false);
141
+ return;
142
+ }
143
+
144
+ var spinner = editorUi.spinner;
145
+
146
+ if (spinner != null && spinner.spin != null)
147
+ {
148
+ spinner.spin(document.body, mxResources.get('loading'));
149
+ }
150
+
151
+ ready.then(function(Avoid)
152
+ {
153
+ if (spinner != null && spinner.stop != null)
154
+ {
155
+ spinner.stop();
156
+ }
157
+
158
+ if (Avoid == null)
159
+ {
160
+ editorUi.handleError(new Error(mxResources.get('libavoidUnavailable')));
161
+ finish(false);
162
+ return;
163
+ }
164
+
165
+ try
166
+ {
167
+ var graph = editorUi.editor.graph;
168
+
169
+ // Records the run for Arrange > Layout > Run Last Layout (replayed
170
+ // through the shared layout-spec pipeline, see
171
+ // EditorUi.executeLayoutSpec).
172
+ var spec = {layout: LibavoidRouting.LAYOUT_NAME};
173
+
174
+ if (opts != null)
175
+ {
176
+ spec.config = opts;
177
+ }
178
+
179
+ editorUi.lastLayoutSpec = [spec];
180
+
181
+ // Selected layout containers take the run as their new
182
+ // childLayout — live orthogonal routing of the container's edges —
183
+ // instead of a one-shot routing pass.
184
+ if (editorUi.applyLayoutToSelectedContainers(editorUi.lastLayoutSpec))
185
+ {
186
+ finish(true);
187
+ return;
188
+ }
189
+
190
+ var edgeCells = LibavoidRouting.getTargetEdges(graph);
191
+
192
+ editorUi.executeLayout(function()
193
+ {
194
+ LibavoidRouting.routeCells(graph, Avoid, edgeCells, opts, true);
195
+ }, false, function()
196
+ {
197
+ finish(true);
198
+ });
199
+ }
200
+ catch (e)
201
+ {
202
+ editorUi.handleError(e);
203
+ finish(false);
204
+ }
205
+ });
206
+ };
207
+
208
+ /**
209
+ * A layout-pipeline adapter (NOT an mxGraphLayout) that routes the page's edges via
210
+ * libavoid. Like ElkLayout it is asynchronous — the WASM may still be loading — so it
211
+ * exposes prepare(parent, callback): callback(err) on failure, else callback(null,
212
+ * applyFn) where applyFn does the routing. The caller (EditorUi.executeLayouts and the
213
+ * headless export runner) wraps applyFn in the model update, so applyFn must NOT open
214
+ * its own. Built from Graph.createLayouts when the JSON {layout: LAYOUT_NAME} is run.
215
+ *
216
+ * @param {mxGraph} graph
217
+ * @param {{shapeBufferDistance?:number, idealNudgingDistance?:number}} [config]
218
+ * computeRoutes options; null/absent uses the LibavoidRouting defaults.
219
+ */
220
+ LibavoidRouting.createLayout = function(graph, config)
221
+ {
222
+ var opts = (config != null) ? config : null;
223
+
224
+ // Collect and route the run's edges — synchronous once the WASM
225
+ // namespace exists. Shared by executeSync and the prepare apply
226
+ // closure.
227
+ var runRouting = function(Avoid, parent)
228
+ {
229
+ var model = graph.getModel();
230
+ var edges;
231
+
232
+ if (layout.cellFilter != null)
233
+ {
234
+ // Scoped run: route the subset's edges regardless of
235
+ // the current selection.
236
+ edges = [];
237
+
238
+ for (var id in model.cells)
239
+ {
240
+ var cell = model.cells[id];
241
+
242
+ if (model.isEdge(cell) && layout.cellFilter(cell))
243
+ {
244
+ edges.push(cell);
245
+ }
246
+ }
247
+ }
248
+ else if (parent != null && model.isVertex(parent) &&
249
+ graph.getCellStyle(parent)['childLayout'] != null)
250
+ {
251
+ // A childLayout run: the layout root is a container, so
252
+ // route its descendant edges — not the page or the
253
+ // current selection, which a manager-triggered re-run
254
+ // must not depend on. One-shot pipeline runs keep the
255
+ // legacy selected-or-all scope: their prepare parent is
256
+ // the default parent, which is a layer or (after Enter
257
+ // Group) a plain vertex without childLayout.
258
+ edges = [];
259
+
260
+ var collect = function(cell)
261
+ {
262
+ for (var i = 0; i < model.getChildCount(cell); i++)
263
+ {
264
+ var child = model.getChildAt(cell, i);
265
+
266
+ if (model.isEdge(child))
267
+ {
268
+ edges.push(child);
269
+ }
270
+ else
271
+ {
272
+ collect(child);
273
+ }
274
+ }
275
+ };
276
+
277
+ collect(parent);
278
+ }
279
+ else
280
+ {
281
+ edges = LibavoidRouting.getTargetEdges(graph);
282
+ }
283
+
284
+ LibavoidRouting.routeCells(graph, Avoid, edges, opts, true);
285
+ };
286
+
287
+ var layout = {
288
+ // Set by EditorUi.scopeLayoutsToCells to restrict the routing to a
289
+ // cell subset (a CSV import only routes the edges it just created);
290
+ // null routes the selected (or all) edges.
291
+ cellFilter: null,
292
+
293
+ // mxLayoutManager delegates interactive child moves/resizes to the
294
+ // container's layout via mxCompositeLayout when this runs as a
295
+ // childLayout; edge routing has nothing to do per-cell, so these
296
+ // are no-ops (same as ElkLayout's) — without moveCell, dragging a
297
+ // child inside an orthogonalEdge container throws.
298
+ moveCell: function() { },
299
+ resizeCell: function() { },
300
+
301
+ // Sync execution once the WASM is loaded, so Graph.
302
+ // canExecuteLayoutSync routes a composite childLayout containing
303
+ // this adapter through the manager's sync path (inside the
304
+ // triggering edit, where the endingUpdate latch makes re-triggering
305
+ // impossible). Without these the WHOLE composite — including its
306
+ // ELK members — was forced onto the async scheduler, whose
307
+ // cross-container termination depends on converged applies being
308
+ // empty edits: two NESTED containers with e.g.
309
+ // [elkLayered, orthogonalEdge] childLayouts could re-schedule each
310
+ // other forever (an autosave loop). Only the WASM load window still
311
+ // uses prepare below.
312
+ canExecuteSync: function()
313
+ {
314
+ return typeof window !== 'undefined' && window.Avoid != null;
315
+ },
316
+
317
+ executeSync: function(parent)
318
+ {
319
+ runRouting(window.Avoid, parent);
320
+ },
321
+
322
+ prepare: function(parent, callback)
323
+ {
324
+ var run = function(Avoid)
325
+ {
326
+ if (Avoid == null)
327
+ {
328
+ callback(new Error(mxResources.get('libavoidUnavailable')));
329
+ return;
330
+ }
331
+
332
+ callback(null, function()
333
+ {
334
+ runRouting(Avoid, parent);
335
+ });
336
+ };
337
+
338
+ if (typeof window !== 'undefined' && window.Avoid != null)
339
+ {
340
+ run(window.Avoid);
341
+ }
342
+ else if (typeof window !== 'undefined' && window.__libavoidReady != null)
343
+ {
344
+ window.__libavoidReady.then(run);
345
+ }
346
+ else
347
+ {
348
+ run(null);
349
+ }
350
+ }
351
+ };
352
+
353
+ return layout;
354
+ };
355
+
356
+ /**
357
+ * Install the live auto-routing listeners for `libavoidRouting=1` edges on the
358
+ * given editor's graph. Called once per editor from diagramly EditorUi.init.
359
+ */
360
+ LibavoidRouting.installAutoRouting = function(editorUi)
361
+ {
362
+ var graph = editorUi.editor.graph;
363
+ var model = graph.getModel();
364
+
365
+ // Per-session override of the routing strategy: ?libavoid-independent=0 forces the
366
+ // previous multi-edge/nudged routing; =1 forces independent per-edge routing.
367
+ if (typeof urlParams !== 'undefined' && urlParams != null &&
368
+ urlParams['libavoid-independent'] != null)
369
+ {
370
+ LibavoidRouting.independentRouting = urlParams['libavoid-independent'] != '0';
371
+ }
372
+
373
+ // Shows solver errors, which are not thrown from the edit (see disable)
374
+ graph.addListener('libavoidError', function()
375
+ {
376
+ editorUi.handleError({message: mxResources.get('libavoidUnavailable')});
377
+ });
378
+
379
+ var values = function(map)
380
+ {
381
+ var a = [];
382
+
383
+ for (var id in map)
384
+ {
385
+ a.push(map[id]);
386
+ }
387
+
388
+ return a;
389
+ };
390
+
391
+ // Inserted cells: route flagged edges that were added, and re-route existing
392
+ // flagged edges that a newly inserted SHAPE now overlaps (same idea as a move
393
+ // dropping a shape onto an edge). Atomic with the insert (CELLS_ADDED fires
394
+ // inside the insert's update).
395
+ graph.addListener(mxEvent.CELLS_ADDED, function(sender, evt)
396
+ {
397
+ var cells = evt.getProperty('cells');
398
+
399
+ if (cells == null)
400
+ {
401
+ return;
402
+ }
403
+
404
+ var map = Object.create(null);
405
+ var regions = [];
406
+ var inserted = [];
407
+
408
+ for (var i = 0; i < cells.length; i++)
409
+ {
410
+ if (LibavoidRouting.isAutoEdge(graph, cells[i]))
411
+ {
412
+ map[cells[i].getId()] = cells[i];
413
+ }
414
+ else if (model.isVertex(cells[i]))
415
+ {
416
+ inserted.push(cells[i]);
417
+ var b = LibavoidRouting.getAbsoluteObstacleBounds(graph, cells[i]);
418
+
419
+ if (b != null)
420
+ {
421
+ regions.push({x: b.x, y: b.y, width: b.w, height: b.h});
422
+ }
423
+
424
+ LibavoidRouting.addOutsideLabelRegions(graph, cells[i], regions);
425
+ }
426
+ }
427
+
428
+ LibavoidRouting.collectOverlappingEdges(graph, regions, map);
429
+ LibavoidRouting.autoReroute(graph, values(map), inserted);
430
+ });
431
+
432
+ // A new label of a shape with a label outside (getOutsideLabel) changes
433
+ // the box that its edges end at and that other edges route around. Fired
434
+ // by the in-place editor inside its update, so the solve joins the edit.
435
+ graph.addListener(mxEvent.LABEL_CHANGED, function(sender, evt)
436
+ {
437
+ var cell = evt.getProperty('cell');
438
+
439
+ if (cell != null && model.isVertex(cell))
440
+ {
441
+ var map = Object.create(null);
442
+ var regions = [];
443
+
444
+ collectRestyled(['value'], [cell], map, regions);
445
+ LibavoidRouting.collectOverlappingEdges(graph, regions, map);
446
+ LibavoidRouting.autoReroute(graph, values(map));
447
+ }
448
+ });
449
+
450
+ // Edge reconnected to a different terminal.
451
+ graph.addListener(mxEvent.CELL_CONNECTED, function(sender, evt)
452
+ {
453
+ var edge = evt.getProperty('edge');
454
+
455
+ if (LibavoidRouting.isAutoEdge(graph, edge))
456
+ {
457
+ LibavoidRouting.autoReroute(graph, [edge]);
458
+ }
459
+ });
460
+
461
+ // New edge finished via the connection handler (hover-icon drag). Re-route it
462
+ // here, not just at CELLS_ADDED: a FIXED connection point (exitX/entryX) is set
463
+ // by setConnectionConstraint AFTER the edge is added, and mxEvent.CONNECT fires
464
+ // after that (still inside the connect update, so the re-route is atomic). The
465
+ // drag preview routed center-to-center (it doesn't see the constraint), so this
466
+ // is what makes the committed route honour the fixed anchor's direction.
467
+ if (graph.connectionHandler != null)
468
+ {
469
+ graph.connectionHandler.addListener(mxEvent.CONNECT, function(sender, evt)
470
+ {
471
+ var edge = evt.getProperty('cell');
472
+
473
+ if (LibavoidRouting.isAutoEdge(graph, edge))
474
+ {
475
+ LibavoidRouting.autoReroute(graph, [edge]);
476
+ }
477
+ });
478
+ }
479
+
480
+ // Port-constraint edits re-route the affected flagged edges: the masks are
481
+ // routing input (maskSides), but they arrive as pure STYLE writes from the
482
+ // Format panel (property grid Constraint / Source & Target Constraint /
483
+ // Rotate Constraint rows), which fire none of the graph events above. The
484
+ // editor's 'styleChanged' event is the forward-gesture signal for those —
485
+ // fired by the panel editors only, never on undo/redo replay, so the
486
+ // no-model-CHANGE-listener rule above holds. A vertex mask affects every
487
+ // flagged edge at the shape. So does a rotation, direction or flip edit:
488
+ // the shape's transform moves its connection points and turns its drawn
489
+ // box (AvoidRouting.shapeFrame), so edges crossing the new box re-route
490
+ // too. The other end styles (endStyles: fixed connection points,
491
+ // snapToPoint anchors, jetty sizes) re-route like a mask, and turning the
492
+ // routing flag on routes the edge. An arrow edit re-routes an edge with a
493
+ // jettySize=auto end (arrowStyles). The size and the place of a label
494
+ // outside a shape (labelBoxStyles, and its value for a LABEL_CHANGED) are
495
+ // routing input of a vertex with such a label like its frame. The regions
496
+ // of a framed vertex include the boxes of its label outside the shape
497
+ // before and after the edit. collectRestyled adds those edges for a
498
+ // change of the given style keys of the given cells to map, and the new
499
+ // boxes to regions.
500
+ var collectRestyled = function(keys, cells, map, regions)
501
+ {
502
+ var relevant = false;
503
+ var frameOnly = true;
504
+ var framed = false;
505
+ var labelled = false;
506
+ var arrows = false;
507
+ var i, j;
508
+
509
+ for (i = 0; i < keys.length; i++)
510
+ {
511
+ if (mxUtils.indexOf(LibavoidRouting.constraintStyles, keys[i]) >= 0 ||
512
+ mxUtils.indexOf(LibavoidRouting.endStyles, keys[i]) >= 0)
513
+ {
514
+ relevant = true;
515
+ frameOnly = false;
516
+ }
517
+ else if (mxUtils.indexOf(LibavoidRouting.frameStyles, keys[i]) >= 0 ||
518
+ mxUtils.indexOf(LibavoidRouting.labelStyles, keys[i]) >= 0)
519
+ {
520
+ relevant = true;
521
+ framed = true;
522
+ }
523
+ else if (keys[i] == 'value' ||
524
+ mxUtils.indexOf(LibavoidRouting.labelBoxStyles, keys[i]) >= 0)
525
+ {
526
+ labelled = true;
527
+ }
528
+ else if (mxUtils.indexOf(LibavoidRouting.arrowStyles, keys[i]) >= 0)
529
+ {
530
+ arrows = true;
531
+ }
532
+ }
533
+
534
+ if (!relevant && !arrows && !labelled)
535
+ {
536
+ return;
537
+ }
538
+
539
+ for (i = 0; i < cells.length; i++)
540
+ {
541
+ var cell = cells[i];
542
+
543
+ if ((!frameOnly || arrows) && LibavoidRouting.isAutoEdge(graph, cell) &&
544
+ (!frameOnly || LibavoidRouting.hasAutoJetty(graph.getCellStyle(cell))))
545
+ {
546
+ map[cell.getId()] = cell;
547
+ }
548
+ else if (model.isVertex(cell) && (relevant || (labelled &&
549
+ LibavoidRouting.hasOutsideLabel(graph, cell))))
550
+ {
551
+ if (framed || labelled)
552
+ {
553
+ var fb = LibavoidRouting.getAbsoluteObstacleBounds(graph, cell);
554
+
555
+ if (fb != null)
556
+ {
557
+ regions.push({x: fb.x, y: fb.y, width: fb.w, height: fb.h});
558
+ }
559
+
560
+ LibavoidRouting.addOutsideLabelRegions(graph, cell, regions);
561
+ }
562
+
563
+ var edges = model.getEdges(cell);
564
+
565
+ for (j = 0; j < edges.length; j++)
566
+ {
567
+ if (LibavoidRouting.isAutoEdge(graph, edges[j]))
568
+ {
569
+ map[edges[j].getId()] = edges[j];
570
+ }
571
+ }
572
+ }
573
+ }
574
+ };
575
+
576
+ editorUi.addListener('styleChanged', function(sender, evt)
577
+ {
578
+ var keys = evt.getProperty('keys');
579
+ var cells = evt.getProperty('cells');
580
+
581
+ if (keys != null && cells != null)
582
+ {
583
+ var map = Object.create(null);
584
+ var regions = [];
585
+
586
+ collectRestyled(keys, cells, map, regions);
587
+ LibavoidRouting.collectOverlappingEdges(graph, regions, map);
588
+ LibavoidRouting.autoReroute(graph, values(map));
589
+ }
590
+ });
591
+
592
+ // Style writes of arrange actions come with their cellsArranged (see
593
+ // Graph.beginArrange): eg. Edit Style, or Ctrl+R on a swimlane, which
594
+ // turns only its direction. The event carries the replaced styles, so each
595
+ // cell is handled like a styleChanged edit of the routing keys that differ
596
+ // (getChangedStyles). Forward actions only, like styleChanged — and unlike
597
+ // styleChanged, the event does not feed the sticky default styles.
598
+ graph.addListener('cellsArranged', function(sender, evt)
599
+ {
600
+ var cells = evt.getProperty('restyled');
601
+ var previous = evt.getProperty('previousStyles');
602
+ var map = Object.create(null);
603
+ var regions = [];
604
+
605
+ for (var i = 0; i < cells.length; i++)
606
+ {
607
+ collectRestyled(LibavoidRouting.getChangedStyles(graph, cells[i],
608
+ previous[i]), [cells[i]], map, regions);
609
+ }
610
+
611
+ LibavoidRouting.collectOverlappingEdges(graph, regions, map);
612
+ LibavoidRouting.autoReroute(graph, values(map));
613
+ });
614
+
615
+ // A shape moved or resized => re-route flagged edges that are affected: the
616
+ // ones CONNECTED to it, plus any whose route the shape now overlaps (an
617
+ // obstacle dropped onto an edge) or just vacated. autoReroute parks the solve
618
+ // until this edit's BEFORE_UNDO (after any childLayout of the gesture has
619
+ // run), still atomic with the move (one undo).
620
+ var onMoveResize = function(sender, evt)
621
+ {
622
+ var cells = evt.getProperty('cells');
623
+
624
+ if (cells == null)
625
+ {
626
+ return;
627
+ }
628
+
629
+ var dx = evt.getProperty('dx'), dy = evt.getProperty('dy'); // CELLS_MOVED
630
+ var previous = evt.getProperty('previous'); // CELLS_RESIZED, cellsArranged
631
+ var map = Object.create(null);
632
+ var regions = [];
633
+ var moved = [];
634
+ var i, j;
635
+
636
+ for (i = 0; i < cells.length; i++)
637
+ {
638
+ if (!model.isVertex(cells[i]))
639
+ {
640
+ continue;
641
+ }
642
+
643
+ moved.push(cells[i]);
644
+
645
+ // Connected auto-edges always re-route.
646
+ var edges = model.getEdges(cells[i]);
647
+
648
+ for (j = 0; j < edges.length; j++)
649
+ {
650
+ if (LibavoidRouting.isAutoEdge(graph, edges[j]))
651
+ {
652
+ map[edges[j].getId()] = edges[j];
653
+ }
654
+ }
655
+
656
+ // Affected region = union of the shape's OLD and NEW drawn boxes
657
+ // (a move or resize keeps its rotation, so both turn the same way).
658
+ var nv = LibavoidRouting.getVertex(graph, cells[i]);
659
+
660
+ if (nv == null)
661
+ {
662
+ continue;
663
+ }
664
+
665
+ var nb = AvoidRouting.obstacleBounds(nv);
666
+ var ox = nb.x, oy = nb.y, ow = nb.w, oh = nb.h;
667
+
668
+ if (previous != null && previous[i] != null)
669
+ {
670
+ var pb = LibavoidRouting.getAbsoluteGeometryBounds(graph, cells[i], previous[i]);
671
+ pb.frame = nv.frame;
672
+ var ob = AvoidRouting.obstacleBounds(pb);
673
+ ox = ob.x; oy = ob.y; ow = ob.w; oh = ob.h;
674
+ }
675
+ else if (dx != null && dy != null)
676
+ {
677
+ ox = nb.x - dx; oy = nb.y - dy;
678
+ }
679
+
680
+ var ux = Math.min(ox, nb.x), uy = Math.min(oy, nb.y);
681
+ regions.push({x: ux, y: uy,
682
+ width: Math.max(ox + ow, nb.x + nb.w) - ux,
683
+ height: Math.max(oy + oh, nb.y + nb.h) - uy});
684
+ }
685
+
686
+ LibavoidRouting.collectOverlappingEdges(graph, regions, map);
687
+
688
+ // Re-route the affected edges (routeCells uses the same configured strategy as
689
+ // the live preview, so the dropped route matches what was previewed). The moved
690
+ // vertices ride along so the parked flush can re-expand the affected set
691
+ // against their POST-childLayout positions.
692
+ LibavoidRouting.autoReroute(graph, values(map), moved);
693
+ };
694
+
695
+ graph.addListener(mxEvent.CELLS_MOVED, onMoveResize);
696
+ graph.addListener(mxEvent.CELLS_RESIZED, onMoveResize);
697
+
698
+ // Arrange actions that write vertex geometries directly (Arrange panel,
699
+ // distribute, turn, layouts — see Graph.beginArrange) fire neither of the
700
+ // above, only this dedicated forward-gesture event.
701
+ graph.addListener('cellsArranged', onMoveResize);
702
+
703
+ // The rotation HANDLE (a drag, or a click on it: rotateClick) writes the
704
+ // rotation style directly and fires none of the events above for the
705
+ // shape itself (only a group's moved children come as cellsArranged), yet a
706
+ // rotation moves the shape's connection points and turns its drawn box
707
+ // (AvoidRouting.shapeFrame). So this graph's vertex handlers are wrapped:
708
+ // the gesture's top-level rotation collects the affected edges like a
709
+ // move — the ones connected to the shape or to a group's children (turned
710
+ // with it), plus those crossing any of their drawn boxes before or after.
711
+ // It runs inside the handler's model update, so the parked solve lands in
712
+ // the same undoable edit. (Ctrl+R turns shapes through cellsArranged, the
713
+ // Format panel's angle through styleChanged — both handled above.)
714
+ var rotatedBoxes = function(cell)
715
+ {
716
+ var boxes = Object.create(null);
717
+ var cells = model.getDescendants(cell);
718
+
719
+ for (var i = 0; i < cells.length; i++)
720
+ {
721
+ if (model.isVertex(cells[i]))
722
+ {
723
+ var b = LibavoidRouting.getAbsoluteObstacleBounds(graph, cells[i]);
724
+
725
+ if (b != null)
726
+ {
727
+ boxes[cells[i].getId()] = b;
728
+ }
729
+ }
730
+ }
731
+
732
+ return boxes;
733
+ };
734
+
735
+ var rerouteRotated = function(cell, before)
736
+ {
737
+ var after = rotatedBoxes(cell);
738
+ var map = Object.create(null);
739
+ var regions = [];
740
+ var rotated = [];
741
+ var id, j;
742
+
743
+ for (id in before)
744
+ {
745
+ regions.push({x: before[id].x, y: before[id].y,
746
+ width: before[id].w, height: before[id].h});
747
+ }
748
+
749
+ for (id in after)
750
+ {
751
+ var vertex = model.getCell(id);
752
+ var edges = model.getEdges(vertex);
753
+
754
+ regions.push({x: after[id].x, y: after[id].y,
755
+ width: after[id].w, height: after[id].h});
756
+ rotated.push(vertex);
757
+
758
+ for (j = 0; j < edges.length; j++)
759
+ {
760
+ if (LibavoidRouting.isAutoEdge(graph, edges[j]))
761
+ {
762
+ map[edges[j].getId()] = edges[j];
763
+ }
764
+ }
765
+ }
766
+
767
+ LibavoidRouting.collectOverlappingEdges(graph, regions, map);
768
+ LibavoidRouting.autoReroute(graph, values(map), rotated);
769
+ };
770
+
771
+ var createVertexHandler = graph.createVertexHandler;
772
+
773
+ graph.createVertexHandler = function(state)
774
+ {
775
+ var handler = createVertexHandler.apply(this, arguments);
776
+
777
+ if (handler != null)
778
+ {
779
+ var rotateCell = handler.rotateCell;
780
+ var rotateClick = handler.rotateClick;
781
+
782
+ handler.rotateCell = function(cell, angle, parent)
783
+ {
784
+ // A group's children are turned by the recursion; the
785
+ // top-level call covers them.
786
+ if (parent != null)
787
+ {
788
+ rotateCell.apply(this, arguments);
789
+ }
790
+ else
791
+ {
792
+ var before = rotatedBoxes(cell);
793
+ rotateCell.apply(this, arguments);
794
+ rerouteRotated(cell, before);
795
+ }
796
+ };
797
+
798
+ handler.rotateClick = function()
799
+ {
800
+ var cell = this.state.cell;
801
+ var before = rotatedBoxes(cell);
802
+ rotateClick.apply(this, arguments);
803
+ rerouteRotated(cell, before);
804
+ };
805
+ }
806
+
807
+ return handler;
808
+ };
809
+
810
+ // The box of a multi-selection (Graph.MultiSelectionHandler) previews the
811
+ // routes of the auto-edges that a resize re-routes on mouse up through the
812
+ // events above (addResizePreviewRoutes). Checked as the core can be older
813
+ // than the extensions. Never lets a preview route break the gesture.
814
+ var createMultiSelectionHandler = graph.createMultiSelectionHandler;
815
+
816
+ if (createMultiSelectionHandler != null)
817
+ {
818
+ graph.createMultiSelectionHandler = function(cells)
819
+ {
820
+ var handler = createMultiSelectionHandler.apply(this, arguments);
821
+ var resetPreview = handler.resetPreview;
822
+
823
+ handler.addPreviewGeometries = function(geometries)
824
+ {
825
+ try
826
+ {
827
+ return LibavoidRouting.addResizePreviewRoutes(graph, this, geometries);
828
+ }
829
+ catch (e)
830
+ {
831
+ return null;
832
+ }
833
+ };
834
+
835
+ handler.resetPreview = function()
836
+ {
837
+ LibavoidRouting.endResizePreview(this);
838
+ resetPreview.apply(this, arguments);
839
+ };
840
+
841
+ return handler;
842
+ };
843
+ }
844
+ };
845
+
846
+ /**
847
+ * Add to `map` every auto-edge (not already present) whose route bbox overlaps
848
+ * any of the given regions ({x,y,width,height}, absolute model coords). Inclusive
849
+ * test so zero-width vertical / zero-height horizontal edges still match; over-
850
+ * inclusion is harmless (re-routing an unaffected edge yields the same geometry,
851
+ * which routeCells then skips writing — see samePoints). O(cells) — callers run it
852
+ * on commit (drag-end / insert), not per frame. edgeBounds is an optional array of
853
+ * the auto-edges with their route bounds (getAutoEdgeBounds) collected before, so
854
+ * the shape-move preview walks the cells once per drag (getMovePreviewCache).
855
+ */
856
+ LibavoidRouting.collectOverlappingEdges = function(graph, regions, map, edgeBounds)
857
+ {
858
+ if (regions == null || regions.length == 0)
859
+ {
860
+ return;
861
+ }
862
+
863
+ edgeBounds = (edgeBounds != null) ? edgeBounds :
864
+ LibavoidRouting.getAutoEdgeBounds(graph, map);
865
+
866
+ for (var i = 0; i < edgeBounds.length; i++)
867
+ {
868
+ var c = edgeBounds[i].cell;
869
+ var bb = edgeBounds[i].bounds;
870
+ var id = c.getId();
871
+
872
+ if (map[id] != null)
873
+ {
874
+ continue;
875
+ }
876
+
877
+ for (var j = 0; j < regions.length; j++)
878
+ {
879
+ var r = regions[j];
880
+
881
+ if (bb.x <= r.x + r.width && r.x <= bb.x + bb.width &&
882
+ bb.y <= r.y + r.height && r.y <= bb.y + bb.height)
883
+ {
884
+ map[id] = c;
885
+ break;
886
+ }
887
+ }
888
+ }
889
+ };
890
+
891
+ /**
892
+ * The auto-edges of the model that are not in the optional skip map (by id)
893
+ * with their route bounds (edgeRouteBounds) as an array of {cell, bounds}.
894
+ * Edges without route bounds are left out. The edges are in the order of
895
+ * model.cells (see getFlaggedCells).
896
+ */
897
+ LibavoidRouting.getAutoEdgeBounds = function(graph, skip)
898
+ {
899
+ var model = graph.getModel();
900
+ var index = LibavoidRouting.getFlaggedCells(graph);
901
+ var result = [];
902
+
903
+ for (var id in index.cells)
904
+ {
905
+ var c = index.cells[id];
906
+
907
+ if (model.getCell(id) == c && (skip == null || skip[id] == null) && model.isEdge(c) &&
908
+ index.flagged(c) && LibavoidRouting.isAutoEdge(graph, c))
909
+ {
910
+ var bb = LibavoidRouting.edgeRouteBounds(graph, c);
911
+
912
+ if (bb != null)
913
+ {
914
+ result.push({cell: c, bounds: bb});
915
+ }
916
+ }
917
+ }
918
+
919
+ return result;
920
+ };
921
+
922
+ /**
923
+ * Returns the cells that the walks over the auto-edges look at
924
+ * (getAutoEdgeBounds) as {cells, flagged}: the cells of the model whose style
925
+ * names the routing flag by ID in the order of model.cells, or model.cells if
926
+ * a style of the stylesheet sets the flag, and the filter for the flag
927
+ * (createStyleFilter). The walks visited all cells for each insert, which
928
+ * made a burst of inserts in one edit quadratic (eg. a CSV import adds one
929
+ * cell after the other). The list follows the changes of the model (see
930
+ * installFlaggedCells) and is created again for a new model.cells (a new
931
+ * root, also the same root again after a page switch, as model.cells then
932
+ * has the order of the cells in the tree) and if the stylesheet starts or
933
+ * stops setting the flag. It may contain cells that are no longer in the
934
+ * model or no longer have the flag.
935
+ */
936
+ LibavoidRouting.getFlaggedCells = function(graph)
937
+ {
938
+ var model = graph.getModel();
939
+
940
+ if (LibavoidRouting.isStylesheetKey(graph, [LibavoidRouting.STYLE]))
941
+ {
942
+ return {cells: model.cells, flagged: function()
943
+ {
944
+ return true;
945
+ }};
946
+ }
947
+
948
+ var index = graph.__libavoidFlaggedCells;
949
+
950
+ if (index == null || index.lookup != model.cells)
951
+ {
952
+ LibavoidRouting.installFlaggedCells(graph);
953
+ index = {lookup: model.cells, cells: Object.create(null),
954
+ flagged: LibavoidRouting.createStyleFilter(graph, [LibavoidRouting.STYLE])};
955
+
956
+ for (var id in model.cells)
957
+ {
958
+ if (index.flagged(model.cells[id]))
959
+ {
960
+ index.cells[id] = model.cells[id];
961
+ }
962
+ }
963
+
964
+ graph.__libavoidFlaggedCells = index;
965
+ }
966
+
967
+ return index;
968
+ };
969
+
970
+ /**
971
+ * Keeps the flagged cells of the given graph (getFlaggedCells) in the order of
972
+ * model.cells for each change that the model executes, also for undo and
973
+ * redo (EXECUTED): an added cell and its descendants go to the end of both in
974
+ * the order of cellAdded, removed cells are deleted. The list is created again
975
+ * if a cell in the model gets the flag, as its place in model.cells is not
976
+ * known, and for other changes than those of the cells (eg. a new ID).
977
+ */
978
+ LibavoidRouting.installFlaggedCells = function(graph)
979
+ {
980
+ if (graph.__libavoidFlaggedCellsInstalled)
981
+ {
982
+ return;
983
+ }
984
+
985
+ graph.__libavoidFlaggedCellsInstalled = true;
986
+ var model = graph.getModel();
987
+
988
+ // Adds the flagged cells of the given cell that was added to the model or
989
+ // deletes the cells of a removed cell, without recursion
990
+ var update = function(index, cell, added)
991
+ {
992
+ var stack = [cell];
993
+
994
+ while (stack.length > 0)
995
+ {
996
+ var c = stack.pop();
997
+ var id = c.getId();
998
+
999
+ if (!added)
1000
+ {
1001
+ if (index.cells[id] == c)
1002
+ {
1003
+ delete index.cells[id];
1004
+ }
1005
+ }
1006
+ else if (index.cells[id] == null && index.flagged(c))
1007
+ {
1008
+ index.cells[id] = c;
1009
+ }
1010
+
1011
+ for (var i = model.getChildCount(c) - 1; i >= 0; i--)
1012
+ {
1013
+ stack.push(model.getChildAt(c, i));
1014
+ }
1015
+ }
1016
+ };
1017
+
1018
+ model.addListener(mxEvent.EXECUTED, function(sender, evt)
1019
+ {
1020
+ var index = graph.__libavoidFlaggedCells;
1021
+ var change = evt.getProperty('change');
1022
+
1023
+ if (index != null && index.lookup == model.cells)
1024
+ {
1025
+ if (change instanceof mxChildChange)
1026
+ {
1027
+ // Cells that are moved in the model keep their places in model.cells
1028
+ var child = change.child;
1029
+ var added = child != null && model.getCell(child.getId()) == child;
1030
+
1031
+ if (child != null && added != (change.previous != null &&
1032
+ model.contains(change.previous)))
1033
+ {
1034
+ update(index, child, added);
1035
+ }
1036
+ }
1037
+ else if (change instanceof mxStyleChange)
1038
+ {
1039
+ var cell = change.cell;
1040
+
1041
+ if (cell != null && index.cells[cell.getId()] != cell &&
1042
+ model.getCell(cell.getId()) == cell && index.flagged(cell))
1043
+ {
1044
+ graph.__libavoidFlaggedCells = null;
1045
+ }
1046
+ }
1047
+ else if (!(change instanceof mxGeometryChange || change instanceof mxValueChange ||
1048
+ change instanceof mxTerminalChange || change instanceof mxCollapseChange ||
1049
+ change instanceof mxVisibleChange || change instanceof mxCellAttributeChange))
1050
+ {
1051
+ graph.__libavoidFlaggedCells = null;
1052
+ }
1053
+ }
1054
+ });
1055
+ };
1056
+
1057
+ /**
1058
+ * Returns true if a style of the stylesheet sets one of the given keys.
1059
+ */
1060
+ LibavoidRouting.isStylesheetKey = function(graph, keys)
1061
+ {
1062
+ var styles = graph.getStylesheet().styles;
1063
+
1064
+ for (var name in styles)
1065
+ {
1066
+ for (var i = 0; i < keys.length; i++)
1067
+ {
1068
+ if (styles[name] != null && styles[name][keys[i]] != null)
1069
+ {
1070
+ return true;
1071
+ }
1072
+ }
1073
+ }
1074
+
1075
+ return false;
1076
+ };
1077
+
1078
+ /**
1079
+ * A function that returns false for a cell whose style (getCellStyle) sets
1080
+ * none of the given keys, without resolving the style. A resolved style takes
1081
+ * its keys only from the style string of the cell and from the styles of the
1082
+ * stylesheet (postProcessCellStyle sets no routing keys), so a key that
1083
+ * neither names is not set. The walks over all cells test this first, as
1084
+ * resolving the style of every cell took most of their time. If a style of
1085
+ * the stylesheet sets one of the keys, the function returns true for all
1086
+ * cells.
1087
+ */
1088
+ LibavoidRouting.createStyleFilter = function(graph, keys)
1089
+ {
1090
+ if (LibavoidRouting.isStylesheetKey(graph, keys))
1091
+ {
1092
+ return function()
1093
+ {
1094
+ return true;
1095
+ };
1096
+ }
1097
+
1098
+ var model = graph.getModel();
1099
+
1100
+ return function(cell)
1101
+ {
1102
+ var style = model.getStyle(cell);
1103
+
1104
+ if (style != null)
1105
+ {
1106
+ for (var j = 0; j < keys.length; j++)
1107
+ {
1108
+ if (style.indexOf(keys[j]) >= 0)
1109
+ {
1110
+ return true;
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ return false;
1116
+ };
1117
+ };
1118
+
1119
+ /**
1120
+ * Re-route the given flagged edge cells, honoring any pending childLayout of
1121
+ * the same edit. Called inside a model update (every auto-routing event fires
1122
+ * within one), the request is PARKED and solved on the model's BEFORE_UNDO
1123
+ * instead of immediately: the layout manager runs the edit's synchronous
1124
+ * childLayouts from its own BEFORE_UNDO handler (registered at Graph
1125
+ * construction, i.e. before the lazily installed flush below), so a terminal
1126
+ * dropped into a stack/table/tree container is routed against its final
1127
+ * laid-out position — an event-time solve would route against the drop point,
1128
+ * and the layout's own writes are model-level, so they re-fire no graph event
1129
+ * that could fix it up. BEFORE_UNDO fires before the edit is closed and the
1130
+ * endingUpdate latch swallows nested dispatches, so the deferred route still
1131
+ * joins the same undoable edit (one undo reverts move + layout + route).
1132
+ * Outside an update the solve runs immediately as before.
1133
+ *
1134
+ * vertexCells (optional) are the gesture's moved/resized/inserted vertices:
1135
+ * the flush re-expands the affected-edge set against their post-layout
1136
+ * positions (see flushReroute). An edge-less park only matters when such a
1137
+ * vertex sits under a live layout container — anywhere else nothing moves
1138
+ * after the event — so plain-canvas gestures skip the parking entirely.
1139
+ */
1140
+ LibavoidRouting.autoReroute = function(graph, edgeCells, vertexCells)
1141
+ {
1142
+ var hasEdges = (edgeCells != null && edgeCells.length > 0);
1143
+
1144
+ if (graph.getModel().updateLevel > 0)
1145
+ {
1146
+ if (hasEdges || LibavoidRouting.anyInLayoutContainer(graph, vertexCells))
1147
+ {
1148
+ LibavoidRouting.parkReroute(graph, edgeCells, vertexCells);
1149
+ }
1150
+ }
1151
+ else if (hasEdges)
1152
+ {
1153
+ LibavoidRouting.solveReroute(graph, edgeCells);
1154
+ }
1155
+ };
1156
+
1157
+ /**
1158
+ * True if any of the given cells lives under a childLayout ancestor — the only
1159
+ * case an edge-less gesture park matters (the container's layout re-runs at
1160
+ * BEFORE_UNDO and can move cells over flagged edges).
1161
+ */
1162
+ LibavoidRouting.anyInLayoutContainer = function(graph, cells)
1163
+ {
1164
+ if (cells != null)
1165
+ {
1166
+ for (var i = 0; i < cells.length; i++)
1167
+ {
1168
+ if (cells[i] != null &&
1169
+ LibavoidRouting.layoutContainerOf(graph, cells[i]) != null)
1170
+ {
1171
+ return true;
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ return false;
1177
+ };
1178
+
1179
+ /**
1180
+ * Parks a reroute request until the current edit's BEFORE_UNDO (see
1181
+ * autoReroute). The flush listener is installed lazily on first use, which
1182
+ * puts it AFTER the layout manager's own BEFORE_UNDO handler in the model's
1183
+ * listener list — so the flush runs once the edit's synchronous childLayouts
1184
+ * have written their geometry. The parked vertices' childLayout ancestors are
1185
+ * resolved and measured NOW as well as at flush time: at park time the chain
1186
+ * and bounds still reflect the pre-layout state (a vertex dragged OUT of a
1187
+ * stack records the container it left before resizeParent shrinks it), the
1188
+ * flush adds the post-layout boxes.
1189
+ */
1190
+ LibavoidRouting.parkReroute = function(graph, edgeCells, vertexCells)
1191
+ {
1192
+ var pending = graph.__libavoidPendingReroute;
1193
+ var i;
1194
+
1195
+ if (pending == null)
1196
+ {
1197
+ pending = graph.__libavoidPendingReroute = {edges: Object.create(null),
1198
+ vertices: Object.create(null), containers: Object.create(null),
1199
+ regions: []};
1200
+ }
1201
+
1202
+ if (edgeCells != null)
1203
+ {
1204
+ for (i = 0; i < edgeCells.length; i++)
1205
+ {
1206
+ if (edgeCells[i] != null)
1207
+ {
1208
+ pending.edges[edgeCells[i].getId()] = edgeCells[i];
1209
+ }
1210
+ }
1211
+ }
1212
+
1213
+ if (vertexCells != null)
1214
+ {
1215
+ for (i = 0; i < vertexCells.length; i++)
1216
+ {
1217
+ if (vertexCells[i] != null)
1218
+ {
1219
+ pending.vertices[vertexCells[i].getId()] = vertexCells[i];
1220
+ LibavoidRouting.addLayoutContainers(graph, vertexCells[i],
1221
+ pending.containers, pending.regions);
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ if (!graph.__libavoidRerouteFlush)
1227
+ {
1228
+ graph.__libavoidRerouteFlush = true;
1229
+
1230
+ // Fires per non-empty edit; a null pending returns immediately. The
1231
+ // theoretical leak — parking into an edit that ends up EMPTY, so its
1232
+ // BEFORE_UNDO never fires — can't happen from the auto-routing events
1233
+ // (they all record model changes), and a leftover would only re-solve
1234
+ // already-routed edges (samePoints skips the writes).
1235
+ graph.getModel().addListener(mxEvent.BEFORE_UNDO, function()
1236
+ {
1237
+ LibavoidRouting.flushReroute(graph);
1238
+ });
1239
+ }
1240
+ };
1241
+
1242
+ /**
1243
+ * Adds every childLayout ancestor of the cell (nested containers all re-run
1244
+ * under the layout manager) to the containers map; a NEWLY seen container also
1245
+ * pushes its current absolute bounds onto regions (when non-null) as an
1246
+ * affected region.
1247
+ */
1248
+ LibavoidRouting.addLayoutContainers = function(graph, cell, containers, regions)
1249
+ {
1250
+ var model = graph.getModel();
1251
+ var p = model.getParent(cell);
1252
+
1253
+ while (p != null && model.isVertex(p))
1254
+ {
1255
+ if (containers[p.getId()] == null &&
1256
+ graph.getCurrentCellStyle(p)['childLayout'] != null)
1257
+ {
1258
+ containers[p.getId()] = p;
1259
+
1260
+ if (regions != null)
1261
+ {
1262
+ var b = LibavoidRouting.getAbsoluteObstacleBounds(graph, p);
1263
+
1264
+ if (b != null)
1265
+ {
1266
+ regions.push({x: b.x, y: b.y, width: b.w, height: b.h});
1267
+ }
1268
+ }
1269
+ }
1270
+
1271
+ p = model.getParent(p);
1272
+ }
1273
+ };
1274
+
1275
+ /**
1276
+ * Solves a parked reroute request from the model's BEFORE_UNDO, after the
1277
+ * layout manager has run the edit's synchronous childLayouts (see
1278
+ * autoReroute). The affected-edge set is re-expanded against the POST-layout
1279
+ * state: the parked vertices' current bounds plus the current bounds of every
1280
+ * childLayout ancestor — resolved both at park time (the pre-gesture chain and
1281
+ * boxes) and now (a drop INTO a container resolves its new chain only here;
1282
+ * CELLS_MOVED fires before the reparent) — become overlap regions, so edges
1283
+ * whose obstacles the layout shifted (a stack re-flowing its items,
1284
+ * resizeParent growing the container) are caught even though those writes fire
1285
+ * no graph events. Cleared before solving so a listener firing mid-flush
1286
+ * starts a new cycle instead of extending this one.
1287
+ */
1288
+ LibavoidRouting.flushReroute = function(graph)
1289
+ {
1290
+ var pending = graph.__libavoidPendingReroute;
1291
+
1292
+ if (pending == null)
1293
+ {
1294
+ return;
1295
+ }
1296
+
1297
+ graph.__libavoidPendingReroute = null;
1298
+
1299
+ var regions = pending.regions;
1300
+ var id, b;
1301
+
1302
+ for (id in pending.vertices)
1303
+ {
1304
+ b = LibavoidRouting.getAbsoluteObstacleBounds(graph, pending.vertices[id]);
1305
+
1306
+ if (b != null)
1307
+ {
1308
+ regions.push({x: b.x, y: b.y, width: b.w, height: b.h});
1309
+ }
1310
+
1311
+ LibavoidRouting.addLayoutContainers(graph, pending.vertices[id],
1312
+ pending.containers, null);
1313
+ }
1314
+
1315
+ for (id in pending.containers)
1316
+ {
1317
+ b = LibavoidRouting.getAbsoluteObstacleBounds(graph, pending.containers[id]);
1318
+
1319
+ if (b != null)
1320
+ {
1321
+ regions.push({x: b.x, y: b.y, width: b.w, height: b.h});
1322
+ }
1323
+ }
1324
+
1325
+ LibavoidRouting.collectOverlappingEdges(graph, regions, pending.edges);
1326
+
1327
+ var edges = [];
1328
+
1329
+ for (id in pending.edges)
1330
+ {
1331
+ edges.push(pending.edges[id]);
1332
+ }
1333
+
1334
+ LibavoidRouting.solveReroute(graph, edges);
1335
+ };
1336
+
1337
+ /**
1338
+ * Runs the actual solve for the given flagged edges: synchronously when the
1339
+ * WASM is already warm (so it merges into the caller's ongoing edit for atomic
1340
+ * undo), else deferred until the loader resolves (cold start, first use only).
1341
+ * Writes only waypoints — the edge already carries orthogonalEdgeStyle (paired
1342
+ * with the flag), so no style churn per move.
1343
+ */
1344
+ LibavoidRouting.solveReroute = function(graph, edgeCells)
1345
+ {
1346
+ if (edgeCells == null || edgeCells.length == 0)
1347
+ {
1348
+ return;
1349
+ }
1350
+
1351
+ var run = function(Avoid)
1352
+ {
1353
+ if (Avoid == null)
1354
+ {
1355
+ return;
1356
+ }
1357
+
1358
+ // Re-check at solve time. On a cold start this runs only after the
1359
+ // __libavoidReady promise resolves, by when the user may have switched the
1360
+ // edge to another routing style, added a manual waypoint (both clear the
1361
+ // flag via isAutoEdge), or deleted it (contains) — don't overwrite that.
1362
+ var model = graph.getModel();
1363
+ var live = [];
1364
+
1365
+ for (var i = 0; i < edgeCells.length; i++)
1366
+ {
1367
+ if (model.contains(edgeCells[i]) &&
1368
+ LibavoidRouting.isAutoEdge(graph, edgeCells[i]))
1369
+ {
1370
+ live.push(edgeCells[i]);
1371
+ }
1372
+ }
1373
+
1374
+ if (live.length == 0)
1375
+ {
1376
+ return;
1377
+ }
1378
+
1379
+ model.beginUpdate();
1380
+
1381
+ try
1382
+ {
1383
+ LibavoidRouting.routeCells(graph, Avoid, live, null, false);
1384
+ }
1385
+ finally
1386
+ {
1387
+ model.endUpdate();
1388
+ }
1389
+ };
1390
+
1391
+ if (typeof window === 'undefined')
1392
+ {
1393
+ return;
1394
+ }
1395
+
1396
+ if (window.Avoid != null)
1397
+ {
1398
+ run(window.Avoid);
1399
+ }
1400
+ else if (window.__libavoidReady != null)
1401
+ {
1402
+ window.__libavoidReady.then(run);
1403
+ }
1404
+ };
1405
+
1406
+ /**
1407
+ * The nearest ancestor vertex carrying a childLayout style (the same test as
1408
+ * the mxLayoutManager.hasLayout override in grapheditor/Graph.js), or null.
1409
+ * Such a LIVE LAYOUT CONTAINER owns the geometry of everything inside it: the
1410
+ * manager re-runs its layout on every change within the container, so any
1411
+ * route libavoid writes to a child edge is immediately overwritten (and a
1412
+ * live preview of that route shows bends that never commit).
1413
+ */
1414
+ LibavoidRouting.layoutContainerOf = function(graph, cell)
1415
+ {
1416
+ var model = graph.getModel();
1417
+ var p = model.getParent(cell);
1418
+
1419
+ while (p != null && model.isVertex(p))
1420
+ {
1421
+ if (graph.getCurrentCellStyle(p)['childLayout'] != null)
1422
+ {
1423
+ return p;
1424
+ }
1425
+
1426
+ p = model.getParent(p);
1427
+ }
1428
+
1429
+ return null;
1430
+ };
1431
+
1432
+ /**
1433
+ * True if the cell is an edge carrying the auto-routing flag AND living
1434
+ * outside any live layout container: inside one, the container's childLayout
1435
+ * owns the edge geometry (see layoutContainerOf), so the flag stays on the
1436
+ * style but is inert — it takes effect again when the edge leaves the
1437
+ * container (e.g. is copied out onto the canvas).
1438
+ */
1439
+ LibavoidRouting.isAutoEdge = function(graph, edge)
1440
+ {
1441
+ if (edge == null || !graph.getModel().isEdge(edge))
1442
+ {
1443
+ return false;
1444
+ }
1445
+
1446
+ var style = graph.getCellStyle(edge);
1447
+
1448
+ return style != null && mxUtils.getValue(style, LibavoidRouting.STYLE, null) == '1' &&
1449
+ LibavoidRouting.layoutContainerOf(graph, edge) == null;
1450
+ };
1451
+
1452
+ /**
1453
+ * The routing input keys (constraintStyles, frameStyles, endStyles) whose
1454
+ * values differ between the given replaced style of the cell and its current
1455
+ * style. A changed jetty size of an edge is reported as jettySize also if it
1456
+ * comes from the arrow of a jettySize=auto end (jettyFor).
1457
+ */
1458
+ LibavoidRouting.getChangedStyles = function(graph, cell, previous)
1459
+ {
1460
+ var model = graph.getModel();
1461
+ var defaults = (model.isEdge(cell)) ? graph.stylesheet.getDefaultEdgeStyle() :
1462
+ graph.stylesheet.getDefaultVertexStyle();
1463
+ var before = graph.stylesheet.getCellStyle(previous, defaults);
1464
+ var after = graph.stylesheet.getCellStyle(model.getStyle(cell), defaults);
1465
+ var keys = LibavoidRouting.constraintStyles.concat(LibavoidRouting.frameStyles,
1466
+ LibavoidRouting.labelStyles, LibavoidRouting.labelBoxStyles,
1467
+ LibavoidRouting.endStyles);
1468
+ var changed = [];
1469
+
1470
+ for (var i = 0; i < keys.length; i++)
1471
+ {
1472
+ if (before[keys[i]] != after[keys[i]])
1473
+ {
1474
+ changed.push(keys[i]);
1475
+ }
1476
+ }
1477
+
1478
+ if (model.isEdge(cell) && mxUtils.indexOf(changed, 'jettySize') < 0 &&
1479
+ (LibavoidRouting.jettyFor(before, true) != LibavoidRouting.jettyFor(after, true) ||
1480
+ LibavoidRouting.jettyFor(before, false) != LibavoidRouting.jettyFor(after, false)))
1481
+ {
1482
+ changed.push('jettySize');
1483
+ }
1484
+
1485
+ return changed;
1486
+ };
1487
+
1488
+ /**
1489
+ * Turn libavoid auto-routing OFF (libavoidRouting=0) on every visible edge
1490
+ * under parent that carries the flag, honoring the optional cell filter
1491
+ * (function(cell) -> boolean, e.g. an ELK run's selection-as-root component
1492
+ * scope; a filtered-out cell is skipped INCLUDING its subtree, matching the
1493
+ * adapter's cellFilter semantics). Called by layouts that stamp their own
1494
+ * edge routing (diagramly/ElkLayout.js wraps ElkLayout._applyResult):
1495
+ * leaving the flag enabled would let the next gesture re-route the
1496
+ * just-laid-out edges per-edge via libavoid — with the layout's stamped
1497
+ * exit/entry pins forcing fanned detours. Writes only where the flag is
1498
+ * currently '1', so converged childLayout re-runs stay empty edits, and
1499
+ * runs inside the caller's model edit (one undo reverts layout + flags).
1500
+ */
1501
+ LibavoidRouting.releaseEdges = function(graph, parent, filter)
1502
+ {
1503
+ var model = graph.getModel();
1504
+ var release = [];
1505
+
1506
+ var collect = function(cell)
1507
+ {
1508
+ var n = model.getChildCount(cell);
1509
+
1510
+ for (var i = 0; i < n; i++)
1511
+ {
1512
+ var child = model.getChildAt(cell, i);
1513
+
1514
+ if (filter == null || filter(child))
1515
+ {
1516
+ if (model.isEdge(child) && model.isVisible(child) &&
1517
+ mxUtils.getValue(graph.getCellStyle(child),
1518
+ LibavoidRouting.STYLE, null) == '1')
1519
+ {
1520
+ release.push(child);
1521
+ }
1522
+
1523
+ collect(child);
1524
+ }
1525
+ }
1526
+ };
1527
+
1528
+ collect(parent);
1529
+
1530
+ if (release.length > 0)
1531
+ {
1532
+ graph.setCellStyles(LibavoidRouting.STYLE, '0', release);
1533
+ }
1534
+ };
1535
+
1536
+ /**
1537
+ * The edges the menu action should route: selected edges if any, else all edges.
1538
+ */
1539
+ LibavoidRouting.getTargetEdges = function(graph)
1540
+ {
1541
+ var model = graph.getModel();
1542
+ var out = [];
1543
+
1544
+ if (!graph.isSelectionEmpty())
1545
+ {
1546
+ var sel = graph.getSelectionCells();
1547
+
1548
+ for (var i = 0; i < sel.length; i++)
1549
+ {
1550
+ if (model.isEdge(sel[i]))
1551
+ {
1552
+ out.push(sel[i]);
1553
+ }
1554
+ }
1555
+
1556
+ if (out.length > 0)
1557
+ {
1558
+ return out;
1559
+ }
1560
+ }
1561
+
1562
+ for (var id in model.cells)
1563
+ {
1564
+ if (model.isEdge(model.cells[id]))
1565
+ {
1566
+ out.push(model.cells[id]);
1567
+ }
1568
+ }
1569
+
1570
+ return out;
1571
+ };
1572
+
1573
+ /**
1574
+ * A fixed connection point on one end of an edge (exitX/exitY for the source,
1575
+ * entryX/entryY for the target), as {x, y, dir}: proportional position in [0,1]
1576
+ * plus the ConnDirFlags approach direction inferred from which shape edge the
1577
+ * point sits on (AvoidRouting.constraintForPoint — it clamps to the pin's
1578
+ * [0,1] domain and derives the direction from the ORIGINAL values). Returns
1579
+ * null for a floating endpoint (no fixed point).
1580
+ */
1581
+ LibavoidRouting.fixedConstraint = function(style, source)
1582
+ {
1583
+ var fx = mxUtils.getValue(style, source ? 'exitX' : 'entryX', null);
1584
+ var fy = mxUtils.getValue(style, source ? 'exitY' : 'entryY', null);
1585
+
1586
+ // exitPerimeter/entryPerimeter decide whether flips move the point on a
1587
+ // transformed terminal (AvoidRouting.shapePin).
1588
+ return (fx != null && fy != null) ?
1589
+ AvoidRouting.constraintForPoint(parseFloat(fx), parseFloat(fy),
1590
+ mxUtils.getValue(style, source ? 'exitPerimeter' : 'entryPerimeter', 1) != 0) : null;
1591
+ };
1592
+
1593
+ /**
1594
+ * Allowed attach sides for one end of an edge per the legacy mxGraph
1595
+ * port-constraint styles — the terminal's `portConstraint` (rotated with the
1596
+ * shape when portConstraintRotation=1), else the edge's
1597
+ * `sourcePortConstraint`/`targetPortConstraint` — as AvoidRouting side
1598
+ * letters ('N'/'S'/'E'/'W') for computeRoutes' sourceSides/targetSides, or
1599
+ * null when the end is unconstrained. Resolved through
1600
+ * mxUtils.getPortConstraints, the exact semantics the orthogonal router
1601
+ * enforces on floating ends at render time, so the committed route and the
1602
+ * rendered attach agree. Takes parsed STYLE maps like fixedConstraint (the
1603
+ * function only reads .style, so plain wrappers suffice).
1604
+ */
1605
+ LibavoidRouting.maskSides = function(edgeStyle, terminalStyle, source)
1606
+ {
1607
+ if (edgeStyle == null || terminalStyle == null)
1608
+ {
1609
+ return null;
1610
+ }
1611
+
1612
+ var mask = mxUtils.getPortConstraints({style: terminalStyle},
1613
+ {style: edgeStyle}, source, mxConstants.DIRECTION_MASK_NONE);
1614
+
1615
+ if (mask == mxConstants.DIRECTION_MASK_NONE ||
1616
+ mask == mxConstants.DIRECTION_MASK_ALL)
1617
+ {
1618
+ return null;
1619
+ }
1620
+
1621
+ var sides = [];
1622
+
1623
+ if ((mask & mxConstants.DIRECTION_MASK_NORTH) != 0) sides.push('N');
1624
+ if ((mask & mxConstants.DIRECTION_MASK_SOUTH) != 0) sides.push('S');
1625
+ if ((mask & mxConstants.DIRECTION_MASK_WEST) != 0) sides.push('W');
1626
+ if ((mask & mxConstants.DIRECTION_MASK_EAST) != 0) sides.push('E');
1627
+
1628
+ return (sides.length > 0) ? sides : null;
1629
+ };
1630
+
1631
+ /**
1632
+ * The terminal's declared connection points for one end of an edge as routing
1633
+ * pin candidates [{x, y, dir}] for computeRoutes' sourcePoints/targetPoints,
1634
+ * or null when snapToPoint doesn't apply. Mirrors the render-time gate
1635
+ * (mxGraphView.updateFloatingTerminalPoint in grapheditor/Graph.js: the
1636
+ * terminal OR the edge carries snapToPoint=1) so the committed route attaches
1637
+ * where the floating end will snap at render time. The anchors come from
1638
+ * graph.getAllConnectionConstraints for a shape created for the given
1639
+ * terminal style (stencil and shape constraints need a shape). A terminal the
1640
+ * view does not render (hidden, or inside a collapsed cell — isRendered) has
1641
+ * no snap, matching a render that draws nothing; one added in the same edit,
1642
+ * which has no view state yet when the auto-routing solves, does. Each
1643
+ * constraint's dx/dy offset (model px) is folded into the fraction over the
1644
+ * terminal's routing box (getAbsoluteModelBounds — the same frame the
1645
+ * obstacle box uses, incl. the derived hull of transparentBounds cells) and
1646
+ * the approach direction derives from the resulting point (constraintForPoint).
1647
+ * Only meaningful for FLOATING ends: the callers' precedence gives an
1648
+ * explicit exit/entry anchor the win, like the renderer.
1649
+ */
1650
+ LibavoidRouting.snapPoints = function(graph, terminal, edgeStyle, terminalStyle)
1651
+ {
1652
+ if (terminal == null || edgeStyle == null || terminalStyle == null)
1653
+ {
1654
+ return null;
1655
+ }
1656
+
1657
+ if (mxUtils.getValue(terminalStyle, 'snapToPoint', '0') != '1' &&
1658
+ mxUtils.getValue(edgeStyle, 'snapToPoint', '0') != '1')
1659
+ {
1660
+ return null;
1661
+ }
1662
+
1663
+ var b = LibavoidRouting.getAbsoluteModelBounds(graph, terminal);
1664
+
1665
+ if (!LibavoidRouting.isRendered(graph, terminal) || b == null ||
1666
+ !(b.w > 0) || !(b.h > 0))
1667
+ {
1668
+ return null;
1669
+ }
1670
+
1671
+ // The auto-routing solves before the view is revalidated (at BEFORE_UNDO),
1672
+ // when the view state can still hold the points and shape from before the
1673
+ // edit (eg. after Edit Style), so the constraints come from the given style
1674
+ // and a shape at the routing size. The rendered shape is copied if the
1675
+ // shape name is the same, as some shapes only declare their constraints
1676
+ // when painted (mxgraph.bpmn.shape).
1677
+ var view = graph.view.getState(terminal);
1678
+ var state = new mxCellState(graph.view, terminal, terminalStyle);
1679
+ // Constraints computed from the outline of the shape use the size of the
1680
+ // state (Graph.getOutlineConstraints)
1681
+ state.width = b.w * graph.view.scale;
1682
+ state.height = b.h * graph.view.scale;
1683
+ // The copy of the rendered shape has the styles from before the edit and
1684
+ // a new shape has none, but the points along the outline of the shape
1685
+ // depend on them (Graph.getOutlineConstraints), so the shape of an
1686
+ // outline perimeter is configured for the given style as the renderer
1687
+ // does after a change of the style. Its copy has the fields of the
1688
+ // rendered shape as its own, so that resetStyles restores the defaults of
1689
+ // the keys that the style does not have.
1690
+ var outline = graph.isOutlinePerimeter != null && graph.isOutlinePerimeter(state);
1691
+ state.shape = (view != null && view.shape != null && view.style != null &&
1692
+ view.style[mxConstants.STYLE_SHAPE] == terminalStyle[mxConstants.STYLE_SHAPE]) ?
1693
+ ((outline) ? Object.assign(Object.create(Object.getPrototypeOf(view.shape)), view.shape) :
1694
+ Object.create(view.shape)) : graph.cellRenderer.createShape(state);
1695
+
1696
+ if (state.shape != null)
1697
+ {
1698
+ if (outline)
1699
+ {
1700
+ state.shape.resetStyles();
1701
+ graph.cellRenderer.configureShape(state);
1702
+ }
1703
+
1704
+ state.shape.direction = mxUtils.getValue(terminalStyle,
1705
+ mxConstants.STYLE_DIRECTION, mxConstants.DIRECTION_EAST);
1706
+ state.shape.bounds = new mxRectangle(0, 0, b.w, b.h);
1707
+ state.shape.scale = 1;
1708
+ }
1709
+
1710
+ var constraints = graph.getAllConnectionConstraints(state);
1711
+
1712
+ if (constraints == null || constraints.length == 0)
1713
+ {
1714
+ return null;
1715
+ }
1716
+
1717
+ // Points stay in the shape's own frame (the core maps them to where it
1718
+ // draws them); a pixel offset (dx/dy) scales by the bounds they are placed
1719
+ // in, which direction north/south turn by 90 degrees (shapePin).
1720
+ var f = AvoidRouting.shapeFrame(terminalStyle, state.shape != null && state.shape.stencil != null);
1721
+ var swap = f != null && (f.direction == 'north' || f.direction == 'south') &&
1722
+ (f.anchorPointDirection || !f.legacy);
1723
+ var fw = swap ? b.h : b.w;
1724
+ var fh = swap ? b.w : b.h;
1725
+ var points = [];
1726
+
1727
+ for (var i = 0; i < constraints.length; i++)
1728
+ {
1729
+ var c = constraints[i];
1730
+
1731
+ if (c == null || c.point == null)
1732
+ {
1733
+ continue;
1734
+ }
1735
+
1736
+ var p = AvoidRouting.constraintForPoint(
1737
+ c.point.x + ((c.dx || 0) / fw),
1738
+ c.point.y + ((c.dy || 0) / fh), c.perimeter);
1739
+
1740
+ if (p != null)
1741
+ {
1742
+ points.push(p);
1743
+ }
1744
+ }
1745
+
1746
+ return (points.length > 0) ? points : null;
1747
+ };
1748
+
1749
+ /**
1750
+ * Resolved jetty size for one end of an edge (the minimum length of the first/
1751
+ * last visible segment, model px) from the edge's parsed style. Same lookup as
1752
+ * mxEdgeStyle.getJettySize — sourceJettySize/targetJettySize over jettySize over
1753
+ * orthBuffer, with 'auto' derived from the end's arrow size — but returning a
1754
+ * number (the style map holds strings). Routed edges render via SegmentConnector
1755
+ * (orthogonalEdgeStyle + waypoints trips mxEdgeStyle.orthPointsFallback), which
1756
+ * has no jetty of its own, so the stub only exists if the ROUTE respects it.
1757
+ */
1758
+ LibavoidRouting.jettyFor = function(style, source)
1759
+ {
1760
+ var value = mxUtils.getValue(style, (source) ? mxConstants.STYLE_SOURCE_JETTY_SIZE :
1761
+ mxConstants.STYLE_TARGET_JETTY_SIZE, mxUtils.getValue(style,
1762
+ mxConstants.STYLE_JETTY_SIZE, mxEdgeStyle.orthBuffer));
1763
+
1764
+ if (value == 'auto')
1765
+ {
1766
+ var type = mxUtils.getValue(style, (source) ? mxConstants.STYLE_STARTARROW :
1767
+ mxConstants.STYLE_ENDARROW, mxConstants.NONE);
1768
+
1769
+ if (type != mxConstants.NONE)
1770
+ {
1771
+ var size = mxUtils.getNumber(style, (source) ? mxConstants.STYLE_STARTSIZE :
1772
+ mxConstants.STYLE_ENDSIZE, mxConstants.DEFAULT_MARKERSIZE);
1773
+ value = Math.max(2, Math.ceil((size + mxEdgeStyle.orthBuffer) /
1774
+ mxEdgeStyle.orthBuffer)) * mxEdgeStyle.orthBuffer;
1775
+ }
1776
+ else
1777
+ {
1778
+ value = 2 * mxEdgeStyle.orthBuffer;
1779
+ }
1780
+ }
1781
+
1782
+ value = parseFloat(value);
1783
+
1784
+ return (isNaN(value)) ? 0 : value;
1785
+ };
1786
+
1787
+ /**
1788
+ * Clearance between the edge and the shapes (shapeBufferDistance, model px)
1789
+ * from the libavoidSpacing style of the edge, or null to use the clearance of
1790
+ * the routing run. Edges without the style route as before. The value is
1791
+ * limited to the range of the Spacing input of the Style tab (2 to 999), as
1792
+ * the style of a crafted diagram could set any number (eg. Infinity).
1793
+ */
1794
+ LibavoidRouting.spacingFor = function(style)
1795
+ {
1796
+ var value = parseFloat(mxUtils.getValue(style, 'libavoidSpacing', null));
1797
+
1798
+ return (isFinite(value)) ? Math.min(999, Math.max(2, value)) : null;
1799
+ };
1800
+
1801
+ /**
1802
+ * True if the view renders the given cell (mxGraphView.validateCell): the cell
1803
+ * and its ancestors up to the view's root are visible and no ancestor below
1804
+ * that root is collapsed. Unlike a view state, this also holds for a cell
1805
+ * added in the current edit before the view is revalidated.
1806
+ */
1807
+ LibavoidRouting.isRendered = function(graph, cell)
1808
+ {
1809
+ var model = graph.getModel();
1810
+ var root = (graph.view.currentRoot != null) ? graph.view.currentRoot : model.getRoot();
1811
+
1812
+ for (var c = cell; c != root; c = model.getParent(c))
1813
+ {
1814
+ if (c == null || !graph.isCellVisible(c) || (c != cell && graph.isCellCollapsed(c)))
1815
+ {
1816
+ return false;
1817
+ }
1818
+ }
1819
+
1820
+ return graph.isCellVisible(root);
1821
+ };
1822
+
1823
+ /**
1824
+ * True if an end of the edge with the given parsed style has jettySize=auto,
1825
+ * whose jetty depends on the arrow (jettyFor).
1826
+ */
1827
+ LibavoidRouting.hasAutoJetty = function(style)
1828
+ {
1829
+ var jetty = mxUtils.getValue(style, mxConstants.STYLE_JETTY_SIZE, null);
1830
+
1831
+ return mxUtils.getValue(style, mxConstants.STYLE_SOURCE_JETTY_SIZE, jetty) == 'auto' ||
1832
+ mxUtils.getValue(style, mxConstants.STYLE_TARGET_JETTY_SIZE, jetty) == 'auto';
1833
+ };
1834
+
1835
+ /**
1836
+ * Two waypoint lists equal? Treats null/empty as equal and compares rounded
1837
+ * coordinates (routeCells rounds its bends), so it matches when a re-route produces
1838
+ * the same geometry. Used to avoid recording no-op mxGeometryChanges.
1839
+ */
1840
+ LibavoidRouting.samePoints = function(a, b)
1841
+ {
1842
+ var an = (a != null) ? a.length : 0;
1843
+ var bn = (b != null) ? b.length : 0;
1844
+
1845
+ if (an !== bn)
1846
+ {
1847
+ return false;
1848
+ }
1849
+
1850
+ for (var i = 0; i < an; i++)
1851
+ {
1852
+ var p = a[i], q = b[i];
1853
+
1854
+ if (p == null || q == null)
1855
+ {
1856
+ if (p !== q)
1857
+ {
1858
+ return false;
1859
+ }
1860
+ }
1861
+ else if (Math.round(p.x) !== Math.round(q.x) || Math.round(p.y) !== Math.round(q.y))
1862
+ {
1863
+ return false;
1864
+ }
1865
+ }
1866
+
1867
+ return true;
1868
+ };
1869
+
1870
+ /**
1871
+ * Collect, route, and write back: registers every vertex as an obstacle, routes
1872
+ * the given edge cells (those with both terminals on vertices), and writes the
1873
+ * bend points back parent-relative. Pure model mutations — the caller owns the
1874
+ * update (executeLayout for the menu, a plain beginUpdate for auto). When
1875
+ * setStyle is true, also forces orthogonalEdgeStyle (the menu converts edges);
1876
+ * auto-routing leaves the style alone (the flag already pairs with it).
1877
+ *
1878
+ * @returns {boolean} whether any edge was routed.
1879
+ */
1880
+ /**
1881
+ * The absolute-model free point of a DANGLING (unconnected) edge end, or null
1882
+ * when that end is connected to a terminal cell. Reads the edge geometry's
1883
+ * terminal point (parent-relative) and shifts it by the edge's absolute parent
1884
+ * offset — the same frame collectVertices and the committed waypoints use — so
1885
+ * the core can route an unconnected end to the exact point the drag preview
1886
+ * showed (computeRoutes' sourcePoint/targetPoint).
1887
+ */
1888
+ LibavoidRouting.danglingPoint = function(graph, edge, source)
1889
+ {
1890
+ var geo = graph.getCellGeometry(edge);
1891
+ var p = (geo != null) ? geo.getTerminalPoint(source) : null;
1892
+
1893
+ if (p == null)
1894
+ {
1895
+ return null;
1896
+ }
1897
+
1898
+ var off = LibavoidRouting.getAbsoluteParentOffset(graph, edge);
1899
+
1900
+ return {x: p.x + off.x, y: p.y + off.y};
1901
+ };
1902
+
1903
+ LibavoidRouting.routeCells = function(graph, Avoid, edgeCells, opts, setStyle)
1904
+ {
1905
+ var model = graph.getModel();
1906
+ var vertices = LibavoidRouting.collectVertices(graph);
1907
+ var byId = Object.create(null);
1908
+ var edges = LibavoidRouting.createRouteEdges(graph, vertices, edgeCells, byId);
1909
+
1910
+ if (edges.length == 0)
1911
+ {
1912
+ return false;
1913
+ }
1914
+
1915
+ // Route via the configured strategy (independentRouting). Independent per-edge
1916
+ // solves route competing edges deterministically (no live-preview oscillation) at
1917
+ // the cost of edge-edge nudging; the multi-edge path nudges but is non-deterministic
1918
+ // for competing edges. Same strategy here as in the preview, so they match.
1919
+ var routes = null;
1920
+
1921
+ try
1922
+ {
1923
+ routes = LibavoidRouting.routeEdgeSet(Avoid, vertices, edges, opts);
1924
+ }
1925
+ catch (e)
1926
+ {
1927
+ LibavoidRouting.disable(graph, e);
1928
+
1929
+ return false;
1930
+ }
1931
+
1932
+ var routedEdges = [];
1933
+
1934
+ // libavoid waypoints are absolute; convert to each edge's parent frame.
1935
+ for (var id in routes)
1936
+ {
1937
+ var edge = byId[id];
1938
+
1939
+ if (edge == null)
1940
+ {
1941
+ continue;
1942
+ }
1943
+
1944
+ var wpsAbs = routes[id];
1945
+ var off = LibavoidRouting.getAbsoluteParentOffset(graph, edge);
1946
+ var pts = [];
1947
+
1948
+ for (var k = 0; k < wpsAbs.length; k++)
1949
+ {
1950
+ pts.push(new mxPoint(wpsAbs[k].x - off.x, wpsAbs[k].y - off.y));
1951
+ }
1952
+
1953
+ var newPoints = (pts.length > 0) ? pts : null;
1954
+ var geo = model.getGeometry(edge);
1955
+
1956
+ // Skip the model write when the route is unchanged: setGeometry only
1957
+ // reference-compares, so a fresh clone would always record an
1958
+ // mxGeometryChange (undo noise, dirty flag, redundant collab deltas) even for
1959
+ // a byte-identical route — common because collectOverlappingEdges is
1960
+ // deliberately over-inclusive. Still track the edge (for the style pass below).
1961
+ if (geo == null || !LibavoidRouting.samePoints(geo.points, newPoints))
1962
+ {
1963
+ geo = (geo != null) ? geo.clone() : new mxGeometry();
1964
+ geo.points = newPoints;
1965
+ model.setGeometry(edge, geo);
1966
+ }
1967
+
1968
+ routedEdges.push(edge);
1969
+ }
1970
+
1971
+ if (routedEdges.length > 0 && setStyle)
1972
+ {
1973
+ // Keep orthogonalEdgeStyle (libavoid is already orthogonal) so segments
1974
+ // stay draggable; leave endpoints floating — each route meets a shape at
1975
+ // a side midpoint, which is where a floating orthogonal endpoint connects.
1976
+ graph.setCellStyles(mxConstants.STYLE_EDGE, 'orthogonalEdgeStyle', routedEdges);
1977
+ graph.setCellStyles(mxConstants.STYLE_CURVED, '0', routedEdges);
1978
+ }
1979
+
1980
+ return routedEdges.length > 0;
1981
+ };
1982
+
1983
+ /**
1984
+ * Returns the routing input of the given edges (see routeCells) and adds the
1985
+ * edges to byId and their transparentBounds terminals to the given obstacles.
1986
+ * The optional offset function returns the offset of a moved cell, which
1987
+ * moves the free points of dangling ends and the added terminals (see
1988
+ * routeMovedEdges).
1989
+ */
1990
+ LibavoidRouting.createRouteEdges = function(graph, vertices, edgeCells, byId, offset)
1991
+ {
1992
+ var model = graph.getModel();
1993
+ var edges = [];
1994
+ var added = Object.create(null);
1995
+ var first = vertices.length;
1996
+ var i;
1997
+
1998
+ for (i = 0; i < edgeCells.length; i++)
1999
+ {
2000
+ var c = edgeCells[i];
2001
+
2002
+ if (c == null || !model.isEdge(c))
2003
+ {
2004
+ continue;
2005
+ }
2006
+
2007
+ var s = model.getTerminal(c, true);
2008
+ var t = model.getTerminal(c, false);
2009
+ var sVertex = (s != null && model.isVertex(s));
2010
+ var tVertex = (t != null && model.isVertex(t));
2011
+
2012
+ // A DANGLING end (no terminal vertex) routes to its free point, so a
2013
+ // committed edge with one unconnected end matches its drag preview
2014
+ // (previewRouteToPoint, the same descriptor).
2015
+ var sPoint = sVertex ? null : LibavoidRouting.danglingPoint(graph, c, true);
2016
+ var tPoint = tVertex ? null : LibavoidRouting.danglingPoint(graph, c, false);
2017
+ var o = (offset != null) ? offset(c) : null;
2018
+
2019
+ if (o != null)
2020
+ {
2021
+ sPoint = (sPoint != null) ? {x: sPoint.x + o.x, y: sPoint.y + o.y} : null;
2022
+ tPoint = (tPoint != null) ? {x: tPoint.x + o.x, y: tPoint.y + o.y} : null;
2023
+ }
2024
+
2025
+ // Need at least one real vertex end (like the preview, which routes from a
2026
+ // fixed shape); an edge dangling at BOTH ends is left alone.
2027
+ if ((sVertex || sPoint != null) && (tVertex || tPoint != null) &&
2028
+ (sVertex || tVertex))
2029
+ {
2030
+ // transparentBounds terminals are not in collectVertices —
2031
+ // register their derived hull so the core can route from/to them.
2032
+ if (sVertex) { LibavoidRouting.addTerminalVertex(graph, vertices, added, s); }
2033
+ if (tVertex) { LibavoidRouting.addTerminalVertex(graph, vertices, added, t); }
2034
+
2035
+ // getCellStyle (parses the cell's current style string), NOT
2036
+ // getCurrentCellStyle (the cached state style): on a fresh insert the
2037
+ // state hasn't re-validated yet, so its cached style is missing the
2038
+ // exitX/entryX that setConnectionConstraint just set. A dangling end
2039
+ // takes no constraint/pin/mask/jetty — it's a bare free point.
2040
+ var st = graph.getCellStyle(c);
2041
+ edges.push({id: c.getId(),
2042
+ source: sVertex ? s.getId() : null,
2043
+ target: tVertex ? t.getId() : null,
2044
+ sourcePoint: sPoint, targetPoint: tPoint,
2045
+ sourceConstraint: sVertex ? LibavoidRouting.fixedConstraint(st, true) : null,
2046
+ targetConstraint: tVertex ? LibavoidRouting.fixedConstraint(st, false) : null,
2047
+ sourcePoints: sVertex ? LibavoidRouting.snapPoints(graph, s, st, graph.getCellStyle(s)) : null,
2048
+ targetPoints: tVertex ? LibavoidRouting.snapPoints(graph, t, st, graph.getCellStyle(t)) : null,
2049
+ sourceSides: sVertex ? LibavoidRouting.maskSides(st, graph.getCellStyle(s), true) : null,
2050
+ targetSides: tVertex ? LibavoidRouting.maskSides(st, graph.getCellStyle(t), false) : null,
2051
+ sourceJetty: sVertex ? LibavoidRouting.jettyFor(st, true) : 0,
2052
+ targetJetty: tVertex ? LibavoidRouting.jettyFor(st, false) : 0,
2053
+ shapeBufferDistance: LibavoidRouting.spacingFor(st)});
2054
+ byId[c.getId()] = c;
2055
+ }
2056
+ }
2057
+
2058
+ // Added terminals of moved edges or terminals move with them
2059
+ for (i = first; offset != null && i < vertices.length; i++)
2060
+ {
2061
+ var to = offset(model.getCell(vertices[i].id));
2062
+
2063
+ if (to != null)
2064
+ {
2065
+ vertices[i].x += to.x;
2066
+ vertices[i].y += to.y;
2067
+ }
2068
+ }
2069
+
2070
+ return edges;
2071
+ };
2072
+
2073
+ /**
2074
+ * Returns the auto-edges that the auto-routing re-routes after the given
2075
+ * vertices moved by the offsets of the given function, without changing the
2076
+ * model, eg. for the preview of a spring move (see routeMovedEdges): the
2077
+ * connected ones and those whose route overlaps the old or new box of a
2078
+ * vertex, like the re-route of a move (installAutoRouting). offset(cell)
2079
+ * returns the offset {x, y} in model units of the given moved cell or null.
2080
+ * edgeBounds is the optional result of getAutoEdgeBounds.
2081
+ */
2082
+ LibavoidRouting.getMovedEdges = function(graph, cells, offset, edgeBounds)
2083
+ {
2084
+ var model = graph.getModel();
2085
+ var map = Object.create(null);
2086
+ var regions = [];
2087
+
2088
+ for (var i = 0; i < cells.length; i++)
2089
+ {
2090
+ if (model.isVertex(cells[i]))
2091
+ {
2092
+ var edges = model.getEdges(cells[i]);
2093
+
2094
+ for (var j = 0; j < edges.length; j++)
2095
+ {
2096
+ if (LibavoidRouting.isAutoEdge(graph, edges[j]))
2097
+ {
2098
+ map[edges[j].getId()] = edges[j];
2099
+ }
2100
+ }
2101
+
2102
+ var v = LibavoidRouting.getVertex(graph, cells[i]);
2103
+ var o = offset(cells[i]);
2104
+
2105
+ if (v != null && o != null)
2106
+ {
2107
+ var nb = AvoidRouting.obstacleBounds(LibavoidRouting.moveVertex(v, o));
2108
+ var ob = AvoidRouting.obstacleBounds({x: v.x, y: v.y, w: v.w, h: v.h,
2109
+ frame: v.frame});
2110
+ var x = Math.min(ob.x, nb.x), y = Math.min(ob.y, nb.y);
2111
+ regions.push({x: x, y: y,
2112
+ width: Math.max(ob.x + ob.w, nb.x + nb.w) - x,
2113
+ height: Math.max(ob.y + ob.h, nb.y + nb.h) - y});
2114
+ }
2115
+ }
2116
+ }
2117
+
2118
+ LibavoidRouting.collectOverlappingEdges(graph, regions, map, edgeBounds);
2119
+ var result = [];
2120
+
2121
+ for (var id in map)
2122
+ {
2123
+ result.push(map[id]);
2124
+ }
2125
+
2126
+ return result;
2127
+ };
2128
+
2129
+ /**
2130
+ * Returns a copy of the given obstacle (see getVertex) and its label outside
2131
+ * the shape moved by the given offset.
2132
+ */
2133
+ LibavoidRouting.moveVertex = function(vertex, offset)
2134
+ {
2135
+ var result = LibavoidRouting.shiftVertex(vertex, offset.x, offset.y);
2136
+
2137
+ if (vertex.label != null)
2138
+ {
2139
+ result.label = LibavoidRouting.shiftVertex(vertex.label, offset.x, offset.y);
2140
+ }
2141
+
2142
+ return result;
2143
+ };
2144
+
2145
+ /**
2146
+ * Returns the routes of the given auto-edges after the vertices moved by the
2147
+ * offsets of the given function (see getMovedEdges), without changing the
2148
+ * model, eg. for the preview of a spring move. vertices are the obstacles of
2149
+ * collectVertices before the move, ie. with the view states of the moved
2150
+ * cells at their positions in the model. The input is the same as in
2151
+ * routeCells after the move, so the commit routes the same. Returns the
2152
+ * absolute bends by edge ID.
2153
+ */
2154
+ LibavoidRouting.routeMovedEdges = function(graph, Avoid, vertices, edgeCells, offset)
2155
+ {
2156
+ var model = graph.getModel();
2157
+ var moved = [];
2158
+
2159
+ for (var i = 0; i < vertices.length; i++)
2160
+ {
2161
+ var o = offset(model.getCell(vertices[i].id));
2162
+ moved.push((o != null) ? LibavoidRouting.moveVertex(vertices[i], o) : vertices[i]);
2163
+ }
2164
+
2165
+ var byId = Object.create(null);
2166
+ var edges = LibavoidRouting.createRouteEdges(graph, moved, edgeCells, byId, offset);
2167
+
2168
+ return (edges.length > 0) ? LibavoidRouting.routeEdgeSet(Avoid, moved, edges, null) :
2169
+ Object.create(null);
2170
+ };
2171
+
2172
+ /**
2173
+ * Disables routing for the rest of the session after the solver threw, eg.
2174
+ * Aborted() from the routing module, which leaves the module unusable. All
2175
+ * entry points check window.Avoid, and __libavoidReady resolves null as for
2176
+ * a module that failed to load. The error is logged and fired as a
2177
+ * libavoidError event on the graph, which installAutoRouting shows to the
2178
+ * user, instead of being thrown from the running edit, where it would skip
2179
+ * the end of the edit (see solveReroute).
2180
+ */
2181
+ LibavoidRouting.disable = function(graph, e)
2182
+ {
2183
+ if (typeof window !== 'undefined')
2184
+ {
2185
+ window.Avoid = null;
2186
+ window.__libavoidReady = Promise.resolve(null);
2187
+ }
2188
+
2189
+ if (typeof EditorUi !== 'undefined' && EditorUi.logError != null)
2190
+ {
2191
+ EditorUi.logError('Error in LibavoidRouting: ' + e.message, null, null, null, e);
2192
+ }
2193
+
2194
+ graph.fireEvent(new mxEventObject('libavoidError', 'error', e));
2195
+ };
2196
+
2197
+ /**
2198
+ * Every vertex as a libavoid obstacle, in absolute model coordinates.
2199
+ */
2200
+ LibavoidRouting.collectVertices = function(graph)
2201
+ {
2202
+ var model = graph.getModel();
2203
+ var framed = LibavoidRouting.createStyleFilter(graph, LibavoidRouting.frameStyles);
2204
+ var labelled = LibavoidRouting.createStyleFilter(graph, ['labelPerimeter']);
2205
+ var vertices = [];
2206
+
2207
+ for (var id in model.cells)
2208
+ {
2209
+ var c = model.cells[id];
2210
+
2211
+ if (c != null && model.isVertex(c))
2212
+ {
2213
+ // transparentBounds groups (Mermaid/PlantUML wrappers, layout
2214
+ // containers) are not general obstacles: their children are
2215
+ // obstacles individually and the padding band inside the hull
2216
+ // stays crossable. As a routed edge's TERMINAL the derived
2217
+ // hull is registered ad hoc — see addTerminalVertex.
2218
+ if (graph.isTransparentBounds(c))
2219
+ {
2220
+ continue;
2221
+ }
2222
+
2223
+ // With its frame: the core routes around the drawn (rotated) box.
2224
+ var v = LibavoidRouting.getVertex(graph, c, framed, labelled);
2225
+
2226
+ if (v != null && v.w > 0 && v.h > 0)
2227
+ {
2228
+ vertices.push(v);
2229
+ }
2230
+ }
2231
+ }
2232
+
2233
+ return vertices;
2234
+ };
2235
+
2236
+ /**
2237
+ * Registers a routed edge's transparentBounds terminal as an obstacle:
2238
+ * collectVertices skips these hulls (not general obstacles), but the core
2239
+ * resolves terminal boxes from the obstacle list by id — without an entry
2240
+ * the edge is silently dropped (bounds[id] == null in computeRoutes). The
2241
+ * derived hull is the box the view renders, so anchors, mask pins and jetty
2242
+ * stubs land where the user sees them; filterEnclosing never drops a routed
2243
+ * edge's own terminals, so for its own edge the hull always stays. No-op for
2244
+ * other cells; added dedupes across calls (collectVertices never emits
2245
+ * transparentBounds entries, so only appended ids can repeat).
2246
+ */
2247
+ LibavoidRouting.addTerminalVertex = function(graph, vertices, added, cell)
2248
+ {
2249
+ if (cell == null || !graph.getModel().isVertex(cell) ||
2250
+ !graph.isTransparentBounds(cell))
2251
+ {
2252
+ return;
2253
+ }
2254
+
2255
+ var id = cell.getId();
2256
+
2257
+ if (added[id])
2258
+ {
2259
+ return;
2260
+ }
2261
+
2262
+ added[id] = true;
2263
+
2264
+ var b = LibavoidRouting.getAbsoluteModelBounds(graph, cell);
2265
+
2266
+ if (b != null && b.w > 0 && b.h > 0)
2267
+ {
2268
+ vertices.push({id: id, x: b.x, y: b.y, w: b.w, h: b.h});
2269
+ }
2270
+ };
2271
+
2272
+ /**
2273
+ * Absolute model-coordinate offset of a cell's parent chain. Geometry is stored
2274
+ * relative to the parent; for flat diagrams (parent = the default layer) this is
2275
+ * {0,0}, but a cell nested in a container needs its ancestors' origins summed
2276
+ * (getGeometryOrigin). An edge adds nothing: like the view, it places its
2277
+ * absolute children in its parent's frame (relative ones ride the route, see
2278
+ * isOnEdge). Stops at the layer. Ported from drawio-mcp.
2279
+ */
2280
+ LibavoidRouting.getAbsoluteParentOffset = function(graph, cell)
2281
+ {
2282
+ var model = graph.getModel();
2283
+ var x = 0, y = 0;
2284
+ var p = model.getParent(cell);
2285
+
2286
+ while (p != null && (model.isVertex(p) || model.isEdge(p)))
2287
+ {
2288
+ var pg = model.isVertex(p) ? graph.getCellGeometry(p) : null;
2289
+
2290
+ if (pg != null)
2291
+ {
2292
+ var o = LibavoidRouting.getGeometryOrigin(graph, p, pg);
2293
+ x += o.x;
2294
+ y += o.y;
2295
+ }
2296
+
2297
+ p = model.getParent(p);
2298
+ }
2299
+
2300
+ return {x: x, y: y};
2301
+ };
2302
+
2303
+ /**
2304
+ * True when geometry geo positions cell RELATIVE to a vertex parent (a port):
2305
+ * geo.x/geo.y are fractions of the parent's size, not coordinates.
2306
+ */
2307
+ LibavoidRouting.isRelativeToVertex = function(graph, cell, geo)
2308
+ {
2309
+ var parent = graph.getModel().getParent(cell);
2310
+
2311
+ return geo.relative && parent != null && graph.getModel().isVertex(parent);
2312
+ };
2313
+
2314
+ /**
2315
+ * Where geometry geo puts cell in its parent's frame, as the view computes
2316
+ * the cell's origin (mxGraphView.updateCellState): geo.x/geo.y, or for a
2317
+ * relative child of a vertex the fraction of the parent's size the view
2318
+ * uses (its geometry, or the derived hull of a transparentBounds parent)
2319
+ * plus geo.offset.
2320
+ */
2321
+ LibavoidRouting.getGeometryOrigin = function(graph, cell, geo)
2322
+ {
2323
+ if (LibavoidRouting.isRelativeToVertex(graph, cell, geo))
2324
+ {
2325
+ var parent = graph.getModel().getParent(cell);
2326
+ var size = graph.isTransparentBounds(parent) ?
2327
+ graph.getTransparentBounds(parent) : graph.getCellGeometry(parent);
2328
+ var offset = (geo.offset != null) ? geo.offset : new mxPoint();
2329
+
2330
+ return {x: geo.x * ((size != null) ? size.width : 0) + offset.x,
2331
+ y: geo.y * ((size != null) ? size.height : 0) + offset.y};
2332
+ }
2333
+
2334
+ return {x: geo.x, y: geo.y};
2335
+ };
2336
+
2337
+ /**
2338
+ * The box geometry geo gives cell in absolute model coordinates: its origin
2339
+ * (getGeometryOrigin) plus the parent chain's offset. Like
2340
+ * mxGraphView.updateVertexState, a relative child of a ROTATED vertex is
2341
+ * turned with it about the parent's centre (a port stays on its side); the
2342
+ * cell's own rotation is not applied (see getVertex).
2343
+ */
2344
+ LibavoidRouting.getAbsoluteGeometryBounds = function(graph, cell, geo)
2345
+ {
2346
+ var off = LibavoidRouting.getAbsoluteParentOffset(graph, cell);
2347
+ var o = LibavoidRouting.getGeometryOrigin(graph, cell, geo);
2348
+ var b = {x: o.x + off.x, y: o.y + off.y, w: geo.width, h: geo.height};
2349
+
2350
+ if (LibavoidRouting.isRelativeToVertex(graph, cell, geo))
2351
+ {
2352
+ var parent = graph.getModel().getParent(cell);
2353
+ var alpha = mxUtils.toRadians(mxUtils.getNumber(graph.getCellStyle(parent),
2354
+ mxConstants.STYLE_ROTATION, 0));
2355
+ // A non-numeric rotation is none, as in AvoidRouting.shapeFrame.
2356
+ var pb = (alpha != 0 && isFinite(alpha)) ?
2357
+ LibavoidRouting.getAbsoluteModelBounds(graph, parent) : null;
2358
+
2359
+ if (pb != null)
2360
+ {
2361
+ var ct = mxUtils.getRotatedPoint(new mxPoint(b.x + b.w / 2, b.y + b.h / 2),
2362
+ Math.cos(alpha), Math.sin(alpha), new mxPoint(pb.x + pb.w / 2, pb.y + pb.h / 2));
2363
+ b.x = ct.x - b.w / 2;
2364
+ b.y = ct.y - b.h / 2;
2365
+ }
2366
+ }
2367
+
2368
+ return b;
2369
+ };
2370
+
2371
+ /**
2372
+ * True when cell rides an edge: it or a vertex ancestor is a RELATIVE child
2373
+ * of an edge (an edge label). The view places such a cell along the edge's
2374
+ * drawn route (mxGraphView.getPoint), which the model does not give and the
2375
+ * routing itself changes.
2376
+ */
2377
+ LibavoidRouting.isOnEdge = function(graph, cell)
2378
+ {
2379
+ var model = graph.getModel();
2380
+
2381
+ while (cell != null && model.isVertex(cell))
2382
+ {
2383
+ var geo = model.getGeometry(cell);
2384
+ var parent = model.getParent(cell);
2385
+
2386
+ if (geo != null && geo.relative && model.isEdge(parent))
2387
+ {
2388
+ return true;
2389
+ }
2390
+
2391
+ cell = parent;
2392
+ }
2393
+
2394
+ return false;
2395
+ };
2396
+
2397
+ /**
2398
+ * A vertex's bounds in absolute model coordinates (getAbsoluteGeometryBounds
2399
+ * of its geometry). For a transparentBounds cell the stored geometry is a
2400
+ * pinned origin by design ((0,0,0,0) for layout containers) — its routing box
2401
+ * is the DERIVED hull the view renders (children + padding,
2402
+ * Graph.getTransparentBounds, in the cell's local space), shifted by that
2403
+ * origin. null for a cell on an edge (isOnEdge): it moves with the route, so
2404
+ * it is no obstacle, and an edge connected to it is not routed. Ported from
2405
+ * drawio-mcp.
2406
+ */
2407
+ LibavoidRouting.getAbsoluteModelBounds = function(graph, cell)
2408
+ {
2409
+ var geo = graph.getCellGeometry(cell);
2410
+
2411
+ if (geo == null || LibavoidRouting.isOnEdge(graph, cell))
2412
+ {
2413
+ return null;
2414
+ }
2415
+
2416
+ if (graph.isTransparentBounds(cell))
2417
+ {
2418
+ var off = LibavoidRouting.getAbsoluteParentOffset(graph, cell);
2419
+ var local = graph.getTransparentBounds(cell);
2420
+
2421
+ return (local == null) ? null : {x: geo.x + off.x + local.x,
2422
+ y: geo.y + off.y + local.y, w: local.width, h: local.height};
2423
+ }
2424
+
2425
+ return LibavoidRouting.getAbsoluteGeometryBounds(graph, cell, geo);
2426
+ };
2427
+
2428
+ /**
2429
+ * A vertex as the routing core takes it: getAbsoluteModelBounds (UNROTATED)
2430
+ * plus the transform its style applies to its connection points
2431
+ * (AvoidRouting.shapeFrame — rotation, direction, flips), from which the
2432
+ * core derives the drawn obstacle box and maps each end's constraint and
2433
+ * snap points. getCellStyle, not the cached state style, like routeCells: a
2434
+ * rotation just written may not be validated yet. framed is an optional
2435
+ * createStyleFilter of the frameStyles: the frame of a cell it returns false
2436
+ * for is null without resolving the style. labelled is an optional
2437
+ * createStyleFilter of labelPerimeter for the label in the same way. null
2438
+ * without bounds.
2439
+ */
2440
+ LibavoidRouting.getVertex = function(graph, cell, framed, labelled)
2441
+ {
2442
+ var v = LibavoidRouting.getAbsoluteModelBounds(graph, cell);
2443
+
2444
+ if (v != null)
2445
+ {
2446
+ v.id = cell.getId();
2447
+
2448
+ // A transparentBounds hull is a derived box, not a drawn shape.
2449
+ if (!graph.isTransparentBounds(cell))
2450
+ {
2451
+ var style = null;
2452
+
2453
+ if (framed != null && !framed(cell))
2454
+ {
2455
+ v.frame = null;
2456
+ }
2457
+ else
2458
+ {
2459
+ var state = graph.view.getState(cell);
2460
+ style = graph.getCellStyle(cell);
2461
+
2462
+ v.frame = AvoidRouting.shapeFrame(style,
2463
+ state != null && state.shape != null && state.shape.stencil != null);
2464
+ }
2465
+
2466
+ var label = (labelled == null || labelled(cell)) ?
2467
+ LibavoidRouting.getOutsideLabel(graph, cell, v, style) : null;
2468
+
2469
+ if (label != null)
2470
+ {
2471
+ v.label = label;
2472
+ }
2473
+ }
2474
+ }
2475
+
2476
+ return v;
2477
+ };
2478
+
2479
+ /**
2480
+ * The label of a vertex with labelPerimeter=1 that is placed outside the
2481
+ * shape on one side (mxGraphView.getLabelPerimeterSide), as the routing core
2482
+ * takes it (AvoidRouting.withOutsideLabels): {side, x, y, w, h} with side
2483
+ * 'N', 'S', 'E' or 'W' and the box in absolute model coordinates, placed
2484
+ * relative to the vertex bounds b (getAbsoluteModelBounds), which may be
2485
+ * newer than the painted label. null for other vertices. The side comes from
2486
+ * getCellStyle, not the cached state style, and the box is the painted label
2487
+ * only if its style and value are current: a parked solve runs before the
2488
+ * view is validated, eg. after a Position change, an edit of the label or an
2489
+ * insert (getLabelBox). style is the optional current style of the cell.
2490
+ */
2491
+ LibavoidRouting.getOutsideLabel = function(graph, cell, b, style)
2492
+ {
2493
+ style = (b != null && style == null) ? graph.getCellStyle(cell) : style;
2494
+ var side = (b != null) ? LibavoidRouting.getOutsideLabelSide(graph, cell, style) : null;
2495
+
2496
+ if (side == null)
2497
+ {
2498
+ return null;
2499
+ }
2500
+
2501
+ var view = graph.view;
2502
+ var state = view.getState(cell);
2503
+ var painted = LibavoidRouting.isPaintedLabel(graph, state, style);
2504
+ var box = null;
2505
+
2506
+ if (painted && view.getLabelPerimeterSide(state) == side)
2507
+ {
2508
+ var bb = state.text.boundingBox;
2509
+ var s = view.scale;
2510
+ box = {x: b.x + (bb.x - state.x) / s, y: b.y + (bb.y - state.y) / s,
2511
+ w: bb.width / s, h: bb.height / s};
2512
+ }
2513
+ else
2514
+ {
2515
+ box = LibavoidRouting.getLabelBox(graph, cell, style, side, b,
2516
+ (painted || LibavoidRouting.isPaintedLabel(graph, state, style,
2517
+ LibavoidRouting.labelSizeStyles)) ? state : null);
2518
+ }
2519
+
2520
+ if (box == null)
2521
+ {
2522
+ return null;
2523
+ }
2524
+
2525
+ box.side = (side == mxConstants.DIRECTION_SOUTH) ? 'S' :
2526
+ ((side == mxConstants.DIRECTION_NORTH) ? 'N' :
2527
+ ((side == mxConstants.DIRECTION_EAST) ? 'E' : 'W'));
2528
+
2529
+ return box;
2530
+ };
2531
+
2532
+ /**
2533
+ * Style keys of a label outside the shape that change its size (see
2534
+ * getLabelBox).
2535
+ */
2536
+ LibavoidRouting.labelSizeStyles = ['fontSize', 'fontFamily', 'fontStyle',
2537
+ 'whiteSpace', 'html', 'labelWidth'];
2538
+
2539
+ /**
2540
+ * True if the given vertex has a label outside the shape that edges end at
2541
+ * (getOutsideLabelSide of its current style) or if its painted label is
2542
+ * such a label (mxGraphView.getLabelPerimeterSide).
2543
+ */
2544
+ LibavoidRouting.hasOutsideLabel = function(graph, cell)
2545
+ {
2546
+ return LibavoidRouting.getOutsideLabelSide(graph, cell, graph.getCellStyle(cell)) != null ||
2547
+ LibavoidRouting.getLabelPerimeterSide(graph.view, graph.view.getState(cell)) != null;
2548
+ };
2549
+
2550
+ /**
2551
+ * True if the core supports labelPerimeter (mxGraphView.isLabelPerimeter and
2552
+ * getLabelPerimeterSide, since 32.5.6). extensions.min.js can run with the
2553
+ * app.min.js of an older release, eg. if a service worker installed right
2554
+ * after a release cached the previous app.min.js from the edge. Routing then
2555
+ * ignores outside labels, as that core paints them.
2556
+ */
2557
+ LibavoidRouting.isLabelPerimeterSupported = function(view)
2558
+ {
2559
+ return typeof view.isLabelPerimeter === 'function' &&
2560
+ typeof view.getLabelPerimeterSide === 'function';
2561
+ };
2562
+
2563
+ /**
2564
+ * mxGraphView.getLabelPerimeterSide of the given state, or null if the core
2565
+ * does not support labelPerimeter (isLabelPerimeterSupported).
2566
+ */
2567
+ LibavoidRouting.getLabelPerimeterSide = function(view, state)
2568
+ {
2569
+ return LibavoidRouting.isLabelPerimeterSupported(view) ?
2570
+ view.getLabelPerimeterSide(state) : null;
2571
+ };
2572
+
2573
+ /**
2574
+ * The side of the shape where the label of the given vertex with the given
2575
+ * style is placed outside the shape with labelPerimeter=1 (see
2576
+ * mxGraphView.isLabelPerimeter and getLabelPerimeterSide), or null if the
2577
+ * label is empty or not outside the shape on one side, or if the core does
2578
+ * not support labelPerimeter (isLabelPerimeterSupported).
2579
+ */
2580
+ LibavoidRouting.getOutsideLabelSide = function(graph, cell, style)
2581
+ {
2582
+ var view = graph.view;
2583
+
2584
+ if (!LibavoidRouting.isLabelPerimeterSupported(view))
2585
+ {
2586
+ return null;
2587
+ }
2588
+
2589
+ var value = graph.getLabel(cell);
2590
+
2591
+ if (!view.isLabelPerimeter(new mxCellState(view, cell, style)) || value == null ||
2592
+ (!mxUtils.isNode(value) && value.length == 0))
2593
+ {
2594
+ return null;
2595
+ }
2596
+
2597
+ var lp = mxUtils.getValue(style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
2598
+ var vlp = mxUtils.getValue(style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
2599
+
2600
+ if (lp == mxConstants.ALIGN_CENTER)
2601
+ {
2602
+ if (vlp == mxConstants.ALIGN_BOTTOM)
2603
+ {
2604
+ return mxConstants.DIRECTION_SOUTH;
2605
+ }
2606
+ else if (vlp == mxConstants.ALIGN_TOP)
2607
+ {
2608
+ return mxConstants.DIRECTION_NORTH;
2609
+ }
2610
+ }
2611
+ else if (vlp == mxConstants.ALIGN_MIDDLE)
2612
+ {
2613
+ if (lp == mxConstants.ALIGN_RIGHT)
2614
+ {
2615
+ return mxConstants.DIRECTION_EAST;
2616
+ }
2617
+ else if (lp == mxConstants.ALIGN_LEFT)
2618
+ {
2619
+ return mxConstants.DIRECTION_WEST;
2620
+ }
2621
+ }
2622
+
2623
+ return null;
2624
+ };
2625
+
2626
+ /**
2627
+ * True if the painted label of the given state has the value of its cell and
2628
+ * the given style for the given keys (default labelStyles and
2629
+ * labelBoxStyles). Measures a label that was painted but not measured yet.
2630
+ */
2631
+ LibavoidRouting.isPaintedLabel = function(graph, state, style, keys)
2632
+ {
2633
+ if (state == null || state.text == null)
2634
+ {
2635
+ return false;
2636
+ }
2637
+
2638
+ if (state.text.invalidBoundingBox)
2639
+ {
2640
+ graph.view.updateBoundingBox(state);
2641
+ }
2642
+
2643
+ if (state.text.boundingBox == null || state.style == null)
2644
+ {
2645
+ return false;
2646
+ }
2647
+
2648
+ keys = (keys != null) ? keys : LibavoidRouting.labelStyles.concat(
2649
+ LibavoidRouting.labelBoxStyles);
2650
+
2651
+ for (var i = 0; i < keys.length; i++)
2652
+ {
2653
+ if (state.style[keys[i]] != style[keys[i]])
2654
+ {
2655
+ return false;
2656
+ }
2657
+ }
2658
+
2659
+ return state.text.value == graph.cellRenderer.getLabelValue(state);
2660
+ };
2661
+
2662
+ /**
2663
+ * The box {x, y, w, h} of the label of a vertex with the given style placed
2664
+ * outside the vertex bounds b on the given side, in absolute model
2665
+ * coordinates, as the view and mxText place it: in the bounds of the vertex
2666
+ * moved to that side (mxGraphView.updateVertexLabelOffset), aligned with its
2667
+ * align, verticalAlign and spacing. The size is the one of the label of the
2668
+ * given state, or the size of the text of the cell if it is null. null if the
2669
+ * label is not outside the shape on that side.
2670
+ */
2671
+ LibavoidRouting.getLabelBox = function(graph, cell, style, side, b, state)
2672
+ {
2673
+ var lw = mxUtils.getValue(style, mxConstants.STYLE_LABEL_WIDTH, null);
2674
+ var align = mxUtils.getValue(style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER);
2675
+ var valign = mxUtils.getValue(style, mxConstants.STYLE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE);
2676
+ var spacing = mxUtils.getNumber(style, mxConstants.STYLE_SPACING, 2);
2677
+ var top = spacing + mxUtils.getNumber(style, mxConstants.STYLE_SPACING_TOP, 0);
2678
+ var bottom = spacing + mxUtils.getNumber(style, mxConstants.STYLE_SPACING_BOTTOM, 0);
2679
+ var left = spacing + mxUtils.getNumber(style, mxConstants.STYLE_SPACING_LEFT, 0);
2680
+ var right = spacing + mxUtils.getNumber(style, mxConstants.STYLE_SPACING_RIGHT, 0);
2681
+ var rw = (lw != null && side != mxConstants.DIRECTION_EAST) ? parseFloat(lw) : b.w;
2682
+ var w = 0;
2683
+ var h = 0;
2684
+
2685
+ if (state != null)
2686
+ {
2687
+ w = state.text.boundingBox.width / graph.view.scale;
2688
+ h = state.text.boundingBox.height / graph.view.scale;
2689
+ }
2690
+ else
2691
+ {
2692
+ var value = graph.getLabel(cell);
2693
+
2694
+ if (mxUtils.isNode(value))
2695
+ {
2696
+ value = value.outerHTML;
2697
+ }
2698
+ else if (mxUtils.getValue(style, 'html', '0') != '1')
2699
+ {
2700
+ value = mxUtils.htmlEntities(value, false).replace(/\n/g, '<br>');
2701
+ }
2702
+
2703
+ // Sanitized in mxUtils.getSizeForString (see Graph.js)
2704
+ var size = mxUtils.getSizeForString(value,
2705
+ mxUtils.getValue(style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE),
2706
+ mxUtils.getValue(style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY),
2707
+ (mxUtils.getValue(style, mxConstants.STYLE_WHITE_SPACE, 'nowrap') == 'wrap') ?
2708
+ Math.max(1, rw - left - right) : null,
2709
+ mxUtils.getValue(style, mxConstants.STYLE_FONTSTYLE, 0));
2710
+ w = size.width;
2711
+ h = size.height;
2712
+ }
2713
+
2714
+ // The bounds of the vertex on the side of the label
2715
+ var rx = b.x - (rw - b.w) * ((align == mxConstants.ALIGN_RIGHT) ? 1 :
2716
+ ((align == mxConstants.ALIGN_CENTER) ? 0.5 : 0));
2717
+ var ry = b.y;
2718
+
2719
+ if (side == mxConstants.DIRECTION_SOUTH)
2720
+ {
2721
+ ry = b.y + b.h;
2722
+ }
2723
+ else if (side == mxConstants.DIRECTION_NORTH)
2724
+ {
2725
+ ry = b.y - b.h;
2726
+ }
2727
+ else if (side == mxConstants.DIRECTION_EAST)
2728
+ {
2729
+ rx = b.x + b.w;
2730
+ }
2731
+ else
2732
+ {
2733
+ rx = b.x - rw;
2734
+ }
2735
+
2736
+ var x = (align == mxConstants.ALIGN_LEFT) ? rx + left :
2737
+ ((align == mxConstants.ALIGN_RIGHT) ? rx + rw - right - w :
2738
+ rx + (rw - w + left - right) / 2);
2739
+ var y = (valign == mxConstants.ALIGN_TOP) ? ry + top :
2740
+ ((valign == mxConstants.ALIGN_BOTTOM) ? ry + b.h - bottom - h :
2741
+ ry + (b.h - h + top - bottom) / 2);
2742
+
2743
+ return ((side == mxConstants.DIRECTION_SOUTH && y + h > b.y + b.h) ||
2744
+ (side == mxConstants.DIRECTION_NORTH && y < b.y) ||
2745
+ (side == mxConstants.DIRECTION_EAST && x + w > b.x + b.w) ||
2746
+ (side == mxConstants.DIRECTION_WEST && x < b.x)) ?
2747
+ {x: x, y: y, w: w, h: h} : null;
2748
+ };
2749
+
2750
+ /**
2751
+ * Adds the boxes of the label outside the shape of the given vertex to the
2752
+ * given regions: the painted one and the one of its current style and
2753
+ * value (getOutsideLabel).
2754
+ */
2755
+ LibavoidRouting.addOutsideLabelRegions = function(graph, cell, regions)
2756
+ {
2757
+ var view = graph.view;
2758
+ var state = view.getState(cell);
2759
+
2760
+ if (state != null && LibavoidRouting.getLabelPerimeterSide(view, state) != null)
2761
+ {
2762
+ var bb = state.text.boundingBox;
2763
+ var s = view.scale;
2764
+ regions.push({x: mxUtils.unscale(bb.x, s, view.translate.x),
2765
+ y: mxUtils.unscale(bb.y, s, view.translate.y),
2766
+ width: bb.width / s, height: bb.height / s});
2767
+ }
2768
+
2769
+ var v = LibavoidRouting.getVertex(graph, cell);
2770
+
2771
+ if (v != null && v.label != null)
2772
+ {
2773
+ regions.push({x: v.label.x, y: v.label.y, width: v.label.w, height: v.label.h});
2774
+ }
2775
+ };
2776
+
2777
+ /**
2778
+ * The box a vertex occupies as drawn (its bounds rotated), in ABSOLUTE MODEL
2779
+ * coords: the obstacle the core routes around, and the area a changed shape
2780
+ * affects. null without bounds.
2781
+ */
2782
+ LibavoidRouting.getAbsoluteObstacleBounds = function(graph, cell)
2783
+ {
2784
+ return AvoidRouting.obstacleBounds(LibavoidRouting.getVertex(graph, cell));
2785
+ };
2786
+
2787
+ /**
2788
+ * Where a connection point (a constraint in the shape's own frame, as from
2789
+ * AvoidRouting.constraintForPoint) is drawn, in ABSOLUTE MODEL coords — the
2790
+ * position the core pins the route to. null without bounds.
2791
+ */
2792
+ LibavoidRouting.getAbsoluteAnchor = function(graph, cell, constraint)
2793
+ {
2794
+ var v = LibavoidRouting.getVertex(graph, cell);
2795
+
2796
+ if (v == null)
2797
+ {
2798
+ return null;
2799
+ }
2800
+
2801
+ var b = AvoidRouting.obstacleBounds(v);
2802
+ var p = AvoidRouting.shapePin(v, constraint);
2803
+
2804
+ return {x: b.x + p.x * b.w, y: b.y + p.y * b.h};
2805
+ };
2806
+
2807
+ /**
2808
+ * The bounding box of an edge's route in ABSOLUTE MODEL coords — source + target
2809
+ * centres plus any waypoints. Used to detect auto-edges a moved shape overlaps.
2810
+ * Returns {x, y, width, height} (a coarse over-approximation; harmless since
2811
+ * re-routing is idempotent) or null if either terminal isn't a vertex.
2812
+ */
2813
+ LibavoidRouting.edgeRouteBounds = function(graph, edge)
2814
+ {
2815
+ var model = graph.getModel();
2816
+ var s = model.getTerminal(edge, true);
2817
+ var t = model.getTerminal(edge, false);
2818
+ var sVertex = (s != null && model.isVertex(s));
2819
+ var tVertex = (t != null && model.isVertex(t));
2820
+
2821
+ // A dangling end contributes its free point (not a terminal centre) to the
2822
+ // route bbox, so a moved shape still detects the auto-edge it's connected to.
2823
+ // Need at least one vertex end.
2824
+ if (!sVertex && !tVertex)
2825
+ {
2826
+ return null;
2827
+ }
2828
+
2829
+ var sb = sVertex ? LibavoidRouting.getAbsoluteModelBounds(graph, s) : null;
2830
+ var tb = tVertex ? LibavoidRouting.getAbsoluteModelBounds(graph, t) : null;
2831
+ var sPt = sVertex ? null : LibavoidRouting.danglingPoint(graph, edge, true);
2832
+ var tPt = tVertex ? null : LibavoidRouting.danglingPoint(graph, edge, false);
2833
+
2834
+ if ((sVertex && sb == null) || (tVertex && tb == null) ||
2835
+ (sPt == null && !sVertex) || (tPt == null && !tVertex))
2836
+ {
2837
+ return null;
2838
+ }
2839
+
2840
+ var scx = sVertex ? sb.x + sb.w / 2 : sPt.x, scy = sVertex ? sb.y + sb.h / 2 : sPt.y;
2841
+ var tcx = tVertex ? tb.x + tb.w / 2 : tPt.x, tcy = tVertex ? tb.y + tb.h / 2 : tPt.y;
2842
+ var minX = Math.min(scx, tcx), maxX = Math.max(scx, tcx);
2843
+ var minY = Math.min(scy, tcy), maxY = Math.max(scy, tcy);
2844
+
2845
+ var geo = graph.getCellGeometry(edge);
2846
+
2847
+ if (geo != null && geo.points != null)
2848
+ {
2849
+ var off = LibavoidRouting.getAbsoluteParentOffset(graph, edge);
2850
+
2851
+ for (var k = 0; k < geo.points.length; k++)
2852
+ {
2853
+ if (geo.points[k] == null)
2854
+ {
2855
+ continue;
2856
+ }
2857
+
2858
+ var px = geo.points[k].x + off.x, py = geo.points[k].y + off.y;
2859
+ minX = Math.min(minX, px); maxX = Math.max(maxX, px);
2860
+ minY = Math.min(minY, py); maxY = Math.max(maxY, py);
2861
+ }
2862
+ }
2863
+
2864
+ return {x: minX, y: minY, width: maxX - minX, height: maxY - minY};
2865
+ };
2866
+
2867
+ // ── Live endpoint-drag preview (tier 2) ──────────────────────────────────────
2868
+ // While the user drags an edge's source/target handle, route the dashed preview
2869
+ // EXACTLY the way the drop commits. Wired from mxEdgeSegmentHandler.getPreviewPoints
2870
+ // (orthogonal edges use that handler) and mxEdgeHandler.reset in
2871
+ // grapheditor/Graph.js. Only for flagged edges with no manual waypoints; requires
2872
+ // the WASM warm (window.Avoid) — synchronous, no async preview.
2873
+ //
2874
+ // The route is computed in ABSOLUTE MODEL coords over ALL model vertices
2875
+ // (collectVertices, buffer 16) for a single connector
2876
+ // fixedCellCenter -> (targetCellCenter when hovering a target, else cursor-model
2877
+ // point), and bends are converted to control points via getAbsoluteParentOffset —
2878
+ // byte-for-byte the same pipeline as routeCells/computeRoutes. When the cursor is
2879
+ // over a valid target cell, the preview connector is identical to the one the
2880
+ // commit builds (fixed center -> target center over the same obstacles + buffer),
2881
+ // so the preview route == the committed route.
2882
+ //
2883
+ // Every frame is a fresh computeRoutes solve, which registers only the
2884
+ // obstacles in a region around the ends (see Obstacle regions in
2885
+ // docs/claude/libavoid-routing.md). A per-drag Router with all obstacles
2886
+ // registered once was ~20x slower per frame in large diagrams: libavoid
2887
+ // rebuilds the orthogonal visibility graph over all its obstacles whenever an
2888
+ // end moves. The fixed end's parameters are kept on the handler for the drag
2889
+ // (buildPreviewSession) and dropped in endPreview (mxEdgeHandler.reset).
2890
+
2891
+ // Min ms between preview re-solves (between them the last result is reused).
2892
+ LibavoidRouting.previewThrottleMs = 30;
2893
+
2894
+ /**
2895
+ * Fresh obstacle-avoiding route between two cells — the SAME computeRoutes call the
2896
+ * commit (routeCells) makes, so a preview built from it is byte-identical to the
2897
+ * dropped route. Used while the dragged end hovers a target cell. Returns interior
2898
+ * bends in ABSOLUTE model coords.
2899
+ */
2900
+ LibavoidRouting.previewRouteToCell = function(graph, Avoid, sourceCell, targetCell, srcConstr, dstConstr, srcJetty, dstJetty, srcSides, dstSides, srcPoints, dstPoints, spacing)
2901
+ {
2902
+ if (sourceCell == null || targetCell == null || sourceCell == targetCell)
2903
+ {
2904
+ return [];
2905
+ }
2906
+
2907
+ var vertices = LibavoidRouting.collectVertices(graph);
2908
+ var added = Object.create(null);
2909
+
2910
+ // transparentBounds terminals are registered ad hoc, like the commit.
2911
+ LibavoidRouting.addTerminalVertex(graph, vertices, added, sourceCell);
2912
+ LibavoidRouting.addTerminalVertex(graph, vertices, added, targetCell);
2913
+
2914
+ var routes = LibavoidRouting.computeRoutes(Avoid, vertices, [{
2915
+ id: 'preview', source: sourceCell.getId(), target: targetCell.getId(),
2916
+ sourceConstraint: srcConstr || null, targetConstraint: dstConstr || null,
2917
+ sourcePoints: srcPoints || null, targetPoints: dstPoints || null,
2918
+ sourceSides: srcSides || null, targetSides: dstSides || null,
2919
+ sourceJetty: srcJetty || 0, targetJetty: dstJetty || 0,
2920
+ shapeBufferDistance: (spacing != null) ? spacing : null
2921
+ }], null);
2922
+
2923
+ return routes['preview'] || [];
2924
+ };
2925
+
2926
+ /**
2927
+ * Fresh commit-parity solve from the fixed cell to a FREE POINT — used while
2928
+ * the cursor tracks over empty space (or a cell that is no target). Mirrors
2929
+ * routeCells' dangling-end descriptor exactly, so releasing the endpoint there
2930
+ * commits this route: the dragged end is a bare free point with no
2931
+ * constraint, pins, mask or jetty.
2932
+ */
2933
+ LibavoidRouting.previewRouteToPoint = function(graph, Avoid, fixedCell, draggingSource, point, fixedConstr, fixedJetty, fixedSides, fixedPoints, spacing)
2934
+ {
2935
+ if (fixedCell == null || point == null)
2936
+ {
2937
+ return [];
2938
+ }
2939
+
2940
+ var vertices = LibavoidRouting.collectVertices(graph);
2941
+
2942
+ // transparentBounds terminals are registered ad hoc, like the commit.
2943
+ LibavoidRouting.addTerminalVertex(graph, vertices, Object.create(null), fixedCell);
2944
+
2945
+ var fixedId = fixedCell.getId();
2946
+ var edge = (draggingSource) ?
2947
+ {id: 'preview', source: null, target: fixedId,
2948
+ sourcePoint: {x: point.x, y: point.y}, targetPoint: null,
2949
+ sourceConstraint: null, targetConstraint: fixedConstr || null,
2950
+ sourcePoints: null, targetPoints: fixedPoints || null,
2951
+ sourceSides: null, targetSides: fixedSides || null,
2952
+ sourceJetty: 0, targetJetty: fixedJetty || 0} :
2953
+ {id: 'preview', source: fixedId, target: null,
2954
+ sourcePoint: null, targetPoint: {x: point.x, y: point.y},
2955
+ sourceConstraint: fixedConstr || null, targetConstraint: null,
2956
+ sourcePoints: fixedPoints || null, targetPoints: null,
2957
+ sourceSides: fixedSides || null, targetSides: null,
2958
+ sourceJetty: fixedJetty || 0, targetJetty: 0};
2959
+ edge.shapeBufferDistance = (spacing != null) ? spacing : null;
2960
+
2961
+ var routes = LibavoidRouting.computeRoutes(Avoid, vertices, [edge], null);
2962
+
2963
+ return routes['preview'] || [];
2964
+ };
2965
+
2966
+ /**
2967
+ * @returns {Array.<mxPoint>|null} preview control points (parent-relative), or
2968
+ * null to fall back to the handler's default preview.
2969
+ */
2970
+ LibavoidRouting.previewEndpointDrag = function(handler, point)
2971
+ {
2972
+ if (typeof window === 'undefined' || window.Avoid == null)
2973
+ {
2974
+ return null; // not warm: no live preview (commit-time routing still runs)
2975
+ }
2976
+
2977
+ if (!(handler.isSource || handler.isTarget))
2978
+ {
2979
+ return null;
2980
+ }
2981
+
2982
+ var graph = handler.graph;
2983
+ var edge = handler.state.cell;
2984
+
2985
+ // isAutoEdge, not just the style flag: inside a live layout container the
2986
+ // commit is the container's layout re-run, not a libavoid route — reconnect
2987
+ // does not re-parent the edge, so the current parent chain decides.
2988
+ if (!LibavoidRouting.isAutoEdge(graph, edge))
2989
+ {
2990
+ return null;
2991
+ }
2992
+
2993
+ // NOTE: do NOT bail when geo.points are present. We've already confirmed the
2994
+ // edge is flagged (above), and a manual waypoint edit CLEARS the flag
2995
+ // (mxEdgeHandler.changePoints) — so a flagged edge's geo.points are always
2996
+ // libavoid's own auto-routed bends, not user waypoints. Bailing here made the
2997
+ // endpoint-drag preview fall back to the default the moment an auto-edge had
2998
+ // any bend (e.g. after it routed around an obstacle, or after a shape move
2999
+ // committed a bent route), which read as "the preview stopped working".
3000
+
3001
+ // Fixed terminal (the end NOT being dragged): its STATE gives the model cell,
3002
+ // and computeRoutes uses the cell's model-coord center — same as the commit.
3003
+ var source = handler.state.getVisibleTerminalState(true);
3004
+ var target = handler.state.getVisibleTerminalState(false);
3005
+ var fixedState = handler.isSource ? target : source;
3006
+
3007
+ if (fixedState == null)
3008
+ {
3009
+ return null; // need a fixed shape to route from
3010
+ }
3011
+
3012
+ var fixedCell = fixedState.cell;
3013
+
3014
+ if (fixedCell == null || !graph.getModel().isVertex(fixedCell))
3015
+ {
3016
+ return null;
3017
+ }
3018
+
3019
+ // Prospective target cell under the cursor, resolved by the base handler's
3020
+ // previous mouseMove (getPreviewTerminalState ran AFTER getPreviewPoints, so
3021
+ // marker/constraint state here reflects the prior frame — a one-frame lag the
3022
+ // throttle already tolerates). null over empty space.
3023
+ var targetCell = LibavoidRouting.getPreviewTargetCell(handler, fixedCell);
3024
+
3025
+ // Skip self-loops (target resolves back to the fixed cell).
3026
+ if (targetCell != null && targetCell == fixedCell)
3027
+ {
3028
+ return null;
3029
+ }
3030
+
3031
+ var sess = handler.__libavoidPreview;
3032
+
3033
+ if (sess == null)
3034
+ {
3035
+ // Fixed end's connection constraint: dragging the SOURCE leaves the TARGET
3036
+ // fixed (entryX/entryY); dragging the TARGET leaves the SOURCE fixed
3037
+ // (exitX/exitY). fixedConstraint(style, source) reads exit* when source=true,
3038
+ // so pass handler.isTarget. Same source-selector for the fixed end's jetty,
3039
+ // its snapToPoint anchor set and its port-constraint mask (an explicit
3040
+ // anchor wins over both, like the commit).
3041
+ var fixedConstr = LibavoidRouting.fixedConstraint(handler.state.style, handler.isTarget);
3042
+ var fixedSides = (fixedConstr == null) ? LibavoidRouting.maskSides(
3043
+ handler.state.style, fixedState.style, handler.isTarget) : null;
3044
+ var fixedPoints = (fixedConstr == null) ? LibavoidRouting.snapPoints(
3045
+ graph, fixedCell, handler.state.style, fixedState.style) : null;
3046
+ sess = handler.__libavoidPreview = LibavoidRouting.buildPreviewSession(
3047
+ graph, fixedCell, fixedConstr,
3048
+ LibavoidRouting.jettyFor(handler.state.style, handler.isTarget),
3049
+ fixedSides, fixedPoints);
3050
+
3051
+ if (sess != null)
3052
+ {
3053
+ // The edge's per-end jetty (stable for the drag) for the pinned path.
3054
+ sess.srcJetty = LibavoidRouting.jettyFor(handler.state.style, true);
3055
+ sess.dstJetty = LibavoidRouting.jettyFor(handler.state.style, false);
3056
+ }
3057
+ }
3058
+
3059
+ if (sess == null)
3060
+ {
3061
+ return null;
3062
+ }
3063
+
3064
+ // Does the dragged end snap to a FIXED connection point on the target? If so the
3065
+ // commit pins it (exitX/entryX) — preview to that anchor too, with a directed pin,
3066
+ // not the shape centre. The constraint must belong to the resolved target.
3067
+ var ch = (handler.getConstraintHandler != null) ? handler.getConstraintHandler() :
3068
+ handler.constraintHandler;
3069
+ var dragConstraint = null;
3070
+
3071
+ if (targetCell != null && ch != null && ch.currentFocus != null &&
3072
+ ch.currentFocus.cell == targetCell && ch.currentConstraint != null &&
3073
+ ch.currentConstraint.point != null)
3074
+ {
3075
+ var cp = ch.currentConstraint.point;
3076
+ dragConstraint = AvoidRouting.constraintForPoint(cp.x, cp.y,
3077
+ ch.currentConstraint.perimeter);
3078
+ }
3079
+
3080
+ // The dragged endpoint, in ABSOLUTE MODEL coords:
3081
+ // - snapped to a target connection point => that anchor's model position (a pin
3082
+ // is attached below so the route enters perpendicular, like the commit).
3083
+ // - hovering a target cell (no snap) => the target's model-coord center.
3084
+ // - empty space (no target) => the cursor point in model coords.
3085
+ var dragModel;
3086
+
3087
+ if (targetCell != null)
3088
+ {
3089
+ var tb = LibavoidRouting.getAbsoluteModelBounds(graph, targetCell);
3090
+
3091
+ if (tb == null)
3092
+ {
3093
+ return null;
3094
+ }
3095
+
3096
+ // A snapped anchor where the (possibly rotated) target draws it.
3097
+ dragModel = (dragConstraint != null) ?
3098
+ LibavoidRouting.getAbsoluteAnchor(graph, targetCell, dragConstraint) :
3099
+ {x: tb.x + tb.w / 2, y: tb.y + tb.h / 2};
3100
+ }
3101
+ else
3102
+ {
3103
+ dragModel = LibavoidRouting.viewToModelPoint(graph, point);
3104
+ }
3105
+
3106
+ // A snapToPoint anchor set or a port-constraint mask on the hovered target
3107
+ // (dragged end): route with the commit's candidate pins. An explicit anchor
3108
+ // snap wins over both (computeRoutes' makeEnd precedence).
3109
+ var dragSides = null;
3110
+ var dragPoints = null;
3111
+
3112
+ if (targetCell != null && dragConstraint == null)
3113
+ {
3114
+ var dts = graph.view.getState(targetCell);
3115
+ var dtStyle = (dts != null) ? dts.style : graph.getCellStyle(targetCell);
3116
+ dragSides = LibavoidRouting.maskSides(handler.state.style, dtStyle,
3117
+ handler.isSource);
3118
+ dragPoints = LibavoidRouting.snapPoints(graph, targetCell,
3119
+ handler.state.style, dtStyle);
3120
+ }
3121
+
3122
+ // Throttle: reuse the last result between re-solves (the dragged endpoint
3123
+ // itself is rendered by the handler, so only the bends lag, briefly). Re-solve
3124
+ // immediately when the dragged endpoint snaps to/from a target center, so the
3125
+ // commit-matching route appears the instant the cursor enters a shape. The
3126
+ // pinned flag is part of the key so float<->pin transitions always re-solve.
3127
+ // A resolved hover target routes to the cell (the commit's solve), anything
3128
+ // else to a free point (the commit's dangling solve). The target must be an
3129
+ // obstacle of the drag (buildPreviewSession) or a transparentBounds cell,
3130
+ // whose derived hull previewRouteToCell registers itself.
3131
+ var pinned = (targetCell != null && (sess.obstacles[targetCell.getId()] ||
3132
+ graph.isTransparentBounds(targetCell)));
3133
+ var now = Date.now();
3134
+
3135
+ if (sess.lastResult !== undefined && (now - sess.lastT) < LibavoidRouting.previewThrottleMs &&
3136
+ sess.lastDragX === dragModel.x && sess.lastDragY === dragModel.y &&
3137
+ sess.lastPinned === pinned)
3138
+ {
3139
+ return sess.lastResult;
3140
+ }
3141
+
3142
+ var Avoid = window.Avoid;
3143
+ var result = [];
3144
+ var off = LibavoidRouting.getAbsoluteParentOffset(graph, edge);
3145
+
3146
+ // Keep the edge's source->target order (dragged end first when dragging the
3147
+ // source) so the preview control points line up with the committed geometry.
3148
+ var spacing = LibavoidRouting.spacingFor(graph.getCellStyle(edge));
3149
+ var bendsAbs = (!pinned) ?
3150
+ LibavoidRouting.previewRouteToPoint(graph, Avoid,
3151
+ fixedCell, handler.isSource, dragModel, sess.fixedConstr,
3152
+ sess.fixedJetty, sess.fixedSides, sess.fixedPoints, spacing) :
3153
+ ((handler.isSource) ?
3154
+ LibavoidRouting.previewRouteToCell(graph, Avoid, targetCell, fixedCell,
3155
+ dragConstraint, sess.fixedConstr, sess.srcJetty, sess.dstJetty,
3156
+ dragSides, sess.fixedSides, dragPoints, sess.fixedPoints, spacing) :
3157
+ LibavoidRouting.previewRouteToCell(graph, Avoid, fixedCell, targetCell,
3158
+ sess.fixedConstr, dragConstraint, sess.srcJetty, sess.dstJetty,
3159
+ sess.fixedSides, dragSides, sess.fixedPoints, dragPoints, spacing));
3160
+
3161
+ for (var bi = 0; bi < bendsAbs.length; bi++)
3162
+ {
3163
+ result.push(new mxPoint(bendsAbs[bi].x - off.x, bendsAbs[bi].y - off.y));
3164
+ }
3165
+
3166
+ sess.lastT = now;
3167
+ sess.lastDragX = dragModel.x;
3168
+ sess.lastDragY = dragModel.y;
3169
+ sess.lastPinned = pinned;
3170
+ // A successful solve is authoritative even when its route is STRAIGHT: keep the
3171
+ // (possibly empty) bend list, never collapse it to null. Empty => "preview with no
3172
+ // waypoints" (a straight line, matching the commit, which writes geo.points=null).
3173
+ // null is reserved for the not-applicable/cold early-returns above — returning it
3174
+ // here would make getPreviewPoints fall back to the DEFAULT preview, which re-reads
3175
+ // the edge's stale committed geo.points (the old route's waypoint) and draws the
3176
+ // pre-drag detour instead of the straight route the drop will produce.
3177
+ sess.lastResult = result;
3178
+
3179
+ return sess.lastResult;
3180
+ };
3181
+
3182
+ /**
3183
+ * The prospective target vertex under the cursor during an endpoint drag, or null
3184
+ * over empty space. Reads the base mxEdgeHandler's resolved drag state:
3185
+ * constraintHandler.currentFocus with a SNAPPED currentConstraint (the drop
3186
+ * will pin to that anchor), else marker.validState (a floating target STATE —
3187
+ * the drop's actual connect decision). These are set by the prior mouseMove
3188
+ * (mxEdgeHandler.getPreviewTerminalState). A focus WITHOUT a snapped
3189
+ * constraint is not a target: the focused cell merely displays its
3190
+ * connection points and the release does not connect — e.g. hovering a
3191
+ * container's body focuses the container while the marker never validates
3192
+ * it, and the drop commits a dangling end at the cursor. The fixed cell is
3193
+ * excluded so a hover over the opposite terminal isn't treated as the target.
3194
+ */
3195
+ LibavoidRouting.getPreviewTargetCell = function(handler, fixedCell)
3196
+ {
3197
+ var ch = (handler.getConstraintHandler != null) ? handler.getConstraintHandler() :
3198
+ handler.constraintHandler;
3199
+ var state = null;
3200
+
3201
+ if (ch != null && ch.currentFocus != null && ch.currentConstraint != null)
3202
+ {
3203
+ state = ch.currentFocus;
3204
+ }
3205
+ else if (handler.marker != null)
3206
+ {
3207
+ state = (handler.marker.validState != null) ? handler.marker.validState :
3208
+ ((handler.marker.getValidState != null) ? handler.marker.getValidState() : null);
3209
+ }
3210
+
3211
+ if (state == null || state.cell == null)
3212
+ {
3213
+ return null;
3214
+ }
3215
+
3216
+ var model = handler.graph.getModel();
3217
+
3218
+ return (model.isVertex(state.cell) && state.cell != fixedCell) ? state.cell : null;
3219
+ };
3220
+
3221
+ /**
3222
+ * Convert a scaled-view point (the getPreviewPoints `point` arg) to ABSOLUTE
3223
+ * MODEL coords — mxEdgeHandler.convertPoint's scale/translate math WITHOUT its final
3224
+ * Math.round (intentional: sub-pixel cursor tracking; bends are rounded downstream).
3225
+ * Used only for the empty-space endpoint (no target cell), so the preview tracks the
3226
+ * cursor in model space.
3227
+ */
3228
+ LibavoidRouting.viewToModelPoint = function(graph, point)
3229
+ {
3230
+ var view = graph.getView();
3231
+ var scale = view.getScale();
3232
+ var tr = view.getTranslate();
3233
+
3234
+ return {x: point.x / scale - tr.x, y: point.y / scale - tr.y};
3235
+ };
3236
+
3237
+ /**
3238
+ * The per-drag state of a drag preview: the fixed end's parameters (stable for
3239
+ * the drag) and the ids of the drag's OBSTACLES, i.e. the cells a hovered target
3240
+ * routes to as a cell: every model vertex the commit registers (collectVertices),
3241
+ * minus the shapes enclosing the fixed terminal (computeRoutes drops the routed
3242
+ * terminals' containers via filterEnclosing). Collected once per drag, as the
3243
+ * model does not change during it. Returns null if the fixed cell has no bounds.
3244
+ */
3245
+ LibavoidRouting.buildPreviewSession = function(graph, fixedCell, fixedConstraint, fixedJetty, fixedSides, fixedPoints)
3246
+ {
3247
+ if (LibavoidRouting.getAbsoluteObstacleBounds(graph, fixedCell) == null)
3248
+ {
3249
+ return null;
3250
+ }
3251
+
3252
+ var fixedId = fixedCell.getId();
3253
+ var vertices = LibavoidRouting.collectVertices(graph);
3254
+
3255
+ // A transparentBounds fixed terminal is not in collectVertices — register
3256
+ // its derived hull like the commit (filterEnclosing keeps it: terminals are
3257
+ // never dropped).
3258
+ LibavoidRouting.addTerminalVertex(graph, vertices, Object.create(null), fixedCell);
3259
+
3260
+ // Containment of the drawn (rotated) boxes, as the commit tests it
3261
+ // (computeRoutes' toWorldFrame).
3262
+ vertices = AvoidRouting.filterEnclosing(AvoidRouting.toWorldFrame(
3263
+ vertices, []).vertices, [{source: fixedId, target: fixedId}]);
3264
+ var obstacles = Object.create(null);
3265
+
3266
+ for (var i = 0; i < vertices.length; i++)
3267
+ {
3268
+ obstacles[vertices[i].id] = true;
3269
+ }
3270
+
3271
+ return {obstacles: obstacles, fixedConstr: fixedConstraint || null,
3272
+ fixedJetty: fixedJetty, fixedSides: fixedSides || null,
3273
+ fixedPoints: fixedPoints || null};
3274
+ };
3275
+
3276
+ /**
3277
+ * Drops the per-drag preview state. Wired from mxEdgeHandler.reset (endpoint
3278
+ * drag), mxConnectionHandler.reset (new-edge drag) and the hover-icon shape
3279
+ * picker's preview.
3280
+ */
3281
+ LibavoidRouting.endPreview = function(handler)
3282
+ {
3283
+ if (handler != null)
3284
+ {
3285
+ handler.__libavoidPreview = null;
3286
+ }
3287
+ };
3288
+
3289
+ /**
3290
+ * Live route for a NEW edge dragged from the connection handler (hover-icon blue
3291
+ * arrows). When the preview edge carries libavoidRouting=1 (libavoid is the
3292
+ * current edge style), routes it source-centre -> target-cell-centre (or cursor)
3293
+ * around obstacles — the same model-coord pipeline as the commit. Returns the
3294
+ * interior bend points in ABSOLUTE MODEL coords (mxPoint[]) or null. Keeps the
3295
+ * per-drag session (dropped in endPreview on mxConnectionHandler.reset).
3296
+ */
3297
+ LibavoidRouting.connectionPreview = function(handler)
3298
+ {
3299
+ if (typeof window === 'undefined' || window.Avoid == null)
3300
+ {
3301
+ return null;
3302
+ }
3303
+
3304
+ var es = handler.edgeState;
3305
+
3306
+ if (es == null || mxUtils.getValue(es.style, LibavoidRouting.STYLE, null) != '1')
3307
+ {
3308
+ return null;
3309
+ }
3310
+
3311
+ var graph = handler.graph;
3312
+ var model = graph.getModel();
3313
+ var sourceState = handler.previous;
3314
+
3315
+ if (sourceState == null || sourceState.cell == null || !model.isVertex(sourceState.cell))
3316
+ {
3317
+ return null;
3318
+ }
3319
+
3320
+ var sess = handler.__libavoidPreview;
3321
+
3322
+ if (sess == null)
3323
+ {
3324
+ // Source's fixed exit, if the drag started on a connection point
3325
+ // (handler.sourceConstraint; the commit pins it via setConnectionConstraint).
3326
+ // Usually null for a hover-icon drag => source floats to centre, like the commit.
3327
+ var srcConstr = null;
3328
+
3329
+ if (handler.sourceConstraint != null && handler.sourceConstraint.point != null)
3330
+ {
3331
+ var sp = handler.sourceConstraint.point;
3332
+ srcConstr = AvoidRouting.constraintForPoint(sp.x, sp.y,
3333
+ handler.sourceConstraint.perimeter);
3334
+ }
3335
+
3336
+ // The source's snapToPoint anchor set and port-constraint mask, unless
3337
+ // the drag started on an explicit anchor (which wins, like the commit).
3338
+ var srcSides = (srcConstr == null) ? LibavoidRouting.maskSides(
3339
+ es.style, sourceState.style, true) : null;
3340
+ var srcPoints = (srcConstr == null) ? LibavoidRouting.snapPoints(
3341
+ graph, sourceState.cell, es.style, sourceState.style) : null;
3342
+
3343
+ sess = handler.__libavoidPreview = LibavoidRouting.buildPreviewSession(
3344
+ graph, sourceState.cell, srcConstr,
3345
+ LibavoidRouting.jettyFor(es.style, true), srcSides, srcPoints);
3346
+
3347
+ if (sess != null)
3348
+ {
3349
+ // The preview edge's per-end jetty (stable for the drag) for the pinned path.
3350
+ sess.srcJetty = LibavoidRouting.jettyFor(es.style, true);
3351
+ sess.dstJetty = LibavoidRouting.jettyFor(es.style, false);
3352
+ }
3353
+ }
3354
+
3355
+ if (sess == null)
3356
+ {
3357
+ return null;
3358
+ }
3359
+
3360
+ var targetState = handler.currentState;
3361
+ var targetCell = (targetState != null && model.isVertex(targetState.cell)) ? targetState.cell : null;
3362
+
3363
+ // The connect re-parents the edge under the terminals' nearest common
3364
+ // ancestor (mxGraphModel.maintainEdgeParent). If that leaves it inside a
3365
+ // live layout container, the childLayout owns its geometry (isAutoEdge is
3366
+ // false), so the CELLS_ADDED/CONNECT re-route never replaces the preview
3367
+ // bends that connect() bakes into geometry.points — and those are ABSOLUTE
3368
+ // coords, stale garbage in the container parent's frame (e.g. an edge from
3369
+ // a nested list to its outer list: common ancestor = the outer list, whose
3370
+ // nearest layout containers differ per end). No libavoid preview there —
3371
+ // the edge commits without waypoints, like any layout-owned edge. Drags
3372
+ // whose common ancestor is outside every layout container (edge lands in
3373
+ // the default parent or a plain group) keep the preview.
3374
+ if (targetCell != null)
3375
+ {
3376
+ var ca = model.getNearestCommonAncestor(sourceState.cell, targetCell);
3377
+
3378
+ if (ca != null && ((model.isVertex(ca) &&
3379
+ graph.getCurrentCellStyle(ca)['childLayout'] != null) ||
3380
+ LibavoidRouting.layoutContainerOf(graph, ca) != null))
3381
+ {
3382
+ return null;
3383
+ }
3384
+ }
3385
+
3386
+ // Dragged (dest) end snapped to a fixed connection point on the target? The
3387
+ // commit pins it (constraintHandler.currentConstraint, setConnectionConstraint) —
3388
+ // preview to that anchor with a directed pin, else to the shape centre.
3389
+ var ch = handler.constraintHandler;
3390
+ var dragConstraint = null;
3391
+
3392
+ if (targetCell != null && ch != null && ch.currentFocus != null &&
3393
+ ch.currentFocus.cell == targetCell && ch.currentConstraint != null &&
3394
+ ch.currentConstraint.point != null)
3395
+ {
3396
+ var cp = ch.currentConstraint.point;
3397
+ dragConstraint = AvoidRouting.constraintForPoint(cp.x, cp.y,
3398
+ ch.currentConstraint.perimeter);
3399
+ }
3400
+
3401
+ // Dragged (dest) endpoint in model coords: snapped anchor pos, else target centre
3402
+ // (matches the committed route), else the cursor point over empty space.
3403
+ var dragModel;
3404
+
3405
+ if (targetCell != null)
3406
+ {
3407
+ var tb = LibavoidRouting.getAbsoluteModelBounds(graph, targetCell);
3408
+
3409
+ if (tb == null)
3410
+ {
3411
+ return null;
3412
+ }
3413
+
3414
+ // A snapped anchor where the (possibly rotated) target draws it.
3415
+ dragModel = (dragConstraint != null) ?
3416
+ LibavoidRouting.getAbsoluteAnchor(graph, targetCell, dragConstraint) :
3417
+ {x: tb.x + tb.w / 2, y: tb.y + tb.h / 2};
3418
+ }
3419
+ else if (handler.currentPoint != null)
3420
+ {
3421
+ dragModel = LibavoidRouting.viewToModelPoint(graph, handler.currentPoint);
3422
+ }
3423
+ else
3424
+ {
3425
+ return null;
3426
+ }
3427
+
3428
+ // A snapToPoint anchor set or a port-constraint mask on the hovered target
3429
+ // routes through the fresh path with the commit's candidate pins (an
3430
+ // explicit anchor snap wins over both).
3431
+ var dragSides = null;
3432
+ var dragPoints = null;
3433
+
3434
+ if (targetCell != null && dragConstraint == null)
3435
+ {
3436
+ var tgStyle = (targetState != null) ? targetState.style :
3437
+ graph.getCellStyle(targetCell);
3438
+ dragSides = LibavoidRouting.maskSides(es.style, tgStyle, false);
3439
+ dragPoints = LibavoidRouting.snapPoints(graph, targetCell, es.style, tgStyle);
3440
+ }
3441
+
3442
+ // A resolved hover target routes to the cell (the commit's solve), anything
3443
+ // else to a free point (see previewEndpointDrag).
3444
+ var pinned = (targetCell != null && (sess.obstacles[targetCell.getId()] ||
3445
+ graph.isTransparentBounds(targetCell)));
3446
+ var now = Date.now();
3447
+
3448
+ if (sess.lastResult !== undefined && (now - sess.lastT) < LibavoidRouting.previewThrottleMs &&
3449
+ sess.lastDragX === dragModel.x && sess.lastDragY === dragModel.y &&
3450
+ sess.lastPinned === pinned)
3451
+ {
3452
+ return sess.lastResult;
3453
+ }
3454
+
3455
+ var Avoid = window.Avoid;
3456
+ var out = [];
3457
+ var spacing = LibavoidRouting.spacingFor(es.style);
3458
+ var bendsAbs = (pinned) ?
3459
+ LibavoidRouting.previewRouteToCell(graph, Avoid,
3460
+ sourceState.cell, targetCell, sess.fixedConstr, dragConstraint,
3461
+ sess.srcJetty, sess.dstJetty, sess.fixedSides, dragSides,
3462
+ sess.fixedPoints, dragPoints, spacing) :
3463
+ LibavoidRouting.previewRouteToPoint(graph, Avoid,
3464
+ sourceState.cell, false, dragModel, sess.fixedConstr,
3465
+ sess.fixedJetty, sess.fixedSides, sess.fixedPoints, spacing);
3466
+
3467
+ for (var bi = 0; bi < bendsAbs.length; bi++)
3468
+ {
3469
+ out.push(new mxPoint(bendsAbs[bi].x, bendsAbs[bi].y));
3470
+ }
3471
+
3472
+ sess.lastT = now;
3473
+ sess.lastDragX = dragModel.x;
3474
+ sess.lastDragY = dragModel.y;
3475
+ sess.lastPinned = pinned;
3476
+ sess.lastResult = (out.length > 0) ? out : null;
3477
+
3478
+ return sess.lastResult;
3479
+ };
3480
+
3481
+ /**
3482
+ * Apply the connection-handler preview route to handler.edgeState: store the
3483
+ * bends on the PREVIEW edge's geometry (so mxConnectionHandler.connect copies the
3484
+ * route onto the committed edge — it does setGeometry(edge, edgeState.cell.geometry)
3485
+ * after insert) AND re-run the edge style so the dashed preview renders orthogonally
3486
+ * routed via the connection handler's OWN waypoint machinery.
3487
+ *
3488
+ * The bends are fed in as handler.waypoints (in scaled-VIEW coords, the space
3489
+ * mxConnectionHandler uses): the base updateEdgeState then routes the preview
3490
+ * through them with orthogonalEdgeStyle — identical to user-added waypoints, so
3491
+ * the endpoint segments stay orthogonal (setting absolutePoints directly left
3492
+ * diagonal endpoints) — and mxConnectionHandler.connect bakes them into the
3493
+ * committed edge's geometry.points. Returns the waypoints (mxPoint[] in view
3494
+ * coords), or null for a straight route / unflagged edge / cold WASM.
3495
+ */
3496
+ LibavoidRouting.connectionWaypoints = function(handler)
3497
+ {
3498
+ var es = handler.edgeState;
3499
+
3500
+ if (es == null || mxUtils.getValue(es.style, LibavoidRouting.STYLE, null) != '1')
3501
+ {
3502
+ return null;
3503
+ }
3504
+
3505
+ var bends = LibavoidRouting.connectionPreview(handler); // absolute model coords
3506
+
3507
+ if (bends == null || bends.length == 0)
3508
+ {
3509
+ return null;
3510
+ }
3511
+
3512
+ var view = handler.graph.view;
3513
+ var scale = view.scale, tr = view.translate;
3514
+ var wp = [];
3515
+
3516
+ for (var i = 0; i < bends.length; i++)
3517
+ {
3518
+ wp.push(new mxPoint((bends[i].x + tr.x) * scale, (bends[i].y + tr.y) * scale));
3519
+ }
3520
+
3521
+ return wp;
3522
+ };
3523
+
3524
+ /**
3525
+ * The parts of the shape-move preview that only depend on the model, which does
3526
+ * not change during a drag, collected once per drag instead of on every mouse
3527
+ * move (each one walks all cells): the moving vertices with their obstacle
3528
+ * bounds, their connected auto-edges, the route bounds of all auto-edges
3529
+ * (getAutoEdgeBounds) and the obstacles (collectVertices) with the indices of
3530
+ * the moving ones. Stored in handler.__libavoidMoveCache until the model changes
3531
+ * (eg. a remote change during the drag or the drop) or endMovePreview.
3532
+ */
3533
+ LibavoidRouting.getMovePreviewCache = function(graph, handler)
3534
+ {
3535
+ var cache = handler.__libavoidMoveCache;
3536
+
3537
+ if (cache == null)
3538
+ {
3539
+ var model = graph.getModel();
3540
+ var moving = [];
3541
+ var connected = Object.create(null);
3542
+ var id, c, i;
3543
+
3544
+ for (id in model.cells)
3545
+ {
3546
+ c = model.cells[id];
3547
+
3548
+ if (c != null && model.isVertex(c) && handler.isCellMoving(c))
3549
+ {
3550
+ var conn = model.getEdges(c);
3551
+
3552
+ for (i = 0; i < conn.length; i++)
3553
+ {
3554
+ if (LibavoidRouting.isAutoEdge(graph, conn[i]))
3555
+ {
3556
+ connected[conn[i].getId()] = conn[i];
3557
+ }
3558
+ }
3559
+
3560
+ moving.push({cell: c, bounds: LibavoidRouting.getAbsoluteObstacleBounds(graph, c)});
3561
+ }
3562
+ }
3563
+
3564
+ var vertices = LibavoidRouting.collectVertices(graph);
3565
+ var movingIndex = [];
3566
+ var stationary = [];
3567
+
3568
+ for (i = 0; i < vertices.length; i++)
3569
+ {
3570
+ c = model.getCell(vertices[i].id);
3571
+
3572
+ if (c != null && handler.isCellMoving(c))
3573
+ {
3574
+ movingIndex.push(i);
3575
+ }
3576
+ else
3577
+ {
3578
+ stationary.push(vertices[i]);
3579
+ }
3580
+ }
3581
+
3582
+ cache = {moving: moving, connected: connected,
3583
+ edgeBounds: LibavoidRouting.getAutoEdgeBounds(graph),
3584
+ vertices: vertices, movingIndex: movingIndex, stationary: stationary};
3585
+
3586
+ cache.listener = function()
3587
+ {
3588
+ model.removeListener(cache.listener);
3589
+
3590
+ if (handler.__libavoidMoveCache === cache)
3591
+ {
3592
+ handler.__libavoidMoveCache = null;
3593
+ }
3594
+ };
3595
+
3596
+ model.addListener(mxEvent.CHANGE, cache.listener);
3597
+ handler.__libavoidMoveCache = cache;
3598
+ }
3599
+
3600
+ return cache;
3601
+ };
3602
+
3603
+ /**
3604
+ * A copy of the given vertex (getVertex) moved by dx, dy.
3605
+ */
3606
+ LibavoidRouting.shiftVertex = function(vertex, dx, dy)
3607
+ {
3608
+ var copy = {};
3609
+
3610
+ for (var key in vertex)
3611
+ {
3612
+ copy[key] = vertex[key];
3613
+ }
3614
+
3615
+ copy.x += dx;
3616
+ copy.y += dy;
3617
+
3618
+ return copy;
3619
+ };
3620
+
3621
+ /**
3622
+ * One obstacle-avoiding solve for a shape-move preview at drag delta (mdx,mdy) in
3623
+ * MODEL coords: the affected edge set (connected auto-edges of the moving shapes PLUS
3624
+ * edges whose route a moving shape now overlaps) routed (via routeEdgeSet — the
3625
+ * configured strategy) with the moving shapes at their preview position. Returns
3626
+ * {routes:{edgeId->absolute bends}, byId:{edgeId->edge cell}} or null when there are
3627
+ * no routable edges. The SAME pipeline + strategy as the commit (onMoveResize/
3628
+ * routeCells), so the dropped route matches the preview (and with independent
3629
+ * per-edge routing it's deterministic, with no caching needed).
3630
+ *
3631
+ * Edges that move RIGIDLY with the drag (edge and both terminals in the moving
3632
+ * set — e.g. the whole container/selection dragged) are skipped: the base
3633
+ * preview has already translated their displayed route, which IS the correct
3634
+ * preview, and a fresh solve would visibly replace it for no reason. They are
3635
+ * re-routed only when their translated route could newly conflict with a
3636
+ * STATIONARY obstacle (buffer-inflated bbox test).
3637
+ */
3638
+ LibavoidRouting.solveMovePreview = function(graph, handler, mdx, mdy)
3639
+ {
3640
+ var model = graph.getModel();
3641
+ var cache = LibavoidRouting.getMovePreviewCache(graph, handler);
3642
+ var id, c, i;
3643
+ var map = Object.create(null);
3644
+ var regions = [];
3645
+
3646
+ for (id in cache.connected)
3647
+ {
3648
+ map[id] = cache.connected[id];
3649
+ }
3650
+
3651
+ for (i = 0; i < cache.moving.length; i++)
3652
+ {
3653
+ var mb = cache.moving[i].bounds;
3654
+
3655
+ if (mb != null)
3656
+ {
3657
+ var nx = mb.x + mdx, ny = mb.y + mdy;
3658
+ var rx = Math.min(mb.x, nx), ry = Math.min(mb.y, ny);
3659
+ regions.push({x: rx, y: ry,
3660
+ width: Math.max(mb.x + mb.w, nx + mb.w) - rx,
3661
+ height: Math.max(mb.y + mb.h, ny + mb.h) - ry});
3662
+ }
3663
+ }
3664
+
3665
+ LibavoidRouting.collectOverlappingEdges(graph, regions, map, cache.edgeBounds);
3666
+
3667
+ var hasEdges = false;
3668
+
3669
+ for (id in map)
3670
+ {
3671
+ hasEdges = true;
3672
+ break;
3673
+ }
3674
+
3675
+ if (!hasEdges)
3676
+ {
3677
+ return null;
3678
+ }
3679
+
3680
+ // Obstacles: the SAME collection as the commit (collectVertices, incl. its
3681
+ // transparentBounds skip), moving ones at their preview position (model
3682
+ // bounds + the drag delta in model coords). Stationary ones are kept aside
3683
+ // for the rigid-translation test below. The cached obstacles are copied
3684
+ // before the moving ones are shifted.
3685
+ var vertices = cache.vertices.slice();
3686
+ var stationary = cache.stationary;
3687
+
3688
+ for (i = 0; i < cache.movingIndex.length; i++)
3689
+ {
3690
+ vertices[cache.movingIndex[i]] = LibavoidRouting.shiftVertex(
3691
+ vertices[cache.movingIndex[i]], mdx, mdy);
3692
+ }
3693
+
3694
+ // transparentBounds terminals of the affected edges are registered ad hoc,
3695
+ // like the commit — and shifted below, so a moving container hull rides
3696
+ // the drag like any obstacle.
3697
+ var added = Object.create(null);
3698
+ var first = vertices.length;
3699
+
3700
+ for (id in map)
3701
+ {
3702
+ LibavoidRouting.addTerminalVertex(graph, vertices, added,
3703
+ model.getTerminal(map[id], true));
3704
+ LibavoidRouting.addTerminalVertex(graph, vertices, added,
3705
+ model.getTerminal(map[id], false));
3706
+ }
3707
+
3708
+ for (i = first; i < vertices.length; i++)
3709
+ {
3710
+ c = model.getCell(vertices[i].id);
3711
+
3712
+ if (c != null && handler.isCellMoving(c))
3713
+ {
3714
+ vertices[i].x += mdx;
3715
+ vertices[i].y += mdy;
3716
+ }
3717
+ else
3718
+ {
3719
+ stationary = (stationary === cache.stationary) ?
3720
+ stationary.slice() : stationary;
3721
+ stationary.push(vertices[i]);
3722
+ }
3723
+ }
3724
+
3725
+ var buffer = LibavoidRouting.shapeBufferDistance;
3726
+ var edgeArray = [];
3727
+ var byId = Object.create(null);
3728
+
3729
+ for (id in map)
3730
+ {
3731
+ c = map[id];
3732
+ var s = model.getTerminal(c, true);
3733
+ var t = model.getTerminal(c, false);
3734
+ var sVertex = (s != null && model.isVertex(s));
3735
+ var tVertex = (t != null && model.isVertex(t));
3736
+
3737
+ // A dangling end re-routes to its (stationary) free point when the
3738
+ // connected shape moves — same as routeCells. Need one vertex end.
3739
+ var sPoint = sVertex ? null : LibavoidRouting.danglingPoint(graph, c, true);
3740
+ var tPoint = tVertex ? null : LibavoidRouting.danglingPoint(graph, c, false);
3741
+
3742
+ // The free point rides the drag when the edge itself moves, as the
3743
+ // commit translates the edge geometry including its terminal points
3744
+ if (handler.isCellMoving(c))
3745
+ {
3746
+ if (sPoint != null)
3747
+ {
3748
+ sPoint.x += mdx;
3749
+ sPoint.y += mdy;
3750
+ }
3751
+
3752
+ if (tPoint != null)
3753
+ {
3754
+ tPoint.x += mdx;
3755
+ tPoint.y += mdy;
3756
+ }
3757
+ }
3758
+
3759
+ if (!((sVertex || sPoint != null) && (tVertex || tPoint != null) &&
3760
+ (sVertex || tVertex)))
3761
+ {
3762
+ continue;
3763
+ }
3764
+
3765
+ // Rigid move (edge + both terminals share the one drag delta): the base
3766
+ // preview's translation of the displayed route is already correct — skip
3767
+ // the re-solve unless the translated route lands within buffer distance
3768
+ // of a stationary obstacle (only those can newly conflict; everything
3769
+ // moving keeps its relative geometry). Only applies with two vertex ends;
3770
+ // a dangling end's free point stays put, so always re-solve those.
3771
+ if (sVertex && tVertex &&
3772
+ handler.isCellMoving(c) && handler.isCellMoving(s) && handler.isCellMoving(t))
3773
+ {
3774
+ var bb = LibavoidRouting.edgeRouteBounds(graph, c);
3775
+
3776
+ if (bb != null)
3777
+ {
3778
+ var hit = false;
3779
+ var eb = LibavoidRouting.spacingFor(graph.getCellStyle(c));
3780
+ eb = (eb != null) ? eb : buffer;
3781
+
3782
+ for (i = 0; i < stationary.length; i++)
3783
+ {
3784
+ var o = stationary[i];
3785
+
3786
+ if (bb.x + mdx <= o.x + o.w + eb && o.x - eb <= bb.x + mdx + bb.width &&
3787
+ bb.y + mdy <= o.y + o.h + eb && o.y - eb <= bb.y + mdy + bb.height)
3788
+ {
3789
+ hit = true;
3790
+ break;
3791
+ }
3792
+ }
3793
+
3794
+ if (!hit)
3795
+ {
3796
+ continue;
3797
+ }
3798
+ }
3799
+ }
3800
+
3801
+ var est = graph.getCellStyle(c);
3802
+ edgeArray.push({id: c.getId(),
3803
+ source: sVertex ? s.getId() : null,
3804
+ target: tVertex ? t.getId() : null,
3805
+ sourcePoint: sPoint, targetPoint: tPoint,
3806
+ sourceConstraint: sVertex ? LibavoidRouting.fixedConstraint(est, true) : null,
3807
+ targetConstraint: tVertex ? LibavoidRouting.fixedConstraint(est, false) : null,
3808
+ sourcePoints: sVertex ? LibavoidRouting.snapPoints(graph, s, est, graph.getCellStyle(s)) : null,
3809
+ targetPoints: tVertex ? LibavoidRouting.snapPoints(graph, t, est, graph.getCellStyle(t)) : null,
3810
+ sourceSides: sVertex ? LibavoidRouting.maskSides(est, graph.getCellStyle(s), true) : null,
3811
+ targetSides: tVertex ? LibavoidRouting.maskSides(est, graph.getCellStyle(t), false) : null,
3812
+ sourceJetty: sVertex ? LibavoidRouting.jettyFor(est, true) : 0,
3813
+ targetJetty: tVertex ? LibavoidRouting.jettyFor(est, false) : 0,
3814
+ shapeBufferDistance: LibavoidRouting.spacingFor(est)});
3815
+ byId[c.getId()] = c;
3816
+ }
3817
+
3818
+ if (edgeArray.length == 0)
3819
+ {
3820
+ return null;
3821
+ }
3822
+
3823
+ return {routes: LibavoidRouting.routeEdgeSet(window.Avoid, vertices, edgeArray, null), byId: byId};
3824
+ };
3825
+
3826
+ /**
3827
+ * Live preview while DRAGGING a shape (mxGraphHandler): re-route the affected
3828
+ * libavoidRouting=1 edges around obstacles at the shape's CURRENT preview position,
3829
+ * transiently (edge STATES only — the model is untouched and reverts on cancel; the
3830
+ * real CELLS_MOVED re-route commits the final geometry on drop).
3831
+ *
3832
+ * Routes come from solveMovePreview via routeEdgeSet (the configured strategy):
3833
+ * independent per-edge routing is deterministic and matches the commit exactly (no
3834
+ * flicker); the multi-edge default nudges but can oscillate for competing edges.
3835
+ * Either way the preview and commit use the same strategy, so no caching is needed.
3836
+ * Wired from an mxGraphHandler.updateLivePreview override (Graph.js), after the base
3837
+ * has translated the moving cell states.
3838
+ *
3839
+ * @param {mxGraphHandler} handler
3840
+ * @param {number} dx,dy drag delta in SCALED-VIEW px (model delta = d/scale).
3841
+ */
3842
+ LibavoidRouting.livePreviewMove = function(handler, dx, dy)
3843
+ {
3844
+ if (typeof window === 'undefined' || window.Avoid == null)
3845
+ {
3846
+ return;
3847
+ }
3848
+
3849
+ var graph = handler.graph;
3850
+ var model = graph.getModel();
3851
+ var view = graph.view;
3852
+ var mdx = dx / view.scale, mdy = dy / view.scale;
3853
+ var id;
3854
+
3855
+ // Clone drags leave the originals in place (the preview moves the handler
3856
+ // borders only), so there is nothing to re-route — the clone's edges are
3857
+ // routed by the CELLS_MOVED commit on drop. The empty route set drops
3858
+ // every touched edge from the affected set below, so edges routed before
3859
+ // a mid-drag switch to cloning revert to their model route.
3860
+ var fr = (handler.cloning) ? null :
3861
+ LibavoidRouting.solveMovePreview(graph, handler, mdx, mdy);
3862
+ var routes = (fr != null) ? fr.routes : Object.create(null);
3863
+ var byId = (fr != null) ? fr.byId : Object.create(null);
3864
+
3865
+ // Transient-state lifecycle: every edge state this preview mutates is
3866
+ // recorded on the handler until the drag ends (endMovePreview, wired from
3867
+ // mxGraphHandler.reset, restores them all). An edge that leaves the
3868
+ // affected set mid-drag (the drag moved off its corridor) is restored
3869
+ // HERE: its state keeps the last transient route (invalid == false, model
3870
+ // untouched), and neither the base preview nor a later validate would
3871
+ // ever repaint it — the ghost route would survive the drop and be
3872
+ // un-undoable. Edges that are themselves moving are left to the base
3873
+ // preview (it re-derives their states every frame; invalidating them here
3874
+ // would flash them at the model position for a frame).
3875
+ var touched = handler.__libavoidMoveTouched;
3876
+
3877
+ if (touched == null)
3878
+ {
3879
+ touched = handler.__libavoidMoveTouched = Object.create(null);
3880
+ }
3881
+
3882
+ var stale = false;
3883
+
3884
+ for (id in touched)
3885
+ {
3886
+ if (routes[id] == null && !handler.isCellMoving(touched[id]))
3887
+ {
3888
+ view.invalidate(touched[id]);
3889
+ delete touched[id];
3890
+ stale = true;
3891
+ }
3892
+ }
3893
+
3894
+ if (stale)
3895
+ {
3896
+ view.validate();
3897
+ }
3898
+
3899
+ if (fr == null)
3900
+ {
3901
+ return;
3902
+ }
3903
+
3904
+ // mxGraphHandler.updateLivePreview translates the moving cell STATES (state.x +=
3905
+ // dx) but RESETS them at its end (resetPreviewStates), so by the time this runs
3906
+ // the moving terminals are back at their original positions. Re-apply the drag
3907
+ // offset to those states while we re-route: otherwise updatePoints derives the
3908
+ // segment directions from the ORIGINAL terminal position while the endpoint sits
3909
+ // at the moved one, skewing the preview route. Restored in the finally so the
3910
+ // post-condition (states reset, DOM moved) matches the base.
3911
+ var movedStates = [];
3912
+ var moving = LibavoidRouting.getMovePreviewCache(graph, handler).moving;
3913
+
3914
+ for (var i = 0; i < moving.length; i++)
3915
+ {
3916
+ var vst = view.getState(moving[i].cell);
3917
+
3918
+ if (vst != null)
3919
+ {
3920
+ vst.x += dx;
3921
+ vst.y += dy;
3922
+ movedStates.push(vst);
3923
+ }
3924
+ }
3925
+
3926
+ try
3927
+ {
3928
+ // Apply each route to the edge STATE (transient) and redraw. updatePoints
3929
+ // feeds the bends through the edge style so endpoints stay orthogonal; the
3930
+ // moved terminal states give the preview endpoints.
3931
+ for (id in routes)
3932
+ {
3933
+ var edge = byId[id];
3934
+ var state = view.getState(edge);
3935
+
3936
+ if (state == null)
3937
+ {
3938
+ continue;
3939
+ }
3940
+
3941
+ var wpsAbs = routes[id];
3942
+ var off = LibavoidRouting.getAbsoluteParentOffset(graph, edge);
3943
+ var pts = [];
3944
+
3945
+ for (var k = 0; k < wpsAbs.length; k++)
3946
+ {
3947
+ pts.push(new mxPoint(wpsAbs[k].x - off.x, wpsAbs[k].y - off.y));
3948
+ }
3949
+
3950
+ var src = state.getVisibleTerminalState(true);
3951
+ var trg = state.getVisibleTerminalState(false);
3952
+
3953
+ // Replicate mxGraphView.updateEdgeState exactly (with the libavoid bends
3954
+ // as the control points). updateFixedTerminalPoints FIRST is essential:
3955
+ // it resets the floating endpoints to null (and sets fixed exit/entry),
3956
+ // so updateFloatingTerminalPoints then recomputes them toward the NEW
3957
+ // route. Without it the base's default-route endpoints survive (non-null
3958
+ // => updateFloatingTerminalPoints skips them) and the bends meet the
3959
+ // shape on the wrong side — the preview skews while the dropped route,
3960
+ // which goes through the full updateEdgeState, is correct.
3961
+ view.updateFixedTerminalPoints(state, src, trg);
3962
+
3963
+ // A dangling terminal point of a moving edge rides the drag like
3964
+ // in the base preview: updateFixedTerminalPoints reads it from
3965
+ // the model geometry, which only translates when the drop commits
3966
+ if (handler.isCellMoving(edge) && state.absolutePoints != null)
3967
+ {
3968
+ if (src == null && state.absolutePoints[0] != null)
3969
+ {
3970
+ var p0 = state.absolutePoints[0];
3971
+ state.setAbsoluteTerminalPoint(
3972
+ new mxPoint(p0.x + dx, p0.y + dy), true);
3973
+ }
3974
+
3975
+ var pn = state.absolutePoints[state.absolutePoints.length - 1];
3976
+
3977
+ if (trg == null && pn != null)
3978
+ {
3979
+ state.setAbsoluteTerminalPoint(
3980
+ new mxPoint(pn.x + dx, pn.y + dy), false);
3981
+ }
3982
+ }
3983
+
3984
+ view.updatePoints(state, pts, src, trg);
3985
+ view.updateFloatingTerminalPoints(state, src, trg);
3986
+ view.updateEdgeBounds(state);
3987
+ view.updateEdgeLabelOffset(state);
3988
+
3989
+ // Refresh the line-jump cache from the new absolutePoints. A jumpStyle edge
3990
+ // renders from state.routedPoints (the cache updateLineJumps builds during
3991
+ // validate), NOT directly from absolutePoints — so without this the redraw
3992
+ // repaints the stale (base) route and the live preview appears to do nothing.
3993
+ if (typeof view.updateLineJumps === 'function')
3994
+ {
3995
+ view.updateLineJumps(state);
3996
+ }
3997
+
3998
+ state.invalid = false;
3999
+ graph.cellRenderer.redraw(state, true);
4000
+ touched[id] = edge;
4001
+
4002
+ // Repositions the edge's child labels: their states derive from the
4003
+ // parent edge state only at validation time, which the transient
4004
+ // preview bypasses (and the base preview never re-validates edges
4005
+ // that are merely crossed by the drag), so they would keep painting
4006
+ // on the old route. The restore paths need no equivalent —
4007
+ // invalidate recurses into the children.
4008
+ var childCount = model.getChildCount(edge);
4009
+
4010
+ for (var j = 0; j < childCount; j++)
4011
+ {
4012
+ var childState = view.getState(model.getChildAt(edge, j));
4013
+
4014
+ if (childState != null)
4015
+ {
4016
+ view.updateCellState(childState);
4017
+ childState.invalid = false;
4018
+ graph.cellRenderer.redraw(childState, true);
4019
+ }
4020
+ }
4021
+ }
4022
+ }
4023
+ finally
4024
+ {
4025
+ for (var m = 0; m < movedStates.length; m++)
4026
+ {
4027
+ movedStates[m].x -= dx;
4028
+ movedStates[m].y -= dy;
4029
+ }
4030
+ }
4031
+ };
4032
+
4033
+ /**
4034
+ * End-of-drag cleanup for the shape-move preview: restore every edge state
4035
+ * livePreviewMove mutated during the drag. Wired from mxGraphHandler.reset
4036
+ * (grapheditor/Graph.js), which runs on BOTH drop (after moveCells committed —
4037
+ * the model already carries any re-routes, so re-deriving shows them) and
4038
+ * cancel/escape. Without this, an edge whose commit ends up writing nothing
4039
+ * (the drag crossed it and moved on, so the re-route equals the stored route
4040
+ * and samePoints skips the write) keeps the transient preview route on
4041
+ * screen: a ghost that is not in the model, survives the drop, and cannot be
4042
+ * undone.
4043
+ */
4044
+ LibavoidRouting.endMovePreview = function(handler)
4045
+ {
4046
+ var cache = (handler != null) ? handler.__libavoidMoveCache : null;
4047
+
4048
+ if (cache != null)
4049
+ {
4050
+ handler.graph.getModel().removeListener(cache.listener);
4051
+ handler.__libavoidMoveCache = null;
4052
+ }
4053
+
4054
+ var touched = (handler != null) ? handler.__libavoidMoveTouched : null;
4055
+
4056
+ if (touched == null)
4057
+ {
4058
+ return;
4059
+ }
4060
+
4061
+ handler.__libavoidMoveTouched = null;
4062
+
4063
+ var view = handler.graph.view;
4064
+ var any = false;
4065
+
4066
+ for (var id in touched)
4067
+ {
4068
+ view.invalidate(touched[id]);
4069
+ any = true;
4070
+ }
4071
+
4072
+ if (any)
4073
+ {
4074
+ view.validate();
4075
+ }
4076
+ };
4077
+
4078
+ /**
4079
+ * The parts of the preview of a resize of the box of a multi-selection
4080
+ * (addResizePreviewRoutes) that only depend on the model, collected once per
4081
+ * gesture like getMovePreviewCache: the vertices that the box changes (the
4082
+ * cells of the box and their descendants) with their obstacle bounds before
4083
+ * the resize and their indices in the obstacles, their connected auto-edges,
4084
+ * the route bounds of the other auto-edges (getAutoEdgeBounds) with the
4085
+ * indices of the edges that the box changes, and the obstacles
4086
+ * (collectVertices), which are null if there are no auto-edges to route.
4087
+ * Stored in handler.__libavoidResizeCache until the model changes or the
4088
+ * preview ends (endResizePreview).
4089
+ */
4090
+ LibavoidRouting.getResizePreviewCache = function(graph, handler)
4091
+ {
4092
+ var cache = handler.__libavoidResizeCache;
4093
+
4094
+ if (cache == null)
4095
+ {
4096
+ var model = graph.getModel();
4097
+ var handled = [];
4098
+ var connected = Object.create(null);
4099
+ var hasEdges = false;
4100
+ var i, j, k;
4101
+
4102
+ for (i = 0; i < handler.cells.length; i++)
4103
+ {
4104
+ var cells = model.getDescendants(handler.cells[i]);
4105
+
4106
+ for (j = 0; j < cells.length; j++)
4107
+ {
4108
+ if (model.isVertex(cells[j]))
4109
+ {
4110
+ var edges = model.getEdges(cells[j]);
4111
+ handled.push({cell: cells[j]});
4112
+
4113
+ for (k = 0; k < edges.length; k++)
4114
+ {
4115
+ if (LibavoidRouting.isAutoEdge(graph, edges[k]))
4116
+ {
4117
+ connected[edges[k].getId()] = edges[k];
4118
+ hasEdges = true;
4119
+ }
4120
+ }
4121
+ }
4122
+ }
4123
+ }
4124
+
4125
+ cache = {handled: handled, connected: connected, handledEdges: [],
4126
+ edgeBounds: LibavoidRouting.getAutoEdgeBounds(graph, connected),
4127
+ vertices: null};
4128
+
4129
+ if (hasEdges || cache.edgeBounds.length > 0)
4130
+ {
4131
+ var vertices = LibavoidRouting.collectVertices(graph);
4132
+ var index = Object.create(null);
4133
+ cache.framed = LibavoidRouting.createStyleFilter(graph, LibavoidRouting.frameStyles);
4134
+ cache.labelled = LibavoidRouting.createStyleFilter(graph, ['labelPerimeter']);
4135
+
4136
+ for (i = 0; i < vertices.length; i++)
4137
+ {
4138
+ index[vertices[i].id] = i;
4139
+ }
4140
+
4141
+ for (i = 0; i < handled.length; i++)
4142
+ {
4143
+ handled[i].index = index[handled[i].cell.getId()];
4144
+ handled[i].bounds = AvoidRouting.obstacleBounds(LibavoidRouting.getVertex(
4145
+ graph, handled[i].cell, cache.framed, cache.labelled));
4146
+ }
4147
+
4148
+ for (i = 0; i < cache.edgeBounds.length; i++)
4149
+ {
4150
+ if (handler.isCellHandled(cache.edgeBounds[i].cell))
4151
+ {
4152
+ cache.handledEdges.push(i);
4153
+ }
4154
+ }
4155
+
4156
+ cache.vertices = vertices;
4157
+ }
4158
+
4159
+ cache.listener = function()
4160
+ {
4161
+ model.removeListener(cache.listener);
4162
+
4163
+ if (handler.__libavoidResizeCache === cache)
4164
+ {
4165
+ handler.__libavoidResizeCache = null;
4166
+ }
4167
+ };
4168
+
4169
+ model.addListener(mxEvent.CHANGE, cache.listener);
4170
+ handler.__libavoidResizeCache = cache;
4171
+ }
4172
+
4173
+ return cache;
4174
+ };
4175
+
4176
+ /**
4177
+ * Adds the routes of the auto-edges that a resize of the box of a
4178
+ * multi-selection re-routes (Graph.MultiSelectionHandler) to the given
4179
+ * geometries of its preview and returns the edges, or null without the
4180
+ * routing module or auto-edges. The vertices that the box changes are taken
4181
+ * like the cells of the CELLS_RESIZED and cellsArranged of the resize
4182
+ * (onMoveResize in installAutoRouting): their connected auto-edges and those
4183
+ * whose route overlaps their box before or after the resize are routed
4184
+ * around the obstacles after the resize. The routing reads the geometries of
4185
+ * the preview as Graph.previewGeometries, which getCellGeometry returns
4186
+ * instead of the geometries in the model, so the input is the one of
4187
+ * routeCells after the resize and the preview shows the routes of the mouse
4188
+ * up. Wired from the addPreviewGeometries of the box (installAutoRouting),
4189
+ * which runs before the geometries are set as Graph.previewGeometries.
4190
+ */
4191
+ LibavoidRouting.addResizePreviewRoutes = function(graph, handler, geometries)
4192
+ {
4193
+ var cache = (typeof window !== 'undefined' && window.Avoid != null) ?
4194
+ LibavoidRouting.getResizePreviewCache(graph, handler) : null;
4195
+ var result = null;
4196
+
4197
+ if (cache != null && cache.vertices != null)
4198
+ {
4199
+ var preview = graph.previewGeometries;
4200
+ graph.previewGeometries = geometries;
4201
+
4202
+ try
4203
+ {
4204
+ var map = Object.create(null);
4205
+ var vertices = cache.vertices.slice();
4206
+ var edgeBounds = cache.edgeBounds;
4207
+ var regions = [];
4208
+ var id, i;
4209
+
4210
+ for (id in cache.connected)
4211
+ {
4212
+ map[id] = cache.connected[id];
4213
+ }
4214
+
4215
+ // Obstacles after the resize and the areas they leave or cover
4216
+ for (i = 0; i < cache.handled.length; i++)
4217
+ {
4218
+ var h = cache.handled[i];
4219
+ var v = LibavoidRouting.getVertex(graph, h.cell, cache.framed, cache.labelled);
4220
+ var nb = AvoidRouting.obstacleBounds(v);
4221
+
4222
+ if (h.index != null && v != null)
4223
+ {
4224
+ vertices[h.index] = v;
4225
+ }
4226
+
4227
+ if (nb != null)
4228
+ {
4229
+ var ob = (h.bounds != null) ? h.bounds : nb;
4230
+ var x = Math.min(ob.x, nb.x), y = Math.min(ob.y, nb.y);
4231
+ regions.push({x: x, y: y,
4232
+ width: Math.max(ob.x + ob.w, nb.x + nb.w) - x,
4233
+ height: Math.max(ob.y + ob.h, nb.y + nb.h) - y});
4234
+ }
4235
+ }
4236
+
4237
+ // Route bounds of the edges that the box changes after the resize
4238
+ if (cache.handledEdges.length > 0)
4239
+ {
4240
+ edgeBounds = edgeBounds.slice();
4241
+
4242
+ for (i = 0; i < cache.handledEdges.length; i++)
4243
+ {
4244
+ var e = edgeBounds[cache.handledEdges[i]];
4245
+ var bb = LibavoidRouting.edgeRouteBounds(graph, e.cell);
4246
+
4247
+ if (bb != null)
4248
+ {
4249
+ edgeBounds[cache.handledEdges[i]] = {cell: e.cell, bounds: bb};
4250
+ }
4251
+ }
4252
+ }
4253
+
4254
+ LibavoidRouting.collectOverlappingEdges(graph, regions, map, edgeBounds);
4255
+ var cells = [];
4256
+
4257
+ for (id in map)
4258
+ {
4259
+ cells.push(map[id]);
4260
+ }
4261
+
4262
+ var byId = Object.create(null);
4263
+ var edges = LibavoidRouting.createRouteEdges(graph, vertices, cells, byId);
4264
+ var routes = (edges.length > 0) ? LibavoidRouting.routeEdgeSet(
4265
+ window.Avoid, vertices, edges, null) : Object.create(null);
4266
+ result = [];
4267
+
4268
+ // Bends in the frame of the parent of the edge as in routeCells
4269
+ for (id in routes)
4270
+ {
4271
+ var edge = byId[id];
4272
+ var off = LibavoidRouting.getAbsoluteParentOffset(graph, edge);
4273
+ var geo = graph.getCellGeometry(edge);
4274
+ var pts = [];
4275
+
4276
+ for (i = 0; i < routes[id].length; i++)
4277
+ {
4278
+ pts.push(new mxPoint(routes[id][i].x - off.x, routes[id][i].y - off.y));
4279
+ }
4280
+
4281
+ geo = (geo != null) ? geo.clone() : new mxGeometry();
4282
+ geo.points = (pts.length > 0) ? pts : null;
4283
+ geometries.put(edge, geo);
4284
+ result.push(edge);
4285
+ }
4286
+ }
4287
+ finally
4288
+ {
4289
+ graph.previewGeometries = preview;
4290
+ }
4291
+ }
4292
+
4293
+ return result;
4294
+ };
4295
+
4296
+ /**
4297
+ * Drops the cache of the preview of a resize of the box of a multi-selection
4298
+ * (getResizePreviewCache). Wired from the resetPreview of the box
4299
+ * (installAutoRouting), which runs at the end of each gesture.
4300
+ */
4301
+ LibavoidRouting.endResizePreview = function(handler)
4302
+ {
4303
+ var cache = handler.__libavoidResizeCache;
4304
+
4305
+ if (cache != null)
4306
+ {
4307
+ handler.graph.getModel().removeListener(cache.listener);
4308
+ handler.__libavoidResizeCache = null;
4309
+ }
4310
+ };
4311
+
4312
+ /**
4313
+ * Compute obstacle-avoiding orthogonal routes for a set of edges. Thin wrapper
4314
+ * around AvoidRouting.computeRoutes (js/libavoid-js/libavoid-routing.js — the
4315
+ * shared routing core, vendored verbatim by drawio-mcp) that fills the option
4316
+ * defaults from the LibavoidRouting statics, so the menu's spacing prompt and
4317
+ * console overrides keep working. Same contract: edge id -> interior bend
4318
+ * points (ABSOLUTE, collinear-filtered); endpoints dropped; straight => [].
4319
+ */
4320
+ LibavoidRouting.computeRoutes = function(Avoid, vertices, edges, opts)
4321
+ {
4322
+ var buffer = (opts != null && opts.shapeBufferDistance != null) ?
4323
+ opts.shapeBufferDistance : LibavoidRouting.shapeBufferDistance;
4324
+ var nudge = (opts != null && opts.idealNudgingDistance != null) ?
4325
+ opts.idealNudgingDistance : LibavoidRouting.idealNudgingDistance;
4326
+
4327
+ // Cap the clearance PER EDGE to what the terminal pair's gap physically
4328
+ // allows: 2*buffer + a 4px routing channel must fit between the shapes,
4329
+ // or the buffered obstacles swallow the corridor and the jetty
4330
+ // checkpoints force degenerate side-jogs (visible on compact Mermaid/
4331
+ // PlantUML layouts with 32px row gaps at the default 16). The buffer is
4332
+ // a per-Router parameter, so edges are BUCKETED by their effective
4333
+ // buffer and each bucket routed in its own solve; roomy edges keep the
4334
+ // configured buffer (single bucket = single solve, as before). The
4335
+ // jetty minimums get the matching per-end treatment INSIDE the core
4336
+ // (AvoidRouting.cappedJetty — clearance along each stub's own axis),
4337
+ // so drawio-mcp's direct core callers inherit it. Applies to every
4338
+ // strategy and to the previews, which route through this wrapper too —
4339
+ // commit and preview stay byte-identical.
4340
+ var byId = Object.create(null);
4341
+ var i;
4342
+
4343
+ if (vertices != null)
4344
+ {
4345
+ for (i = 0; i < vertices.length; i++)
4346
+ {
4347
+ if (vertices[i] != null)
4348
+ {
4349
+ byId[vertices[i].id] = vertices[i];
4350
+ }
4351
+ }
4352
+ }
4353
+
4354
+ var buckets = {};
4355
+
4356
+ for (i = 0; i < edges.length; i++)
4357
+ {
4358
+ var e = edges[i];
4359
+
4360
+ if (e == null)
4361
+ {
4362
+ continue;
4363
+ }
4364
+
4365
+ // The spacing of the edge (libavoidSpacing, see spacingFor) replaces
4366
+ // the clearance of the run, so such edges are solved in their own bucket
4367
+ var eff = (e.shapeBufferDistance != null) ? e.shapeBufferDistance : buffer;
4368
+ var s = AvoidRouting.obstacleBounds(byId[e.source]);
4369
+ var t = AvoidRouting.obstacleBounds(byId[e.target]);
4370
+
4371
+ if (s != null && t != null)
4372
+ {
4373
+ var gap = Math.max(
4374
+ Math.max(s.x, t.x) - Math.min(s.x + s.w, t.x + t.w),
4375
+ Math.max(s.y, t.y) - Math.min(s.y + s.h, t.y + t.h));
4376
+
4377
+ if (gap > 0 && Math.floor((gap - 4) / 2) < eff)
4378
+ {
4379
+ eff = Math.max(2, Math.floor((gap - 4) / 2));
4380
+ }
4381
+ }
4382
+
4383
+ (buckets[eff] = buckets[eff] || []).push(e);
4384
+ }
4385
+
4386
+ var out = Object.create(null);
4387
+
4388
+ for (var key in buckets)
4389
+ {
4390
+ var r = AvoidRouting.computeRoutes(Avoid, vertices, buckets[key], {
4391
+ shapeBufferDistance: parseFloat(key),
4392
+ idealNudgingDistance: nudge
4393
+ });
4394
+
4395
+ for (var id in r)
4396
+ {
4397
+ out[id] = r[id];
4398
+ }
4399
+ }
4400
+
4401
+ return out;
4402
+ };
4403
+
4404
+ /**
4405
+ * Route each edge in its OWN single-connector libavoid solve (all vertices as
4406
+ * obstacles, exactly ONE connector per Router), merging the results. With a single
4407
+ * connector there is no other connector to compete with, so libavoid's route is
4408
+ * DETERMINISTIC — this avoids the non-deterministic "which competing edge takes the
4409
+ * detour" tie that makes a multi-edge solve flip between equal-cost topologies across
4410
+ * calls (the live-preview oscillation, observed only when 2+ edges run between the
4411
+ * same shape pair). Because each edge is solved in isolation, its route is also
4412
+ * independent of which OTHER edges are in the batch, so the live preview and the
4413
+ * commit produce identical routes with no need for caching/hysteresis.
4414
+ *
4415
+ * Trade-off vs computeRoutes: no edge-edge nudging — two edges that share a path are
4416
+ * not separated (they can overlap). Same {edgeId -> bends} contract as computeRoutes.
4417
+ * App-only: drawio-mcp routes multi-edge via AvoidRouting.computeRoutes directly
4418
+ * (server-side one-shot routing never oscillates).
4419
+ */
4420
+ LibavoidRouting.computeRoutesIndependent = function(Avoid, vertices, edges, opts)
4421
+ {
4422
+ var out = Object.create(null);
4423
+
4424
+ if (edges == null)
4425
+ {
4426
+ return out;
4427
+ }
4428
+
4429
+ for (var i = 0; i < edges.length; i++)
4430
+ {
4431
+ if (edges[i] == null)
4432
+ {
4433
+ continue;
4434
+ }
4435
+
4436
+ var r = LibavoidRouting.computeRoutes(Avoid, vertices, [edges[i]], opts);
4437
+
4438
+ for (var id in r)
4439
+ {
4440
+ out[id] = r[id];
4441
+ }
4442
+ }
4443
+
4444
+ return out;
4445
+ };
4446
+
4447
+ /**
4448
+ * Route a set of edges using the configured strategy (LibavoidRouting.
4449
+ * independentRouting): independent per-edge solves (deterministic, no nudging) or a
4450
+ * single multi-edge computeRoutes (nudged, non-deterministic for competing edges).
4451
+ * Same {edgeId -> bends} contract. Used by both routeCells (commit) and
4452
+ * solveMovePreview (live preview) so they always agree.
4453
+ */
4454
+ LibavoidRouting.routeEdgeSet = function(Avoid, vertices, edges, opts)
4455
+ {
4456
+ return LibavoidRouting.independentRouting ?
4457
+ LibavoidRouting.computeRoutesIndependent(Avoid, vertices, edges, opts) :
4458
+ LibavoidRouting.computeRoutes(Avoid, vertices, edges, opts);
4459
+ };