Skip to content

Commit c208346

Browse files
committed
Improve curvature of curved edges. They are now perfectly symmetric between vertices.
1 parent 0e928c1 commit c208346

2 files changed

Lines changed: 97 additions & 48 deletions

File tree

src/main/java/com/brunomnsilva/smartgraph/graphview/SmartGraphEdgeCurve.java

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
package com.brunomnsilva.smartgraph.graphview;
2525

2626
import com.brunomnsilva.smartgraph.graph.Edge;
27-
import javafx.beans.value.ObservableValue;
2827
import javafx.geometry.Point2D;
2928
import javafx.scene.shape.CubicCurve;
3029
import javafx.scene.transform.Rotate;
@@ -51,7 +50,10 @@
5150
*/
5251
public class SmartGraphEdgeCurve<E, V> extends CubicCurve implements SmartGraphEdgeBase<E, V> {
5352

54-
private static final double MAX_EDGE_CURVE_ANGLE = 20;
53+
private static final double MAX_EDGE_CURVE_ANGLE = 45;
54+
private static final double MIN_EDGE_CURVE_ANGLE = 3;
55+
public static final int DISTANCE_THRESHOLD = 400;
56+
public static final int LOOP_RADIUS_FACTOR = 4;
5557

5658
private final Edge<E, V> underlyingEdge;
5759

@@ -61,7 +63,7 @@ public class SmartGraphEdgeCurve<E, V> extends CubicCurve implements SmartGraphE
6163
private SmartLabel attachedLabel = null;
6264
private SmartArrow attachedArrow = null;
6365

64-
private double randomAngleFactor = 0;
66+
private double randomAngleFactor;
6567

6668
/* Styling proxy */
6769
private final SmartStyleProxy styleProxy;
@@ -112,63 +114,70 @@ private void update() {
112114
/* Make a loop using the control points proportional to the vertex radius */
113115

114116
//TODO: take into account several "self-loops" with randomAngleFactor
115-
double midpointX1 = outbound.getCenterX() - inbound.getRadius() * 5;
116-
double midpointY1 = outbound.getCenterY() - inbound.getRadius() * 2;
117+
double midpointX1 = outbound.getCenterX() - inbound.getRadius() * LOOP_RADIUS_FACTOR;
118+
double midpointY1 = outbound.getCenterY() - inbound.getRadius() * LOOP_RADIUS_FACTOR;
117119

118-
double midpointX2 = outbound.getCenterX() + inbound.getRadius() * 5;
119-
double midpointY2 = outbound.getCenterY() - inbound.getRadius() * 2;
120+
double midpointX2 = outbound.getCenterX() + inbound.getRadius() * LOOP_RADIUS_FACTOR;
121+
double midpointY2 = outbound.getCenterY() - inbound.getRadius() * LOOP_RADIUS_FACTOR;
120122

121123
setControlX1(midpointX1);
122124
setControlY1(midpointY1);
123125
setControlX2(midpointX2);
124126
setControlY2(midpointY2);
125127

126128
} else {
127-
/* Make a curved edge. The curve is proportional to the distance */
128-
double midpointX = (outbound.getCenterX() + inbound.getCenterX()) / 2;
129-
double midpointY = (outbound.getCenterY() + inbound.getCenterY()) / 2;
130-
131-
Point2D midpoint = new Point2D(midpointX, midpointY);
129+
/* Make a curved edge. The curvature is bounded and proportional to the distance;
130+
higher curvature for closer vertices */
132131

133132
Point2D startpoint = new Point2D(inbound.getCenterX(), inbound.getCenterY());
134133
Point2D endpoint = new Point2D(outbound.getCenterX(), outbound.getCenterY());
135134

136-
//TODO: improvement lower max_angle_placement according to distance between vertices
137-
double angle = MAX_EDGE_CURVE_ANGLE;
138-
139135
double distance = startpoint.distance(endpoint);
140136

141-
//TODO: remove "magic number" 1500 and provide a distance function for the
142-
//decreasing angle with distance
143-
angle = angle - (distance / 1500 * angle);
137+
double angle = linearDecay(MAX_EDGE_CURVE_ANGLE, MIN_EDGE_CURVE_ANGLE, distance, DISTANCE_THRESHOLD);
144138

145-
midpoint = UtilitiesPoint2D.rotate(midpoint,
146-
startpoint,
147-
(-angle) + randomAngleFactor * (angle - (-angle)));
139+
Point2D midpoint = UtilitiesPoint2D.calculateTriangleBetween(startpoint, endpoint,
140+
(-angle) + randomAngleFactor * 2 * angle);
148141

149142
setControlX1(midpoint.getX());
150143
setControlY1(midpoint.getY());
151144
setControlX2(midpoint.getX());
152145
setControlY2(midpoint.getY());
153146
}
147+
}
154148

149+
/**
150+
* Provides the decreasing linear function decay.
151+
* @param initialValue initial value
152+
* @param finalValue maximum value
153+
* @param distance current distance
154+
* @param distanceThreshold distance threshold (maximum distance -> maximum value)
155+
* @return the decay function value for <code>distance</code>
156+
*/
157+
private static double linearDecay(double initialValue, double finalValue, double distance, double distanceThreshold) {
158+
//Args.requireNonNegative(distance, "distance");
159+
//Args.requireNonNegative(distanceThreshold, "distanceThreshold");
160+
// Parameters are internally guaranteed to be positive. We avoid two method calls.
161+
162+
if(distance >= distanceThreshold) return finalValue;
163+
164+
return initialValue + (finalValue - initialValue) * distance / distanceThreshold;
155165
}
156166

157-
/*
158-
With a curved edge we need to continuously update the control points.
159-
TODO: Maybe we can achieve this solely with bindings.
160-
*/
161167
private void enableListeners() {
162-
this.startXProperty().addListener((ObservableValue<? extends Number> ov, Number t, Number t1) -> {
168+
// With a curved edge we need to continuously update the control points.
169+
// TODO: Maybe we can achieve this solely with bindings? Maybe there's no performance gain in doing so.
170+
171+
this.startXProperty().addListener((ov, oldValue, newValue) -> {
163172
update();
164173
});
165-
this.startYProperty().addListener((ObservableValue<? extends Number> ov, Number t, Number t1) -> {
174+
this.startYProperty().addListener((ov, oldValue, newValue) -> {
166175
update();
167176
});
168-
this.endXProperty().addListener((ObservableValue<? extends Number> ov, Number t, Number t1) -> {
177+
this.endXProperty().addListener((ov, oldValue, newValue) -> {
169178
update();
170179
});
171-
this.endYProperty().addListener((ObservableValue<? extends Number> ov, Number t, Number t1) -> {
180+
this.endYProperty().addListener((ov, oldValue, newValue) -> {
172181
update();
173182
});
174183
}

src/main/java/com/brunomnsilva/smartgraph/graphview/UtilitiesPoint2D.java

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,67 @@ public class UtilitiesPoint2D {
3636
* Rotate a point around a pivot point by a specific degrees amount
3737
* @param point point to rotate
3838
* @param pivot pivot point
39-
* @param angle_degrees rotation degrees
39+
* @param angleDegrees rotation degrees
4040
* @return rotated point
4141
*/
42-
public static Point2D rotate(final Point2D point, final Point2D pivot, double angle_degrees) {
43-
double angle = Math.toRadians(angle_degrees); //angle_degrees * (Math.PI/180); //to radians
44-
45-
double sin = Math.sin(angle);
46-
double cos = Math.cos(angle);
47-
48-
//translate to origin
49-
Point2D result = point.subtract(pivot);
50-
51-
// rotate point
52-
Point2D rotatedOrigin = new Point2D(
53-
result.getX() * cos - result.getY() * sin,
54-
result.getX() * sin + result.getY() * cos);
55-
56-
// translate point back
57-
result = rotatedOrigin.add(pivot);
58-
59-
return result;
42+
public static Point2D rotate(final Point2D point, final Point2D pivot, final double angleDegrees) {
43+
double angleRadians = Math.toRadians(angleDegrees); // Convert angle to radians
44+
45+
double sin = Math.sin(angleRadians);
46+
double cos = Math.cos(angleRadians);
47+
48+
// Translate the point relative to the pivot
49+
double translatedX = point.getX() - pivot.getX();
50+
double translatedY = point.getY() - pivot.getY();
51+
52+
// Apply rotation using trigonometric functions
53+
double rotatedX = translatedX * cos - translatedY * sin;
54+
double rotatedY = translatedX * sin + translatedY * cos;
55+
56+
// Translate the rotated point back to the original position
57+
rotatedX += pivot.getX();
58+
rotatedY += pivot.getY();
59+
60+
return new Point2D(rotatedX, rotatedY);
61+
}
62+
63+
/**
64+
* Calculates the third vertex point that forms a triangle with segment AB as the base and C equidistant to A and B;
65+
* <code>angleDegrees</code> is the angle formed between A and C.
66+
*
67+
* @param pointA the point a
68+
* @param pointB the point b
69+
* @param angleDegrees desired angle (in degrees)
70+
* @return the point c
71+
*/
72+
public static Point2D calculateTriangleBetween(final Point2D pointA, final Point2D pointB, final double angleDegrees) {
73+
// Calculate the midpoint of AB
74+
Point2D midpointAB = pointA.midpoint(pointB);
75+
76+
// Calculate the perpendicular bisector of AB
77+
double slopeAB = (pointB.getY() - pointA.getY()) / (pointB.getX() - pointA.getX());
78+
double perpendicularSlope = -1 / slopeAB;
79+
80+
// Handle special cases where the perpendicular bisector is vertical or horizontal
81+
if (Double.isInfinite(perpendicularSlope)) {
82+
double yC = midpointAB.getY() + Math.tan(Math.toRadians(angleDegrees)) * midpointAB.getX();
83+
return new Point2D(midpointAB.getX(), yC);
84+
} else if (perpendicularSlope == 0) {
85+
return new Point2D(pointA.getX(), midpointAB.getY());
86+
}
87+
88+
// Calculate the angle between AB and the x-axis
89+
double angleAB = Math.toDegrees(Math.atan2(pointB.getY() - pointA.getY(), pointB.getX() - pointA.getX()));
90+
91+
// Calculate the angle between AB and AC
92+
double angleAC = angleAB + angleDegrees;
93+
94+
// Calculate the coordinates of point C
95+
double distanceAC = pointA.distance(midpointAB) / Math.cos(Math.toRadians(angleDegrees));
96+
double xC = pointA.getX() + distanceAC * Math.cos(Math.toRadians(angleAC));
97+
double yC = perpendicularSlope * (xC - midpointAB.getX()) + midpointAB.getY();
98+
99+
return new Point2D(xC, yC);
60100
}
61101

62102
}

0 commit comments

Comments
 (0)