Skip to content

Commit

Permalink
Compute the bounds of a PathIterator based on the extreme points of q…
Browse files Browse the repository at this point in the history
…uadratic and cubic bezier curves.
  • Loading branch information
wrandelshofer committed Jan 20, 2024
1 parent 3d9e001 commit eda8806
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.util.List;

/**
Expand Down Expand Up @@ -90,18 +91,8 @@ public void createHandles(@NonNull HandleType handleType, @NonNull List<Handle>

@Override
public @NonNull Bounds getLayoutBounds() {
// XXX should be cached
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
for (BezierNode p : getNonNull(PATH)) {
minX = Math.min(minX, p.getMinX());
minY = Math.min(minY, p.getMinY());
maxX = Math.max(maxX, p.getMaxX());
maxY = Math.max(maxY, p.getMaxY());
}
return new BoundingBox(minX, minY, maxX - minX, maxY - minY);
Rectangle2D b = getNonNull(PATH).getBounds2D();
return new BoundingBox(b.getX(), b.getY(), b.getWidth(), b.getHeight());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import javafx.geometry.BoundingBox;
import javafx.scene.shape.Rectangle;
import org.jhotdraw8.annotation.NonNull;
import org.jhotdraw8.annotation.Nullable;

import java.awt.geom.Rectangle2D;

import static java.lang.Math.max;
import static java.lang.Math.min;
Expand Down Expand Up @@ -41,8 +42,10 @@ public void addToBounds(double x, double y) {

@Override
protected void doCurveTo(double lastX, double lastY, double x1, double y1, double x2, double y2, double x3, double y3) {
addToBounds(x1, y1);
addToBounds(x2, y2);
for (double t : CubicCurveCharacteristics.extremePoints(lastX, lastY, x1, y1, x2, y2, x3, y3)) {
PointAndDerivative eval = CubicCurves.eval(lastX, lastY, x1, y1, x2, y2, x3, y3, t);
addToBounds(eval.x(), eval.y());
}
addToBounds(x3, y3);
}

Expand All @@ -58,17 +61,27 @@ protected void doMoveTo(double x, double y) {

@Override
protected void doQuadTo(double lastX, double lastY, double x1, double y1, double x2, double y2) {
addToBounds(x1, y1);
for (double t : QuadCurveCharacteristics.extremePoints(lastX, lastY, x1, y1, x2, y2)) {
PointAndDerivative eval = QuadCurves.eval(lastX, lastY, x1, y1, x2, y2, t);
addToBounds(eval.x(), eval.y());
}
addToBounds(x2, y2);
}

public @Nullable Rectangle getRectangle() {
public @NonNull Rectangle buildRectangle() {
if (Double.isNaN(minx)) {
return null;
return new Rectangle(0, 0, 0, 0);
}
return new Rectangle(minx, miny, maxx - minx, maxy - miny);
}

public Rectangle2D.@NonNull Double buildRectangle2D() {
if (Double.isNaN(minx)) {
return new Rectangle2D.Double(0, 0, 0, 0);
}
return new Rectangle2D.Double(minx, miny, maxx - minx, maxy - miny);
}

@Override
public @NonNull BoundingBox build() {
if (Double.isNaN(minx)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,51 @@ public static double[] align(double x0, double y0,
}
return null;
}

/**
* Computes the extreme points of the given cubic curve.
*/
public static @NonNull DoubleArrayList extremePoints(CubicCurve2D.Double c) {
return extremePoints(c.x1, c.y1, c.ctrlx1, c.ctrly1, c.ctrlx2, c.ctrly2, c.x2, c.y2);
}

/**
* Computes the extreme points of the given cubic curve.
* <p>
* References:
* <dl>
* <dt>Extremes for Bézier curves</dt>
* <dd><a href="https://github.polettix.it/ETOOBUSY/2020/07/09/bezier-extremes/">github.polettix.it</a></dd>
* </dl>
*/
public static @NonNull DoubleArrayList extremePoints(double x0, double y0,
double x1, double y1,
double x2, double y2,
double x3, double y3) {
double ax, ay, bx, by, cx, cy, t0, t1, t2, t3;
double detx, dety;

cx = 3 * (x1 - x0);
cy = 3 * (y1 - y0);
bx = 6 * (x0 - 2 * x1 + x2);
by = 6 * (y0 - 2 * y1 + y2);
ax = 3 * (-x0 + 3 * x1 - 3 * x2 + x3);
ay = 3 * (-y0 + 3 * y1 - 3 * y2 + y3);

detx = sqrt((bx / 2) * (bx / 2) - ax * cx);
dety = sqrt((by / 2) * (by / 2) - ay * cy);
t0 = (-bx / 2 + detx) / ax;
t2 = (-bx / 2 - detx) / ax;
t1 = (-by / 2 + dety) / ay;
t3 = (-by / 2 - dety) / ay;


var list = new DoubleArrayList();
if (0 <= t0 && t0 <= 1) list.add(t0);
if (0 <= t1 && t1 <= 1) list.add(t1);
if (0 <= t2 && t2 <= 1) list.add(t2);
if (0 <= t3 && t3 <= 1) list.add(t3);
return list;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.jhotdraw8.geom;

import org.jhotdraw8.annotation.NonNull;
import org.jhotdraw8.collection.primitive.DoubleArrayList;

import java.awt.geom.QuadCurve2D;

public class QuadCurveCharacteristics {
/**
* Don't let anyone instantiate this class.
*/
private QuadCurveCharacteristics() {
}

/**
* Computes the extreme points of the given quadratic curve.
*/
public static @NonNull DoubleArrayList extremePoints(QuadCurve2D.Double c) {
return extremePoints(c.x1, c.y1, c.ctrlx, c.ctrly, c.x2, c.y2);
}

/**
* Computes the extreme points of the given quadratic curve.
* <p>
* References:
* <dl>
* <dt>Extremes for Bézier curves</dt>
* <dd><a href="https://github.polettix.it/ETOOBUSY/2020/07/09/bezier-extremes/">github.polettix.it</a></dd>
* </dl>
*/
public static @NonNull DoubleArrayList extremePoints(double x0, double y0,
double x1, double y1,
double x2, double y2) {
double qx, qy, mx, my, t0, t1;

qx = x1 - x0;
qy = y1 - y0;
mx = x0 - 2 * x1 + x2;
my = y0 - 2 * y1 + y2;

t0 = -qx / mx;
t1 = -qy / my;

var list = new DoubleArrayList();
if (0 <= t0 && t0 <= 1) {
list.add(t0);
}
if (0 <= t1 && t1 <= 1) {
list.add(t1);
}
return list;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.jhotdraw8.annotation.NonNull;
import org.jhotdraw8.annotation.Nullable;
import org.jhotdraw8.geom.AwtShapes;
import org.jhotdraw8.geom.BoundingBoxBuilder;
import org.jhotdraw8.geom.CubicCurves;
import org.jhotdraw8.geom.PathMetrics;
import org.jhotdraw8.geom.PointAndDerivative;
Expand Down Expand Up @@ -42,6 +43,10 @@ public class BezierPath extends SimpleImmutableList<BezierNode> implements Shape
* This field is used for memoizing PathMetrics that have been built fom this instance.
*/
private transient @Nullable PathMetrics pathMetrics;
/**
* This field is used for memoizing Bounds that have been built fom this instance.
*/
private transient Rectangle2D.@Nullable Double bounds;
private final int windingRule;

private BezierPath(int windingRule) {
Expand Down Expand Up @@ -101,57 +106,10 @@ public Rectangle getBounds() {

@Override
public @NonNull Rectangle2D getBounds2D() {
double x1 = Double.POSITIVE_INFINITY, y1 = Double.POSITIVE_INFINITY,
x2 = Double.NEGATIVE_INFINITY, y2 = Double.NEGATIVE_INFINITY;
for (BezierNode n : this) {
double y = n.pointY();
double x = n.pointX();
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
if (n.hasIn()) {
y = n.inY();
x = n.inX();
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
}
if (n.hasOut()) {
y = n.outY();
x = n.outX();
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
}
if (bounds == null) {
bounds = AwtShapes.buildFromPathIterator(new BoundingBoxBuilder(), getPathIterator(null)).buildRectangle2D();
}
return new Rectangle2D.Double(x1, y1, x2 - x1, y2 - y1);
return new Rectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height);
}

@Override
Expand Down

0 comments on commit eda8806

Please sign in to comment.