Fix pill shape self-loop geometry to follow cap curvature
Some checks failed
CI / test (push) Has been cancelled

Previously the pill branches behaved like rectangles: the target point
was pinned to the rightmost cap point with a linearly growing y, and the
vertical pill source started from the wrong end of the cap.

- Horizontal pill: target now travels along the right cap surface via
  a cap angle (-PI/2 → 0), so both tx/ty stay on the curve
- Horizontal pill: source clamped to the straight top section so it
  never crosses into the left cap area
- Vertical pill: source now starts at the corner (angle=0, rightmost
  point of top cap) and rotates toward the top as loopOffset grows,
  matching the corner-anchoring used by other shapes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik 2026-03-09 15:38:14 +01:00
parent ed18b11dc9
commit 4322f3cf63

View file

@ -574,25 +574,36 @@ function getSelfLoopParams(
const capRadius = isHorizontal ? nodeHeight / 2 : nodeWidth / 2; const capRadius = isHorizontal ? nodeHeight / 2 : nodeWidth / 2;
if (isHorizontal) { if (isHorizontal) {
// Horizontal pill: source on top straight edge (left of corner), target on right cap (below corner) // The top-right corner is where the top straight edge meets the right semicircle cap.
sx = rightEdge - loopOffset * 0.8; // Source: spreads leftward along the top straight edge from the corner.
// Target: spreads downward along the right cap surface from the top of the cap.
const rightCapCenterX = nodePosition.x + nodeWidth - capRadius;
sx = Math.max(nodePosition.x + capRadius, rightCapCenterX - loopOffset * 0.6);
sy = topEdge; sy = topEdge;
sourceAngle = -Math.PI / 2; sourceAngle = -Math.PI / 2;
const rightCapCenterX = nodePosition.x + nodeWidth - capRadius; // Angle on the right cap: -PI/2 = top of cap (corner), 0 = rightmost point.
tx = rightCapCenterX + capRadius + 2; // Innermost loop targets near the top; outer loops rotate toward the right.
ty = topEdge + loopOffset * 0.5; const targetCapAngle = -Math.PI / 2 + Math.min(Math.PI / 2, (loopOffset / capRadius) * 0.8);
targetAngle = 0; tx = rightCapCenterX + Math.cos(targetCapAngle) * (capRadius + 2);
ty = centerY + Math.sin(targetCapAngle) * (capRadius + 2);
targetAngle = targetCapAngle;
} else { } else {
// Vertical pill: source on top cap (near right), target on right straight edge (near top) // The top-right corner is where the right straight edge meets the top semicircle cap.
// Source: spreads upward along the top cap from the corner.
// Target: spreads downward along the right straight edge from the corner.
const topCapCenterY = nodePosition.y + capRadius; const topCapCenterY = nodePosition.y + capRadius;
const sourceAngleRad = -Math.PI / 2 + (loopOffset / nodeWidth) * Math.PI / 4;
sx = centerX + Math.cos(sourceAngleRad) * (capRadius + 2); // Angle on the top cap: 0 = rightmost point of cap (corner), -PI/2 = top of cap.
sy = topCapCenterY + Math.sin(sourceAngleRad) * (capRadius + 2); // Innermost loop sources near the corner; outer loops rotate toward the top.
sourceAngle = sourceAngleRad; const sourceCapAngle = Math.max(-Math.PI / 2, -(loopOffset / capRadius) * 0.8);
sx = centerX + Math.cos(sourceCapAngle) * (capRadius + 2);
sy = topCapCenterY + Math.sin(sourceCapAngle) * (capRadius + 2);
sourceAngle = sourceCapAngle;
tx = rightEdge; tx = rightEdge;
ty = topEdge + loopOffset * 0.5; ty = topCapCenterY + loopOffset * 0.5;
targetAngle = 0; targetAngle = 0;
} }
} else if (sourceShape === 'ellipse') { } else if (sourceShape === 'ellipse') {