Plan-graph round 12 (owner): docking gap + straight shaft stub
Two edge-drawing refinements (owner), both pure post-processing in edgePathD on dagre's waypoints: - Docking gap (4 units ≈ half a head length): the head's tip floats short of the target face instead of touching it — the mouth pulls back by tip overhang + gap, so the head kisses nothing and tilted entries stop biting into the box. - Shaft stub (12 units ≈ 1.5 head lengths): from Q, every control point sits exactly on the approach line, so the basis spline renders that stretch perfectly straight and the head lands on a straight shaft; the corner where the route squares up is rounded by the curve, and the repeated mouth point makes the end tangent exactly the approach direction. Collinear routes render identically (pinned: on-axis start to mouth). Angle alignment/perpendicular entry deliberately NOT touched yet — owner wants to evaluate this first (round 13 candidate).
This commit is contained in:
@@ -213,6 +213,16 @@ const HEAD_W = 50.2 * HEAD_SCALE;
|
||||
* overhangs the line's end by this much, so edges shorten by it to land
|
||||
* the tip exactly on the target node's border. */
|
||||
const HEAD_TIP_OVERHANG = (50.2 - 27.2) * HEAD_SCALE;
|
||||
/** The head floats this far off the target face — the tip kisses nothing
|
||||
* (owner, round 12). ~half a head length: clearly docked-off, still
|
||||
* visually attached. */
|
||||
const DOCK_GAP = 4;
|
||||
/** The shaft stub — the straight run before the head (owner, round 12):
|
||||
* control points from Q on sit exactly on the approach line, so the
|
||||
* basis spline renders that stretch perfectly straight and the head
|
||||
* lands on a straight shaft; the corner where the route squares up onto
|
||||
* the line is rounded by the curve. ~1.5 head lengths of shaft. */
|
||||
const STRAIGHT_RUN = 12;
|
||||
const edgeLine = line<{ x: number; y: number }>()
|
||||
.x((p) => p.x)
|
||||
.y((p) => p.y)
|
||||
@@ -221,9 +231,10 @@ const edgeLine = line<{ x: number; y: number }>()
|
||||
/** The edge path — mermaid's own line drawing: dagre's border-intersected
|
||||
* waypoints through d3's curveBasis (mermaid's default flowchart curve —
|
||||
* the same recipe dagre-d3's renderer uses). The final point pulls back
|
||||
* along the last segment by the tip overhang so the head's TIP lands on
|
||||
* the target border while the mouth sits at the path's end. Pure,
|
||||
* exported for tests. */
|
||||
* along the last segment by the tip overhang + the docking gap so the
|
||||
* head's TIP floats DOCK_GAP short of the target border with its mouth
|
||||
* at the path's end, on a straight run (the shaft stub). Straight
|
||||
* collinear routes render identically. Pure, exported for tests. */
|
||||
export function edgePathD(points: Array<{ x: number; y: number }>): string {
|
||||
const pts = points.map((p) => ({ ...p }));
|
||||
if (pts.length >= 2) {
|
||||
@@ -232,8 +243,18 @@ export function edgePathD(points: Array<{ x: number; y: number }>): string {
|
||||
const dx = last.x - prev.x;
|
||||
const dy = last.y - prev.y;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
const t = Math.min(HEAD_TIP_OVERHANG / len, 0.5);
|
||||
pts[pts.length - 1] = { x: last.x - dx * t, y: last.y - dy * t };
|
||||
const ux = dx / len;
|
||||
const uy = dy / len;
|
||||
// The mouth: tip overhang + the docking gap back from the border.
|
||||
const back = HEAD_TIP_OVERHANG + DOCK_GAP;
|
||||
const M = { x: last.x - ux * back, y: last.y - uy * back };
|
||||
// From Q every control point lies on the approach line — the basis
|
||||
// spline renders Q→M perfectly straight (a B-spline span with
|
||||
// collinear control points IS the line), and the repeated M makes
|
||||
// the end tangent exactly the approach direction, so the head
|
||||
// squares onto its shaft.
|
||||
const Q = { x: M.x - ux * STRAIGHT_RUN, y: M.y - uy * STRAIGHT_RUN };
|
||||
pts.splice(pts.length - 1, 1, Q, M, M);
|
||||
}
|
||||
return edgeLine(pts) ?? "";
|
||||
}
|
||||
|
||||
@@ -61,12 +61,17 @@ describe("plan block layout (pure)", () => {
|
||||
|
||||
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).
|
||||
// basis curve starts exactly at the border point; the END pulls
|
||||
// back by HEAD_TIP_OVERHANG + the docking gap (92 − 3.538 − 4) so the
|
||||
// head's TIP floats 4 short of the border with its mouth at the
|
||||
// path's end, on a straight shaft (owner, rounds 11–12).
|
||||
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");
|
||||
assert.ok(d.endsWith("L80,84.462"), "mouth pulled back; tip floats 4 off the border");
|
||||
// The whole run stays on-axis (every x is 80): a collinear route
|
||||
// renders straight — the shaft stub never bends a straight edge.
|
||||
const xs = [...d.matchAll(/(?:[MLC]|,)(-?[\d.]+),/g)].map((m) => m[1]);
|
||||
assert.ok(xs.length > 0 && xs.every((v) => v === "80"), "collinear route renders on-axis");
|
||||
});
|
||||
|
||||
it("wrapTitle wraps greedily and NEVER drops content (boxes grow instead)", () => {
|
||||
|
||||
Reference in New Issue
Block a user