feat: handle backward curve drawing

This commit is contained in:
Mohamed Marrouchi 2025-04-04 12:26:36 +01:00
parent 8680f48688
commit 74470ef8b8

View File

@ -11,6 +11,7 @@ import styled from "@emotion/styled";
import {
DefaultLinkFactory,
DefaultLinkWidget,
PortModel
} from "@projectstorm/react-diagrams";
import { AdvancedLinkModel } from "./AdvancedLinkModel";
@ -28,6 +29,112 @@ const createCurvedPath = (start: Point, end: Point, nodeHeight: number) => {
return `M ${start.x},${start.y} C ${controlPoint1X},${controlPoint1Y} ${controlPoint2X},${controlPoint2Y} ${end.x},${end.y}`;
};
const createBackwardCurvedPath = (
sourcePort: PortModel,
targetPort: PortModel,
) => {
const sourceNode = sourcePort.getNode();
const targetNode = targetPort.getNode();
// **NEW:** Get port dimensions for better alignment
const sourcePortSize = { width: sourcePort.width || 10, height: sourcePort.height || 10 };
const targetPortSize = { width: targetPort.width || 10, height: targetPort.height || 10 };
// Get node dimensions
const sourceNodeWidth = sourceNode.width;
const targetNodeWidth = targetNode.width;
const sourceNodeHeight = sourceNode.height;
const targetNodeHeight = targetNode.height;
// Get node boundaries
const sourceNodeBounds = {
left: sourceNode.getPosition().x,
right: sourceNode.getPosition().x + sourceNodeWidth,
top: sourceNode.getPosition().y,
bottom: sourceNode.getPosition().y + sourceNodeHeight,
};
const targetNodeBounds = {
left: targetNode.getPosition().x,
right: targetNode.getPosition().x + targetNodeWidth,
top: targetNode.getPosition().y,
bottom: targetNode.getPosition().y +targetNodeHeight,
};
// **NEW:** Adjust `start` and `end` to match the exact center of ports
const adjustedStart: Point = {
x: sourcePort.getPosition().x + sourcePortSize.width / 2,
y: sourcePort.getPosition().y + sourcePortSize.height / 2,
};
const adjustedEnd: Point = {
x: targetPort.getPosition().x + targetPortSize.width / 2,
y: targetPort.getPosition().y + targetPortSize.height / 2,
};
// Calculate the distance between nodes
const nodeDistance = Math.sqrt(
Math.pow(adjustedEnd.x - adjustedStart.x, 2) + Math.pow(adjustedEnd.y - adjustedStart.y, 2)
);
// Logarithmic scaling function that adjusts between 1.5 and 2 based on distance
const logFactor = (distance) => {
const minDistance = 0.1; // A small value to prevent division by zero or too small values
const maxDistance = 2000; // A maximum value for nodeDistance where the function plateaus
// Logarithmic scale function to map distance to a factor between 1.5 and 2
const scale = Math.log(distance + minDistance) / Math.log(maxDistance + minDistance);
// Scale result to range between 1.5 and 2
return 1.5 + scale * (2 - 1.5);
};
// Use node dimensions and distance to calculate dynamic offsets
const horizontalOffset = Math.max(sourceNodeWidth, targetNodeWidth);
const verticalOffset = Math.max(sourceNodeHeight, targetNodeHeight);
// Dynamic factor, adjusting horizontal and vertical offsets based on the distance
let adjustedHorizontalOffset = horizontalOffset* logFactor(nodeDistance);
;
let adjustedVerticalOffset = verticalOffset * logFactor(nodeDistance);
// Horizontal overlap ratio (0 = no overlap, 1 = fully overlapping horizontally)
const xOverlapAmount = Math.max(
0,
Math.min(sourceNodeBounds.right, targetNodeBounds.right) -
Math.max(sourceNodeBounds.left, targetNodeBounds.left)
);
const maxXRange = Math.max(sourceNodeWidth, targetNodeWidth);
const xOverlapRatio = xOverlapAmount / maxXRange;
// Vertical overlap ratio (0 = no overlap, 1 = fully overlapping vertically)
const yOverlapAmount = Math.max(
0,
Math.min(sourceNodeBounds.bottom, targetNodeBounds.bottom) -
Math.max(sourceNodeBounds.top, targetNodeBounds.top)
);
const maxYRange = Math.max(sourceNodeHeight, targetNodeHeight);
const yOverlapRatio = yOverlapAmount / maxYRange;
// Determine vertical direction for Y alignment
const verticalDirection = adjustedEnd.y >= adjustedStart.y ? 1 : -1;
// If Node Distance is small, multiply offsets by overlap ratios
// to avoid abrupt curve steepness
if (nodeDistance < 500) {
adjustedHorizontalOffset *= xOverlapRatio;
adjustedVerticalOffset *= yOverlapRatio;
}
// Compute control points with dynamic offset
let controlPoint1X = adjustedStart.x + adjustedHorizontalOffset;
let controlPoint1Y = adjustedStart.y + verticalDirection * adjustedVerticalOffset;
let controlPoint2X = adjustedEnd.x - adjustedHorizontalOffset;
let controlPoint2Y = adjustedEnd.y - verticalDirection * adjustedVerticalOffset;
controlPoint1X = Math.max(controlPoint1X, sourceNodeBounds.right + 10);
controlPoint2X = Math.min(controlPoint2X, targetNodeBounds.left - 10);
controlPoint1Y = verticalDirection > 0
? Math.max(controlPoint1Y, sourceNodeBounds.bottom + 10)
: Math.min(controlPoint1Y, sourceNodeBounds.top - 10);
controlPoint2Y = verticalDirection > 0
? Math.min(controlPoint2Y, targetNodeBounds.top - 10)
: Math.max(controlPoint2Y, targetNodeBounds.bottom + 10);
// Return the cubic Bezier curve
return `M ${adjustedStart.x},${adjustedStart.y} C ${controlPoint1X},${controlPoint1Y} ${controlPoint2X},${controlPoint2Y} ${adjustedEnd.x},${adjustedEnd.y}`;
};
namespace S {
export const Keyframes = keyframes`
@ -68,41 +175,57 @@ export class AdvancedLinkFactory extends DefaultLinkFactory {
model: AdvancedLinkModel,
selected: boolean,
path: string,
) {
) {
const sourcePort = model.getSourcePort();
const targetPort = model.getTargetPort();
const isSelfLoop =
model.getSourcePort().getNode() === model.getTargetPort().getNode();
sourcePort.getNode() === targetPort.getNode();
const sourcePortPosition = sourcePort.getPosition();
const targetPortPosition = targetPort.getPosition();
const startPoint: Point = {
x: sourcePortPosition.x + 20,
y: sourcePortPosition.y + 20,
};
const endPoint: Point = {
x: targetPortPosition.x + 20,
y: targetPortPosition.y + 20,
};
// Check if it's a backward link (moving left)
const isBackward = startPoint.x - endPoint.x > 12;
if (isSelfLoop) {
// Adjust the path to create a curve
const sourcePortPosition = model.getSourcePort().getPosition();
const targetPortPosition = model.getTargetPort().getPosition();
const startPoint: Point = {
x: sourcePortPosition.x + 20,
y: sourcePortPosition.y + 20,
const sourcePortSize = { width:sourcePort.width || 10, height:sourcePort.height || 10 };
// Adjust start Point to match the exact source port's centre
const adjustedStartPoint: Point = {
x: sourcePortPosition.x + sourcePortSize.width / 2,
y: sourcePortPosition.y + sourcePortSize.height / 2,
};
const endPoint: Point = {
x: targetPortPosition.x + 20,
y: targetPortPosition.y + 20,
};
const targetPortHeight = model.getTargetPort().height;
// Handle self-loop (curved) links
const targetPortHeight =targetPort.height;
const targetNdeHeight =
(model.getTargetPort().getPosition().y -
model.getTargetPort().getNode().getPosition().y) *
(targetPort.getPosition().y -
targetPort.getNode().getPosition().y) *
2 +
targetPortHeight;
path = createCurvedPath(startPoint, endPoint, targetNdeHeight);
}
return (
path = createCurvedPath(adjustedStartPoint, endPoint, targetNdeHeight);
} else if (isBackward) {
// Handle backward (leftward) link with refined function
path = createBackwardCurvedPath(sourcePort, targetPort);
}
return (
<S.Path
selected={selected}
stroke={
selected ? model.getOptions().selectedColor : model.getOptions().color
selected
? model.getOptions().selectedColor
: model.getOptions().color
}
strokeWidth={model.getOptions().width}
d={path}
/>
);
}
}
}