Plan-graph round 11 (owner): mermaid's edge drawing — dagre routes, curveBasis draws

The old lines were our hand-rolled shortcut: every arrow from the
source's bottom-center to the target's top-center as a single S-bezier.
Dagre actually routes each edge as border-intersected waypoints that
leave/enter nodes on whatever side the route needs — mermaid's
behavior. We now keep that routing (owner: adopt it).

blockLayout also returns edgePts — dagre's per-edge waypoint lists.
edgePathD draws them through d3's curveBasis, the exact recipe
dagre-d3's own renderer uses (and mermaid's default flowchart curve):
endpoints repeat so the line starts at the source border and the END
pulls back by the tip overhang so the arrowhead's tip lands on the
target border with its mouth at the path's end. Pinned in tests.

Dangling/unroutable edges draw nothing. d3-shape added as a direct
dependency (^3.2.0, already in the tree via dagre-d3-es — lane touch,
flagged to master with dagre-d3-es).
This commit is contained in:
amosblomqvist
2026-09-14 07:38:13 +03:00
parent 90ed65f208
commit 0f6b2d5c1e
3 changed files with 62 additions and 10 deletions
+11
View File
@@ -6,6 +6,7 @@ import {
PlanPopup,
blockLayout,
topoFor,
edgePathD,
wrapTitle,
GAP_X,
NODE_H,
@@ -58,6 +59,16 @@ describe("plan block layout (pure)", () => {
assert.ok(layout.x.get("x") === 0 && layout.x.get("y") === NODE_W + GAP_X);
});
it("edgePathD draws dagre's route through curveBasis — mouth pulled back from the border", () => {
// Straight 3-point route (source border → mid → target border): the
// basis curve starts exactly at the border point and the END pulls
// back by HEAD_TIP_OVERHANG so the arrowhead's TIP lands on the
// border while its mouth sits at the path's end (owner, round 11).
const d = edgePathD([{ x: 80, y: 44 }, { x: 80, y: 68 }, { x: 80, y: 92 }]);
assert.ok(d.startsWith("M80,44"), "starts at the source border point");
assert.ok(d.endsWith("L80,88.462"), "ends pulled back by the tip overhang");
});
it("wrapTitle wraps greedily and NEVER drops content (boxes grow instead)", () => {
assert.deepEqual(wrapTitle("Chain rule intuition"), ["Chain rule intuition"]); // exactly 20 chars — fits one line
assert.deepEqual(wrapTitle("Chain rule intuition and fluency"), ["Chain rule intuition", "and fluency"]);