@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.
- package/LICENSE +21 -0
- package/NOTICE +11 -0
- package/README.md +164 -2
- package/assets/drawio/LICENSE +201 -0
- package/assets/drawio/VERSION +1 -0
- package/assets/drawio/favicon.ico +0 -0
- package/assets/drawio/images/1x1.png +0 -0
- package/assets/drawio/images/2x2.png +0 -0
- package/assets/drawio/images/3x3.png +0 -0
- package/assets/drawio/images/ajax-loader.gif +0 -0
- package/assets/drawio/images/android-chrome-512x512.png +0 -0
- package/assets/drawio/images/apple-touch-icon.png +0 -0
- package/assets/drawio/images/aui-wait.gif +0 -0
- package/assets/drawio/images/broken.png +0 -0
- package/assets/drawio/images/browserconfig.xml +9 -0
- package/assets/drawio/images/close.png +0 -0
- package/assets/drawio/images/cloud-connector-atlas.png +0 -0
- package/assets/drawio/images/collapsed.gif +0 -0
- package/assets/drawio/images/delete.png +0 -0
- package/assets/drawio/images/document-google-drive-icon-80.png +0 -0
- package/assets/drawio/images/draw-search.png +0 -0
- package/assets/drawio/images/drawio.svg +19 -0
- package/assets/drawio/images/drawlogo-color.svg +50 -0
- package/assets/drawio/images/drawlogo-gray.svg +18 -0
- package/assets/drawio/images/drawlogo-text-bottom.svg +75 -0
- package/assets/drawio/images/drawlogo.svg +18 -0
- package/assets/drawio/images/drawlogo128.png +0 -0
- package/assets/drawio/images/drawlogo144.png +0 -0
- package/assets/drawio/images/drawlogo256.png +0 -0
- package/assets/drawio/images/drawlogo48-gray.png +0 -0
- package/assets/drawio/images/drawlogo48.png +0 -0
- package/assets/drawio/images/drawlogo80.png +0 -0
- package/assets/drawio/images/droptarget.png +0 -0
- package/assets/drawio/images/edit.gif +0 -0
- package/assets/drawio/images/edit16.png +0 -0
- package/assets/drawio/images/edit32.png +0 -0
- package/assets/drawio/images/edit80.png +0 -0
- package/assets/drawio/images/embed-icon.png +0 -0
- package/assets/drawio/images/expanded.gif +0 -0
- package/assets/drawio/images/facebook.png +0 -0
- package/assets/drawio/images/favicon-16x16.png +0 -0
- package/assets/drawio/images/favicon-32x32.png +0 -0
- package/assets/drawio/images/file.png +0 -0
- package/assets/drawio/images/folder.png +0 -0
- package/assets/drawio/images/gadget-screenshot.png +0 -0
- package/assets/drawio/images/gadget-thumb.png +0 -0
- package/assets/drawio/images/github-logo-white.svg +6 -0
- package/assets/drawio/images/github-logo.svg +17 -0
- package/assets/drawio/images/gitlab-logo-white.svg +1 -0
- package/assets/drawio/images/gitlab-logo.svg +1 -0
- package/assets/drawio/images/glyphicons_blogger.png +0 -0
- package/assets/drawio/images/glyphicons_facebook.png +0 -0
- package/assets/drawio/images/glyphicons_github.png +0 -0
- package/assets/drawio/images/glyphicons_google.png +0 -0
- package/assets/drawio/images/glyphicons_star.png +0 -0
- package/assets/drawio/images/glyphicons_twitter.png +0 -0
- package/assets/drawio/images/glyphicons_youtube.png +0 -0
- package/assets/drawio/images/google-drive-connector-atlas.png +0 -0
- package/assets/drawio/images/google-drive-connector-atlas.svg +121 -0
- package/assets/drawio/images/google-drive-logo-white.svg +9 -0
- package/assets/drawio/images/google-drive-logo.svg +17 -0
- package/assets/drawio/images/google-share.png +0 -0
- package/assets/drawio/images/icon-192-maskable.png +0 -0
- package/assets/drawio/images/icon-192.png +0 -0
- package/assets/drawio/images/icon-512-maskable.png +0 -0
- package/assets/drawio/images/icon-512.png +0 -0
- package/assets/drawio/images/icon-search.svg +21 -0
- package/assets/drawio/images/img-hi-res.png +0 -0
- package/assets/drawio/images/img-lo-res.png +0 -0
- package/assets/drawio/images/list.svg +16 -0
- package/assets/drawio/images/locked.png +0 -0
- package/assets/drawio/images/log.png +0 -0
- package/assets/drawio/images/logo-confluence.png +0 -0
- package/assets/drawio/images/logo-flat-small.png +0 -0
- package/assets/drawio/images/logo-flat.png +0 -0
- package/assets/drawio/images/logo-jira.png +0 -0
- package/assets/drawio/images/logo-small.gif +0 -0
- package/assets/drawio/images/logo-white.png +0 -0
- package/assets/drawio/images/logo.png +0 -0
- package/assets/drawio/images/manifest.json +25 -0
- package/assets/drawio/images/move.png +0 -0
- package/assets/drawio/images/notion-logo-white.svg +1 -0
- package/assets/drawio/images/notion-logo.svg +1 -0
- package/assets/drawio/images/onedrive-connector-atlas.png +0 -0
- package/assets/drawio/images/onedrive-connector-atlas.svg +248 -0
- package/assets/drawio/images/onedrive-logo-white.svg +29 -0
- package/assets/drawio/images/onedrive-logo.svg +29 -0
- package/assets/drawio/images/osa_database.png +0 -0
- package/assets/drawio/images/osa_drive-harddisk.png +0 -0
- package/assets/drawio/images/plant-uml-atlas.png +0 -0
- package/assets/drawio/images/samepage-icon-color.svg +37 -0
- package/assets/drawio/images/save.png +0 -0
- package/assets/drawio/images/sidebar-android.png +0 -0
- package/assets/drawio/images/sidebar-archimate.png +0 -0
- package/assets/drawio/images/sidebar-arrows.png +0 -0
- package/assets/drawio/images/sidebar-arrows2.png +0 -0
- package/assets/drawio/images/sidebar-basic.png +0 -0
- package/assets/drawio/images/sidebar-c4.png +0 -0
- package/assets/drawio/images/sidebar-cabinets.png +0 -0
- package/assets/drawio/images/sidebar-cumulus.png +0 -0
- package/assets/drawio/images/sidebar-dfd.png +0 -0
- package/assets/drawio/images/sidebar-dynamics365.png +0 -0
- package/assets/drawio/images/sidebar-eip.png +0 -0
- package/assets/drawio/images/sidebar-er.png +0 -0
- package/assets/drawio/images/sidebar-floorplans.png +0 -0
- package/assets/drawio/images/sidebar-flowchart.png +0 -0
- package/assets/drawio/images/sidebar-gcp3.png +0 -0
- package/assets/drawio/images/sidebar-general.png +0 -0
- package/assets/drawio/images/sidebar-kubernetes.png +0 -0
- package/assets/drawio/images/sidebar-leanmapping.png +0 -0
- package/assets/drawio/images/sidebar-mockups.png +0 -0
- package/assets/drawio/images/sidebar-network.png +0 -0
- package/assets/drawio/images/sidebar-network2.png +0 -0
- package/assets/drawio/images/sidebar-openstack.png +0 -0
- package/assets/drawio/images/sidebar-sitemap.png +0 -0
- package/assets/drawio/images/sidebar-threatmodeling.png +0 -0
- package/assets/drawio/images/sidebar-uml.png +0 -0
- package/assets/drawio/images/sketch-logo128.png +0 -0
- package/assets/drawio/images/spin.gif +0 -0
- package/assets/drawio/images/sprites.png +0 -0
- package/assets/drawio/images/stop-flat-icon-80.png +0 -0
- package/assets/drawio/images/submenu.gif +0 -0
- package/assets/drawio/images/touch-submenu.png +0 -0
- package/assets/drawio/images/transparent.gif +0 -0
- package/assets/drawio/images/trello-logo-white-orange.svg +6 -0
- package/assets/drawio/images/trello-logo-white.svg +6 -0
- package/assets/drawio/images/trello-logo.svg +1 -0
- package/assets/drawio/images/tweet.png +0 -0
- package/assets/drawio/images/update16.png +0 -0
- package/assets/drawio/images/update32.png +0 -0
- package/assets/drawio/images/update80.png +0 -0
- package/assets/drawio/images/youtube.png +0 -0
- package/assets/drawio/img/clipart/Battery_0_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_100_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_50_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_75_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_allstates_128x128.png +0 -0
- package/assets/drawio/img/clipart/Bluetooth_128x128.png +0 -0
- package/assets/drawio/img/clipart/Earth_globe_128x128.png +0 -0
- package/assets/drawio/img/clipart/Empty_Folder_128x128.png +0 -0
- package/assets/drawio/img/clipart/Full_Folder_128x128.png +0 -0
- package/assets/drawio/img/clipart/Gear_128x128.png +0 -0
- package/assets/drawio/img/clipart/Keys_128x128.png +0 -0
- package/assets/drawio/img/clipart/Lock_128x128.png +0 -0
- package/assets/drawio/img/clipart/Mouse_Pointer_128x128.png +0 -0
- package/assets/drawio/img/clipart/Plug_128x128.png +0 -0
- package/assets/drawio/img/clipart/Ships_Wheel_128x128.png +0 -0
- package/assets/drawio/img/clipart/Star_128x128.png +0 -0
- package/assets/drawio/img/clipart/Tire_128x128.png +0 -0
- package/assets/drawio/index.html +92 -0
- package/assets/drawio/js/PostConfig.js +7 -0
- package/assets/drawio/js/PreConfig.js +13 -0
- package/assets/drawio/js/app.min.js +18466 -0
- package/assets/drawio/js/bootstrap.js +428 -0
- package/assets/drawio/js/diagramly/LibavoidRouting.js +4459 -0
- package/assets/drawio/js/export-init.js +21 -0
- package/assets/drawio/js/extensions.min.js +10478 -0
- package/assets/drawio/js/libavoid-js/LICENSE +505 -0
- package/assets/drawio/js/libavoid-js/libavoid-routing.js +1633 -0
- package/assets/drawio/js/libavoid-js/libavoid.min.js +59 -0
- package/assets/drawio/js/main.js +22 -0
- package/assets/drawio/js/rough/rough.min.js +1 -0
- package/assets/drawio/js/shapes-14-6-5.min.js +3457 -0
- package/assets/drawio/js/stencils.min.js +447 -0
- package/assets/drawio/mxgraph/css/common.css +147 -0
- package/assets/drawio/resources/dia.txt +2901 -0
- package/assets/drawio/styles/dark-default.xml +109 -0
- package/assets/drawio/styles/default-old.xml +109 -0
- package/assets/drawio/styles/default.xml +203 -0
- package/assets/drawio/styles/fonts/ArchitectsDaughter-Regular.ttf +0 -0
- package/assets/drawio/styles/grapheditor.css +4501 -0
- package/assets/drawio/styles/high-contrast.css +128 -0
- package/assets/drawio/styles/thumb_horz.png +0 -0
- package/assets/drawio/styles/thumb_vertical.png +0 -0
- package/assets/fonts/ibm-plex-mono-LICENSE.txt +93 -0
- package/assets/fonts/ibm-plex-sans-LICENSE.txt +93 -0
- package/assets/fonts/kalam-400.woff2 +0 -0
- package/assets/fonts/kalam-700.woff2 +0 -0
- package/assets/fonts/kalam-LICENSE.txt +93 -0
- package/assets/fonts/plex-mono-400.woff2 +0 -0
- package/assets/fonts/plex-mono-600.woff2 +0 -0
- package/assets/fonts/plex-sans-400.woff2 +0 -0
- package/assets/fonts/plex-sans-600.woff2 +0 -0
- package/dist/api.js +181 -0
- package/dist/cli.js +262 -0
- package/dist/drawio/icons.js +121 -0
- package/dist/drawio/merge.js +309 -0
- package/dist/drawio/read.js +180 -0
- package/dist/drawio/show.js +501 -0
- package/dist/drawio/theme.js +425 -0
- package/dist/drawio/write.js +300 -0
- package/dist/edit/server.js +212 -0
- package/dist/git.js +46 -0
- package/dist/import/mermaid.js +569 -0
- package/dist/layout/layout.js +212 -0
- package/dist/layout/sequence.js +333 -0
- package/dist/layout/text.js +73 -0
- package/dist/lint/check.js +184 -0
- package/dist/lint/fixlabels.js +128 -0
- package/dist/mcp/server.js +239 -0
- package/dist/publish.js +121 -0
- package/dist/render/browser.js +210 -0
- package/dist/render/fonts.js +66 -0
- package/dist/report.js +36 -0
- package/dist/spec/model.js +53 -0
- package/dist/spec/parse.js +497 -0
- package/package.json +71 -4
- package/skills/linework/SKILL.md +214 -0
- package/skills/linework/examples/billing-er.drawio.svg +4 -0
- package/skills/linework/examples/billing-er.yaml +31 -0
- package/skills/linework/examples/checkout.drawio.svg +4 -0
- package/skills/linework/examples/checkout.yaml +28 -0
- package/skills/linework/examples/pay-sequence.drawio.svg +4 -0
- package/skills/linework/examples/pay-sequence.yaml +28 -0
- package/skills/linework/examples/payment-state.drawio.svg +4 -0
- package/skills/linework/examples/payment-state.yaml +22 -0
- package/skills/linework/examples/refund-flow.drawio.svg +4 -0
- package/skills/linework/examples/refund-flow.yaml +29 -0
- package/skills/linework/examples/serverless-aws.drawio.svg +4 -0
- package/skills/linework/examples/serverless-aws.yaml +23 -0
- package/skills/linework/references/google-docs.md +46 -0
- package/skills/linework/references/spec.md +312 -0
- 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
|
+
};
|