@@ -87,7 +87,7 @@ import { getPortPosition, calculateStaggeredPosition } from './utils/canvas/port
8787import { computeCleanPolylineFromPorts, generateManhattanRoutingPath, generateCleanRoutingPath } from './utils/canvas/edgeRouting.js';
8888import * as GeometryUtils from './utils/canvas/geometryUtils.js';
8989import EdgeRenderer from './components/EdgeRenderer.jsx';
90- import { calculateParallelEdgePath, distanceToQuadraticBezier, calculateCurveControlPoint, getTrimmedBezierPath } from './utils/canvas/parallelEdgeUtils.js';
90+ import { calculateParallelEdgePath, distanceToQuadraticBezier, calculateCurveControlPoint, getTrimmedBezierPath, getPointOnQuadraticBezier } from './utils/canvas/parallelEdgeUtils.js';
9191import Panel from './Panel'; // This is now used for both sides
9292import TypeList from './TypeList'; // Re-add TypeList component
9393import SaveStatusDisplay from './SaveStatusDisplay'; // Import the save status display
@@ -10517,13 +10517,13 @@ function NodeCanvas() {
1051710517 const isCurvedEdge = curveInfo && curveInfo.totalInPair > 1;
1051810518
1051910519 // Only shorten connections at ends with arrows or hover state
10520- // For curved edges, never shorten for hover - only for arrows
10521- // This ensures the curve shape stays consistent when hovered
10520+ // For curved edges, NEVER change endpoints - we use trimmed paths instead
10521+ // This ensures the curve shape stays consistent
1052210522 let shouldShortenSource = isCurvedEdge
10523- ? arrowsToward.has(sourceNode.id)
10523+ ? false // Never change curve endpoints
1052410524 : (isHovered || arrowsToward.has(sourceNode.id));
1052510525 let shouldShortenDest = isCurvedEdge
10526- ? arrowsToward.has(destNode.id)
10526+ ? false // Never change curve endpoints
1052710527 : (isHovered || arrowsToward.has(destNode.id));
1052810528 if (enableAutoRouting && routingStyle === 'manhattan') {
1052910529 // In Manhattan mode, never shorten for hover—only for actual arrows
@@ -10699,10 +10699,12 @@ function NodeCanvas() {
1069910699 const parallelPath = calculateParallelEdgePath(startX, startY, endX, endY, curveInfo);
1070010700 const useCurve = parallelPath.type === 'curve';
1070110701
10702- // For hover effect on curved edges, trim the curve to create "shorten" visual
10702+ // For hover effect or arrows on curved edges, trim the curve to create "shorten" visual
1070310703 // This keeps the curve shape consistent but renders a shorter portion
1070410704 let trimmedPath = null;
10705- if (useCurve && isHovered && parallelPath.ctrlX !== null) {
10705+ const shouldTrimCurve = useCurve && parallelPath.ctrlX !== null &&
10706+ (isHovered || arrowsToward.has(sourceNode.id) || arrowsToward.has(destNode.id));
10707+ if (shouldTrimCurve) {
1070610708 trimmedPath = getTrimmedBezierPath(
1070710709 parallelPath.startX, parallelPath.startY,
1070810710 parallelPath.ctrlX, parallelPath.ctrlY,
@@ -11187,7 +11189,57 @@ function NodeCanvas() {
1118711189 // Calculate arrow positions (use fallback if intersections fail)
1118811190 let sourceArrowX, sourceArrowY, destArrowX, destArrowY, sourceArrowAngle, destArrowAngle;
1118911191
11190- if (enableAutoRouting && routingStyle === 'clean') {
11192+ // For curved edges, calculate arrow/dot positions along the curve
11193+ if (useCurve && parallelPath.ctrlX !== null) {
11194+ const tSource = 0.08; // Position near source (8% along curve)
11195+ const tDest = 0.92; // Position near dest (92% along curve)
11196+
11197+ // Get positions along the curve
11198+ const sourcePoint = getPointOnQuadraticBezier(
11199+ tSource,
11200+ parallelPath.startX, parallelPath.startY,
11201+ parallelPath.ctrlX, parallelPath.ctrlY,
11202+ parallelPath.endX, parallelPath.endY
11203+ );
11204+ const destPoint = getPointOnQuadraticBezier(
11205+ tDest,
11206+ parallelPath.startX, parallelPath.startY,
11207+ parallelPath.ctrlX, parallelPath.ctrlY,
11208+ parallelPath.endX, parallelPath.endY
11209+ );
11210+
11211+ sourceArrowX = sourcePoint.x;
11212+ sourceArrowY = sourcePoint.y;
11213+ destArrowX = destPoint.x;
11214+ destArrowY = destPoint.y;
11215+
11216+ // Calculate tangent angles at these points
11217+ // Derivative of quadratic Bézier: B'(t) = 2(1-t)(P1-P0) + 2t(P2-P1)
11218+ const calcTangentAngle = (t, x0, y0, cx, cy, x1, y1) => {
11219+ const invT = 1 - t;
11220+ const tangentX = 2 * invT * (cx - x0) + 2 * t * (x1 - cx);
11221+ const tangentY = 2 * invT * (cy - y0) + 2 * t * (y1 - cy);
11222+ return Math.atan2(tangentY, tangentX) * (180 / Math.PI);
11223+ };
11224+
11225+ // Source arrow points backward (toward source node)
11226+ const sourceTangent = calcTangentAngle(
11227+ tSource,
11228+ parallelPath.startX, parallelPath.startY,
11229+ parallelPath.ctrlX, parallelPath.ctrlY,
11230+ parallelPath.endX, parallelPath.endY
11231+ );
11232+ sourceArrowAngle = sourceTangent + 180; // Point back toward source
11233+
11234+ // Dest arrow points forward (toward dest node)
11235+ const destTangent = calcTangentAngle(
11236+ tDest,
11237+ parallelPath.startX, parallelPath.startY,
11238+ parallelPath.ctrlX, parallelPath.ctrlY,
11239+ parallelPath.endX, parallelPath.endY
11240+ );
11241+ destArrowAngle = destTangent; // Point toward dest
11242+ } else if (enableAutoRouting && routingStyle === 'clean') {
1119111243 // Clean mode: use actual port assignments for proper arrow positioning
1119211244 const offset = showConnectionNames ? 6 : (shouldShortenSource || shouldShortenDest ? 3 : 5);
1119311245 const portAssignment = cleanLaneOffsets.get(edge.id);
@@ -11515,8 +11567,8 @@ function NodeCanvas() {
1151511567 </g>
1151611568 )}
1151711569
11518- {/* Hover Dots - only visible when hovering and using straight routing */}
11519- {isHovered && (!enableAutoRouting || routingStyle === 'straight') && (
11570+ {/* Hover Dots - visible when hovering straight edges or curved parallel edges */}
11571+ {isHovered && (!enableAutoRouting || routingStyle === 'straight' || useCurve ) && (
1152011572 <>
1152111573 {/* Source Dot - only show if arrow not pointing toward source */}
1152211574 {!arrowsToward.has(sourceNode.id) && (
@@ -11677,13 +11729,13 @@ function NodeCanvas() {
1167711729 const isCurvedEdge = curveInfo && curveInfo.totalInPair > 1;
1167811730
1167911731 // Only shorten connections at ends with arrows or hover state
11680- // For curved edges, never shorten for hover - only for arrows
11681- // This ensures the curve shape stays consistent when hovered
11732+ // For curved edges, NEVER change endpoints - we use trimmed paths instead
11733+ // This ensures the curve shape stays consistent
1168211734 let shouldShortenSource = isCurvedEdge
11683- ? arrowsToward.has(sourceNode.id)
11735+ ? false // Never change curve endpoints
1168411736 : (isHovered || arrowsToward.has(sourceNode.id));
1168511737 let shouldShortenDest = isCurvedEdge
11686- ? arrowsToward.has(destNode.id)
11738+ ? false // Never change curve endpoints
1168711739 : (isHovered || arrowsToward.has(destNode.id));
1168811740 if (enableAutoRouting && routingStyle === 'manhattan') {
1168911741 // In Manhattan mode, never shorten for hover—only for actual arrows
@@ -11859,10 +11911,12 @@ function NodeCanvas() {
1185911911 const parallelPath = calculateParallelEdgePath(startX, startY, endX, endY, curveInfo);
1186011912 const useCurve = parallelPath.type === 'curve';
1186111913
11862- // For hover effect on curved edges, trim the curve to create "shorten" visual
11914+ // For hover effect or arrows on curved edges, trim the curve to create "shorten" visual
1186311915 // This keeps the curve shape consistent but renders a shorter portion
1186411916 let trimmedPath = null;
11865- if (useCurve && isHovered && parallelPath.ctrlX !== null) {
11917+ const shouldTrimCurve = useCurve && parallelPath.ctrlX !== null &&
11918+ (isHovered || arrowsToward.has(sourceNode.id) || arrowsToward.has(destNode.id));
11919+ if (shouldTrimCurve) {
1186611920 trimmedPath = getTrimmedBezierPath(
1186711921 parallelPath.startX, parallelPath.startY,
1186811922 parallelPath.ctrlX, parallelPath.ctrlY,
@@ -12212,7 +12266,57 @@ function NodeCanvas() {
1221212266 // Calculate arrow positions (use fallback if intersections fail)
1221312267 let sourceArrowX, sourceArrowY, destArrowX, destArrowY, sourceArrowAngle, destArrowAngle;
1221412268
12215- if (enableAutoRouting && routingStyle === 'clean') {
12269+ // For curved edges, calculate arrow/dot positions along the curve
12270+ if (useCurve && parallelPath.ctrlX !== null) {
12271+ const tSource = 0.08; // Position near source (8% along curve)
12272+ const tDest = 0.92; // Position near dest (92% along curve)
12273+
12274+ // Get positions along the curve
12275+ const sourcePoint = getPointOnQuadraticBezier(
12276+ tSource,
12277+ parallelPath.startX, parallelPath.startY,
12278+ parallelPath.ctrlX, parallelPath.ctrlY,
12279+ parallelPath.endX, parallelPath.endY
12280+ );
12281+ const destPoint = getPointOnQuadraticBezier(
12282+ tDest,
12283+ parallelPath.startX, parallelPath.startY,
12284+ parallelPath.ctrlX, parallelPath.ctrlY,
12285+ parallelPath.endX, parallelPath.endY
12286+ );
12287+
12288+ sourceArrowX = sourcePoint.x;
12289+ sourceArrowY = sourcePoint.y;
12290+ destArrowX = destPoint.x;
12291+ destArrowY = destPoint.y;
12292+
12293+ // Calculate tangent angles at these points
12294+ // Derivative of quadratic Bézier: B'(t) = 2(1-t)(P1-P0) + 2t(P2-P1)
12295+ const calcTangentAngle = (t, x0, y0, cx, cy, x1, y1) => {
12296+ const invT = 1 - t;
12297+ const tangentX = 2 * invT * (cx - x0) + 2 * t * (x1 - cx);
12298+ const tangentY = 2 * invT * (cy - y0) + 2 * t * (y1 - cy);
12299+ return Math.atan2(tangentY, tangentX) * (180 / Math.PI);
12300+ };
12301+
12302+ // Source arrow points backward (toward source node)
12303+ const sourceTangent = calcTangentAngle(
12304+ tSource,
12305+ parallelPath.startX, parallelPath.startY,
12306+ parallelPath.ctrlX, parallelPath.ctrlY,
12307+ parallelPath.endX, parallelPath.endY
12308+ );
12309+ sourceArrowAngle = sourceTangent + 180; // Point back toward source
12310+
12311+ // Dest arrow points forward (toward dest node)
12312+ const destTangent = calcTangentAngle(
12313+ tDest,
12314+ parallelPath.startX, parallelPath.startY,
12315+ parallelPath.ctrlX, parallelPath.ctrlY,
12316+ parallelPath.endX, parallelPath.endY
12317+ );
12318+ destArrowAngle = destTangent; // Point toward dest
12319+ } else if (enableAutoRouting && routingStyle === 'clean') {
1221612320 // Clean mode: use actual port assignments for proper arrow positioning
1221712321 const offset = showConnectionNames ? 6 : (shouldShortenSource || shouldShortenDest ? 3 : 5);
1221812322 const portAssignment = cleanLaneOffsets.get(edge.id);
@@ -12540,8 +12644,8 @@ function NodeCanvas() {
1254012644 </g>
1254112645 )}
1254212646
12543- {/* Hover Dots - only visible when hovering and using straight routing */}
12544- {isHovered && (!enableAutoRouting || routingStyle === 'straight') && (
12647+ {/* Hover Dots - visible when hovering straight edges or curved parallel edges */}
12648+ {isHovered && (!enableAutoRouting || routingStyle === 'straight' || useCurve ) && (
1254512649 <>
1254612650 {/* Source Dot - only show if arrow not pointing toward source */}
1254712651 {!arrowsToward.has(sourceNode.id) && (
0 commit comments