Skip to content

Commit

Permalink
Fix layout algorithm in AbstractElbowLineConnectionWithMarkersFigure.
Browse files Browse the repository at this point in the history
  • Loading branch information
wrandelshofer committed Sep 27, 2024
1 parent 3a8f701 commit 7219487
Show file tree
Hide file tree
Showing 30 changed files with 334 additions and 215 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,28 +40,4 @@ public Locator getLocator() {
public PointAndDerivative getPointAndDerivativeInLocal(Figure connection, Figure target) {
return new PointAndDerivative(locator.locate(target).getX(), locator.locate(target).getY(), new Point2D(1, 0).getX(), new Point2D(1, 0).getY());
}

/*
@Override
public IntersectionPointEx chopStart(RenderContext ctx, Figure connection, Figure target, double startX, double startY, double endX, double endY) {
final Bounds b = target.getLayoutBounds();
Point2D center = new Point2D(b.getMinX() + b.getWidth() * 0.5, b.getMinY() + b.getHeight() * 0.5);
Point2D location = locator.locate(target);
Point2D direction = location.subtract(center);
Point2D derivative1 = new Point2D(direction.getY(), -direction.getX());
Point2D derivative2 = new Point2D(direction.getX(), direction.getY());
if (FXGeom.squaredMagnitude(derivative1) < 1e-6) {
derivative1 = new Point2D(1, 0);
derivative2 = new Point2D(0, -1);
}
Transform localToWorld = target.getLocalToWorld();
Point2D targetP = target.localToWorld(location);
Point2D t1p = localToWorld == null ? derivative1 : localToWorld.deltaTransform(derivative1);
Point2D t2p = localToWorld == null ? derivative2 : localToWorld.deltaTransform(derivative2);
return new IntersectionPointEx(
targetP.getX(), targetP.getY(),
0, t1p.getX(), t1p.getY(),
0, t2p.getX(), t2p.getY());
}*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

import java.awt.geom.PathIterator;

import static org.jhotdraw8.draw.figure.StrokableFigure.STROKE;
import static org.jhotdraw8.draw.figure.StrokableFigure.STROKE_TYPE;

/**
* PathConnector. The target of the connection must implement {@link PathIterableFigure}.
*
Expand All @@ -43,19 +40,7 @@ public IntersectionPointEx intersect(RenderContext ctx, Figure connection, Figur
}
Point2D s = target.worldToLocal(start);
Point2D e = target.worldToLocal(end);
PathIterator pit;

// FIXME does not take line join into account
if (target.getStyled(STROKE) != null) {
pit = switch (target.getStyledNonNull(STROKE_TYPE)) {
default ->
// FIXME must stroke the path
pif.getPathIterator(ctx, null);
case INSIDE -> pif.getPathIterator(ctx, null);
};
} else {
pit = pif.getPathIterator(ctx, null);
}
PathIterator pit = pif.getPathIterator(ctx, null);

IntersectionResultEx i = IntersectLinePathIterator.intersectLinePathIteratorEx(s.getX(), s.getY(), e.getX(), e.getY(), pit);
return i.intersections().peekLast();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.jhotdraw8.geom.FXGeom;
import org.jhotdraw8.geom.FXPreciseRotate;
import org.jhotdraw8.geom.PointAndDerivative;
import org.jhotdraw8.geom.intersect.IntersectRayRay;
import org.jhotdraw8.geom.intersect.IntersectionPointEx;
import org.jhotdraw8.icollection.immutable.ImmutableList;
import org.jspecify.annotations.Nullable;
Expand All @@ -38,8 +39,8 @@
import java.awt.geom.PathIterator;
import java.util.List;

import static java.lang.Math.abs;
import static java.lang.Math.signum;
import static org.jhotdraw8.draw.render.RenderContext.UNIT_CONVERTER_KEY;
import static org.jhotdraw8.geom.Scalars.almostEqual;

/**
* AbstractElbowLineConnectionWithMarkersFigure draws a straight line or an elbow line from start to end.
Expand Down Expand Up @@ -116,7 +117,7 @@ public void createHandles(HandleType handleType, List<Handle> list) {
* @param node the node
*/
protected void updateLineNode(RenderContext ctx, Polyline node) {

// empty
}

/**
Expand All @@ -143,51 +144,49 @@ protected void updateEndMarkerNode(RenderContext ctx, Path node) {

@Override
public void updateNode(RenderContext ctx, Node node) {
Group g = (Group) node;
Polyline lineNode = (Polyline) g.getChildren().get(0);
final Path startMarkerNode = (Path) g.getChildren().get(1);
final Path endMarkerNode = (Path) g.getChildren().get(2);

Point2D start = getNonNull(START).getConvertedValue();
Point2D end = getNonNull(END).getConvertedValue();

final double startInset = getStrokeCutStart(ctx);
final double endInset = getStrokeCutEnd(ctx);
final ImmutableList<PathElement> startMarkerShape = getMarkerStartShape();

ObservableList<Double> points = lineNode.getPoints();

points.setAll(path.getPoints());
int size = points.size();
Point2D p0, p1, p3, p2;
if (size > 4) {
p0 = new Point2D(points.get(0), points.get(1));
p1 = new Point2D(points.get(2), points.get(3));
p3 = new Point2D(points.get(size - 2), points.get(size - 1));
p2 = new Point2D(points.get(size - 4), points.get(size - 3));
} else {
p2 = p0 = new Point2D(points.get(0), points.get(1));
p3 = p1 = new Point2D(points.get(2), points.get(3));
}
updateMarkerNode(ctx, g, startMarkerNode,
new PointAndDerivative(p0.getX(), p0.getY(), p1.getX() - p0.getX(), p1.getY() - p0.getY()),
startMarkerShape, getMarkerStartScaleFactor());
final ImmutableList<PathElement> endMarkerShape = getMarkerEndShape();
updateMarkerNode(ctx, g, endMarkerNode,
new PointAndDerivative(p3.getX(), p3.getY(), p2.getX() - p3.getX(), p2.getY() - p3.getY()),
endMarkerShape, getMarkerEndScaleFactor());

Point2D dir = end.subtract(start).normalize();
if (startInset != 0) {
start = start.add(dir.multiply(startInset));
}
if (endInset != 0) {
end = end.add(dir.multiply(-endInset));
try {
Group g = (Group) node;
Polyline lineNode = (Polyline) g.getChildren().get(0);
final Path startMarkerNode = (Path) g.getChildren().get(1);
final Path endMarkerNode = (Path) g.getChildren().get(2);

Point2D start = getNonNull(START).getConvertedValue();
Point2D end = getNonNull(END).getConvertedValue();

final double startInset = getStrokeCutStart(ctx);
final double endInset = getStrokeCutEnd(ctx);
final ImmutableList<PathElement> startMarkerShape = getMarkerStartShape();

ObservableList<Double> points = lineNode.getPoints();

points.setAll(path.getPoints());
int size = points.size();
Point2D p0, p1, p3, p2;
if (size > 4) {
p0 = new Point2D(points.get(0), points.get(1));
p1 = new Point2D(points.get(2), points.get(3));
p3 = new Point2D(points.get(size - 2), points.get(size - 1));
p2 = new Point2D(points.get(size - 4), points.get(size - 3));
} else if (size == 4) {
p2 = p0 = new Point2D(points.get(0), points.get(1));
p3 = p1 = new Point2D(points.get(2), points.get(3));
} else {
p2 = p0 = p1 = p3 = new Point2D(0, 0);
}
updateMarkerNode(ctx, g, startMarkerNode,
new PointAndDerivative(p0.getX(), p0.getY(), p1.getX() - p0.getX(), p1.getY() - p0.getY()),
startMarkerShape, getMarkerStartScaleFactor());
final ImmutableList<PathElement> endMarkerShape = getMarkerEndShape();
updateMarkerNode(ctx, g, endMarkerNode,
new PointAndDerivative(p3.getX(), p3.getY(), p2.getX() - p3.getX(), p2.getY() - p3.getY()),
endMarkerShape, getMarkerEndScaleFactor());

updateLineNode(ctx, lineNode);
updateStartMarkerNode(ctx, startMarkerNode);
updateEndMarkerNode(ctx, endMarkerNode);
} catch (Throwable t) {
t.printStackTrace();
}

updateLineNode(ctx, lineNode);
updateStartMarkerNode(ctx, startMarkerNode);
updateEndMarkerNode(ctx, endMarkerNode);
}

protected void updateMarkerNode(RenderContext ctx, Group group,
Expand Down Expand Up @@ -243,8 +242,6 @@ public void layout(RenderContext ctx) {
Connector endConnector = get(END_CONNECTOR);
Figure startTarget = get(START_TARGET);
Figure endTarget = get(END_TARGET);
CssSize elbowOffset1 = getElbowOffset();
double elbowOffset = elbowOffset1 == null ? 0.0 : ctx.getNonNull(RenderContext.UNIT_CONVERTER_KEY).convert(elbowOffset1, UnitConverter.DEFAULT);


ObservableList<Double> points = path.getPoints();
Expand All @@ -258,8 +255,10 @@ public void layout(RenderContext ctx) {
end = endConnector.getPointAndDerivativeInWorld(this, endTarget).getPoint(Point2D::new);
}
// Chop start and end points
Point2D startDerivative = null;
if (startConnector != null && startTarget != null) {
IntersectionPointEx intersectionPointEx = startConnector.chopStart(ctx, this, startTarget, start, end);
startDerivative = new Point2D(intersectionPointEx.getDerivativeB().getX(), intersectionPointEx.getDerivativeB().getY());
start = worldToParent(intersectionPointEx.getX(), intersectionPointEx.getY());
set(START, new CssPoint2D(start));
}
Expand All @@ -270,32 +269,110 @@ public void layout(RenderContext ctx) {
end = worldToParent(intersectionPointEx.getX(), intersectionPointEx.getY());
set(END, new CssPoint2D(end));
}
Point2D lineDerivative = end.subtract(start);
if (startDerivative == null) {
startDerivative = lineDerivative;
}
if (endDerivative == null) {
endDerivative = lineDerivative.multiply(-1);
}

CssSize elbowOffsetSize = getElbowOffset();
if (elbowOffset == 0 || endDerivative == null || FXGeom.squaredMagnitude(endDerivative) < 1e-7) {
points.addAll(start.getX(), start.getY());
points.addAll(end.getX(), end.getY());

points.add(start.getX());
points.add(start.getY());
if (start.getX() == end.getX() || start.getY() == end.getY()) {
// case 1: The line is horizontal or vertical: we draw a straight line
// nothing to do
} else {
Point2D endDerivativeNormalized = endDerivative.normalize();
// Enforce perfect vertical or perfect horizontal line
if (abs(endDerivativeNormalized.getX()) > abs(endDerivativeNormalized.getY())) {
endDerivativeNormalized = new Point2D(signum(endDerivativeNormalized.getX()), 0);
} else {
endDerivativeNormalized = new Point2D(0, signum(endDerivativeNormalized.getY()));
// Compute perpendicular to boundary of start and end target shape
Point2D startPerp = FXGeom.perp(startDerivative).normalize();
Point2D endPerp = FXGeom.perp(endDerivative).normalize();
// Flip perpendiculars if necessary, so that each points towards the other shape
Point2D lineDerivativeNormalized = lineDerivative.normalize();
if (!FXGeom.isSameDirection(startPerp, lineDerivativeNormalized)) {
startPerp = new Point2D(-startPerp.getX(), -startPerp.getY());
}
Point2D dir = new Point2D(endDerivative.getY(), -endDerivative.getX()).normalize();
Point2D p1;
Point2D p2;
if (UnitConverter.PERCENTAGE.equals(elbowOffsetSize.getUnits())) {
elbowOffset = elbowOffsetSize.getConvertedValue() * abs(dir.dotProduct(end.subtract(start)));
if (FXGeom.isSameDirection(lineDerivativeNormalized, endPerp)) {
endPerp = new Point2D(-endPerp.getX(), -endPerp.getY());
}
// Round perpendiculars to 90° angles
startPerp = FXGeom.normalizeTo90Degrees(startPerp);
endPerp = FXGeom.normalizeTo90Degrees(endPerp);

CssSize elbowOffsetSize = getElbowOffset();
double elbowOffset = elbowOffsetSize == null ? 0 : ctx.getNonNull(UNIT_CONVERTER_KEY).convert(elbowOffsetSize, UnitConverter.DEFAULT);

double cosine = startPerp.dotProduct(endPerp);
if (almostEqual(cosine, 0)) {
if (elbowOffset <= 0) {
// case 2: the lines meet at a 90 degrees angle: we draw an 'L'-shape
var intersection = IntersectRayRay.intersectRayRayEx(
start.getX(), start.getY(), startPerp.getX(), startPerp.getY(),
end.getX(), end.getY(), endPerp.getX(), endPerp.getY()
).intersections().getFirst();
points.add(intersection.getX());
points.add(intersection.getY());
} else {
// case 3: the lines miss their meeting point, we draw a '|_|⎺'-shape
Point2D p1 = start.add(startPerp.multiply(elbowOffset));
double distance2 = -endPerp.dotProduct(end.subtract(p1));
double actualOffset2 = distance2 * 0.5;
Point2D p2 = p1.add(endPerp.multiply(-actualOffset2));
Point2D p3 = end.add(endPerp.multiply(distance2 - actualOffset2));
points.add(p1.getX());
points.add(p1.getY());
points.add(p2.getX());
points.add(p2.getY());
points.add(p3.getX());
points.add(p3.getY());
}
} else {
double distance = startPerp.dotProduct(lineDerivative);
double actualOffset = (elbowOffset <= 0) ? distance * 0.5 : elbowOffset;
if (actualOffset <= distance) {
// case 4: we draw a Z-shape
Point2D p1 = start.add(startPerp.multiply(actualOffset));
Point2D p2 = end.add(endPerp.multiply(distance - actualOffset));
points.add(p1.getX());
points.add(p1.getY());
points.add(p2.getX());
points.add(p2.getY());

} else {
// case 4: we draw a Z-shape
Point2D p1 = start.add(startPerp.multiply(actualOffset));
points.add(p1.getX());
points.add(p1.getY());
double distance2 = endPerp.dotProduct(end.subtract(p1));
double actualOffset2 = distance2 * 0.5;
Point2D p3 = end.add(endPerp.multiply(distance2 - actualOffset2));
connectWithElbowLine(p1, p3, FXGeom.perp(startPerp), points);
points.add(p3.getX());
points.add(p3.getY());
}
}
p2 = endDerivativeNormalized.multiply(elbowOffset);
p1 = endDerivativeNormalized.multiply(abs(dir.dotProduct(end.subtract(start))) - elbowOffset);

points.addAll(start.getX(), start.getY());
points.addAll(start.getX() - p1.getY(), start.getY() + p1.getX());
points.addAll(end.getX() + p2.getY(), end.getY() - p2.getX());
points.addAll(end.getX(), end.getY());
}

points.add(end.getX());
points.add(end.getY());

}

/**
* Connects two points with a z-shaped elbow line.
*
* @param a the first point
* @param b the second point
* @param dir the direction of the z-shape
* @param points
*/
private void connectWithElbowLine(Point2D a, Point2D b, Point2D dir, ObservableList<Double> points) {
Point2D derivative = b.subtract(a);
double distance = dir.dotProduct(derivative);
double offsetDistance = distance * 0.5;
points.add(a.getX() + dir.getX() * offsetDistance);
points.add(a.getY() + dir.getY() * offsetDistance);
points.add(b.getX() - dir.getX() * (distance - offsetDistance));
points.add(b.getY() - dir.getY() * (distance - offsetDistance));
}
}
Loading

0 comments on commit 7219487

Please sign in to comment.