Skip to content

Commit 5152762

Browse files
committed
refactor: Extract edge rendering logic to EdgeRenderer and parallelEdgeUtils, and improve zoom restoration to maintain cursor position.
1 parent cb3d26e commit 5152762

3 files changed

Lines changed: 111 additions & 37 deletions

File tree

src/NodeCanvas.jsx

Lines changed: 96 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2361,6 +2361,7 @@ function NodeCanvas() {
23612361
// CRITICAL: Update ref synchronously BEFORE setPanOffset
23622362
// This ensures handleMouseMove's RAF sees the new value immediately
23632363
// (useEffect that syncs ref from state runs AFTER RAF callbacks)
2364+
console.log('[PanLoop] Updating panOffsetRef:', newPan);
23642365
panOffsetRef.current = newPan;
23652366

23662367
// Use setPanOffset directly to allow React to batch this update with the
@@ -5208,7 +5209,9 @@ function NodeCanvas() {
52085209
zoomLevel,
52095210
canvasSize.offsetX,
52105211
canvasSize.offsetY,
5211-
storeActions
5212+
canvasSize.offsetY,
5213+
storeActions,
5214+
triggerDragZoomOut
52125215
]);
52135216

52145217
const handleNodeMouseDown = (nodeData, e) => { // nodeData is now a hydrated node (instance + prototype)
@@ -7104,16 +7107,26 @@ function NodeCanvas() {
71047107
const dropWorldX = (dropX - currentPan.x) / currentZoom + canvasSizeRef.current.offsetX;
71057108
const dropWorldY = (dropY - currentPan.y) / currentZoom + canvasSizeRef.current.offsetY;
71067109

7107-
// 2. Calculate NEW pan to keep that world point at the CENTER of the viewport using TARGET zoom
7110+
// 2. Calculate NEW pan to keep that world point under the MOUSE CURSOR using TARGET zoom
71087111
// panX = screenX - (worldX - offsetX) * zoom
7109-
const viewportCenterX = rect.width / 2;
7110-
const viewportCenterY = rect.height / 2;
7111-
const targetPanX = viewportCenterX - (dropWorldX - canvasSizeRef.current.offsetX) * targetZoom;
7112-
const targetPanY = viewportCenterY - (dropWorldY - canvasSizeRef.current.offsetY) * targetZoom;
7113-
7114-
// Clamp the new pan to bounds
7115-
const minPanX = viewportSizeRef.current.width - canvasSizeRef.current.width * targetZoom;
7116-
const minPanY = viewportSizeRef.current.height - canvasSizeRef.current.height * targetZoom;
7112+
// This prevents "snapping" to the center if the node was at the edge
7113+
const targetPanX = dropX - (dropWorldX - canvasSizeRef.current.offsetX) * targetZoom;
7114+
const targetPanY = dropY - (dropWorldY - canvasSizeRef.current.offsetY) * targetZoom;
7115+
7116+
console.log('[ZoomRestore] Debug:', {
7117+
dropX, dropY,
7118+
currentPan, currentZoom,
7119+
dropWorldX, dropWorldY,
7120+
targetZoom,
7121+
targetPanX, targetPanY,
7122+
viewportSize: viewportSizeRef.current
7123+
});
7124+
7125+
// Relaxed clamping - prevent aggressive snap-back by allowing full canvas traverse
7126+
// If node was dragged to edge, targetPan might briefly exceed standard bounds logic
7127+
const minPanX = -(canvasSizeRef.current.width * targetZoom);
7128+
const minPanY = -(canvasSizeRef.current.height * targetZoom);
7129+
71177130
const clampedTargetPanX = Math.min(0, Math.max(targetPanX, minPanX));
71187131
const clampedTargetPanY = Math.min(0, Math.max(targetPanY, minPanY));
71197132

@@ -10597,9 +10610,11 @@ function NodeCanvas() {
1059710610
const curveInfo = edgeCurveInfo.get(edge.id);
1059810611
if (curveInfo && curveInfo.totalInPair > 1) {
1059910612
const { pairIndex, totalInPair } = curveInfo;
10600-
const curveSpacing = 40;
10601-
const offsetIndex = pairIndex - (totalInPair - 1) / 2;
10602-
const perpOffset = offsetIndex * curveSpacing;
10613+
const curveSpacing = 100;
10614+
// Alternating direction logic matching utils
10615+
const direction = pairIndex % 2 === 0 ? 1 : -1;
10616+
const offsetMagnitude = Math.floor((pairIndex + 1) / 2) * curveSpacing;
10617+
const perpOffset = direction * offsetMagnitude;
1060310618
const edgeDx = endX - startX;
1060410619
const edgeDy = endY - startY;
1060510620
const edgeLen = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy);
@@ -10667,10 +10682,10 @@ function NodeCanvas() {
1066710682
if (curveInfo && curveInfo.totalInPair > 1) {
1066810683
// Calculate curve offset for parallel edges
1066910684
const { pairIndex, totalInPair } = curveInfo;
10670-
const curveSpacing = 40; // Pixels between parallel edge curves
10671-
// Center the curves: offset from -half to +half of total spread
10672-
const offsetIndex = pairIndex - (totalInPair - 1) / 2;
10673-
const perpOffset = offsetIndex * curveSpacing;
10685+
const curveSpacing = 100; // Exaggerated curves
10686+
const direction = pairIndex % 2 === 0 ? 1 : -1;
10687+
const offsetMagnitude = Math.floor((pairIndex + 1) / 2) * curveSpacing;
10688+
const perpOffset = direction * offsetMagnitude;
1067410689

1067510690
// Calculate perpendicular direction
1067610691
const edgeDx = endX - startX;
@@ -10728,9 +10743,31 @@ function NodeCanvas() {
1072810743
angle = 90;
1072910744
}
1073010745
} else {
10731-
midX = (x1 + x2) / 2;
10732-
midY = (y1 + y2) / 2;
10733-
angle = Math.atan2(y2 - y1, x2 - x1) * (180 / Math.PI);
10746+
// Curve apex logic for labels
10747+
const curveInfo = edgeCurveInfo.get(edge.id);
10748+
if (curveInfo && curveInfo.totalInPair > 1) {
10749+
const { pairIndex } = curveInfo;
10750+
const curveSpacing = 100;
10751+
const direction = pairIndex % 2 === 0 ? 1 : -1;
10752+
const offsetMagnitude = Math.floor((pairIndex + 1) / 2) * curveSpacing;
10753+
const perpOffset = direction * offsetMagnitude;
10754+
const edgeDx = endX - startX;
10755+
const edgeDy = endY - startY;
10756+
const edgeLen = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy);
10757+
const perpX = edgeLen > 0 ? -edgeDy / edgeLen : 0;
10758+
const perpY = edgeLen > 0 ? edgeDx / edgeLen : 0;
10759+
const curveMidX = (startX + endX) / 2;
10760+
const curveMidY = (startY + endY) / 2;
10761+
const ctrlX = curveMidX + perpX * perpOffset;
10762+
const ctrlY = curveMidY + perpY * perpOffset;
10763+
midX = 0.25 * startX + 0.5 * ctrlX + 0.25 * endX;
10764+
midY = 0.25 * startY + 0.5 * ctrlY + 0.25 * endY;
10765+
angle = Math.atan2(edgeDy, edgeDx) * (180 / Math.PI);
10766+
} else {
10767+
midX = (x1 + x2) / 2;
10768+
midY = (y1 + y2) / 2;
10769+
angle = Math.atan2(y2 - y1, x2 - x1) * (180 / Math.PI);
10770+
}
1073410771
}
1073510772

1073610773
// Determine connection name to display
@@ -11728,9 +11765,11 @@ function NodeCanvas() {
1172811765
const curveInfo = edgeCurveInfo.get(edge.id);
1172911766
if (curveInfo && curveInfo.totalInPair > 1) {
1173011767
const { pairIndex, totalInPair } = curveInfo;
11731-
const curveSpacing = 40;
11732-
const offsetIndex = pairIndex - (totalInPair - 1) / 2;
11733-
const perpOffset = offsetIndex * curveSpacing;
11768+
const curveSpacing = 100;
11769+
// Alternating direction logic matching utils
11770+
const direction = pairIndex % 2 === 0 ? 1 : -1;
11771+
const offsetMagnitude = Math.floor((pairIndex + 1) / 2) * curveSpacing;
11772+
const perpOffset = direction * offsetMagnitude;
1173411773
const edgeDx = endX - startX;
1173511774
const edgeDy = endY - startY;
1173611775
const edgeLen = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy);
@@ -11798,10 +11837,11 @@ function NodeCanvas() {
1179811837
if (curveInfo && curveInfo.totalInPair > 1) {
1179911838
// Calculate curve offset for parallel edges
1180011839
const { pairIndex, totalInPair } = curveInfo;
11801-
const curveSpacing = 40; // Pixels between parallel edge curves
11802-
// Center the curves: offset from -half to +half of total spread
11803-
const offsetIndex = pairIndex - (totalInPair - 1) / 2;
11804-
const perpOffset = offsetIndex * curveSpacing;
11840+
const curveSpacing = 100; // Pixels between parallel edge curves
11841+
// Alternating direction logic matching utils
11842+
const direction = pairIndex % 2 === 0 ? 1 : -1;
11843+
const offsetMagnitude = Math.floor((pairIndex + 1) / 2) * curveSpacing;
11844+
const perpOffset = direction * offsetMagnitude;
1180511845

1180611846
// Calculate perpendicular direction
1180711847
const edgeDx = endX - startX;
@@ -11859,9 +11899,35 @@ function NodeCanvas() {
1185911899
angle = 90;
1186011900
}
1186111901
} else {
11862-
midX = (x1 + x2) / 2;
11863-
midY = (y1 + y2) / 2;
11864-
angle = Math.atan2(y2 - y1, x2 - x1) * (180 / Math.PI);
11902+
// Default Straight / Curve logic
11903+
const curveInfo = edgeCurveInfo.get(edge.id);
11904+
if (curveInfo && curveInfo.totalInPair > 1) {
11905+
// Curve Apex Logic for Labels
11906+
const { pairIndex } = curveInfo;
11907+
const curveSpacing = 100;
11908+
const direction = pairIndex % 2 === 0 ? 1 : -1;
11909+
const offsetMagnitude = Math.floor((pairIndex + 1) / 2) * curveSpacing;
11910+
const perpOffset = direction * offsetMagnitude;
11911+
11912+
const edgeDx = endX - startX;
11913+
const edgeDy = endY - startY;
11914+
const edgeLen = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy);
11915+
const perpX = edgeLen > 0 ? -edgeDy / edgeLen : 0;
11916+
const perpY = edgeLen > 0 ? edgeDx / edgeLen : 0;
11917+
const curveMidX = (startX + endX) / 2;
11918+
const curveMidY = (startY + endY) / 2;
11919+
const ctrlX = curveMidX + perpX * perpOffset;
11920+
const ctrlY = curveMidY + perpY * perpOffset;
11921+
11922+
// Apex at t=0.5
11923+
midX = 0.25 * startX + 0.5 * ctrlX + 0.25 * endX;
11924+
midY = 0.25 * startY + 0.5 * ctrlY + 0.25 * endY;
11925+
angle = Math.atan2(edgeDy, edgeDx) * (180 / Math.PI); // Tangent at apex is parallel to edge
11926+
} else {
11927+
midX = (x1 + x2) / 2;
11928+
midY = (y1 + y2) / 2;
11929+
angle = Math.atan2(y2 - y1, x2 - x1) * (180 / Math.PI);
11930+
}
1186511931
}
1186611932

1186711933
// Determine connection name to display

src/components/EdgeRenderer.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,14 @@ export default function EdgeRenderer({
193193
{showConnectionNames && connectionName && (
194194
<g>
195195
<text
196-
x={midX}
197-
y={midY}
196+
x={useCurve ? parallelPath.apexX : midX}
197+
y={useCurve ? parallelPath.apexY : midY}
198198
fill="#bdb5b5"
199199
fontSize="24"
200200
fontWeight="bold"
201201
textAnchor="middle"
202202
dominantBaseline="middle"
203-
transform={`rotate(${angle > 90 || angle < -90 ? angle + 180 : angle}, ${midX}, ${midY})`}
203+
transform={`rotate(${angle > 90 || angle < -90 ? angle + 180 : angle}, ${useCurve ? parallelPath.apexX : midX}, ${useCurve ? parallelPath.apexY : midY})`}
204204
stroke={edgeColor}
205205
strokeWidth="6"
206206
strokeLinecap="round"

src/utils/canvas/parallelEdgeUtils.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ export function calculateParallelEdgePath(startX, startY, endX, endY, curveInfo)
2525
}
2626

2727
const { pairIndex, totalInPair } = curveInfo;
28-
const curveSpacing = 40; // Pixels between parallel edge curves
28+
const curveSpacing = 100; // Pixels between parallel edge curves - EXAGGERATED CURVES
29+
console.log('calculateParallelEdgePath called with spacing:', curveSpacing, 'curveInfo:', curveInfo);
2930

3031
// KEY FIX: Alternate curve direction based on index
3132
// Even indices (0, 2, 4...) curve one way, odd indices (1, 3, 5...) curve the opposite way
3233
const direction = pairIndex % 2 === 0 ? 1 : -1;
33-
34+
3435
// Calculate offset magnitude: 0, 1, 2... for pairs (0,1), (2,3), (4,5)...
3536
const offsetMagnitude = Math.floor((pairIndex + 1) / 2) * curveSpacing;
3637
const perpOffset = direction * offsetMagnitude;
@@ -39,7 +40,7 @@ export function calculateParallelEdgePath(startX, startY, endX, endY, curveInfo)
3940
const edgeDx = endX - startX;
4041
const edgeDy = endY - startY;
4142
const edgeLen = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy);
42-
43+
4344
if (edgeLen === 0) {
4445
// Degenerate case - same start and end point
4546
return {
@@ -64,6 +65,11 @@ export function calculateParallelEdgePath(startX, startY, endX, endY, curveInfo)
6465
// Generate quadratic Bezier path
6566
const path = `M ${startX} ${startY} Q ${ctrlX} ${ctrlY} ${endX} ${endY}`;
6667

68+
// Calculate apex (t=0.5 on Quadratic Bezier) for label positioning
69+
// B(0.5) = 0.25*P0 + 0.5*P1 + 0.25*P2
70+
const apexX = 0.25 * startX + 0.5 * ctrlX + 0.25 * endX;
71+
const apexY = 0.25 * startY + 0.5 * ctrlY + 0.25 * endY;
72+
6773
return {
6874
type: 'curve',
6975
path,
@@ -72,7 +78,9 @@ export function calculateParallelEdgePath(startX, startY, endX, endY, curveInfo)
7278
startX,
7379
startY,
7480
endX,
75-
endY
81+
endY,
82+
apexX,
83+
apexY
7684
};
7785
}
7886

0 commit comments

Comments
 (0)