From 4322f3cf634bf89dd1fa78f253e675178c05bbc5 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Mon, 9 Mar 2026 15:38:14 +0100 Subject: [PATCH] Fix pill shape self-loop geometry to follow cap curvature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/utils/edgeUtils.ts | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/utils/edgeUtils.ts b/src/utils/edgeUtils.ts index c6f4535..110278d 100644 --- a/src/utils/edgeUtils.ts +++ b/src/utils/edgeUtils.ts @@ -574,25 +574,36 @@ function getSelfLoopParams( const capRadius = isHorizontal ? nodeHeight / 2 : nodeWidth / 2; if (isHorizontal) { - // Horizontal pill: source on top straight edge (left of corner), target on right cap (below corner) - sx = rightEdge - loopOffset * 0.8; + // The top-right corner is where the top straight edge meets the right semicircle cap. + // 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; sourceAngle = -Math.PI / 2; - const rightCapCenterX = nodePosition.x + nodeWidth - capRadius; - tx = rightCapCenterX + capRadius + 2; - ty = topEdge + loopOffset * 0.5; - targetAngle = 0; + // Angle on the right cap: -PI/2 = top of cap (corner), 0 = rightmost point. + // Innermost loop targets near the top; outer loops rotate toward the right. + const targetCapAngle = -Math.PI / 2 + Math.min(Math.PI / 2, (loopOffset / capRadius) * 0.8); + tx = rightCapCenterX + Math.cos(targetCapAngle) * (capRadius + 2); + ty = centerY + Math.sin(targetCapAngle) * (capRadius + 2); + targetAngle = targetCapAngle; } 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 sourceAngleRad = -Math.PI / 2 + (loopOffset / nodeWidth) * Math.PI / 4; - sx = centerX + Math.cos(sourceAngleRad) * (capRadius + 2); - sy = topCapCenterY + Math.sin(sourceAngleRad) * (capRadius + 2); - sourceAngle = sourceAngleRad; + + // Angle on the top cap: 0 = rightmost point of cap (corner), -PI/2 = top of cap. + // Innermost loop sources near the corner; outer loops rotate toward the top. + 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; - ty = topEdge + loopOffset * 0.5; + ty = topCapCenterY + loopOffset * 0.5; targetAngle = 0; } } else if (sourceShape === 'ellipse') {