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