Merge branch 'libavoid-two-file-cdn'

The draw.io release shipping the pure-JS two-file js/libavoid-js layout
is live on viewer.diagrams.net, so the CDN now matches this code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gaudenz Alder
2026-07-15 18:26:45 +02:00
8 changed files with 126 additions and 152 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ At startup (Node.js) or build time (Workers), the HTML is assembled. The draw.io
- **`app-with-deps.js`** (~319 KB, from `node_modules/@modelcontextprotocol/ext-apps`) — MCP Apps SDK browser bundle. The bundle is ESM (ends with `export { ... as App }`), so `processAppBundle()` strips the export statement and creates a local `var App = <minifiedName>` alias. This makes it safe to inline in a plain `<script>` tag inside the sandboxed iframe.
- **`pako_deflate.min.js`** (~28 KB, from `node_modules/pako`) — for compressing XML into the `#create=` URL format.
- **libavoid** (the obstacle-avoiding orthogonal edge router behind `routing: "libavoid"`) is **not vendored/inlined** — the HTML loads glue + base64 wasm payload + loader + shared routing core from the `viewer.diagrams.net` CDN (`js/libavoid-js/`), like drawio-elk and drawio-mermaid. The wasm rides as base64 inside `libavoid-wasm.js` and is decoded by the loader (still no fetch, so the sandbox CSP is satisfied by plain script-src). The routing math is `AvoidRouting.computeRoutes` from `libavoid-routing.js` — the canonical `drawio-dev js/libavoid-js/` artifact, byte-identical to what the draw.io editor bundles and the mcp-tool-server vendors. `buildHtml` still accepts `options.libavoidJs`/`libavoidWasmB64` to inline a local build (dev). See `vendor/libavoid/README.md`.
- **libavoid** (the obstacle-avoiding orthogonal edge router behind `routing: "libavoid"`) is **not vendored/inlined** — the HTML loads the pure-JS router bundle (`libavoid.min.js`, a self-contained classic script that publishes `globalThis.Avoid` and parks `window.__libavoidReady` synchronously — no WASM, no fetch, no `wasm-unsafe-eval` needed) + shared routing core from the `viewer.diagrams.net` CDN (`js/libavoid-js/`), like drawio-elk and drawio-mermaid. Requires the draw.io release that ships the pure-JS two-file layout there. The routing math is `AvoidRouting.computeRoutes` from `libavoid-routing.js` — the canonical `drawio-dev js/libavoid-js/` artifact, byte-identical to what the draw.io editor bundles and the mcp-tool-server vendors. `buildHtml` accepts `options.libavoidJs` to inline a local build instead (dev). See `vendor/libavoid/README.md`.
Loaded from the CDN (not inlined):
+4 -4
View File
@@ -64,10 +64,10 @@ const pakoDeflateJs = fs.readFileSync(
// out of generated-html.js (~1.5 MB), lets browsers cache them cross-session,
// and keeps their versions in sync with the viewer per draw.io release.
// libavoid (WASM edge router) is NOT inlined — the HTML loads glue + base64
// wasm payload + loader + shared routing core from the viewer.diagrams.net
// CDN, like drawio-elk and drawio-mermaid (see buildHtml's libavoidBlock).
// Keeps ~700 KB out of generated-html.js.
// libavoid (edge router) is NOT inlined — the HTML loads the pure-JS router
// bundle + shared routing core from the viewer.diagrams.net CDN, like
// drawio-elk and drawio-mermaid (see buildHtml's libavoidBlock).
// Keeps ~850 KB out of generated-html.js.
// Read the shared XML reference (single source of truth for all prompts)
const xmlReference = fs.readFileSync(
+4 -4
View File
@@ -81,11 +81,11 @@ if (process.env.MERMAID_PATH)
console.log("Inlining local drawio-mermaid from", process.env.MERMAID_PATH);
}
// libavoid (WASM obstacle-avoiding edge router, powers the routing: "libavoid"
// pass) is NOT inlined — the HTML loads glue + base64 wasm payload + loader +
// shared routing core from the viewer.diagrams.net CDN, like drawio-elk and
// libavoid (obstacle-avoiding edge router, powers the routing: "libavoid"
// pass) is NOT inlined — the HTML loads the pure-JS router bundle + shared
// routing core from the viewer.diagrams.net CDN, like drawio-elk and
// drawio-mermaid (see buildHtml's libavoidBlock). Cached cross-session,
// version-synced with each draw.io release, and drops ~700 KB from the HTML.
// version-synced with each draw.io release, and drops ~850 KB from the HTML.
// The URLs are ETag-versioned at startup (and daily on the HTTP transport)
// so a release busts the CDN's 30-day browser cache immediately — see
// libavoid-versions.js; on a failed startup check the plain URLs are used.
+9 -8
View File
@@ -5,22 +5,23 @@
// browser cache can keep serving a stale router for up to a month. The HTML
// is delivered to the host as an MCP resource and rendered in a sandboxed
// iframe, so the scripts cannot be served same-origin from this server -
// but the URLs can be VERSIONED: the Node server HEADs the four files at
// but the URLs can be VERSIONED: the Node server HEADs the two files at
// startup (and daily on the HTTP transport - see index.js), derives a
// version token from each ETag, and bakes it into the script URLs as
// ?v=<token>. Clients keep long-lived caching per URL, and a release
// changes the ETag -> changes the URL -> every client re-fetches exactly
// once. Best effort: a failed or 404 HEAD (the path only exists once a
// draw.io release ships js/libavoid-js/) leaves the plain URL, which is the
// previous behavior with its 30-day worst-case staleness.
// once. Best effort: a failed or 404 HEAD leaves the plain URL, which is
// the previous behavior with its 30-day worst-case staleness.
const CDN_BASE = "https://viewer.diagrams.net/js/libavoid-js/";
// Fixed load order: glue -> wasm payload -> loader -> routing core.
// Fixed load order: pure-JS router bundle -> routing core. Requires the
// draw.io release that ships the pure-JS two-file js/libavoid-js/ layout
// (the bundle publishes globalThis.Avoid and parks window.__libavoidReady
// itself); the earlier WASM layout's libavoid-wasm.js/libavoid-loader.js
// no longer exist and are not referenced.
export const LIBAVOID_FILES = [
"libavoid.min.js",
"libavoid-wasm.js",
"libavoid-loader.js",
"libavoid-routing.js"
];
@@ -45,7 +46,7 @@ async function versionToken(file)
}
/**
* The four libavoid script URLs in load order, ETag-versioned where the CDN
* The two libavoid script URLs in load order, ETag-versioned where the CDN
* answered. Safe to call repeatedly (each call re-checks). `prev` (the
* previous result) keeps a file's last-known URL when its check fails: a
* transient HEAD failure is not a version change and must not downgrade a
+29 -93
View File
@@ -11,12 +11,12 @@ import { buildTagMap, searchShapes } from "../../shared/shape-search.js";
/**
* Build the self-contained HTML string that renders diagrams.
* The MCP Apps App class and pako deflate are inlined. The draw.io viewer,
* drawio-elk, drawio-mermaid, and libavoid (glue + wasm payload + loader +
* routing core from js/libavoid-js/) load from the viewer.diagrams.net CDN
* drawio-elk, drawio-mermaid, and libavoid (pure-JS router bundle + routing
* core from js/libavoid-js/) load from the viewer.diagrams.net CDN
* by default — cached cross-session and kept in version-sync with each
* draw.io release. Pass viewerJs/elkJs/mermaidJs (or libavoidJs +
* libavoidWasmB64) to inline a local build instead (for dev see
* VIEWER_PATH/ELK_PATH/MERMAID_PATH in index.js).
* draw.io release. Pass viewerJs/elkJs/mermaidJs/libavoidJs to inline a
* local build instead (for dev — see VIEWER_PATH/ELK_PATH/MERMAID_PATH in
* index.js).
*
* @param {string} appWithDepsJs - The processed MCP Apps SDK bundle (exports stripped, App alias added).
* @param {string} pakoDeflateJs - The pako deflate browser bundle.
@@ -24,9 +24,8 @@ import { buildTagMap, searchShapes } from "../../shared/shape-search.js";
* @param {object} [options] - Optional configuration.
* @param {string} [options.viewerJs] - If provided, inlines this JS instead of loading viewer-static.min.js from CDN.
* @param {string} [options.elkJs] - If provided, inlines this drawio-elk bundle instead of loading it from CDN. Defines `var ELK` (engine) plus `ElkLayout`/`ElkAdapter`/`ElkApplier` (the mxGraph bridge + postLayout facade), consumed by drawio-mermaid and the postLayout pass. Loaded before mermaid.
* @param {string} [options.libavoidJs] - If provided (together with libavoidWasmB64), inlines this processed libavoid-js bundle (exports stripped, `globalThis.AvoidLib` aliased, loader patched to read `globalThis.__LIBAVOID_WASM_BINARY` see processLibavoidBundle) with libavoid-routing.js (defines `globalThis.AvoidRouting`) appended, instead of loading the libavoid block from the CDN. Powers the `routing: "libavoid"` edge-routing pass.
* @param {string} [options.libavoidWasmB64] - The libavoid.wasm binary, base64-encoded, for the inline case. Decoded to a Uint8Array and handed to the Emscripten module as `wasmBinary` so the router instantiates with no fetch.
* @param {string[]} [options.libavoidUrls] - The four libavoid script URLs in load order (glue, wasm payload, loader, routing core), typically ETag-versioned (libavoid-versions.js) so a draw.io release busts the browser cache immediately. Defaults to the plain CDN URLs; ignored when libavoidJs/libavoidWasmB64 inline a local build.
* @param {string} [options.libavoidJs] - If provided, inlines this libavoid-js bundle (the drawio-dev js/libavoid-js/libavoid.min.js pure-JS build, used as-is — a self-contained classic script that publishes `globalThis.Avoid` and parks `window.__libavoidReady` synchronously) with libavoid-routing.js (defines `globalThis.AvoidRouting`) appended, instead of loading the libavoid block from the CDN. Powers the `routing: "libavoid"` edge-routing pass.
* @param {string[]} [options.libavoidUrls] - The two libavoid script URLs in load order (pure-JS router bundle, routing core), typically ETag-versioned (libavoid-versions.js) so a draw.io release busts the browser cache immediately. Defaults to the plain CDN URLs; ignored when libavoidJs inlines a local build.
* @param {string} [options.buildId] - Build identifier (git SHA + timestamp). Exposed as window.__DRAWIO_BUILD in the iframe.
* @returns {string} Self-contained HTML string.
*/
@@ -35,69 +34,39 @@ export function buildHtml(appWithDepsJs, pakoDeflateJs, mermaidJs, options)
var viewerJs = (options && options.viewerJs) || null;
var elkJs = (options && options.elkJs) || null;
var libavoidJs = (options && options.libavoidJs) || null;
var libavoidWasmB64 = (options && options.libavoidWasmB64) || null;
var buildId = (options && options.buildId) || "unknown";
// libavoid-js (WASM orthogonal edge router). The glue defines
// globalThis.AvoidLib; the loader below decodes the base64 wasm to a
// Uint8Array, hands it to the Emscripten module as wasmBinary (no fetch —
// see processLibavoidBundle), and kicks off async instantiation. The
// resulting promise is parked on window.__libavoidReady (resolving to the
// Avoid namespace) so the routing pass can await it on demand; load runs
// eagerly at page init so the wasm is usually warm by the time a
// routing:"libavoid" diagram finishes rendering. Failures degrade
// gracefully — applyRouting just skips and the diagram renders unrouted.
var libavoidLoader =
'function __libavoidB64ToBytes(b64)\n' +
'{\n' +
' var bin = atob(b64);\n' +
' var bytes = new Uint8Array(bin.length);\n' +
' for (var i = 0; i < bin.length; i++) { bytes[i] = bin.charCodeAt(i); }\n' +
' return bytes;\n' +
'}\n' +
'(function()\n' +
'{\n' +
' if (typeof AvoidLib === "undefined") { console.error("[libavoid] AvoidLib global missing"); return; }\n' +
' try { globalThis.__LIBAVOID_WASM_BINARY = __libavoidB64ToBytes(window.__LIBAVOID_WASM_B64); }\n' +
' catch (e) { console.error("[libavoid] base64 decode failed:", e); return; }\n' +
' window.__libavoidReady = AvoidLib.load().then(function()\n' +
' {\n' +
' window.Avoid = AvoidLib.getInstance();\n' +
' return window.Avoid;\n' +
' }).catch(function(e)\n' +
' {\n' +
' console.error("[libavoid] wasm load failed; routing disabled:", (e && e.message), e);\n' +
' return null;\n' +
' });\n' +
'})();\n';
// libavoid-js (obstacle-avoiding orthogonal edge router). libavoid.min.js
// is the pure-JS Emscripten build from drawio-dev js/libavoid-js/ — a
// self-contained classic script (no WASM, no fetch) that publishes
// globalThis.Avoid and parks window.__libavoidReady (an already-resolved
// promise: the Avoid namespace, or null when init failed) synchronously on
// execution. The routing pass awaits __libavoidReady on demand; failures
// degrade gracefully — applyRouting just skips and the diagram renders
// unrouted.
//
// ETag-versioned URLs from the Node server (options.libavoidUrls, see
// libavoid-versions.js) bust the CDN's 30-day browser cache exactly when a
// draw.io release changes a file. The plain-URL default below applies only
// when the option is omitted — the Worker build (build-html.js) and tests;
// the Node server always passes the option (a failed version check
// surfaces as plain URLs from libavoid-versions.js, not from this array).
// Fixed order: glue -> wasm payload -> loader -> routing core.
// Fixed order: pure-JS router bundle -> routing core.
var libavoidSrcs = (options && options.libavoidUrls) || [
'https://viewer.diagrams.net/js/libavoid-js/libavoid.min.js',
'https://viewer.diagrams.net/js/libavoid-js/libavoid-wasm.js',
'https://viewer.diagrams.net/js/libavoid-js/libavoid-loader.js',
'https://viewer.diagrams.net/js/libavoid-js/libavoid-routing.js'
];
var libavoidBlock = (libavoidJs && libavoidWasmB64)
? '<!-- libavoid-js (inlined WASM edge router). Defines globalThis.AvoidLib; the loader feeds the base64 wasm in as wasmBinary (no fetch). Powers the routing:"libavoid" pass. -->\n' +
' <script>' + libavoidJs + '</script>\n' +
' <script>\n' +
' window.__LIBAVOID_WASM_B64 = "' + libavoidWasmB64 + '";\n' +
' ' + libavoidLoader +
' </script>'
: '<!-- libavoid-js (WASM edge router) + shared routing core from the viewer.diagrams.net CDN,\n' +
' like drawio-elk/drawio-mermaid: cached cross-session, version-synced with each draw.io\n' +
' release, and byte-identical to what the draw.io editor bundles. The wasm rides as\n' +
' base64 inside libavoid-wasm.js; libavoid-loader.js decodes it and parks\n' +
' window.__libavoidReady — still no fetch, so the sandbox CSP is satisfied by plain\n' +
' script-src. Fixed order: glue -> wasm payload -> loader -> routing core. -->\n' +
var libavoidBlock = libavoidJs
? '<!-- libavoid-js (inlined local build: pure-JS router bundle + routing core). Publishes\n' +
' globalThis.Avoid and parks window.__libavoidReady synchronously. Powers the\n' +
' routing:"libavoid" pass. -->\n' +
' <script>' + libavoidJs + '</script>'
: '<!-- libavoid-js (pure-JS edge router bundle) + shared routing core from the viewer.diagrams.net\n' +
' CDN, like drawio-elk/drawio-mermaid: cached cross-session, version-synced with each\n' +
' draw.io release, and byte-identical to what the draw.io editor bundles. The bundle\n' +
' publishes globalThis.Avoid and parks window.__libavoidReady synchronously — no WASM,\n' +
' no fetch, so the sandbox CSP is satisfied by plain script-src. -->\n' +
libavoidSrcs.map(function(u)
{
return ' <script src="' + u + '"></script>';
@@ -1900,7 +1869,7 @@ function routeWithLibavoid(graph, Avoid)
}
/**
* Async wrapper around routeWithLibavoid: await wasm readiness, then route.
* Async wrapper around routeWithLibavoid: await router readiness, then route.
* Degrades gracefully — calls onDone(false) (diagram stays unrouted) when
* libavoid isn't present, failed to load, or routing throws.
*
@@ -4451,7 +4420,7 @@ function finalizeStreamingView(xml, opts)
// Routing without ELK: the author's (LLM's) vertex positions are kept;
// libavoid only computes obstacle-avoiding paths for the edges. The
// normal fit above already ran (no postLayout), and routing doesn't move
// vertices, so no extra camera move is needed. applyRouting awaits wasm
// vertices, so no extra camera move is needed. applyRouting awaits router
// readiness internally, so no rAF gate here.
runRoutingPass(streamGraph);
}
@@ -6401,39 +6370,6 @@ export function processElkBundle(raw)
"drawio-elk.min.js");
}
/**
* Process the libavoid-js browser bundle (obstacle-avoiding orthogonal edge
* routing the `routing: "libavoid"` pass). Ships as ESM ending in
* `export{X as AvoidLib}` and uses `import.meta.url` (illegal in a classic
* <script>). Three transforms before the standard strip-and-alias:
*
* 1. Neutralize `import.meta.url` "". It is only used by the Emscripten
* module to locate the .wasm via fetch which we bypass entirely.
* 2. Patch the loader so the Emscripten factory receives `wasmBinary`
* (a Uint8Array set on globalThis). The sandboxed iframe has no
* allow-same-origin and the host CSP's connect-src forbids data: URIs,
* so we hand the wasm bytes in directly instead of fetching them.
* 3. Strip the ESM export and alias `globalThis.AvoidLib`.
*/
export function processLibavoidBundle(raw)
{
raw = raw.replace(/import\.meta\.url/g, '""');
// `this.avoidLib=await le({locateFile:ce})` →
// `... await le({locateFile:ce,wasmBinary:(globalThis.__LIBAVOID_WASM_BINARY||void 0)})`
var patched = raw.replace(
/(await\s+\w+\(\{\s*locateFile\s*:\s*\w+)\s*\}\)/,
"$1,wasmBinary:(globalThis.__LIBAVOID_WASM_BINARY||void 0)})"
);
if (patched === raw)
{
throw new Error("Could not patch libavoid loader to accept wasmBinary (loader signature changed?)");
}
return stripEsmExportsAndAlias(patched, { AvoidLib: "AvoidLib" }, "libavoid.min.js");
}
// ── Diagram validation ───────────────────────────────────────────────────────
/**
+22 -22
View File
@@ -1,32 +1,34 @@
# libavoid (app server) — loaded from the CDN
Obstacle-avoiding orthogonal **edge routing** (the `routing: "libavoid"`
pass). Since the draw.io release that ships `js/libavoid-js/` on
`viewer.diagrams.net`, the viewer HTML loads libavoid from the CDN like
drawio-elk and drawio-mermaid — nothing is vendored here anymore:
pass). Since the draw.io release that ships the pure-JS two-file
`js/libavoid-js/` layout on `viewer.diagrams.net`, the viewer HTML loads
libavoid from the CDN like drawio-elk and drawio-mermaid — nothing is
vendored here anymore:
```html
<script src="https://viewer.diagrams.net/js/libavoid-js/libavoid.min.js"></script>
<script src="https://viewer.diagrams.net/js/libavoid-js/libavoid-wasm.js"></script>
<script src="https://viewer.diagrams.net/js/libavoid-js/libavoid-loader.js"></script>
<script src="https://viewer.diagrams.net/js/libavoid-js/libavoid-routing.js"></script>
```
(glue → base64 wasm payload → loader → shared routing core; see
`buildHtml`'s `libavoidBlock` in `src/shared.js`). The wasm rides as base64
inside `libavoid-wasm.js` and is decoded by the loader — still **no
`fetch`**, so the sandboxed iframe (no `allow-same-origin`, no `data:` in
`connect-src`) is satisfied by plain `script-src`. The loader parks
`window.__libavoidReady` (resolves to the `Avoid` namespace, or `null` on
failure); the routing core defines `globalThis.AvoidRouting` — the same
canonical artifact the draw.io editor bundles into `extensions.min.js` and
the mcp-tool-server vendors (`mcp-tool-server/vendor/libavoid/`, which stays
vendored: it runs server-side in Node and ships in the npm package).
(pure-JS router bundle → shared routing core; see `buildHtml`'s
`libavoidBlock` in `src/shared.js`, ETag-versioned URLs from
`src/libavoid-versions.js`). `libavoid.min.js` is the pure-JS Emscripten
build — a self-contained classic script, **no WASM and no `fetch`**, so the
sandboxed iframe (no `allow-same-origin`, no `data:` in `connect-src`) is
satisfied by plain `script-src` — no `'wasm-unsafe-eval'` needed. On
execution it publishes `globalThis.Avoid` and synchronously parks
`window.__libavoidReady` (an already-resolved promise: the `Avoid`
namespace, or `null` on an init failure); the routing core defines
`globalThis.AvoidRouting` — the same canonical `drawio-dev js/libavoid-js/`
artifact the draw.io editor bundles and the mcp-tool-server vendors
(`mcp-tool-server/vendor/libavoid/`, which stays vendored: it runs
server-side in Node and ships in the npm package).
`buildHtml` still supports inlining a local build instead (pass
`options.libavoidJs`a `processLibavoidBundle`-processed glue with
`libavoid-routing.js` appended — plus `options.libavoidWasmB64`), e.g. for
testing unreleased libavoid changes.
`options.libavoidJs`the drawio-dev `libavoid.min.js` artifact as-is, with
`libavoid-routing.js` appended), e.g. for testing unreleased libavoid
changes.
Remaining files:
@@ -37,7 +39,5 @@ Remaining files:
- `LICENSE` — libavoid-js is LGPL-2.1-or-later (kept for reference; the
binaries are served by the CDN, not shipped from this repo).
> ⚠️ WebAssembly must instantiate inside the Claude.ai MCP-app iframe,
> which requires the host CSP to allow wasm compilation
> (`'wasm-unsafe-eval'`) and `viewer.diagrams.net` in `script-src` (the
> same allowance drawio-elk/drawio-mermaid already rely on).
> ⚠️ The host CSP must allow `viewer.diagrams.net` in `script-src` — the
> same allowance drawio-elk/drawio-mermaid already rely on.
+7 -5
View File
@@ -3,9 +3,10 @@
Obstacle-avoiding orthogonal **edge routing** for the `routing: "libavoid"`
option on `open_drawio_xml`. Vendored from [`libavoid-js`](https://github.com/Aksem/libavoid-js).
Unlike the app server (which inlines the *browser* build + base64 wasm into a
sandboxed iframe), the tool server runs in plain Node, so we vendor the **node
build** and let it read the wasm from disk — no patching, no base64.
Unlike the app server (which loads the pure-JS *browser* bundle from the
viewer.diagrams.net CDN into a sandboxed iframe), the tool server runs in
plain Node, so we vendor the **node build** and let it read the wasm from
disk — no patching, no base64.
Artifacts:
@@ -15,8 +16,9 @@ Artifacts:
- `libavoid.wasm` — the Emscripten binary (~492 KB). Loaded by path:
`await AvoidLib.load(join(__dirname, "vendor/libavoid/libavoid.wasm"))`.
- `libavoid-routing.js` — the shared routing core (`globalThis.AvoidRouting`:
`computeRoutes` incl. fixed-connection-point pins and jettySize stub
checkpoints, plus the pure geometry helpers). **Verbatim copy** — the
`computeRoutes` incl. fixed-connection-point pins, jetty checkpoints, and
dangling-end free-point routing, plus the pure geometry helpers).
**Verbatim copy** — the
canonical source is `drawio-dev src/main/webapp/js/libavoid-js/
libavoid-routing.js` (the same artifact the draw.io editor bundles and the
app server loads from the CDN); copy it over when it changes there. At
+50 -15
View File
@@ -1,7 +1,8 @@
/*
* libavoid-routing.js — the draw.io routing core for the vendored libavoid-js
* WASM router. Hand-authored (NOT regenerated by ant vendor-libavoid, like
* libavoid-loader.js); it never changes with the upstream version.
* libavoid-routing.js — the draw.io routing core over the libavoid solver.
* Hand-authored; engine-agnostic (takes the `Avoid` namespace as a parameter,
* so it works whether the host loads libavoid as WASM or pure JS) and never
* changes with the libavoid build.
*
* CANONICAL SOURCE: this file (drawio-dev js/libavoid-js/). drawio-mcp vendors
* verbatim copies (mcp-app-server/vendor/libavoid/ and
@@ -216,6 +217,12 @@
*/
AvoidRouting.cappedJetty = function(jetty, constraint, from, to)
{
// A dangling (free-point) end has no bounds to measure the gap against.
if (from == null || to == null)
{
return jetty;
}
var DIR = AvoidRouting.DIR;
var d = (constraint != null) ? constraint.dir : 0;
var dgap = (d == DIR.down) ? to.y - (from.y + from.h) :
@@ -323,11 +330,17 @@
* @param {Array<{id:string,x:number,y:number,w:number,h:number}>} vertices
* Obstacles, in ABSOLUTE coordinates. Shapes enclosing a terminal
* of a routed edge are dropped (filterEnclosing).
* @param {Array<{id,source,target,sourceConstraint?,targetConstraint?,
* @param {Array<{id,source,target,sourcePoint?,targetPoint?,
* sourceConstraint?,targetConstraint?,
* sourcePoints?,targetPoints?,sourceSides?,targetSides?,
* sourceJetty?,targetJetty?}>} edges
* Edges referencing vertex ids (endpoints not both known vertices
* are skipped). *Constraint = {x,y,dir} (constraintForPoint): a
* Edges referencing vertex ids. An end may instead be a DANGLING free
* point — sourcePoint/targetPoint {x,y} in absolute coords, used when
* that terminal has no vertex (an unconnected endpoint): it routes to
* a plain Point ConnEnd (no shape, no direction), matching the
* drag-preview, and takes no constraint/pin/mask/jetty. An edge is
* skipped only when an end is neither a vertex nor a free point.
* *Constraint = {x,y,dir} (constraintForPoint): a
* fixed connection point routed via a directed ShapeConnectionPin;
* absent => the endpoint floats at the shape centre. *Points = an
* array of {x,y,dir} candidate anchors (draw.io's snapToPoint —
@@ -423,8 +436,20 @@
// constraint (pinned exit/entry) wins over both, and a snap-point set
// wins over a mask (the snapToPoint branch of
// updateFloatingTerminalPoint runs before any mask enforcement).
function makeEnd(vid, b, constraint, sides, points)
function makeEnd(vid, b, constraint, sides, points, freePoint)
{
// Dangling end: no vertex bounds, just a free point (the edge's
// unconnected endpoint). Route to a plain Point ConnEnd — no shape,
// no direction — exactly as the warm-session drag preview does, so a
// committed edge with an unconnected end matches its live preview.
if (b == null)
{
var dp = new Avoid.Point(freePoint.x, freePoint.y);
var dce = new Avoid.ConnEnd(dp);
dp.delete();
return dce;
}
if (constraint != null && constraint.dir != null && shapeRefs[vid] != null)
{
pinClass++;
@@ -481,10 +506,16 @@
return ce;
}
// Endpoint anchor: the fixed connection point, else the shape centre —
// the same point the ConnEnd routes from.
function anchor(b, constraint)
// Endpoint anchor: the free point for a dangling end, else the fixed
// connection point, else the shape centre — the point the ConnEnd routes
// from.
function anchor(b, constraint, freePoint)
{
if (b == null)
{
return freePoint;
}
return (constraint != null) ?
{x: b.x + constraint.x * b.w, y: b.y + constraint.y * b.h} :
{x: b.x + b.w / 2, y: b.y + b.h / 2};
@@ -518,15 +549,19 @@
var sb = bounds[e.source];
var tb = bounds[e.target];
if (sb == null || tb == null)
// An end is either a known vertex (sb/tb) or a dangling free point
// (e.sourcePoint / e.targetPoint, absolute coords) for an unconnected
// endpoint. Skip only when an end is neither.
if ((sb == null && e.sourcePoint == null) ||
(tb == null && e.targetPoint == null))
{
continue;
}
// ConnRef copies the ConnEnds, so free them after construction
// (they are not owned by the router).
var se = makeEnd(e.source, sb, e.sourceConstraint, e.sourceSides, e.sourcePoints);
var de = makeEnd(e.target, tb, e.targetConstraint, e.targetSides, e.targetPoints);
var se = makeEnd(e.source, sb, e.sourceConstraint, e.sourceSides, e.sourcePoints, e.sourcePoint);
var de = makeEnd(e.target, tb, e.targetConstraint, e.targetSides, e.targetPoints, e.targetPoint);
var conn = new Avoid.ConnRef(router, se, de);
se.delete();
de.delete();
@@ -543,8 +578,8 @@
// after the first solve (see below).
var sourceJetty = AvoidRouting.cappedJetty(e.sourceJetty, e.sourceConstraint, sb, tb);
var targetJetty = AvoidRouting.cappedJetty(e.targetJetty, e.targetConstraint, tb, sb);
var sa = anchor(sb, e.sourceConstraint);
var ta = anchor(tb, e.targetConstraint);
var sa = anchor(sb, e.sourceConstraint, e.sourcePoint);
var ta = anchor(tb, e.targetConstraint, e.targetPoint);
// A pinned anchor buried inside ANOTHER obstacle (a shape dragged
// over the terminal) puts the whole route into libavoid's escape /