@@ -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 } from './utils/canvas/parallelEdgeUtils.js';
90+ import { calculateParallelEdgePath, distanceToQuadraticBezier, calculateCurveControlPoint, getTrimmedBezierPath } 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
@@ -10390,25 +10390,17 @@ function NodeCanvas() {
1039010390 nodeGroupMemberIds.has(e.sourceId) || nodeGroupMemberIds.has(e.destinationId)
1039110391 );
1039210392
10393- // Group edges by node pairs to calculate curve offsets for multiple edges between same nodes
10394- const edgePairGroups = new Map();
10395- visibleEdges.forEach(e => {
10396- const key = [e.sourceId, e.destinationId].sort().join('--');
10397- if (!edgePairGroups.has(key)) edgePairGroups.set(key, []);
10398- edgePairGroups.get(key).push(e.id);
10399- });
10393+ // edgeCurveInfo is computed via useMemo and available in scope
10394+ // (used for parallel edge curve offset calculation)
1040010395
10401- // Build a map of edge ID -> { pairIndex, totalInPair } for curve offset calculation
10402- const edgeCurveInfo = new Map();
10403- edgePairGroups.forEach((edgeIds, key) => {
10404- const total = edgeIds.length;
10405- edgeIds.forEach((edgeId, idx) => {
10406- edgeCurveInfo.set(edgeId, { pairIndex: idx, totalInPair: total });
10407- });
10396+ // #region agent log - build edgePairGroups locally just for debug logging
10397+ const edgePairGroupsDebug = new Map();
10398+ visibleEdges.forEach(e => {
10399+ const key = [e.sourceId, e.destinationId].sort().join('-');
10400+ if (!edgePairGroupsDebug.has(key)) edgePairGroupsDebug.set(key, []);
10401+ edgePairGroupsDebug.get(key).push(e.id);
1040810402 });
10409-
10410- // #region agent log
10411- const multiEdgePairs = Array.from(edgePairGroups.entries()).filter(([k, v]) => v.length > 1);
10403+ const multiEdgePairs = Array.from(edgePairGroupsDebug.entries()).filter(([k, v]) => v.length > 1);
1041210404 if (multiEdgePairs.length > 0) {
1041310405 debugLogSync('NodeCanvas.jsx:edgeRender', 'Edge rendering info', { totalEdges: visibleEdges.length, multiEdgePairs: multiEdgePairs.map(([k, v]) => ({ pair: k, edgeCount: v.length, edgeIds: v })), enableAutoRouting, routingStyle, willUseCurves: !(enableAutoRouting && (routingStyle === 'manhattan' || routingStyle === 'clean')) }, 'debug-session', 'D-E');
1041410406 }
@@ -10520,9 +10512,19 @@ function NodeCanvas() {
1052010512 ? edge.directionality.arrowsToward
1052110513 : new Set(Array.isArray(edge.directionality?.arrowsToward) ? edge.directionality.arrowsToward : []);
1052210514
10515+ // Check if this is a curved edge (parallel edge)
10516+ const curveInfo = edgeCurveInfo.get(edge.id);
10517+ const isCurvedEdge = curveInfo && curveInfo.totalInPair > 1;
10518+
1052310519 // Only shorten connections at ends with arrows or hover state
10524- let shouldShortenSource = isHovered || arrowsToward.has(sourceNode.id);
10525- let shouldShortenDest = isHovered || arrowsToward.has(destNode.id);
10520+ // For curved edges, never shorten for hover - only for arrows
10521+ // This ensures the curve shape stays consistent when hovered
10522+ let shouldShortenSource = isCurvedEdge
10523+ ? arrowsToward.has(sourceNode.id)
10524+ : (isHovered || arrowsToward.has(sourceNode.id));
10525+ let shouldShortenDest = isCurvedEdge
10526+ ? arrowsToward.has(destNode.id)
10527+ : (isHovered || arrowsToward.has(destNode.id));
1052610528 if (enableAutoRouting && routingStyle === 'manhattan') {
1052710529 // In Manhattan mode, never shorten for hover—only for actual arrows
1052810530 shouldShortenSource = arrowsToward.has(sourceNode.id);
@@ -10693,10 +10695,22 @@ function NodeCanvas() {
1069310695 }
1069410696
1069510697 // Calculate parallel edge path using centralized utility
10696- const curveInfo = edgeCurveInfo.get(edge.id);
10698+ // Note: curveInfo was already retrieved earlier for shouldShorten logic
1069710699 const parallelPath = calculateParallelEdgePath(startX, startY, endX, endY, curveInfo);
1069810700 const useCurve = parallelPath.type === 'curve';
1069910701
10702+ // For hover effect on curved edges, trim the curve to create "shorten" visual
10703+ // This keeps the curve shape consistent but renders a shorter portion
10704+ let trimmedPath = null;
10705+ if (useCurve && isHovered && parallelPath.ctrlX !== null) {
10706+ trimmedPath = getTrimmedBezierPath(
10707+ parallelPath.startX, parallelPath.startY,
10708+ parallelPath.ctrlX, parallelPath.ctrlY,
10709+ parallelPath.endX, parallelPath.endY,
10710+ 0.08, 0.92 // Trim 8% from each end
10711+ );
10712+ }
10713+
1070010714 return (
1070110715 <g key={`edge-above-${edge.id}-${idx}`}>
1070210716 {/* Main edge line - always same thickness */}
@@ -10720,7 +10734,7 @@ function NodeCanvas() {
1072010734 />
1072110735 ) : useCurve ? (
1072210736 <path
10723- d={parallelPath.path}
10737+ d={trimmedPath ? trimmedPath.path : parallelPath.path}
1072410738 fill="none"
1072510739 stroke={edgeColor}
1072610740 strokeWidth="12"
@@ -10769,7 +10783,7 @@ function NodeCanvas() {
1076910783 </>
1077010784 ) : useCurve ? (
1077110785 <path
10772- d={parallelPath.path}
10786+ d={trimmedPath ? trimmedPath.path : parallelPath.path}
1077310787 fill="none"
1077410788 stroke={edgeColor}
1077510789 strokeWidth={showConnectionNames ? "16" : "6"}
@@ -11010,6 +11024,80 @@ function NodeCanvas() {
1101011024 definingNodeId = edge.typeNodeId;
1101111025 }
1101211026
11027+ // Open the panel tab for the defining node
11028+ if (definingNodeId) {
11029+ storeActions.openRightPanelNodeTab(definingNodeId);
11030+ }
11031+ }}
11032+ />
11033+ ) : useCurve ? (
11034+ <path
11035+ d={parallelPath.path}
11036+ fill="none"
11037+ stroke="transparent"
11038+ strokeWidth="40"
11039+ style={{ cursor: 'pointer' }}
11040+ onPointerDown={(e) => {
11041+ if (e.pointerType && e.pointerType !== 'mouse') {
11042+ e.preventDefault?.();
11043+ e.stopPropagation?.();
11044+ ignoreCanvasClick.current = true;
11045+ setLongPressingInstanceId(null);
11046+ setDrawingConnectionFrom(null);
11047+ if (e.ctrlKey || e.metaKey) {
11048+ if (selectedEdgeIds.has(edge.id)) {
11049+ storeActions.removeSelectedEdgeId(edge.id);
11050+ } else {
11051+ storeActions.addSelectedEdgeId(edge.id);
11052+ }
11053+ } else {
11054+ storeActions.clearSelectedEdgeIds();
11055+ storeActions.setSelectedEdgeId(edge.id);
11056+ }
11057+ }
11058+ handleEdgePointerDownTouch(edge.id, e);
11059+ }}
11060+ onTouchStart={(e) => {
11061+ e.preventDefault?.();
11062+ e.stopPropagation?.();
11063+ ignoreCanvasClick.current = true;
11064+ setLongPressingInstanceId(null);
11065+ setDrawingConnectionFrom(null);
11066+ storeActions.clearSelectedEdgeIds();
11067+ storeActions.setSelectedEdgeId(edge.id);
11068+ }}
11069+ onClick={(e) => {
11070+ e.stopPropagation();
11071+ ignoreCanvasClick.current = true;
11072+
11073+ // Handle multiple selection with Ctrl/Cmd key
11074+ if (e.ctrlKey || e.metaKey) {
11075+ // Toggle this edge in the multiple selection
11076+ if (selectedEdgeIds.has(edge.id)) {
11077+ storeActions.removeSelectedEdgeId(edge.id);
11078+ } else {
11079+ storeActions.addSelectedEdgeId(edge.id);
11080+ }
11081+ } else {
11082+ // Single selection - clear multiple selection and set single edge
11083+ storeActions.clearSelectedEdgeIds();
11084+ storeActions.setSelectedEdgeId(edge.id);
11085+ }
11086+ }}
11087+ onDoubleClick={(e) => {
11088+ e.stopPropagation();
11089+
11090+ // Find the defining node for this edge's connection type
11091+ let definingNodeId = null;
11092+
11093+ // Check definitionNodeIds first (for custom connection types)
11094+ if (edge.definitionNodeIds && edge.definitionNodeIds.length > 0) {
11095+ definingNodeId = edge.definitionNodeIds[0];
11096+ } else if (edge.typeNodeId) {
11097+ // Fallback to typeNodeId (for base connection type)
11098+ definingNodeId = edge.typeNodeId;
11099+ }
11100+
1101311101 // Open the panel tab for the defining node
1101411102 if (definingNodeId) {
1101511103 storeActions.openRightPanelNodeTab(definingNodeId);
@@ -11584,9 +11672,19 @@ function NodeCanvas() {
1158411672 ? edge.directionality.arrowsToward
1158511673 : new Set(Array.isArray(edge.directionality?.arrowsToward) ? edge.directionality.arrowsToward : []);
1158611674
11675+ // Check if this is a curved edge (parallel edge)
11676+ const curveInfo = edgeCurveInfo.get(edge.id);
11677+ const isCurvedEdge = curveInfo && curveInfo.totalInPair > 1;
11678+
1158711679 // Only shorten connections at ends with arrows or hover state
11588- let shouldShortenSource = isHovered || arrowsToward.has(sourceNode.id);
11589- let shouldShortenDest = isHovered || arrowsToward.has(destNode.id);
11680+ // For curved edges, never shorten for hover - only for arrows
11681+ // This ensures the curve shape stays consistent when hovered
11682+ let shouldShortenSource = isCurvedEdge
11683+ ? arrowsToward.has(sourceNode.id)
11684+ : (isHovered || arrowsToward.has(sourceNode.id));
11685+ let shouldShortenDest = isCurvedEdge
11686+ ? arrowsToward.has(destNode.id)
11687+ : (isHovered || arrowsToward.has(destNode.id));
1159011688 if (enableAutoRouting && routingStyle === 'manhattan') {
1159111689 // In Manhattan mode, never shorten for hover—only for actual arrows
1159211690 shouldShortenSource = arrowsToward.has(sourceNode.id);
@@ -11757,10 +11855,22 @@ function NodeCanvas() {
1175711855 }
1175811856
1175911857 // Calculate parallel edge path using centralized utility
11760- const curveInfo = edgeCurveInfo.get(edge.id);
11858+ // Note: curveInfo was already retrieved earlier for shouldShorten logic
1176111859 const parallelPath = calculateParallelEdgePath(startX, startY, endX, endY, curveInfo);
1176211860 const useCurve = parallelPath.type === 'curve';
1176311861
11862+ // For hover effect on curved edges, trim the curve to create "shorten" visual
11863+ // This keeps the curve shape consistent but renders a shorter portion
11864+ let trimmedPath = null;
11865+ if (useCurve && isHovered && parallelPath.ctrlX !== null) {
11866+ trimmedPath = getTrimmedBezierPath(
11867+ parallelPath.startX, parallelPath.startY,
11868+ parallelPath.ctrlX, parallelPath.ctrlY,
11869+ parallelPath.endX, parallelPath.endY,
11870+ 0.08, 0.92 // Trim 8% from each end
11871+ );
11872+ }
11873+
1176411874 return (
1176511875 <g key={`edge-above-${edge.id}-${idx}`}>
1176611876 {/* Main edge line - always same thickness */}
@@ -11784,7 +11894,7 @@ function NodeCanvas() {
1178411894 />
1178511895 ) : useCurve ? (
1178611896 <path
11787- d={parallelPath.path}
11897+ d={trimmedPath ? trimmedPath.path : parallelPath.path}
1178811898 fill="none"
1178911899 stroke={edgeColor}
1179011900 strokeWidth="12"
@@ -11833,7 +11943,7 @@ function NodeCanvas() {
1183311943 </>
1183411944 ) : useCurve ? (
1183511945 <path
11836- d={parallelPath.path}
11946+ d={trimmedPath ? trimmedPath.path : parallelPath.path}
1183711947 fill="none"
1183811948 stroke={edgeColor}
1183911949 strokeWidth={showConnectionNames ? "16" : "6"}
0 commit comments