mirror of
https://github.com/jgraph/drawio-mcp.git
synced 2026-09-14 15:42:06 +08:00
0d1148a326
Bring the obstacle-avoiding orthogonal edge routing (shipped for the app server's create_diagram) to the tool server's open_drawio_xml, via a server-side pass, and factor the libavoid-driving logic into one shared helper so the two servers can't drift. - shared/libavoid-routing.js: self-contained pure computeLibavoidRoutes (vertices + edges -> orthogonal obstacle-avoiding waypoints). The app server inlines it into the browser bundle via Function.toString(); the tool server imports it in Node. shared/package.json marks the dir ESM. - mcp-app-server: refactor routeWithLibavoid onto the shared helper (mxGraph model extract + parent-relative write-back stays; routing math is now shared). - mcp-tool-server: new src/libavoid-pass.js — parse the mxGraphModel XML, run the shared helper, write waypoints + orthogonalEdgeStyle back, and fail safe to the original XML on any problem. Add routing:"libavoid" to open_drawio_xml. Vendor the libavoid-js NODE build + wasm (loaded by path; Node needs no inlining/base64). copy-shared copies the helper into src/ (gitignored, with a ../../shared fallback for in-repo runs); vendor/ added to npm files. - docs: routing param + vendored libavoid in root / app-server / tool-server CLAUDE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
4.1 KiB
JavaScript
110 lines
4.1 KiB
JavaScript
// Shared libavoid edge-routing core, used by BOTH delivery mechanisms:
|
|
// - mcp-app-server: inlined into the browser viewer via Function.toString()
|
|
// and run client-side against the live mxGraph model.
|
|
// - mcp-tool-server: imported and run server-side (Node) over parsed XML.
|
|
//
|
|
// Because it is inlined via toString() it MUST be a single self-contained
|
|
// function: no module-scope references, no imports, and NO backticks or `${`
|
|
// (it ends up inside mcp-app-server's buildHtml template literal). Keep helpers
|
|
// nested. Each consumer does its own substrate-specific extract (vertices/edges
|
|
// in absolute coords) and inject (waypoints back as parent-relative points).
|
|
//
|
|
// libavoid API gotchas baked in here:
|
|
// - Router flag is an integer: RouterFlag.OrthogonalRouting.value
|
|
// - setRoutingParameter takes the enum OBJECT (RoutingParameter.x), not .value
|
|
// - cleanup is router.delete() (embind), not Avoid.destroy()
|
|
|
|
/**
|
|
* Compute obstacle-avoiding orthogonal routes for a set of edges.
|
|
*
|
|
* @param {object} Avoid - the libavoid instance (AvoidLib.getInstance()).
|
|
* @param {Array<{id:string,x:number,y:number,w:number,h:number}>} vertices
|
|
* Obstacles, in ABSOLUTE coordinates.
|
|
* @param {Array<{id:string,source:string,target:string}>} edges
|
|
* Edges referencing vertex ids. Edges whose endpoints aren't both known
|
|
* vertices are skipped.
|
|
* @param {{shapeBufferDistance?:number,idealNudgingDistance?:number}} [opts]
|
|
* @returns {Object<string, Array<{x:number,y:number}>>} edge id -> interior
|
|
* bend points (ABSOLUTE, collinear-filtered). The first/last route
|
|
* points (shape centers) are dropped — endpoints connect at the shape
|
|
* side midpoint, which is where a floating orthogonalEdgeStyle endpoint
|
|
* lands anyway. An edge with a straight (bend-free) route maps to [].
|
|
*/
|
|
export function computeLibavoidRoutes(Avoid, vertices, edges, opts)
|
|
{
|
|
var out = {};
|
|
|
|
if (Avoid == null || vertices == null || edges == null) return out;
|
|
|
|
var buffer = (opts && opts.shapeBufferDistance != null) ? opts.shapeBufferDistance : 16;
|
|
var nudge = (opts && opts.idealNudgingDistance != null) ? opts.idealNudgingDistance : 14;
|
|
|
|
function collinear(a, b, c)
|
|
{
|
|
// Zero cross product (1px tolerance) => b lies on segment a..c (redundant).
|
|
return Math.abs((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)) < 1;
|
|
}
|
|
|
|
var router = new Avoid.Router(Avoid.RouterFlag.OrthogonalRouting.value);
|
|
|
|
// setRoutingParameter wants the enum OBJECT, not its .value (passing the
|
|
// integer silently no-ops and routes run flush against the boxes).
|
|
try { router.setRoutingParameter(Avoid.RoutingParameter.shapeBufferDistance, buffer); } catch (e) {}
|
|
try { router.setRoutingParameter(Avoid.RoutingParameter.idealNudgingDistance, nudge); } catch (e) {}
|
|
|
|
var bounds = {};
|
|
var i;
|
|
|
|
for (i = 0; i < vertices.length; i++)
|
|
{
|
|
var v = vertices[i];
|
|
if (v == null || v.id == null || !(v.w > 0) || !(v.h > 0)) continue;
|
|
bounds[v.id] = v;
|
|
new Avoid.ShapeRef(router, new Avoid.Rectangle(
|
|
new Avoid.Point(v.x, v.y), new Avoid.Point(v.x + v.w, v.y + v.h)));
|
|
}
|
|
|
|
var conns = [];
|
|
|
|
for (i = 0; i < edges.length; i++)
|
|
{
|
|
var e = edges[i];
|
|
if (e == null) continue;
|
|
var sb = bounds[e.source];
|
|
var tb = bounds[e.target];
|
|
if (sb == null || tb == null) continue;
|
|
var conn = new Avoid.ConnRef(router,
|
|
new Avoid.ConnEnd(new Avoid.Point(sb.x + sb.w / 2, sb.y + sb.h / 2)),
|
|
new Avoid.ConnEnd(new Avoid.Point(tb.x + tb.w / 2, tb.y + tb.h / 2)));
|
|
conns.push({ id: e.id, conn: conn });
|
|
}
|
|
|
|
if (conns.length === 0) { router.delete(); return out; }
|
|
|
|
router.processTransaction();
|
|
|
|
for (i = 0; i < conns.length; i++)
|
|
{
|
|
var route = conns[i].conn.displayRoute();
|
|
var n = route.size();
|
|
var wps = [];
|
|
|
|
if (n >= 2)
|
|
{
|
|
var pts = [];
|
|
var k;
|
|
for (k = 0; k < n; k++) { var p = route.at(k); pts.push({ x: p.x, y: p.y }); }
|
|
for (k = 1; k < n - 1; k++)
|
|
{
|
|
if (collinear(pts[k - 1], pts[k], pts[k + 1])) continue;
|
|
wps.push({ x: Math.round(pts[k].x), y: Math.round(pts[k].y) });
|
|
}
|
|
}
|
|
|
|
out[conns[i].id] = wps;
|
|
}
|
|
|
|
router.delete();
|
|
return out;
|
|
}
|