/*******************************************************************************
* Copyright (c) 2011, 2017 itemis AG and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Alexander Nyßen (itemis AG) - initial API and implementation
* Matthias Wienand (itemis AG) - contribution for Bugzilla #355997
*
*******************************************************************************/
package org.eclipse.gef.geometry.planar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import org.eclipse.gef.geometry.euclidean.Angle;
import org.eclipse.gef.geometry.euclidean.Straight;
import org.eclipse.gef.geometry.euclidean.Vector;
import org.eclipse.gef.geometry.internal.utils.PointListUtils;
import org.eclipse.gef.geometry.internal.utils.PrecisionUtils;
import org.eclipse.gef.geometry.projective.Straight3D;
import org.eclipse.gef.geometry.projective.Vector3D;
/**
* <p>
* Instances of the {@link BezierCurve} class individually represent an
* arbitrary Bezier curve. This is the base class of the special quadratic and
* cubic Bezier curve classes ({@link QuadraticCurve} and {@link CubicCurve}).
* </p>
*
* @author anyssen
* @author mwienand
*
*/
public class BezierCurve extends AbstractGeometry
implements ICurve, ITranslatable<BezierCurve>, IScalable<BezierCurve>,
IRotatable<BezierCurve> {
private static class CuspAwareOffsetApproximator {
private static class Cusp extends PartialCurve {
public Cusp(BezierCurve c, double t0, double t1) {
super(c, t0, t1);
}
}
private static interface ICurveSimplifier {
public List<PartialCurve> simplify(BezierCurve curve);
}
private static interface ICuspSplitter {
public List<PartialCurve> splitAtCusps(BezierCurve curve);
}
private static interface IOffsetAlgorithm {
public static class PartialOffset {
public BezierCurve offset;
public double curveStart;
public double curveEnd;
public PartialOffset(BezierCurve offset, double t0, double t1) {
curveStart = t0;
curveEnd = t1;
this.offset = offset;
}
public PartialOffset(PartialCurve pc, BezierCurve offset) {
this(offset, pc.start, pc.end);
}
}
public List<PartialOffset> computeOffset(BezierCurve curve,
double distance);
}
private static class LasserCurveSimplifier implements ICurveSimplifier {
private static final int DEFAULT_MAX_DEPTH = 16;
private int maxDepth;
public LasserCurveSimplifier() {
this(DEFAULT_MAX_DEPTH);
}
public LasserCurveSimplifier(int maxDepth) {
this.maxDepth = maxDepth;
}
private double computeAngleSum(BezierCurve curve) {
double angleSum = 0d;
Point[] points = curve.getPoints();
for (int i = 0; i < points.length - 2; i++) {
Vector first = new Vector(points[i], points[i + 1]);
Vector second = new Vector(points[i + 1], points[i + 2]);
if (first.getLength() * second.getLength() > 0) {
Angle angle = first.getAngle(second);
angleSum += angle.rad();
}
}
return angleSum;
}
private List<PartialCurve> computeLasserWithParams(
PartialCurve partialCurve, int currentDepth) {
BezierCurve curve = partialCurve.curve
.getClipped(partialCurve.start, partialCurve.end);
double angleSum = computeAngleSum(curve);
List<PartialCurve> spline = new ArrayList<>();
if (currentDepth < maxDepth && angleSum > Math.PI) {
PartialCurve[] split = partialCurve.split();
List<PartialCurve> left = computeLasserWithParams(split[0],
currentDepth + 1);
List<PartialCurve> right = computeLasserWithParams(split[1],
currentDepth + 1);
spline.addAll(left);
spline.addAll(right);
} else {
spline.add(partialCurve);
}
return spline;
}
@Override
public List<PartialCurve> simplify(BezierCurve curve) {
return computeLasserWithParams(new PartialCurve(curve, 0, 1),
0);
}
}
private static class PartialCurve {
public BezierCurve curve;
public double start;
public double end;
public PartialCurve(BezierCurve c, double t0, double t1) {
curve = c;
start = t0;
end = t1;
}
public PartialCurve[] split() {
double mid = start + 0.5 * (end - start);
return new PartialCurve[] { new PartialCurve(curve, start, mid),
new PartialCurve(curve, mid, end) };
}
}
private static class SamplingCuspSplitter implements ICuspSplitter {
private static final int DEFAULT_MAX_DEPTH = 4;
private static final int DEFAULT_SAMPLE_COUNT = 128;
private static final double DEFAULT_MIN_ANGLE_RAD = Angle
.fromDeg(10).rad();
private int sampleCount;
private double minAngleRad;
private int maxDepth;
private BezierCurve curve;
public SamplingCuspSplitter() {
this(DEFAULT_SAMPLE_COUNT, DEFAULT_MIN_ANGLE_RAD,
DEFAULT_MAX_DEPTH);
}
public SamplingCuspSplitter(int sampleCount, double minAngle,
int maxDepth) {
if (sampleCount < 2) {
throw new IllegalArgumentException("sampleCount < 2");
}
this.sampleCount = sampleCount;
this.minAngleRad = minAngle;
this.maxDepth = maxDepth;
}
private List<Cusp> getCusps() {
List<Cusp> cusps = new ArrayList<>();
BezierCurve hodograph = curve.getDerivative();
Point lastDirection = null;
double lastT = 0;
for (int i = 0; i < sampleCount; i++) {
double t = i / (double) (sampleCount - 1);
Point direction = hodograph.get(t);
if (lastDirection != null && !direction.equals(0, 0)) {
Angle angle = new Vector(direction)
.getAngle(new Vector(lastDirection));
if (angle.rad() > minAngleRad) {
cusps.add(refineCusp(lastT, t, 0));
}
}
if (!direction.equals(0, 0)) {
lastDirection = direction;
lastT = t;
}
}
if (cusps.size() > 1) {
// filter out same cusps
Cusp lastCusp = cusps.get(cusps.size() - 1);
for (int i = cusps.size() - 2; i >= 0; i--) {
Cusp c = cusps.get(i);
if (curve.get(c.start)
.getDistance(curve.get(lastCusp.start)) < 1) {
// advance cusp parameters
if (lastCusp.start > c.start) {
lastCusp.start = c.start;
}
if (lastCusp.end < c.end) {
lastCusp.end = c.end;
}
// remove same cusp
cusps.remove(i);
} else {
lastCusp = c;
}
}
}
return cusps;
}
private Cusp refineCusp(double t0, double t1, int depth) {
// do not refine further if the cusp is already precise
Point pa = curve.get(t0);
Point pb = curve.get(t1);
if (pa.getDistance(pb) < 0.2) {
return new Cusp(curve, t0, t1);
}
BezierCurve hodograph = curve.getDerivative();
Double maxRad = null;
Point lastDirection = null;
double lastT = 0;
double maxA = t0, maxB = t1;
for (int i = 0; i < sampleCount; i++) {
double t = t0 + (t1 - t0) * i / (sampleCount - 1);
Point direction = hodograph.get(t);
if (lastDirection != null && !direction.equals(0, 0)) {
Angle angle = new Vector(direction)
.getAngle(new Vector(lastDirection));
if (maxRad == null || angle.rad() > maxRad) {
maxRad = angle.rad();
maxA = lastT;
maxB = t;
}
}
if (!direction.equals(0, 0)) {
lastDirection = direction;
lastT = t;
}
}
if (depth < maxDepth) {
return refineCusp(maxA, maxB, depth + 1);
} else {
return new Cusp(curve, maxA, maxB);
}
}
@Override
public List<PartialCurve> splitAtCusps(BezierCurve curve) {
this.curve = curve;
List<Cusp> cusps = getCusps();
List<PartialCurve> cc = new ArrayList<>();
if (cusps.isEmpty()) {
cc.add(new PartialCurve(curve, 0, 1));
return cc;
}
// add initial curve
cc.add(new PartialCurve(curve.split(cusps.get(0).start)[0], 0,
1));
// and initial cusp
cc.add(cusps.get(0));
for (int i = 1; i < cusps.size(); i++) {
// add curve from previous end to current start
cc.add(new PartialCurve(curve.getClipped(
cusps.get(i - 1).end, cusps.get(i).start), 0, 1));
// add cusp
cc.add(cusps.get(i));
}
// add final curve
cc.add(new PartialCurve(
curve.split(cusps.get(cusps.size() - 1).end)[1], 0, 1));
return cc;
}
}
public static class TillerHansonOffsetAlgorithm
implements IOffsetAlgorithm {
private static class ControlLeg {
public ControlVertex start;
public ControlVertex end;
public ControlLeg(ControlVertex start, ControlVertex end) {
this.start = new ControlVertex(start.position,
start.multiplicity);
this.end = new ControlVertex(end.position,
end.multiplicity);
}
}
private static class ControlVertex {
public Point position;
public int multiplicity;
public ControlVertex(Point pos) {
this.position = pos.getCopy();
this.multiplicity = 1;
}
public ControlVertex(Point pos, int mult) {
this.position = pos.getCopy();
this.multiplicity = mult;
}
}
private static final double DEFAULT_ACCEPTABLE_ERROR = 0.1;
private static final int DEFAULT_MAX_DEPTH = 32;
private double acceptableError;
private int maxDepth;
private double distance;
public TillerHansonOffsetAlgorithm() {
this(DEFAULT_ACCEPTABLE_ERROR, DEFAULT_MAX_DEPTH);
}
public TillerHansonOffsetAlgorithm(double acceptableError,
int maxDepth) {
this.acceptableError = acceptableError;
this.maxDepth = maxDepth;
}
private BezierCurve approximateOffset(BezierCurve curve) {
// collect ControlVertex objects for all unique subsequent
// points
// of the curve
Point[] curvePoints = curve.getPoints();
List<ControlVertex> curveVertices = new ArrayList<>();
curveVertices.add(new ControlVertex(curvePoints[0]));
for (int i = 1; i < curvePoints.length; i++) {
Point p = curvePoints[i];
ControlVertex lastVertex = curveVertices
.get(curveVertices.size() - 1);
if (lastVertex.position.equals(p)) {
lastVertex.multiplicity++;
} else {
curveVertices.add(new ControlVertex(p));
}
}
// we need at least two vertices to be able to approximate an
// offset
if (curveVertices.size() < 2) {
return curve.getCopy();
}
// build ControlLeg objects for the ControlVertex objects
List<ControlLeg> legs = new ArrayList<>();
for (int i = 0; i < curveVertices.size() - 1; i++) {
ControlVertex start = curveVertices.get(i);
ControlVertex end = curveVertices.get(i + 1);
legs.add(new ControlLeg(start, end));
}
// compute offset control legs
List<ControlLeg> offsetLegs = new ArrayList<>();
for (ControlLeg leg : legs) {
Vector direction = new Vector(leg.start.position,
leg.end.position);
if (direction.isNull()) {
// should not be possible since we eliminated all
// subsequent
// equal points using ControlVertex
throw new IllegalStateException(
"[ERROR] Leg direction cannot be computed because start and end position are the same.");
} else {
Point translation = direction.getOrthogonalComplement()
.getNormalized().getMultiplied(distance)
.toPoint();
ControlVertex offsetStart = new ControlVertex(
leg.start.position.getTranslated(translation));
ControlVertex offsetEnd = new ControlVertex(
leg.end.position.getTranslated(translation));
offsetLegs.add(new ControlLeg(offsetStart, offsetEnd));
}
}
// compute intersections between offset ControlLeg objects
List<ControlVertex> offsetVertices = new ArrayList<>();
offsetVertices.add(offsetLegs.get(0).start);
for (int i = 1; i < offsetLegs.size(); i++) {
// find intersection with previous leg
ControlLeg previousLeg = offsetLegs.get(i - 1);
ControlLeg currentLeg = offsetLegs.get(i);
Straight s1 = new Straight(previousLeg.start.position,
previousLeg.end.position);
Straight s2 = new Straight(currentLeg.start.position,
currentLeg.end.position);
Vector intersection = s1.getIntersection(s2);
if (intersection == null) {
// use mid of legs' endpoints as the intersection
Point p1 = previousLeg.end.position;
Point p2 = currentLeg.start.position;
Point mid = new Point(p1.x + p2.x, p1.y + p2.y)
.getScaled(0.5);
intersection = new Vector(mid);
}
offsetVertices.add(new ControlVertex(intersection.toPoint(),
currentLeg.start.multiplicity));
}
offsetVertices.add(offsetLegs.get(offsetLegs.size() - 1).end);
// collect offset points, respecting multiplicity of vertices
List<Point> offsetPoints = new ArrayList<>();
for (ControlVertex vertex : offsetVertices) {
for (int i = 0; i < vertex.multiplicity; i++) {
offsetPoints.add(vertex.position.getCopy());
}
}
// construct bezier curve from approx offset points
return new BezierCurve(offsetPoints.toArray(new Point[0]));
}
@Override
public List<PartialOffset> computeOffset(BezierCurve curve,
double distance) {
this.distance = distance;
return computeTillerHansonWithParams(
new PartialCurve(curve, 0, 1), 0);
}
private double computeOffsetError(BezierCurve curve,
BezierCurve hodograph, BezierCurve approx) {
Double error = null;
int N = curve.getPoints().length * 4;
for (int i = 0; i < N; i++) {
double t = i / (double) (N - 1);
// evaluate offset
Point position = curve.get(t);
Point derivative = hodograph.get(t);
Vector tangent = new Vector(derivative);
if (tangent.getLength() > 0) {
Point direction = tangent.getNormalized()
.getOrthogonalComplement()
.getMultiplied(distance).toPoint();
Point offset = position.getTranslated(direction);
double delta = approx.get(t).getDistance(offset);
if (error == null || delta > error) {
error = delta;
}
}
}
return error == null ? -1 : error.doubleValue();
}
private List<PartialOffset> computeTillerHansonWithParams(
PartialCurve partialCurve, int currentDepth) {
BezierCurve curve = partialCurve.curve
.getClipped(partialCurve.start, partialCurve.end);
BezierCurve approx = approximateOffset(curve);
double error = computeOffsetError(curve, curve.getDerivative(),
approx);
List<PartialOffset> sapprox = new ArrayList<>();
if (currentDepth < maxDepth && error >= acceptableError) {
PartialCurve[] s = partialCurve.split();
List<PartialOffset> l = computeTillerHansonWithParams(s[0],
currentDepth + 1);
List<PartialOffset> r = computeTillerHansonWithParams(s[1],
currentDepth + 1);
sapprox.addAll(l);
sapprox.addAll(r);
} else if (error >= 0) {
sapprox.add(new PartialOffset(partialCurve, approx));
}
return sapprox;
}
}
private ICurveSimplifier curveSimplifier;
private IOffsetAlgorithm offsetAlgorithm;
private ICuspSplitter cuspSplitter;
public CuspAwareOffsetApproximator() {
this(new LasserCurveSimplifier(), new TillerHansonOffsetAlgorithm(),
new SamplingCuspSplitter());
}
public CuspAwareOffsetApproximator(ICurveSimplifier curveSimplifier,
IOffsetAlgorithm offsetAlgorithm, ICuspSplitter cuspSplitter) {
this.curveSimplifier = curveSimplifier;
this.offsetAlgorithm = offsetAlgorithm;
this.cuspSplitter = cuspSplitter;
}
public OffsetApproximation approximateOffset(BezierCurve curve,
double distance) {
List<BezierCurve> simpleCurve = new ArrayList<>();
List<BezierCurve> approxOffsetCurve = new ArrayList<>();
Map<Integer, Integer> approx2simple = new HashMap<>();
Map<Integer, Double> a2sParamStart = new HashMap<>();
Map<Integer, Double> a2sParamEnd = new HashMap<>();
List<PartialCurve> cuspsExtracted = cuspSplitter
.splitAtCusps(curve);
for (PartialCurve cc : cuspsExtracted) {
if (!(cc instanceof Cusp)) {
// remove self intersections
List<PartialCurve> simplified = curveSimplifier
.simplify(cc.curve);
List<BezierCurve> simplifiedCurves = new ArrayList<>(
simplified.size());
for (PartialCurve pc : simplified) {
simplifiedCurves
.add(pc.curve.getClipped(pc.start, pc.end));
}
int simpleSize = simpleCurve.size();
simpleCurve.addAll(simplifiedCurves);
for (int j = 0; j < simplifiedCurves.size(); j++) {
BezierCurve simple = simplifiedCurves.get(j);
List<IOffsetAlgorithm.PartialOffset> parts = offsetAlgorithm
.computeOffset(simple, distance);
for (IOffsetAlgorithm.PartialOffset part : parts) {
List<PartialCurve> splitApprox = curveSimplifier
.simplify(part.offset);
int approxSize = approxOffsetCurve.size();
for (PartialCurve pc : splitApprox) {
approxOffsetCurve.add(
pc.curve.getClipped(pc.start, pc.end));
}
for (int i = 0; i < splitApprox.size(); i++) {
approx2simple.put(approxSize + i,
simpleSize + j);
PartialCurve pca = splitApprox.get(i);
double width = part.curveEnd - part.curveStart;
double c0 = part.curveStart + pca.start * width;
double c1 = part.curveStart + pca.end * width;
a2sParamStart.put(approxSize + i, c0);
a2sParamEnd.put(approxSize + i, c1);
}
}
}
} else {
// the point of the arc serves as the center of the arc
Point center = curve.get(cc.start / 2 + cc.end / 2);
// compute start and end normals
Point startDirection = curve.getDerivative().get(cc.start);
while (startDirection.equals(0, 0) && cc.start > 0) {
cc.start -= 0.0001;
if (cc.start < 0) {
cc.start = 0;
}
startDirection = curve.getDerivative().get(cc.start);
}
Point endDirection = curve.getDerivative().get(cc.end);
while (endDirection.equals(0, 0) && cc.end < 1) {
cc.end += 0.0001;
if (cc.end > 1) {
cc.end = 1;
}
endDirection = curve.getDerivative().get(cc.end);
}
if (startDirection.equals(0, 0)) {
startDirection.setLocation(endDirection);
}
if (endDirection.equals(0, 0)) {
endDirection.setLocation(startDirection);
}
if (startDirection.equals(0, 0)
|| endDirection.equals(0, 0)) {
System.out.println("ERROR");
Point baselineDirection = new Vector(cc.curve.get(0),
cc.curve.get(1)).toPoint();
startDirection.setLocation(baselineDirection);
endDirection.setLocation(baselineDirection);
}
Vector startNormal = new Vector(startDirection)
.getNormalized().getOrthogonalComplement();
Vector endNormal = new Vector(endDirection).getNormalized()
.getOrthogonalComplement();
// compute angle between start and end normals
Angle angleCCW = startNormal.getAngleCCW(endNormal);
Angle angleCW = startNormal.getAngleCW(endNormal);
// compute start start angle and length angle for the
// arc
Angle arcStartAngle;
Angle arcLengthAngle;
if (angleCCW.rad() < angleCW.rad()) {
arcStartAngle = new Vector(1, 0)
.getAngleCCW(startNormal);
arcLengthAngle = angleCCW;
} else {
arcStartAngle = new Vector(1, 0).getAngleCCW(endNormal);
arcLengthAngle = angleCW;
}
// compute arc approximation
double absDistance = Math.abs(distance);
PolyBezier arc = new Arc(center.x - absDistance,
center.y - absDistance, 2 * absDistance,
2 * absDistance, arcStartAngle,
arcLengthAngle).getRotatedCCW(
Angle.fromDeg(distance < 0 ? 180 : 0));
List<BezierCurve> arcBezier = Arrays.asList(arc.toBezier());
// ensure arc beziers are in the correct order
Point lastOffsetPoint = curve.get(cc.start).getTranslated(
startNormal.getMultiplied(distance).toPoint());
if (lastOffsetPoint.getDistance(
arcBezier.get(0).getP1()) > lastOffsetPoint
.getDistance(
arcBezier.get(arcBezier.size() - 1)
.getP2())) {
// reverse curves
Collections.reverse(arcBezier);
for (int i = 0; i < arcBezier.size(); i++) {
BezierCurve c = arcBezier.get(i);
List<Point> pts = Arrays.asList(c.getPoints());
Collections.reverse(pts);
arcBezier.set(i, new BezierCurve(
pts.toArray(new Point[] {})));
}
}
// add arc and map to simple curve
int approxSize = approxOffsetCurve.size();
int simpleSize = simpleCurve.size();
int i = 0;
for (BezierCurve c : arcBezier) {
approxOffsetCurve.add(c);
approx2simple.put(approxSize + i, simpleSize - 1);
a2sParamStart.put(approxSize + i, 1d);
a2sParamEnd.put(approxSize + i, 1d);
i++;
}
}
}
return new OffsetApproximation(curve, distance, simpleCurve,
approxOffsetCurve, approx2simple, a2sParamStart,
a2sParamEnd);
}
}
/**
* <p>
* A {@link FatLine} combines a {@link Straight3D} with a positive and
* negative distance called dmax and dmin, respectively.
* </p>
* <p>
* It is used to apply a geometric clipping algorithm for finding
* {@link Point}s of intersection on two {@link BezierCurve}s. One of the
* {@link BezierCurve}s is bounded by a {@link FatLine} so that the other
* {@link BezierCurve} can be clipped against that {@link FatLine}.
* </p>
*/
private static class FatLine {
public static FatLine from(BezierCurve c, boolean ortho) {
FatLine L = new FatLine();
L.dmin = L.dmax = 0;
L.line = Straight3D.through(c.points[0],
c.points[c.points.length - 1]);
if (L.line == null) {
return null;
}
if (ortho) {
L.line = L.line.getOrtho();
}
if (L.line == null) {
return null;
}
for (int i = 0; i < c.points.length; i++) {
double d = L.line.getSignedDistanceCW(c.points[i]);
if (d < L.dmin) {
L.dmin = d;
} else if (d > L.dmax) {
L.dmax = d;
}
}
return L;
}
public Straight3D line;
public double dmin, dmax;
private FatLine() {
line = null;
dmin = dmax = 0;
}
}
/**
* An {@link Interval} records a lower and an upper limit that define the
* mathematical interval [a;b] (inclusively). It is used to represent
* sub-curves of a {@link BezierCurve} by bounding the {@link BezierCurve}'s
* parameter value to the respective interval.
*/
static final class Interval {
/**
* Constructs a new {@link Interval} object holding an invalid parameter
* interval.
*
* @return a new {@link Interval} object holding an invalid parameter
* interval
*/
public static Interval getEmpty() {
return new Interval(1, 0);
}
/**
* Constructs a new {@link Interval} object holding the interval [0;1]
* which is the parameter {@link Interval} representing a full
* {@link BezierCurve}.
*
* @return a new {@link Interval} object holding the interval [0;1]
*/
public static Interval getFull() {
return new Interval(0, 1);
}
/**
* Returns the smaller {@link Interval} object, i.e. the one with the
* smallest parameter range.
*
* @param i
* The first operand.
* @param j
* The second operand.
* @return The {@link Interval} with the smallest parameter range.
*/
public static Interval min(Interval i, Interval j) {
return (i.b - i.a) > (j.b - j.a) ? j : i;
}
/**
* An {@link Interval} records the parameter range [a;b]. Valid
* parameter ranges require 0 <= a <= b <= 1.
*/
public double a;
/**
* An {@link Interval} records the parameter range [a;b]. Valid
* parameter ranges require 0 <= a <= b <= 1.
*/
public double b;
/**
* <p>
* Constructs a new {@link Interval} object from the given double
* values. Only the first two double values are of importance as the
* rest of them are ignored.
* </p>
* <p>
* The new {@link Interval} holds the parameter range [a;b] if a is the
* first double value and b is the second double value.
* </p>
*
* @param ds
* the lower and upper limit for the {@link Interval} object
* to be created
*/
public Interval(double... ds) {
if (ds.length > 1) {
a = ds[0];
b = ds[1];
} else {
throw new IllegalArgumentException(
"not enough values to create interval");
}
}
/**
* Checks if this {@link Interval}'s parameter range does converge with
* default imprecision.
*
* @return <code>true</code> if a ~= b (within default imprecision),
* otherwise <code>false</code>
* @see Interval#converges(int)
*/
public boolean converges() {
return converges(0);
}
/**
* <p>
* Checks if this {@link Interval}'s parameter range does converge with
* specified imprecision.
* </p>
* <p>
* The imprecision is specified by providing a shift value which shifts
* the epsilon used for the number comparison. A positive shift demands
* for a smaller epsilon (higher precision) whereas a negative shift
* demands for a greater epsilon (lower precision).
* </p>
*
* @param shift
* precision shift
* @return <code>true</code> if a ~= b (within specified imprecision),
* otherwise <code>false</code>
*/
public boolean converges(int shift) {
return PrecisionUtils.equal(a, b, shift);
}
/**
* Expands this {@link Interval} to include the given other
* {@link Interval}.
*
* @param i
* The other {@link Interval} to which <code>this</code> is
* expanded.
*/
public void expand(Interval i) {
if (i.a < a) {
a = i.a;
}
if (i.b > b) {
b = i.b;
}
}
/**
* Returns a copy of this {@link Interval}.
*
* @return a copy of this {@link Interval}
*/
public Interval getCopy() {
return new Interval(a, b);
}
/**
* Returns the middle parameter value <code>m = (a+b)/2</code> of this
* {@link Interval}.
*
* @return the middle parameter value of this {@link Interval}
*/
public double getMid() {
return (a + b) / 2;
}
/**
* <p>
* Scales this {@link Interval} to the given {@link Interval}. The given
* {@link Interval} specifies the new upper and lower bounds of this
* {@link Interval} in percent.
* </p>
* <p>
* Returns the ratio of this {@link Interval}'s new parameter range to
* its old parameter range.
* </p>
*
* @param interval
* the new upper and lower bounds in percent
* @return the ratio of this {@link Interval}'s new parameter range to
* its old parameter range
*/
public double scaleTo(Interval interval) {
double na = a + interval.a * (b - a);
double nb = a + interval.b * (b - a);
double ratio = (nb - na) / (b - a);
a = na;
b = nb;
// ensure interval stays valid
if (a < 0) {
a = 0;
}
if (a > 1) {
a = 1;
b = 1;
}
if (b < 0) {
a = 0;
b = 0;
}
if (b > 1) {
b = 1;
}
return ratio;
}
}
// TODO: use constants that limit the number of iterations for the
// different iterative/recursive algorithms:
// INTERSECTIONS_MAX_ITERATIONS, APPROXIMATION_MAX_ITERATIONS
/**
* An {@link IntervalPair} combines two {@link BezierCurve}s and their
* corresponding parameter ranges.
*/
static final class IntervalPair {
/**
* The first {@link BezierCurve}.
*/
public BezierCurve p;
/**
* The second {@link BezierCurve}.
*/
public BezierCurve q;
/**
* The parameter {@link Interval} for the first {@link BezierCurve}.
*/
public Interval pi;
/**
* The parameter {@link Interval} for the second {@link BezierCurve}.
*/
public Interval qi;
/**
* Constructs a new {@link IntervalPair} with the given
* {@link BezierCurve}s and their corresponding parameter ranges.
*
* @param pp
* the first {@link BezierCurve}
* @param pt
* the parameter {@link Interval} for the first
* {@link BezierCurve}
* @param pq
* the second {@link BezierCurve}
* @param pu
* the parameter {@link Interval} for the second
* {@link BezierCurve}
*/
public IntervalPair(BezierCurve pp, Interval pt, BezierCurve pq,
Interval pu) {
p = pp;
pi = pt;
q = pq;
qi = pu;
}
/**
* Checks if both parameter {@link Interval}s do converge (@see
* Interval#converges()) or both {@link BezierCurve}s are degenerated,
* i.e. they are collapsed to a single {@link Point}.
*
* @return <code>true</code> if both parameter {@link Interval}s do
* converge, otherwise <code>false</code>
*/
public boolean converges() {
return converges(0);
}
/**
* Checks if both parameter {@link Interval}s do converge (@see
* Interval#converges(int)) or both {@link BezierCurve}s are
* degenerated, i.e. they are collapsed to a single {@link Point}.
*
* @param shift
* the precision shift
* @return <code>true</code> if both parameter {@link Interval}s do
* converge, otherwise <code>false</code>
*/
public boolean converges(int shift) {
return (pi.converges(shift) || pointsEquals(p.getHC(pi.a).toPoint(),
p.getHC(pi.b).toPoint(), shift))
&& (qi.converges(shift)
|| pointsEquals(q.getHC(qi.a).toPoint(),
q.getHC(qi.b).toPoint(), shift));
}
/**
* Returns <code>true</code> if the first interval converges to a single
* point. Otherwise returns <code>false</code>.
*
* @return <code>true</code> if the first interval converges to a single
* point, otherwise <code>false</code>.
*/
public boolean convergesP() {
return pointsEquals(p.getHC(pi.a).toPoint(),
p.getHC(pi.b).toPoint(), 0);
}
/**
* Returns <code>true</code> if the second interval converges to a
* single point. Otherwise returns <code>false</code>.
*
* @return <code>true</code> if the second interval converges to a
* single point, otherwise <code>false</code>.
*/
public boolean convergesQ() {
return pointsEquals(q.getHC(qi.a).toPoint(),
q.getHC(qi.b).toPoint(), 0);
}
/**
* Expands this {@link IntervalPair} to include the given other
* {@link IntervalPair}.
*
* @param ip
* The other {@link IntervalPair} to which <code>this</code>
* is expanded.
*/
public void expand(IntervalPair ip) {
if (p == ip.p) {
pi.expand(ip.pi);
qi.expand(ip.qi);
} else {
pi.expand(ip.qi);
qi.expand(ip.pi);
}
}
/**
* Returns a copy of this {@link IntervalPair}. The underlying
* {@link BezierCurve}s are only shallow copied. The corresponding
* parameter {@link Interval}s, contrairwise, are truly copied.
*
* @return a copy of this {@link IntervalPair}
*/
public IntervalPair getCopy() {
return new IntervalPair(p, pi.getCopy(), q, qi.getCopy());
}
/**
* Returns the first sub-curve of this {@link IntervalPair}. This curve
* is the first {@link BezierCurve} <i>p</i> over its corresponding
* parameter {@link Interval} <i>pi</i>.
*
* @return the first sub-curve of this {@link IntervalPair}
*/
public BezierCurve getPClipped() {
return p.getClipped(Math.max(pi.a, 0), Math.min(pi.b, 1));
}
/**
* Splits the first parameter {@link Interval} <i>pi</i> at half and
* returns the resulting {@link IntervalPair}s.
*
* @return two {@link IntervalPair}s representing a split of the first
* parameter {@link Interval} at half
*/
public IntervalPair[] getPSplit() {
double pm = (pi.a + pi.b) / 2;
return new IntervalPair[] {
new IntervalPair(p, new Interval(pi.a, pm), q,
qi.getCopy()),
new IntervalPair(p,
new Interval(
Math.min(pi.b,
pm + 10 * UNRECOGNIZABLE_PRECISION_FRACTION),
pi.b),
q, qi.getCopy()) };
}
/**
* Returns the second sub-curve of this {@link IntervalPair}. This curve
* is the second {@link BezierCurve} <i>q</i> over its corresponding
* parameter {@link Interval} <i>qi</i>.
*
* @return the second sub-curve of this {@link IntervalPair}
*/
public BezierCurve getQClipped() {
return q.getClipped(Math.max(qi.a, 0), Math.min(qi.b, 1));
}
/**
* Splits the second parameter {@link Interval} <i>qi</i> at half and
* returns the resulting {@link IntervalPair}s.
*
* @return two {@link IntervalPair}s representing a split of the second
* parameter {@link Interval} at half
*/
public IntervalPair[] getQSplit() {
double qm = (qi.a + qi.b) / 2;
return new IntervalPair[] {
new IntervalPair(q, new Interval(qi.a, qm), p,
pi.getCopy()),
new IntervalPair(q,
new Interval(
Math.min(qi.b,
qm + 10 * UNRECOGNIZABLE_PRECISION_FRACTION),
qi.b),
p, pi.getCopy()) };
}
/**
* Creates a new {@link IntervalPair} with swapped {@link BezierCurve}s
* and their parameter {@link Interval}s.
*
* @return a new {@link IntervalPair} with swapped {@link BezierCurve}s
* and their parameter {@link Interval}s
*/
public IntervalPair getSwapped() {
return new IntervalPair(q, qi.getCopy(), p, pi.getCopy());
}
/**
* Calculates which {@link BezierCurve}'s parameter {@link Interval} is
* longer.
*
* @return <code>true</code> if the distance from start to end parameter
* value of the first parameter {@link Interval} <i>pi</i> is
* greater than the distance from start to end parameter value
* of the second parameter {@link Interval} <i>qi</i>. Othwise,
* returns <code>false</code>.
*/
public boolean isPLonger() {
return (pi.b - pi.a) > (qi.b - qi.a);
}
}
/**
* <p>
* The {@link IPointCmp} interface specifies a method to determine which of
* two given {@link Point}s "is better than" the other.
* </p>
* <p>
* It is used to identify the bounding box of an arbitrary
* {@link BezierCurve} by searching for the minimal and maximal x and y
* coordinates while sub-dividing the {@link BezierCurve} until all of its
* control {@link Point}s are not "better than" the one selected.
* </p>
*/
private interface IPointCmp {
public boolean pIsBetterThanQ(Point p, Point q);
}
private static class LocalIntersectionOffsetRefiner {
private static interface ICurveIntersector {
public List<Point> getIntersections(BezierCurve cp, BezierCurve cq);
}
private static interface IGlobalIntersectionDetector {
public boolean isGlobalIntersection(OffsetApproximation oa,
int fstApproxIndex, int sndApproxIndex);
}
private static class Intersection {
public int ai;
public double ap;
public int bi;
public double bp;
public Intersection(int ai, double ap, int bi, double bp) {
this.ai = ai;
this.ap = ap;
this.bi = bi;
this.bp = bp;
}
@Override
public String toString() {
return ai + "," + ap + " : " + bi + "," + bp;
}
}
private static class LineSimilarityCurveIntersector
implements ICurveIntersector {
private static final double DEFAULT_LINE_SIMILARITY_THRESHOLD = 0.2d;
private static final int DEFAULT_MAX_DEPTH = 32;
private double lineSimilarityThreshold;
private int maxDepth;
public LineSimilarityCurveIntersector() {
this(DEFAULT_LINE_SIMILARITY_THRESHOLD, DEFAULT_MAX_DEPTH);
}
public LineSimilarityCurveIntersector(
double lineSimilarityThreshold, int maxDepth) {
this.lineSimilarityThreshold = lineSimilarityThreshold;
this.maxDepth = maxDepth;
}
@Override
public List<Point> getIntersections(BezierCurve cp,
BezierCurve cq) {
return getIntersections(cp, cq, 0);
}
private List<Point> getIntersections(BezierCurve cp, BezierCurve cq,
int currentDepth) {
// throw away curves where the control bounds are separate
if (!cp.getControlBounds().touches(cq.getControlBounds())) {
return Collections.emptyList();
}
// line intersection approximation
double lineSimilarityP = LineSimilarity(cp);
if (lineSimilarityP < lineSimilarityThreshold) {
double lineSimilarityQ = LineSimilarity(cq);
if (lineSimilarityQ < lineSimilarityThreshold) {
// compute line intersection
Point baselineIntersection = cp.toLine()
.getIntersection(cq.toLine());
if (baselineIntersection == null) {
return Collections.emptyList();
}
if (baselineIntersection != null) {
// project onto both curves
Point p = cp.getProjection(baselineIntersection);
Point q = cq.getProjection(baselineIntersection);
// return middle of projections as intersection
Point approxIntersection = new Rectangle(p, q)
.getCenter();
ArrayList<Point> intersections = new ArrayList<>();
intersections.add(approxIntersection);
return intersections;
}
}
}
// subdivision
ArrayList<Point> intersections = new ArrayList<>();
if (currentDepth < maxDepth) {
BezierCurve[] pSplit = cp.split(0.5);
BezierCurve[] qSplit = cq.split(0.5);
intersections.addAll(getIntersections(pSplit[0], qSplit[0],
currentDepth + 1));
intersections.addAll(getIntersections(pSplit[0], qSplit[1],
currentDepth + 1));
intersections.addAll(getIntersections(pSplit[1], qSplit[0],
currentDepth + 1));
intersections.addAll(getIntersections(pSplit[1], qSplit[1],
currentDepth + 1));
}
return intersections;
}
}
private static class WindingGlobalIntersectionDetector
implements IGlobalIntersectionDetector {
private static final double DEFAULT_SAMPLE_DISTANCE = 2d;
private static final int DEFAULT_SAMPLE_COUNT = 36;
private int sampleCount;
private double sampleDistance;
public WindingGlobalIntersectionDetector() {
this(DEFAULT_SAMPLE_COUNT, DEFAULT_SAMPLE_DISTANCE);
}
public WindingGlobalIntersectionDetector(int sampleCount,
double sampleDistance) {
this.sampleCount = sampleCount;
this.sampleDistance = sampleDistance;
}
private double determineAngle(List<BezierCurve> inputCurves) {
// sample input segments with minimum distance
List<Point> samples = new ArrayList<>();
for (BezierCurve c : inputCurves) {
samples.addAll(sample(c));
}
// compute signed angle from the samples
double signedAngleSum = 0d;
for (int s = 0; s < samples.size() - 3; s++) {
Point p = samples.get(s);
Point q = samples.get(s + 1);
Point r = samples.get(s + 2);
Vector u = new Vector(p, q);
Vector v = new Vector(q, r);
if (u.getLength() * v.getLength() > 0) {
double ccw = u.getAngleCCW(v).rad();
double cw = u.getAngleCW(v).rad();
if (ccw < cw) {
signedAngleSum += ccw;
} else {
signedAngleSum -= cw;
}
}
}
return signedAngleSum;
}
@Override
public boolean isGlobalIntersection(OffsetApproximation oa,
int fstApproxIndex, int sndApproxIndex) {
// extract indices of the simplified input curve
Integer simpleI = oa.getInputIndex(fstApproxIndex);
Integer simpleJ = oa.getInputIndex(sndApproxIndex);
if (simpleI == null || simpleJ == null) {
throw new IllegalStateException(
"OffsetApproximator does not map all offset approximation segments to the simplified input curve segments.");
}
// query the simplified input curve
List<BezierCurve> simpleCurve = oa.getSimplifiedInputCurve();
// query parameters for the simplified input curve
Double ips = oa.getInputStartParam(fstApproxIndex);
Double jpe = oa.getInputEndParam(sndApproxIndex);
// construct input curve segments corresponding to
// this part of the offset
List<BezierCurve> inputCurves = new ArrayList<>();
if (simpleJ > simpleI) {
BezierCurve sl = simpleCurve.get(simpleI).split(ips)[1];
inputCurves.add(sl);
for (int n = simpleI + 1; n < simpleJ; n++) {
inputCurves.add(simpleCurve.get(n));
}
BezierCurve sr = simpleCurve.get(simpleJ).split(jpe)[0];
inputCurves.add(sr);
} else {
inputCurves
.add(simpleCurve.get(simpleI).getClipped(ips, jpe));
}
return Math.abs(determineAngle(inputCurves)) >= Math.PI;
}
private List<Point> sample(BezierCurve curve) {
List<Point> pts = new ArrayList<>();
for (int i = -1; i < sampleCount; i++) {
double t = (i + 1) / (double) sampleCount;
if (pts.isEmpty()) {
pts.add(curve.get(t));
} else {
Point pt = curve.get(t);
if (pts.get(pts.size() - 1)
.getDistance(pt) >= sampleDistance) {
pts.add(pt);
}
}
}
return pts;
}
}
private static final double DEFAULT_END_PARAM_PERCENTAGE = 0.02;
// XXX: the containment epsilon has to be greater than the acceptable
// offset error (see
// TillerHansonOffsetAlgorithm#DEFAULT_ACCEPTABLE_ERROR)
private static final double DEFAULT_CONTAINMENT_EPSILON = 0.02;
private double endParamPercentage;
private double containmentEpsilon;
private ICurveIntersector curveIntersector;
private IGlobalIntersectionDetector globalIntersectionDetector;
public LocalIntersectionOffsetRefiner() {
this(new LineSimilarityCurveIntersector(),
new WindingGlobalIntersectionDetector(),
DEFAULT_END_PARAM_PERCENTAGE, DEFAULT_CONTAINMENT_EPSILON);
}
public LocalIntersectionOffsetRefiner(
ICurveIntersector curveIntersector,
IGlobalIntersectionDetector globalIntersectionDetector,
double endParamPercentage, double containmentEpsilon) {
this.curveIntersector = curveIntersector;
this.globalIntersectionDetector = globalIntersectionDetector;
this.endParamPercentage = endParamPercentage;
this.containmentEpsilon = containmentEpsilon;
}
public PolyBezier refine(OffsetApproximation oa) {
// record intersections in the offset that need to be removed
List<BezierCurve> approxOffset = oa.getApproximatedOffsetCurve();
List<Intersection> offsetIntersections = new ArrayList<>();
for (int i = 0; i < approxOffset.size() - 1; i++) {
BezierCurve a = approxOffset.get(i);
for (int j = i + 1; j < approxOffset.size(); j++) {
BezierCurve b = approxOffset.get(j);
Point[] intersections = curveIntersector
.getIntersections(a, b).toArray(new Point[0]);
if (intersections.length > 0) {
// compute intersection clip parameters
double minA = 1, maxB = 0;
for (int k = 0; k < intersections.length; k++) {
double ta = a.getParameterAt(
a.getProjection(intersections[k]));
double tb = b.getParameterAt(
b.getProjection(intersections[k]));
if (ta < minA) {
minA = ta;
}
if (tb > maxB) {
maxB = tb;
}
}
// disregard start/end intersections
if (j == i + 1 && intersections.length == 1
&& minA > (1 - endParamPercentage)
&& maxB < endParamPercentage) {
continue;
}
// disregard global intersections
if (globalIntersectionDetector.isGlobalIntersection(oa,
i, j)) {
continue;
}
offsetIntersections
.add(new Intersection(i, minA, j, maxB));
}
}
}
// sort intersections by nesting
List<Intersection> toRemove = new ArrayList<>();
if (offsetIntersections.size() > 1) {
for (int i = 0; i < offsetIntersections.size(); i++) {
Intersection fst = offsetIntersections.get(i);
boolean nesting = false;
List<Integer> nested = new ArrayList<>();
for (int j = i + 1; j < offsetIntersections.size(); j++) {
Intersection snd = offsetIntersections.get(j);
boolean lo = fst.ai < snd.ai
|| fst.ai == snd.ai && fst.ap <= snd.ap;
boolean hi = fst.bi > snd.bi
|| fst.bi == snd.bi && fst.bp <= snd.bp;
if (lo && hi) {
nesting = true;
nested.add(j);
}
}
if (nesting) {
Collections.reverse(nested);
for (int j : nested) {
offsetIntersections.remove(j);
}
}
toRemove.add(fst);
}
} else if (offsetIntersections.size() == 1) {
toRemove.add(offsetIntersections.get(0));
}
// clip offset at intersections and record indices of offset
// segments that need to be removed completely
List<Integer> indicesToRemove = new ArrayList<>();
for (int i = toRemove.size() - 1; i >= 0; i--) {
Intersection inter = toRemove.get(i);
BezierCurve a = approxOffset.get(inter.ai);
BezierCurve b = approxOffset.get(inter.bi);
BezierCurve[] asplit = a.split(inter.ap);
BezierCurve[] bananaSplit = b.split(inter.bp);
// replace a and b with clipped versions
approxOffset.set(inter.ai, asplit[0]);
approxOffset.set(inter.bi, bananaSplit[1]);
// remove all curves between a and b (if any)
for (int k = inter.bi - 1; k > inter.ai; k--) {
if (!indicesToRemove.contains(k)) {
indicesToRemove.add(k);
}
}
}
// sort indices to remove descendingly so that we can iterate over
// them and remove them without having to adjust the index
Collections.sort(indicesToRemove, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 == o2 ? 0 : o1 < o1 ? -1 : +1;
}
});
for (int k : indicesToRemove) {
approxOffset.remove(k);
}
// find indices of fully contained offset curves
List<Integer> fullyContainedOffsetIndices = new ArrayList<>();
BezierCurve input = oa.getInputCurve();
double dMin = Math.abs(oa.getOffsetDistance()) - containmentEpsilon;
for (int i = approxOffset.size() - 1; i >= 0; i--) {
BezierCurve oi = approxOffset.get(i);
boolean fullyContained = true;
for (Point p : oi.getBounds().getPoints()) {
double dp = input.getProjection(p).getDistance(p);
if (dp > dMin) {
fullyContained = false;
}
}
if (fullyContained) {
fullyContainedOffsetIndices.add(i);
}
}
if (fullyContainedOffsetIndices.size() > 0) {
// search for fully contained start segments
List<Integer> startRemove = new ArrayList<>();
for (int i = 0; i < approxOffset.size()
&& fullyContainedOffsetIndices.contains(i); i++) {
startRemove.add(i);
}
Collections.reverse(startRemove);
// search for fully contained end segments
List<Integer> endRemove = new ArrayList<>();
for (int i = approxOffset.size() - 1; i >= 0
&& fullyContainedOffsetIndices.contains(i); i--) {
endRemove.add(i);
}
// check if startRemove and endRemove overlap
if (startRemove.size() > 0 && endRemove.size() > 0
&& startRemove.get(0) >= endRemove
.get(endRemove.size() - 1)) {
// remove whole curve
approxOffset.clear();
} else {
// remove the fully contained start/end segments
// XXX: to prevent index adjustment, end is removed first
for (int i : endRemove) {
approxOffset.remove(i);
}
for (int i : startRemove) {
approxOffset.remove(i);
}
}
}
// merge the curves to yield a valid PolyBezier
return mergeCurves(approxOffset);
}
}
private static class OffsetApproximation {
private List<BezierCurve> simpleCurve = new ArrayList<>();
private List<BezierCurve> approxOffsetCurve = new ArrayList<>();
private Map<Integer, Integer> approx2simple = new HashMap<>();
private Map<Integer, Double> a2sParamStart = new HashMap<>();
private Map<Integer, Double> a2sParamEnd = new HashMap<>();
private BezierCurve inputCurve;
private double offsetDistance;
public OffsetApproximation(BezierCurve inputCurve,
double offsetDistance, List<BezierCurve> simpleCurve,
List<BezierCurve> approxOffsetCurve,
Map<Integer, Integer> approx2simple,
Map<Integer, Double> a2sParamStart,
Map<Integer, Double> a2sParamEnd) {
this.inputCurve = inputCurve;
this.offsetDistance = offsetDistance;
this.simpleCurve = simpleCurve;
this.approxOffsetCurve = approxOffsetCurve;
this.approx2simple = approx2simple;
this.a2sParamStart = a2sParamStart;
this.a2sParamEnd = a2sParamEnd;
}
public List<BezierCurve> getApproximatedOffsetCurve() {
return approxOffsetCurve;
}
public BezierCurve getInputCurve() {
return inputCurve;
}
public double getInputEndParam(int i) {
return a2sParamEnd.get(i);
}
public int getInputIndex(int i) {
return approx2simple.get(i);
}
public double getInputStartParam(int i) {
return a2sParamStart.get(i);
}
public double getOffsetDistance() {
return offsetDistance;
}
public List<BezierCurve> getSimplifiedInputCurve() {
return simpleCurve;
}
}
private static final long serialVersionUID = 1L;
private static final int CHUNK_SHIFT = -3;
private static final boolean ORTHOGONAL = true;
private static final boolean PARALLEL = false;
private static final double UNRECOGNIZABLE_PRECISION_FRACTION = PrecisionUtils
.calculateFraction(0) / 10;
/**
* An {@link IPointCmp} implementation to find the {@link Point} with the
* minimal x coordinate in a list of {@link Point}s.
*/
private static final IPointCmp xminCmp = new IPointCmp() {
@Override
public boolean pIsBetterThanQ(Point p, Point q) {
return PrecisionUtils.smallerEqual(p.x, q.x);
}
};
/**
* An {@link IPointCmp} implementation to find the {@link Point} with the
* maximal x coordinate in a list of {@link Point}s.
*/
private static final IPointCmp xmaxCmp = new IPointCmp() {
@Override
public boolean pIsBetterThanQ(Point p, Point q) {
return PrecisionUtils.greaterEqual(p.x, q.x);
}
};
/**
* An {@link IPointCmp} implementation to find the {@link Point} with the
* minimal y coordinate in a list of {@link Point}s.
*/
private static final IPointCmp yminCmp = new IPointCmp() {
@Override
public boolean pIsBetterThanQ(Point p, Point q) {
return PrecisionUtils.smallerEqual(p.y, q.y);
}
};
/**
* An {@link IPointCmp} implementation to find the {@link Point} with the
* maximal y coordinate in a list of {@link Point}s.
*/
private static final IPointCmp ymaxCmp = new IPointCmp() {
@Override
public boolean pIsBetterThanQ(Point p, Point q) {
return PrecisionUtils.greaterEqual(p.y, q.y);
}
};
/**
* <p>
* Clusters consecutive {@link IntervalPair}s into a new array of
* {@link IntervalPair}s. Two {@link IntervalPair}s are regarded to be
* consecutive if they are {@link #isNextTo(IntervalPair, IntervalPair, int)
* next to} each other within the imprecision specified by the given
* <i>shift</i>.
* </p>
*
* @param intervalPairs
* the array of {@link IntervalPair}s to cluster
* @param shift
* the precision shift (see
* {@link PrecisionUtils#calculateFraction(int)})
* @return a new array of {@link IntervalPair}s, each of which is the
* composition of an {@link IntervalPair} cluster
*/
private static IntervalPair[] clusterChunks(IntervalPair[] intervalPairs,
int shift) {
ArrayList<IntervalPair> ips = new ArrayList<>();
ips.addAll(Arrays.asList(intervalPairs));
Collections.sort(ips, new Comparator<IntervalPair>() {
@Override
public int compare(IntervalPair i, IntervalPair j) {
if (i.pi.a < j.pi.a) {
return -1;
} else if (i.pi.a > j.pi.a) {
return 1;
}
return 0;
}
});
ArrayList<IntervalPair> clusters = new ArrayList<>();
IntervalPair current = null;
boolean couldMerge;
do {
clusters.clear();
couldMerge = false;
for (IntervalPair i : ips) {
if (current == null) {
current = i.getCopy();
} else if (isNextTo(current, i, shift)) {
couldMerge = true;
current.expand(i);
} else {
isNextTo(current, i, shift);
clusters.add(current);
current = i.getCopy();
}
}
if (current != null) {
clusters.add(current);
current = null;
}
ips.clear();
ips.addAll(clusters);
} while (couldMerge);
return clusters.toArray(new IntervalPair[] {});
}
/**
* Searches the parameter value of the given {@link Point} on the given
* {@link BezierCurve} using de Casteljau subdivision. The resulting
* parameter range for the {@link Point} is recorded in the given
* {@link Interval}. If the {@link Point} could be found on the
* {@link BezierCurve} within the given parameter {@link Interval}. The
* {@link Interval} is set to a convergin (see {@link Interval#converges()})
* parameter range that contains the {@link Point} on the
* {@link BezierCurve}.
*
* @param c
* The {@link BezierCurve} on which the {@link Point} is searched
* for.
* @param interval
* The parameter {@link Interval} on the given
* {@link BezierCurve} which is searched for the {@link Point}.
* The resulting parameter range is recorded in this
* {@link Interval}.
* @param p
* the {@link Point} to find
* @return <code>true</code> if the a converging parameter {@link Interval}
* that contains the {@link Point} can be identified, otherwise
* <code>false</code>
*/
private static boolean containmentParameter(BezierCurve c,
double[] interval, Point p) {
Stack<Interval> parts = new Stack<>();
parts.push(new Interval(interval));
while (!parts.empty()) {
Interval i = parts.pop();
if (i.converges(1)) {
interval[0] = i.a;
interval[1] = i.b;
break;
}
double iMid = i.getMid();
Interval left = new Interval(i.a, iMid);
Interval right = new Interval(iMid, i.b);
BezierCurve clipped = c.getClipped(left.a, left.b);
Rectangle bounds = clipped.getControlBounds();
if (bounds.contains(p)) {
parts.push(left);
}
clipped = c.getClipped(right.a, right.b);
bounds = clipped.getControlBounds();
if (bounds.contains(p)) {
parts.push(right);
}
}
return PrecisionUtils.equal(interval[0], interval[1], 1);
}
/**
* Overwrites the attribute values of {@link IntervalPair} <i>dst</i> with
* the respective attribute values of {@link IntervalPair} <i>src</i>.
*
* @param dst
* the destination {@link IntervalPair}
* @param src
* the source {@link IntervalPair}
*/
private static void copyIntervalPair(IntervalPair dst, IntervalPair src) {
dst.p = src.p;
dst.q = src.q;
dst.pi = src.pi;
dst.qi = src.qi;
}
/**
* <p>
* Returns the similarity of the given {@link BezierCurve} to a {@link Line}
* , which is defined as the absolute distance of its control {@link Point}s
* to the base {@link Line} connecting its end {@link Point}s.
* </p>
* <p>
* A similarity of <code>0</code> means that the given {@link BezierCurve}'s
* control {@link Point}s are on a straight {@link Line}.
* </p>
*
* @param c
* the {@link BezierCurve} of which the distance to its base
* {@link Line} is computed
* @return the distance of the given {@link BezierCurve} to its base
* {@link Line}
*/
private static double distanceToBaseLine(BezierCurve c) {
Straight3D baseLine = Straight3D.through(c.points[0],
c.points[c.points.length - 1]);
if (baseLine == null) {
return 0d;
}
double maxDistance = 0d;
for (int i = 1; i < c.points.length - 1; i++) {
maxDistance = Math.max(maxDistance,
Math.abs(baseLine.getSignedDistanceCW(c.points[i])));
}
return maxDistance;
}
/**
* Searches for an overlapping segment within the given {@link IntervalPair}
* s.
*
* @param intersectionCandidates
* the {@link IntervalPair}s representing non-end-{@link Point}
* intersection candidates
* @param endPoints
* the {@link IntervalPair}s representing end-{@link Point}
* intersections
* @return <code>null</code> if no overlapping segment can be identified,
* otherwise an {@link IntervalPair} representing the overlapping
* segment
*/
private static IntervalPair extractOverlap(
IntervalPair[] intersectionCandidates, IntervalPair[] endPoints) {
// merge intersection candidates and end points
IntervalPair[] fineChunks = new IntervalPair[intersectionCandidates.length
+ endPoints.length];
for (int i = 0; i < intersectionCandidates.length; i++) {
fineChunks[i] = intersectionCandidates[i];
}
for (int i = 0; i < endPoints.length; i++) {
fineChunks[intersectionCandidates.length + i] = endPoints[i];
}
if (fineChunks.length == 0) {
return null;
}
// recluster chunks
normalizeIntervalPairs(fineChunks);
IntervalPair[] chunks = clusterChunks(fineChunks, CHUNK_SHIFT - 1);
/*
* if they overlap, the chunk has to start/end in a start-/endpoint of
* the curves.
*/
for (IntervalPair overlap : chunks) {
if (PrecisionUtils.smallerEqual(overlap.pi.a, 0)
&& PrecisionUtils.greaterEqual(overlap.pi.b, 1)
|| PrecisionUtils.smallerEqual(overlap.qi.a, 0)
&& PrecisionUtils.greaterEqual(overlap.qi.b, 1)
|| (PrecisionUtils.smallerEqual(overlap.pi.a, 0)
|| PrecisionUtils.greaterEqual(overlap.pi.b, 1))
&& (PrecisionUtils.smallerEqual(overlap.qi.a, 0)
|| PrecisionUtils.greaterEqual(overlap.qi.b,
1))) {
// it overlaps
if (PrecisionUtils.smallerEqual(overlap.pi.a, 0,
CHUNK_SHIFT - 1)
&& PrecisionUtils.smallerEqual(overlap.pi.b, 0,
CHUNK_SHIFT - 1)
|| PrecisionUtils.greaterEqual(overlap.pi.a, 1,
CHUNK_SHIFT - 1)
&& PrecisionUtils.greaterEqual(overlap.pi.b, 1,
CHUNK_SHIFT - 1)
|| PrecisionUtils.smallerEqual(overlap.qi.a, 0,
CHUNK_SHIFT - 1)
&& PrecisionUtils.smallerEqual(overlap.qi.b, 0,
CHUNK_SHIFT - 1)
|| PrecisionUtils.greaterEqual(overlap.qi.a, 1,
CHUNK_SHIFT - 1)
&& PrecisionUtils.greaterEqual(overlap.qi.b, 1,
CHUNK_SHIFT - 1)) {
// only end-point-intersection
return null;
}
return refineOverlap(overlap);
}
}
return null;
}
/**
* Computes the intersection of the line from {@link Point} p to
* {@link Point} q with the x-axis-parallel line f(x) = y.
*
* There is always an intersection, because this routine is only called when
* either the lower or the higher fat line bound is crossed.
*
* The following conditions are fulfilled: (p.x!=q.x) and (p.y!=q.y) and
* (p.y<y<q.y) or (p.y>y>q.y).
*
* From these values, one can build a function g(x) = m*x + b where
* m=(q.y-p.y)/(q.x-p.x) and b=p.y-m*p.x.
*
* The point of intersection is given by f(x) = g(x). The x-coordinate of
* this point is x = (y - b) / m.
*
* @param p
* The start point of the {@link Line}
* @param q
* The end point of the {@link Line}
* @param y
* The x-axis-parallel line f(x) = y
* @return the x coordinate of the intersection point.
*/
private static double intersectXAxisParallel(Point p, Point q, double y) {
double m = (q.y - p.y) / (q.x - p.x);
return (y - p.y + m * p.x) / m;
}
/**
* Checks if the given {@link Interval}s are considered to be next to each
* other within the specified imprecision. Two {@link Interval}s are
* considered next to each other, if their limits are overlapping within the
* specified imprecision.
*
* @param i
* @param j
* @param shift
* the precision shift (see
* {@link PrecisionUtils#calculateFraction(int)})
* @return <code>true</code> if the two {@link Interval}s are considered to
* be next to each other within the specified imprecision, otherwise
* <code>false</code>
*/
private static boolean isNextTo(Interval i, Interval j, int shift) {
return PrecisionUtils.smallerEqual(j.a, i.b, shift)
&& PrecisionUtils.greaterEqual(j.b, i.a, shift);
}
/**
* Checks if the two {@link IntervalPair}s are considered next to each
* other. The {@link IntervalPair}s are regarded to be normalized (see
* {@link #normalizeIntervalPairs(IntervalPair[])}). Two
* {@link IntervalPair}s are considered next to each other, if the
* {@link Interval}s of their assigned {@link BezierCurve}s are considered
* next to each other (see {@link #isNextTo(Interval, Interval, int)}).
*
* @param a
* @param b
* @param shift
* the precision shift (see
* {@link PrecisionUtils#calculateFraction(int)})
* @return <code>true</code> if the {@link IntervalPair}s are considered
* next to each other within the specified imprecision, otherwise
* <code>false</code>
*/
private static boolean isNextTo(IntervalPair a, IntervalPair b, int shift) {
return isNextTo(a.pi, b.pi, shift) && isNextTo(a.qi, b.qi, shift);
}
private static double LineSimilarity(BezierCurve cp) {
double max = 0d;
Line baseline = cp.toLine();
int N = cp.getPoints().length;
for (int i = 0; i < N; i++) {
Point p = cp.get(i / (double) (N - 1));
double distance = p.getDistance(baseline.getProjection(p));
if (distance > max) {
max = distance;
}
}
return max;
}
/**
* Merges the given List of {@link BezierCurve}s by setting the end/start
* point of two consecutive segments to the middle point between the two.
* Reteurns a {@link PolyBezier} that is constructed from the adjusted
* curves.
*
* @param curves
* @return A {@link PolyBezier} constructed from the merged curves.
*/
private static PolyBezier mergeCurves(List<BezierCurve> curves) {
if (curves.size() > 1) {
// adjust start/end points within the curves so that they are
// continuous
for (int i = 1; i < curves.size(); i++) {
Point last = curves.get(i - 1).getP2();
Point next = curves.get(i).getP1();
if (!next.equals(last)) {
Point mid = new Rectangle(last, next).getCenter();
curves.get(i - 1).setP2(mid);
curves.get(i).setP1(mid);
}
}
}
// save the refined offset as a PolyBezier
return new PolyBezier(curves.toArray(new BezierCurve[] {}));
}
/**
* Normalizes the given {@link IntervalPair}s so that all
* {@link IntervalPair}s have the same {@link BezierCurve} assigned to their
* <code>p</code> attribute and that all {@link IntervalPair}s have the same
* {@link BezierCurve} assigned to their <code>q</code> attribute.
*
* @param intervalPairs
* the {@link IntervalPair}s to normalize
*/
private static void normalizeIntervalPairs(IntervalPair[] intervalPairs) {
// in every interval, p and q have to be the same curves
if (intervalPairs.length == 0) {
return;
}
BezierCurve pId = intervalPairs[0].p;
BezierCurve qId = intervalPairs[0].q;
for (IntervalPair ip : intervalPairs) {
if (ip.p != pId) {
Interval qi = ip.pi;
Interval pi = ip.qi;
ip.p = pId;
ip.q = qId;
ip.pi = pi;
ip.qi = qi;
}
}
}
private static boolean pointsEquals(Point p1, Point p2, int shift) {
return PrecisionUtils.equal(p1.x, p2.x, shift)
&& PrecisionUtils.equal(p1.y, p2.y, shift);
}
/**
* Binary search from the {@link IntervalPair}'s {@link Interval}s' limits
* to the {@link Interval} s' inner values to refine the overlap represented
* by the given {@link IntervalPair}.
*
* @param overlap
* the {@link IntervalPair} representing the overlap of two
* {@link BezierCurve}s
* @return the given {@link IntervalPair} for convenience
*/
private static IntervalPair refineOverlap(IntervalPair overlap) {
Interval piLo = refineOverlapLo(overlap.p, overlap.pi.a,
overlap.pi.getMid(), overlap.q);
Interval piHi = refineOverlapHi(overlap.p, overlap.pi.getMid(),
overlap.pi.b, overlap.q);
Interval qiLo = refineOverlapLo(overlap.q, overlap.qi.a,
overlap.qi.getMid(), overlap.p);
Interval qiHi = refineOverlapHi(overlap.q, overlap.qi.getMid(),
overlap.qi.b, overlap.p);
overlap.pi.a = piLo.b;
overlap.pi.b = piHi.a;
overlap.qi.a = qiLo.b;
overlap.qi.b = qiHi.a;
return overlap;
}
/**
* Binary search from the {@link Interval}'s limits to the {@link Interval}
* 's inner values of the firstly given {@link BezierCurve} <i>p</i> for the
* outer-most intersection {@link Point} with the secondly given
* {@link BezierCurve} <i>q</i>.
*
* @param p
* @param mid
* the {@link Interval}'s start value (
* <code>mid > 0 ? mid : 0</code> )
* @param b
* the {@link Interval}'s end value (<code>b < 1 ? b : 1</code>)
* @param q
* @return
*/
private static Interval refineOverlapHi(BezierCurve p, double mid, double b,
BezierCurve q) {
Interval i = new Interval(Math.max(mid, 0), Math.min(b, 1));
double prevLo;
Point pLo;
int c = 0;
while (c++ < 30 && !i.converges()) {
prevLo = i.a;
i.a = i.getMid();
pLo = p.get(i.a);
if (!q.contains(pLo)) {
i.b = i.a;
i.a = prevLo;
}
}
return i;
}
/**
* Binary search from the {@link Interval}'s limits to the {@link Interval}
* 's inner values of the firstly given {@link BezierCurve} <i>p</i> for the
* outer-most intersection {@link Point} with the secondly given
* {@link BezierCurve} <i>q</i>.
*
* @param p
* @param a
* the {@link Interval}'s start value (<code>a > 0 ? a : 0</code>
* )
* @param mid
* the {@link Interval}'s end value (
* <code>mid < 1 ? mid : 1</code>)
* @param q
* @return
*/
private static Interval refineOverlapLo(BezierCurve p, double a, double mid,
BezierCurve q) {
Interval i = new Interval(Math.max(a, 0), Math.min(mid, 1));
double prevHi;
Point pHi;
int c = 0;
while (c++ < 30 && !i.converges()) {
prevHi = i.b;
i.b = i.getMid();
pHi = p.get(i.b);
if (!q.contains(pHi)) {
i.a = i.b;
i.b = prevHi;
}
}
return i;
}
/**
* An array of {@link Vector3D}s which represent the control points of this
* {@link BezierCurve}.
*/
private final Vector3D[] points;
/**
* Constructs a new {@link BezierCurve} from the given {@link CubicCurve}.
*
* @param c
* the {@link CubicCurve} of which the new {@link BezierCurve} is
* constructed from
*/
public BezierCurve(CubicCurve c) {
this(c.getP1(), c.getCtrl1(), c.getCtrl2(), c.getP2());
}
/**
* Constructs a new {@link BezierCurve} from the given control {@link Point}
* coordinates. The coordinates are expected to be in x, y order, i.e. x1,
* y1, x2, y2, x3, y3, ...
*
* @param controlPoints
* the control {@link Point} coordinates of the new
* {@link BezierCurve} in x, y order
*/
public BezierCurve(double... controlPoints) {
this(PointListUtils.toPointsArray(controlPoints));
}
/**
* Constructs a new {@link BezierCurve} from the given control {@link Point}
* s.
*
* @param controlPoints
* the control {@link Point}s of the new {@link BezierCurve}
*/
public BezierCurve(Point... controlPoints) {
points = new Vector3D[controlPoints.length];
for (int i = 0; i < points.length; i++) {
points[i] = new Vector3D(controlPoints[i].x, controlPoints[i].y, 1);
}
}
/**
* Constructs a new {@link BezierCurve} from the given
* {@link QuadraticCurve}.
*
* @param c
* the {@link QuadraticCurve} of which the new
* {@link BezierCurve} is constructed from
*/
public BezierCurve(QuadraticCurve c) {
this(c.getP1(), c.getCtrl(), c.getP2());
}
/**
* <p>
* Constructs a new {@link BezierCurve} object from the control points
* represented by the given {@link Vector3D}s.
* </p>
* <p>
* Note that a Point(x, y) is represented by a Vector3D(x, y, 1).
* </p>
*
* @param controlPoints
* the {@link Vector3D}s representing the control points of the
* new {@link BezierCurve}
*/
private BezierCurve(Vector3D... controlPoints) {
points = new Vector3D[controlPoints.length];
for (int i = 0; i < points.length; i++) {
points[i] = controlPoints[i].getCopy();
}
}
/**
* <p>
* Firstly, the difference of this {@link BezierCurve} to the given
* {@link FatLine} is computed. This is another {@link BezierCurve} of which
* the control {@link Point}s are further examined.
* </p>
* <p>
* Every difference control {@link Point} is checked if it is inside the
* given {@link FatLine}. Difference control {@link Point}s within the
* {@link FatLine} represent portions of this {@link BezierCurve} which
* cannot be clipped. Therefore, the {@link Interval} recording the
* parameter range of this {@link BezierCurve} is appropriately modified for
* these difference control {@link Point}s.
* </p>
* <p>
* Subsequently, the {@link Line}s connecting the start/end {@link Point} of
* the difference {@link BezierCurve} and the other control {@link Point}s
* of the difference {@link BezierCurve} are intersected with the
* {@link FatLine}'s border {@link Line}s. The outermost intersections
* identify parameter ranges that can be clipped away from this
* {@link BezierCurve}. Therefore, the {@link Interval} recording the
* parameter range of this {@link BezierCurve} is appropriately modified for
* these intersections.
* </p>
* <p>
* The starting {@link Interval} is chosen to be invalid. The individual
* checks move the lower and upper limits past to one another. If everything
* can be clipped, the resulting {@link Interval} remains invalid. If the
* resulting {@link Interval} <code>I = [a;b]</code> is valid (
* <code>a <= b</code>), then the portions <code>[0;a]</code> and
* <code>[b;1]</code> of this {@link BezierCurve} can be clipped away.
* </p>
*
* @param L
* the {@link FatLine} to clip this {@link BezierCurve} to
* @return the new parameter {@link Interval} for this {@link BezierCurve}
*/
private double[] clipTo(FatLine L) {
double[] interval = new double[] { 1, 0 };
Vector3D[] differenceVectors = genDifferencePoints(L.line);
Point[] differencePoints = new Point[differenceVectors.length];
for (int i = 0; i < differenceVectors.length; i++) {
differencePoints[i] = differenceVectors[i].toPoint();
}
// inside fat line check
for (Point p : differencePoints) {
if (Double.isNaN(p.y) || L.dmin <= p.y && p.y <= L.dmax) {
moveInterval(interval, p.x);
}
}
// intersections from start
for (int i = 1; i < differencePoints.length; i++) {
Line seg = new Line(differencePoints[0], differencePoints[i]);
if (seg.getP1().y < L.dmin != seg.getP2().y < L.dmin) {
double x = intersectXAxisParallel(seg.getP1(), seg.getP2(),
L.dmin);
moveInterval(interval, x);
}
if (seg.getP1().y < L.dmax != seg.getP2().y < L.dmax) {
double x = intersectXAxisParallel(seg.getP1(), seg.getP2(),
L.dmax);
moveInterval(interval, x);
}
}
// intersections from end
for (int i = 0; i < differencePoints.length - 1; i++) {
Line seg = new Line(differencePoints[i],
differencePoints[differencePoints.length - 1]);
if (seg.getP1().y < L.dmin != seg.getP2().y < L.dmin) {
double x = intersectXAxisParallel(seg.getP1(), seg.getP2(),
L.dmin);
moveInterval(interval, x);
}
if (seg.getP1().y < L.dmax != seg.getP2().y < L.dmax) {
double x = intersectXAxisParallel(seg.getP1(), seg.getP2(),
L.dmax);
moveInterval(interval, x);
}
}
return interval;
}
private Point[] constructLUT(double start, double end, int size) {
Point[] lut = new Point[size];
for (int i = 0; i < size; i++) {
lut[i] = get(start + i * (end - start) / (size - 1));
}
return lut;
}
/**
* <p>
* Tests if this {@link BezierCurve} contains the given other
* {@link BezierCurve}.
* </p>
* <p>
* The other {@link BezierCurve} is regarded to be contained by this
* {@link BezierCurve} if its start and end {@link Point} lie on this
* {@link BezierCurve} and an overlapping segment of the two curves can be
* detected.
* </p>
*
* @param o
* the {@link BezierCurve} that is checked to be contained by
* this {@link BezierCurve}
* @return <code>true</code> if the given {@link BezierCurve} is contained
* by this {@link BezierCurve}, otherwise <code>false</code>
*/
public boolean contains(BezierCurve o) {
return contains(o.getP1()) && contains(o.getP2())
&& getOverlap(o) != null;
}
@Override
public boolean contains(final Point p) {
if (p == null) {
return false;
}
return containmentParameter(this, new double[] { 0, 1 }, p);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof BezierCurve)) {
return false;
}
BezierCurve o = (BezierCurve) other;
BezierCurve t = this;
while (o.points.length < t.points.length) {
o = o.getElevated();
}
while (t.points.length < o.points.length) {
t = t.getElevated();
}
Point[] oPoints = o.getPoints();
Point[] tPoints = t.getPoints();
return Arrays.equals(oPoints, tPoints)
|| Arrays.equals(oPoints, Point.getReverseCopy(tPoints));
}
/**
* Checks all end {@link Point}s of the two passed-in {@link BezierCurve}s
* if they are {@link Point}s of intersection.
*
* @param ip
* the {@link IntervalPair} describing both curves
* @param endPointIntervalPairs
* the set of {@link IntervalPair}s to store the results
* @param intersections
* the set of {@link Point}s to additionally store the associated
* intersection {@link Point}s
*/
private void findEndPointIntersections(IntervalPair ip,
Set<IntervalPair> endPointIntervalPairs, Set<Point> intersections) {
final double CHUNK_SHIFT_EPSILON = PrecisionUtils
.calculateFraction(CHUNK_SHIFT);
Point poi = ip.p.points[0].toPoint();
double[] interval = new double[] { 0, 1 };
if (containmentParameter(ip.q, interval, poi)) {
ip.pi.a = CHUNK_SHIFT_EPSILON;
interval[0] = (interval[0] + interval[1]) / 2;
interval[1] = interval[0] + CHUNK_SHIFT_EPSILON / 2;
interval[0] = interval[0] - CHUNK_SHIFT_EPSILON / 2;
endPointIntervalPairs.add(new IntervalPair(ip.p,
new Interval(0, ip.pi.a), ip.q, new Interval(interval)));
intersections.add(poi);
}
poi = ip.p.points[ip.p.points.length - 1].toPoint();
interval[0] = 0;
interval[1] = 1;
if (containmentParameter(ip.q, interval, poi)) {
ip.pi.b = 1 - CHUNK_SHIFT_EPSILON;
interval[0] = (interval[0] + interval[1]) / 2;
interval[1] = interval[0] + CHUNK_SHIFT_EPSILON / 2;
interval[0] = interval[0] - CHUNK_SHIFT_EPSILON / 2;
endPointIntervalPairs.add(new IntervalPair(ip.p,
new Interval(ip.pi.b, 1), ip.q, new Interval(interval)));
intersections.add(poi);
}
poi = ip.q.points[0].toPoint();
interval[0] = 0;
interval[1] = 1;
if (containmentParameter(ip.p, interval, poi)) {
ip.qi.a = CHUNK_SHIFT_EPSILON;
interval[0] = (interval[0] + interval[1]) / 2;
interval[1] = interval[0] + CHUNK_SHIFT_EPSILON / 2;
interval[0] = interval[0] - CHUNK_SHIFT_EPSILON / 2;
endPointIntervalPairs.add(new IntervalPair(ip.p,
new Interval(interval), ip.q, new Interval(0, ip.qi.a)));
intersections.add(poi);
}
poi = ip.q.points[ip.q.points.length - 1].toPoint();
interval[0] = 0;
interval[1] = 1;
if (containmentParameter(ip.p, interval, poi)) {
ip.qi.b = 1 - CHUNK_SHIFT_EPSILON;
interval[0] = (interval[0] + interval[1]) / 2;
interval[1] = interval[0] + CHUNK_SHIFT_EPSILON / 2;
interval[0] = interval[0] - CHUNK_SHIFT_EPSILON / 2;
endPointIntervalPairs.add(new IntervalPair(ip.p,
new Interval(interval), ip.q, new Interval(ip.qi.b, 1)));
intersections.add(poi);
}
}
/**
* Searches for the specified extreme on this {@link BezierCurve}.
*
* @param cmp
* the {@link IPointCmp} that specifies the extreme to search for
* @return the extreme {@link Point} that can be identified
*/
private Point findExtreme(IPointCmp cmp) {
return findExtreme(cmp, Interval.getFull());
}
/**
* <p>
* Searches for an extreme {@link Point} on this {@link BezierCurve}.
* </p>
*
* @param cmp
* the {@link IPointCmp} that is used to find the extreme
* {@link Point}
* @param iStart
* the start {@link Interval} on this {@link BezierCurve} in
* which the extreme {@link Point} is searched for
* @return the extreme {@link Point} that could be found
*/
private Point findExtreme(IPointCmp cmp, Interval iStart) {
Stack<Interval> parts = new Stack<>();
parts.push(iStart);
Point xtreme = getHC(iStart.a).toPoint();
while (!parts.isEmpty()) {
Interval i = parts.pop();
BezierCurve clipped = getClipped(i.a, i.b);
Point sp = clipped.points[0].toPoint();
xtreme = cmp.pIsBetterThanQ(sp, xtreme) ? sp : xtreme;
Point ep = clipped.points[clipped.points.length - 1].toPoint();
xtreme = cmp.pIsBetterThanQ(ep, xtreme) ? ep : xtreme;
boolean everythingWorse = true;
for (int j = 1; j < clipped.points.length - 1; j++) {
if (!cmp.pIsBetterThanQ(xtreme, clipped.points[j].toPoint())) {
everythingWorse = false;
break;
}
}
if (everythingWorse) {
continue;
}
// split interval
if (!i.converges()) {
double im = i.getMid();
parts.push(new Interval(im, i.b));
parts.push(new Interval(i.a, im));
}
}
return xtreme;
}
/**
* <p>
* Find intersection {@link IntervalPair} chunks. The chunks are not very
* precise. We will refine them later.
* </p>
* <p>
* Searches for (imprecise) intersection {@link IntervalPair}s using the
* Bezier clipping algorithm. Every recorded {@link IntervalPair} limits the
* parameter {@link Interval} for a possible intersection on both
* {@link BezierCurve}s.
* </p>
*
* @param ip
* the {@link IntervalPair} that is currently processed
* @param intervalPairs
* the set of {@link IntervalPair}s to store the results
* @param intersections
* the set of intersection {@link Point}s to store those in case
* of a degenerated {@link BezierCurve} (or a degenerated
* sub-curve)
*/
private void findIntersectionChunks(IntervalPair ip,
Set<IntervalPair> intervalPairs, Set<Point> intersections) {
if (ip.converges(CHUNK_SHIFT)) {
intervalPairs.add(ip.getCopy());
return;
}
BezierCurve pClipped = ip.getPClipped();
BezierCurve qClipped = ip.getQClipped();
// construct "parallel" and "orthogonal" fat lines
FatLine L1 = FatLine.from(qClipped, PARALLEL);
FatLine L2 = FatLine.from(qClipped, ORTHOGONAL);
// curve implosion check
if (L1 == null || L2 == null) {
// q is degenerated
Point poi = ip.q.getHC(ip.qi.getMid()).toPoint();
double[] interval = new double[] { 0, 1 };
if (poi != null && containmentParameter(ip.p, interval, poi)) {
intersections.add(poi);
}
return;
}
// clip to the fat lines
Interval interval = new Interval(pClipped.clipTo(L1));
Interval intervalOrtho = new Interval(pClipped.clipTo(L2));
// pick smaller interval range
interval = Interval.min(interval, intervalOrtho);
// re-calculate s and e from the clipped interval
double ratio = ip.pi.scaleTo(interval);
if (ratio < 0) {
// no more intersections
return;
} else if (ratio > 0.8) {
/*
* Split longer curve and find intersections for both halves. Add an
* unrecognizable fraction to the beginning of the second parameter
* interval, so that only one of the getIntersection() calls can
* converge in the middle.
*/
if (ip.isPLonger()) {
IntervalPair[] nip = ip.getPSplit();
findIntersectionChunks(nip[0], intervalPairs, intersections);
findIntersectionChunks(nip[1], intervalPairs, intersections);
} else {
IntervalPair[] nip = ip.getQSplit();
findIntersectionChunks(nip[0], intervalPairs, intersections);
findIntersectionChunks(nip[1], intervalPairs, intersections);
}
return;
} else {
findIntersectionChunks(ip.getSwapped(), intervalPairs,
intersections);
}
}
/**
* This routine is only called for an interval that has been detected to
* contain a single {@link Point} of intersection. We do now try to find it.
*
* @param ipIO
* the {@link IntervalPair} that specifies a single {@link Point}
* of intersection on two {@link BezierCurve}s
*/
private Point findSinglePreciseIntersection(IntervalPair ipIO) {
Stack<IntervalPair> partStack = new Stack<>();
partStack.push(ipIO);
while (!partStack.isEmpty()) {
IntervalPair ip = partStack.pop();
// quick check if intersections can be found
BezierCurve pClipped = ip.getPClipped();
BezierCurve qClipped = ip.getQClipped();
if (!pClipped.getControlBounds()
.touches(qClipped.getControlBounds())) {
continue;
}
if (ip.convergesP()) {
Point p = ip.p.getHC(ip.pi.a).toPoint();
if (ip.q.contains(p)) {
return p;
}
}
if (ip.convergesQ()) {
Point q = ip.q.getHC(ip.qi.a).toPoint();
if (ip.p.contains(q)) {
return q;
}
}
if (ip.converges()) {
// TODO: do another clipping algorithm here. the one that
// uses control bounds.
for (Point pp : ip.p.toPoints(ip.pi)) {
for (Point qp : ip.q.toPoints(ip.qi)) {
if (pp.equals(qp)) {
copyIntervalPair(ipIO, ip);
return pp;
}
}
}
continue;
}
// construct "parallel" and "orthogonal" fat lines
FatLine L1 = FatLine.from(qClipped, PARALLEL);
FatLine L2 = FatLine.from(qClipped, ORTHOGONAL);
// curve implosion check
if (L1 == null || L2 == null) {
// q is degenerated
Point poi = ip.q.getHC(ip.qi.getMid()).toPoint();
if (ip.p.contains(poi)) {
copyIntervalPair(ipIO, ip);
return poi;
}
continue;
}
// clip to the fat lines
Interval interval = new Interval(pClipped.clipTo(L1));
Interval intervalOrtho = new Interval(pClipped.clipTo(L2));
// pick smaller interval range
interval = Interval.min(interval, intervalOrtho);
// re-calculate s and e from the clipped interval
double ratio = ip.pi.scaleTo(interval);
if (ratio < 0) {
// no more intersections
continue;
} else if (ratio > 0.8) {
/*
* Split longer curve and find intersections for both halves.
* Add an unrecognizable fraction to the beginning of the second
* parameter interval, so that only one of the getIntersection()
* calls can converge in the middle.
*/
IntervalPair[] nip = ip.isPLonger() ? ip.getPSplit()
: ip.getQSplit();
partStack.push(nip[1]);
partStack.push(nip[0]);
} else {
partStack.push(ip.getSwapped());
}
}
return null;
}
/**
* <p>
* Generates the difference control {@link Point}s of this
* {@link BezierCurve} to the given {@link Straight3D}.
* </p>
* <p>
* The difference control {@link Point}s are the control {@link Point}s of a
* {@link BezierCurve} that yields the signed distance of each {@link Point}
* on this {@link BezierCurve} to the given {@link Straight3D}.
* </p>
*
* @param line
* the {@link Straight3D} to which the difference
* {@link BezierCurve}'s control {@link Point}s are to be
* computed
* @return the difference {@link BezierCurve}'s control {@link Point}s
*/
private Vector3D[] genDifferencePoints(Straight3D line) {
Vector3D[] D = new Vector3D[points.length];
for (int i = 0; i < points.length; i++) {
double y = line.getSignedDistanceCW(points[i]);
D[i] = new Vector3D((double) (i) / (double) (points.length - 1), y,
1);
}
return D;
}
/**
* Computes the {@link Point} on this {@link BezierCurve} at parameter value
* <i>t</i>, which is expected to lie in the parameter {@link Interval}
* <code>[0;1]</code>.
*
* @param t
* the parameter value for which this {@link BezierCurve} is
* evaluated
* @return the {@link Point} on this {@link BezierCurve} at the given
* parameter value
*/
public Point get(double t) {
return getHC(t).toPoint();
}
@Override
public Rectangle getBounds() {
double xmin = findExtreme(xminCmp).x;
double xmax = findExtreme(xmaxCmp).x;
double ymin = findExtreme(yminCmp).y;
double ymax = findExtreme(ymaxCmp).y;
return new Rectangle(new Point(xmin, ymin), new Point(xmax, ymax));
}
/**
* Returns a new {@link BezierCurve} object representing this
* {@link BezierCurve} on the {@link Interval} <code>[s;e]</code>.
*
* @param s
* the lower limit of the parameter {@link Interval} which is
* clipped out of this {@link BezierCurve}
* @param e
* the upper limit of the parameter {@link Interval} which is
* clipped out of this {@link BezierCurve}
* @return a new {@link BezierCurve} representing this {@link BezierCurve}
* on the {@link Interval} <code>[s;e]</code>
*/
public BezierCurve getClipped(double s, double e) {
if (s == 1) {
return new BezierCurve(points[points.length - 1]);
}
BezierCurve right = split(s)[1];
double rightT2 = (e - s) / (1 - s);
return right.split(rightT2)[0];
}
/**
* Returns a bounding {@link Rectangle} of the control {@link Polygon} of
* this {@link BezierCurve}.
*
* @return a {@link Rectangle} representing the bounds of the control
* {@link Polygon} of this {@link BezierCurve}
*/
public Rectangle getControlBounds() {
Point[] realPoints = getPoints();
double xmin = realPoints[0].x, xmax = realPoints[0].x,
ymin = realPoints[0].y, ymax = realPoints[0].y;
for (int i = 1; i < realPoints.length; i++) {
if (realPoints[i].x < xmin) {
xmin = realPoints[i].x;
} else if (realPoints[i].x > xmax) {
xmax = realPoints[i].x;
}
if (realPoints[i].y < ymin) {
ymin = realPoints[i].y;
} else if (realPoints[i].y > ymax) {
ymax = realPoints[i].y;
}
}
return new Rectangle(xmin, ymin, xmax - xmin, ymax - ymin);
}
@Override
public BezierCurve getCopy() {
return new BezierCurve(points);
}
/**
* Computes the hodograph, the first parametric derivative, of this
* {@link BezierCurve}.
*
* @return the hodograph of this {@link BezierCurve}
*/
public BezierCurve getDerivative() {
Vector3D[] controlPoints = new Vector3D[points.length - 1];
for (int i = 0; i < controlPoints.length; i++) {
controlPoints[i] = points[i + 1].getSubtracted(points[i])
.getScaled(points.length - 1);
// ignore z coordinate:
controlPoints[i].z = 1;
}
return new BezierCurve(controlPoints);
}
/**
* Computes a {@link BezierCurve} with a degree of one higher than this
* {@link BezierCurve}'s degree but of the same shape.
*
* @return a {@link BezierCurve} of the same shape as this
* {@link BezierCurve} but with one more control {@link Point}
*/
public BezierCurve getElevated() {
Point[] p = getPoints();
Point[] q = new Point[p.length + 1];
q[0] = p[0];
q[p.length] = p[p.length - 1];
for (int i = 1; i < p.length; i++) {
double c = (double) i / (double) (p.length);
q[i] = p[i - 1].getScaled(c).getTranslated(p[i].getScaled(1 - c));
}
return new BezierCurve(q);
}
/**
* Returns a {@link Vector3D} representing the {@link Point} at the given
* parameter value.
*
* @param t
* the parameter value for which this {@link BezierCurve} is
* evaluated
* @return the {@link Vector3D} at the given parameter value
*/
private Vector3D getHC(double t) {
if (t < 0 || t > 1) {
throw new IllegalArgumentException("t out of range: " + t);
}
// using horner's scheme:
int n = points.length;
if (n < 1) {
return null;
}
double bn = 1, tn = 1, d = 1d - t;
Vector3D pn = points[0].getScaled(bn * tn);
for (int i = 1; i < n; i++) {
bn = bn * (n - i) / i;
tn = tn * t;
pn = pn.getScaled(d).getAdded(points[i].getScaled(bn * tn));
}
return pn;
}
/**
* <p>
* Computes {@link IntervalPair}s which do reflect {@link Point}s of
* intersection between this and the given other {@link BezierCurve}. Each
* {@link IntervalPair} reflects a single {@link Point} of intersection.
* </p>
* <p>
* For every {@link IntervalPair} a {@link Point} of intersection is
* inserted into the given {@link Set} of {@link Point}s.
* </p>
* <p>
* If there are infinite {@link Point}s of intersection, i.e. the curves do
* overlap, an empty set is returned. (see
* {@link BezierCurve#overlaps(BezierCurve)})
* </p>
*
* @param other
* The {@link BezierCurve} which is searched for {@link Point}s
* of intersection with this {@link BezierCurve}.
* @param intersections
* The {@link Point}-{@link Set} where {@link Point}s of
* intersection are inserted.
* @return For a finite number of intersection {@link Point}s, a {@link Set}
* of {@link IntervalPair}s is returned where every
* {@link IntervalPair} represents a single {@link Point} of
* intersection. For an infinite number of intersection
* {@link Point}s, an empty {@link Set} is returned.
*/
protected Set<IntervalPair> getIntersectionIntervalPairs(BezierCurve other,
Set<Point> intersections) {
Set<IntervalPair> intervalPairs = new HashSet<>();
Set<IntervalPair> endPointIntervalPairs = new HashSet<>();
IntervalPair ip = new IntervalPair(this, Interval.getFull(), other,
Interval.getFull());
findEndPointIntersections(ip, endPointIntervalPairs, intersections);
findIntersectionChunks(ip, intervalPairs, intersections);
normalizeIntervalPairs(intervalPairs.toArray(new IntervalPair[] {}));
IntervalPair[] clusters = clusterChunks(
intervalPairs.toArray(new IntervalPair[] {}), 0);
IntervalPair overlapIntervalPair = extractOverlap(clusters,
endPointIntervalPairs.toArray(new IntervalPair[] {}));
BezierCurve overlap = overlapIntervalPair == null ? null
: overlapIntervalPair.getPClipped();
Set<IntervalPair> results = new HashSet<>();
for (IntervalPair epip : endPointIntervalPairs) {
if (overlapIntervalPair == null
|| !isNextTo(overlapIntervalPair, epip, CHUNK_SHIFT)) {
results.add(epip);
} else {
for (Iterator<Point> iterator = intersections
.iterator(); iterator.hasNext();) {
if (overlap.contains(iterator.next())) {
iterator.remove();
}
}
}
}
outer: for (IntervalPair cluster : clusters) {
if (overlapIntervalPair != null) {
if (isNextTo(overlapIntervalPair, cluster, CHUNK_SHIFT)) {
continue outer;
}
}
for (IntervalPair epip : endPointIntervalPairs) {
if (isNextTo(cluster, epip, CHUNK_SHIFT)) {
continue outer;
}
}
// a.t.m. assume for every cluster just a single point of
// intersection:
Point poi = findSinglePreciseIntersection(cluster);
if (poi != null) {
intersections.add(poi);
if (cluster.converges()) {
results.add(cluster.getCopy());
}
}
}
return results;
}
/**
* Returns the {@link Point}s of intersection of this and the given other
* {@link BezierCurve}.
*
* @param other
* the {@link BezierCurve} which is searched for {@link Point}s
* of intersection with this {@link BezierCurve}
* @return the {@link Point}s of intersection of this {@link BezierCurve}
* and the given other {@link BezierCurve}
*/
public Point[] getIntersections(BezierCurve other) {
Set<Point> intersections = new HashSet<>();
getIntersectionIntervalPairs(other, intersections);
return intersections.toArray(new Point[] {});
}
@Override
public final Point[] getIntersections(ICurve curve) {
Set<Point> intersections = new HashSet<>();
for (BezierCurve c : curve.toBezier()) {
intersections.addAll(Arrays.asList(getIntersections(c)));
}
return intersections.toArray(new Point[] {});
}
/**
* Returns a {@link PolyBezier} that represents an approximation of the
* refined offset of this {@link BezierCurve} where cusps in the input curve
* are approximated by arc segments in the offset and local
* self-intersections in the offset are removed while global
* self-intersections and other singularities in the offset remain
* unprocessed.
*
* @param distance
* The signed distance for which to compute a refined offset
* approximation.
* @return A {@link PolyBezier} representing the refined offset of this
* {@link BezierCurve} for the given distance.
*/
public PolyBezier getOffsetRefined(double distance) {
return new LocalIntersectionOffsetRefiner()
.refine(new CuspAwareOffsetApproximator()
.approximateOffset(this, distance));
}
/**
* Returns a {@link PolyBezier} that represents an approximation of the
* offset of this {@link BezierCurve} where cusps in the input curve are
* approximated by arc segments in the offset but any singularities remain
* unprocessed.
*
* @param distance
* The signed distance for which to compute an offset
* approximation.
* @return A {@link PolyBezier} representing the (unprocessed) offset of
* this {@link BezierCurve} for the given distance.
*/
public PolyBezier getOffsetUnprocessed(double distance) {
// merge the curves to yield a valid PolyBezier
return mergeCurves(new CuspAwareOffsetApproximator()
.approximateOffset(this, distance)
.getApproximatedOffsetCurve());
}
/**
* <p>
* Returns a {@link BezierCurve} that represents the overlap of this
* {@link BezierCurve} and the given other {@link BezierCurve}. If no
* overlap exists, <code>null</code> is returned. An overlap is identified
* by an infinite number of intersection points.
* </p>
*
* @param other
* The {@link BezierCurve} to which an overlap is computed.
* @return a {@link BezierCurve} representing the overlap of this and the
* given other {@link BezierCurve} if an overlap exists, otherwise
* <code>null</code>
*/
public BezierCurve getOverlap(BezierCurve other) {
if (equals(other)) {
return getCopy();
}
Set<Point> intersections = new HashSet<>();
Set<IntervalPair> intervalPairs = new HashSet<>();
Set<IntervalPair> endPointIntervalPairs = new HashSet<>();
IntervalPair ip = new IntervalPair(this, Interval.getFull(), other,
Interval.getFull());
findEndPointIntersections(ip, endPointIntervalPairs, intersections);
findIntersectionChunks(ip, intervalPairs, intersections);
IntervalPair[] intervalPairs2 = intervalPairs
.toArray(new IntervalPair[] {});
normalizeIntervalPairs(intervalPairs2);
IntervalPair[] clusters = clusterChunks(intervalPairs2, 0);
IntervalPair overlap = extractOverlap(clusters,
endPointIntervalPairs.toArray(new IntervalPair[] {}));
return overlap == null ? null : overlap.getPClipped();
}
@Override
public final ICurve[] getOverlaps(ICurve c) {
return CurveUtils.getOverlaps(this, c);
}
@Override
public Point getP1() {
return points[0].toPoint();
}
@Override
public Point getP2() {
return points[points.length - 1].toPoint();
}
/**
* Returns the parameter value of this {@link BezierCurve} for the given
* {@link Point}. If the given {@link Point} is not on this
* {@link BezierCurve} an {@link IllegalArgumentException} is thrown.
*
* @param p
* the {@link Point} for which the parameter value on this
* {@link BezierCurve} is to be found
* @return the corresponding parameter value of the given {@link Point} on
* this {@link BezierCurve}
*/
public double getParameterAt(Point p) {
if (p == null) {
throw new IllegalArgumentException(
"The passed-in Point may not be null: getParameterAt(" + p
+ "), this = " + this);
}
double[] interval = new double[] { 0, 1 };
if (containmentParameter(this, interval, p)) {
return (interval[0] + interval[1]) / 2;
} else {
throw new IllegalArgumentException(
"The given Point does not lie on this BezierCurve: getParameterAt("
+ p + "), this = " + this);
}
}
/**
* Returns the <i>i</i>th control {@link Point} of this {@link BezierCurve}.
* The start {@link Point} is at index <code>0</code>, the first handle-
* {@link Point} is at index <code>1</code>, etc.
*
* @param i
* the index of the control {@link Point} of this
* {@link BezierCurve} to return
* @return the <i>i</i>th control {@link Point} of this {@link BezierCurve}
*/
public Point getPoint(int i) {
if (i < 0 || i >= points.length) {
throw new IllegalArgumentException(
"You can only index this BezierCurve's points from 0 to "
+ (points.length - 1) + ": getPoint(" + i
+ "), this = " + this);
}
return points[i].toPoint();
}
/**
* Returns the control {@link Point}s of this {@link BezierCurve}.
*
* @return the control {@link Point}s of this {@link BezierCurve}
*/
public Point[] getPoints() {
Point[] realPoints = new Point[points.length];
for (int i = 0; i < points.length; i++) {
realPoints[i] = points[i].toPoint();
}
return realPoints;
}
/**
* Returns a copy of the {@link Vector3D} representations of the control
* points of this {@link BezierCurve}.
*
* @return a copy of the {@link Vector3D} representations of the control
* points of this {@link BezierCurve}
*/
private Vector3D[] getPointsCopy() {
Vector3D[] copy = new Vector3D[points.length];
for (int i = 0; i < points.length; i++) {
copy[i] = points[i].getCopy();
}
return copy;
}
@Override
public Point getProjection(final Point reference) {
// construct look up table (LUT)
int size = 100;
Point[] lut = constructLUT(0, 1, size);
// find nearest LUT entry
int nearestLut = 0;
double distance = reference.getDistance(lut[0]);
for (int i = 1; i < lut.length; i++) {
double dist = reference.getDistance(lut[i]);
if (dist < distance) {
distance = dist;
nearestLut = i;
}
}
Point nearest = lut[nearestLut];
// compute interval based on LUT
Interval interval = new Interval(
nearestLut / (double) (size - 1) - 1 / (double) (size - 1),
nearestLut / (double) (size - 1) + 1 / (double) (size - 1));
// ensure interval is valid
interval.a = Math.min(1, Math.max(0, interval.a));
interval.b = Math.min(1, Math.max(0, interval.b));
// refine interval
while (!interval.converges()) {
// compute start point and end point for the current interval
Point sp = get(interval.a);
Point ep = get(interval.b);
// compute distance to reference point
double sDist = reference.getDistance(sp);
double eDist = reference.getDistance(ep);
if (sDist >= distance && eDist >= distance) {
// start point and end point have greater distance
// => reduce interval on both sides
double range = interval.b - interval.a;
interval.b = interval.a + 0.75 * range;
interval.a = interval.a + 0.25 * range;
} else if (sDist < distance && sDist < eDist) {
// start has smaller distance
distance = sDist;
nearest = sp;
// reduce interval to its left side
interval.b = (interval.a + interval.b) / 2;
} else if (eDist < distance) {
// end has smaller distance
distance = eDist;
nearest = ep;
// reduce interval to its right side
interval.a = (interval.a + interval.b) / 2;
} else {
// impossible
throw new IllegalStateException(
"condition should not be reachable");
}
}
return nearest;
}
@Override
public BezierCurve getRotatedCCW(Angle angle) {
return getCopy().rotateCCW(angle);
}
@Override
public BezierCurve getRotatedCCW(Angle angle, double cx, double cy) {
return getCopy().rotateCCW(angle, cx, cy);
}
@Override
public BezierCurve getRotatedCCW(Angle angle, Point center) {
return getCopy().rotateCCW(angle, center);
}
@Override
public BezierCurve getRotatedCW(Angle angle) {
return getCopy().rotateCW(angle);
}
@Override
public BezierCurve getRotatedCW(Angle angle, double cx, double cy) {
return getCopy().rotateCW(angle, cx, cy);
}
@Override
public BezierCurve getRotatedCW(Angle angle, Point center) {
return getCopy().rotateCW(angle, center);
}
@Override
public BezierCurve getScaled(double factor) {
return getCopy().scale(factor);
}
@Override
public BezierCurve getScaled(double fx, double fy) {
return getCopy().scale(fx, fy);
}
@Override
public BezierCurve getScaled(double factor, double cx, double cy) {
return getCopy().scale(factor, cx, cy);
}
@Override
public BezierCurve getScaled(double fx, double fy, double cx, double cy) {
return getCopy().scale(fx, fy, cx, cy);
}
@Override
public BezierCurve getScaled(double fx, double fy, Point center) {
return getCopy().scale(fx, fy, center);
}
@Override
public BezierCurve getScaled(double factor, Point center) {
return getCopy().scale(factor, center);
}
/**
* @see IGeometry#getTransformed(AffineTransform)
*/
@Override
public BezierCurve getTransformed(AffineTransform t) {
return new BezierCurve(t.getTransformed(getPoints()));
}
@Override
public BezierCurve getTranslated(double dx, double dy) {
return getCopy().translate(dx, dy);
}
@Override
public BezierCurve getTranslated(Point d) {
return getCopy().translate(d.x, d.y);
}
@Override
public double getX1() {
return getP1().x;
}
@Override
public double getX2() {
return getP2().x;
}
@Override
public double getY1() {
return getP1().y;
}
@Override
public double getY2() {
return getP2().y;
}
@Override
public boolean intersects(ICurve c) {
return getIntersections(c).length > 0;
}
/**
* Moves the {@link Interval}'s start and end values. The start value is set
* to <i>x</i> if <i>x</i> is smaller than the start value. The end value is
* set to <i>x</i> if <i>x</i> is greater than the end value.
*
* @param interval
* the {@link Interval} to modify
* @param x
* the modification value
*/
private void moveInterval(double[] interval, double x) {
// assure that 0 <= x <= 1 to prevent invalid parameter values
if (x < 0) {
x = 0;
} else if (x > 1) {
x = 1;
}
if (interval[0] > x) {
interval[0] = x;
}
if (interval[1] < x) {
interval[1] = x;
}
}
/**
* Checks if this {@link BezierCurve} and the given other
* {@link BezierCurve} overlap, i.e. an infinite set of intersection
* {@link Point}s exists.
*
* @param other
* the {@link BezierCurve} to check for an overlapping segment
* with this {@link BezierCurve}
* @return <code>true</code> if this and the given other {@link BezierCurve}
* overlap, otherwise <code>false</code>
*/
public boolean overlaps(BezierCurve other) {
return getOverlap(other) != null;
}
@Override
public final boolean overlaps(ICurve c) {
for (BezierCurve seg : c.toBezier()) {
if (overlaps(seg)) {
return true;
}
}
return false;
}
/**
* Directly rotates this {@link BezierCurve} counter-clockwise (CCW) around
* its center {@link Point} by the given {@link Angle}. Direct adaptation
* means, that <code>this</code> {@link BezierCurve} is modified in-place.
*
* @param angle
* the rotation {@link Angle}
* @return <code>this</code> for convenience
*/
public BezierCurve rotateCCW(Angle angle) {
Point centroid = Point.getCentroid(getPoints());
return rotateCCW(angle, centroid.x, centroid.y);
}
/**
* Directly rotates this {@link BezierCurve} counter-clockwise (CCW) around
* the {@link Point} specified by the given x and y coordinate values by the
* given {@link Angle}. Direct adaptation means, that <code>this</code>
* {@link BezierCurve} is modified in-place.
*
* @param angle
* the rotation {@link Angle}
* @param cx
* the x coordinate of the {@link Point} to rotate around
* @param cy
* the y coordinate of the {@link Point} to rotate around
* @return <code>this</code> for convenience
*/
public BezierCurve rotateCCW(Angle angle, double cx, double cy) {
Point[] realPoints = getPoints();
Point.rotateCCW(realPoints, angle, cx, cy);
for (int i = 0; i < realPoints.length; i++) {
setPoint(i, realPoints[i]);
}
return this;
}
/**
* Directly rotates this {@link BezierCurve} counter-clockwise (CCW) around
* the given {@link Point} by the given {@link Angle}. Direct adaptation
* means, that <code>this</code> {@link BezierCurve} is modified in-place.
*
* @param angle
* the rotation {@link Angle}
* @param center
* the {@link Point} to rotate around
* @return <code>this</code> for convenience
*/
public BezierCurve rotateCCW(Angle angle, Point center) {
for (int i = 0; i < points.length; i++) {
points[i] = new Vector3D(new Vector(
points[i].toPoint().getTranslated(center.getNegated()))
.getRotatedCCW(angle).toPoint()
.getTranslated(center));
}
return this;
}
/**
* Directly rotates this {@link BezierCurve} clockwise (CW) around its
* center {@link Point} by the given {@link Angle}. Direct adaptation means,
* that <code>this</code> {@link BezierCurve} is modified in-place.
*
* @param angle
* the rotation {@link Angle}
* @return <code>this</code> for convenience
*/
public BezierCurve rotateCW(Angle angle) {
Point centroid = Point.getCentroid(getPoints());
return rotateCW(angle, centroid.x, centroid.y);
}
/**
* Directly rotates this {@link BezierCurve} clockwise (CW) around the
* {@link Point} specified by the given x and y coordinate values by the
* given {@link Angle}. Direct adaptation means, that <code>this</code>
* {@link BezierCurve} is modified in-place.
*
* @param angle
* the rotation {@link Angle}
* @param cx
* the x coordinate of the {@link Point} to rotate around
* @param cy
* the y coordinate of the {@link Point} to rotate around
* @return <code>this</code> for convenience
*/
public BezierCurve rotateCW(Angle angle, double cx, double cy) {
Point[] realPoints = getPoints();
Point.rotateCW(realPoints, angle, cx, cy);
for (int i = 0; i < realPoints.length; i++) {
setPoint(i, realPoints[i]);
}
return this;
}
/**
* Directly rotates this {@link BezierCurve} clockwise (CW) around the given
* {@link Point} by the given {@link Angle}. Direct adaptation means, that
* <code>this</code> {@link BezierCurve} is modified in-place.
*
* @param angle
* the rotation {@link Angle}
* @param center
* the {@link Point} to rotate around
* @return <code>this</code> for convenience
*/
public BezierCurve rotateCW(Angle angle, Point center) {
return rotateCW(angle, center.x, center.y);
}
@Override
public BezierCurve scale(double factor) {
return scale(factor, factor);
}
@Override
public BezierCurve scale(double fx, double fy) {
Point centroid = Point.getCentroid(getPoints());
return scale(fx, fy, centroid.x, centroid.y);
}
@Override
public BezierCurve scale(double factor, double cx, double cy) {
return scale(factor, factor, cx, cy);
}
@Override
public BezierCurve scale(double fx, double fy, double cx, double cy) {
Point[] realPoints = getPoints();
Point.scale(realPoints, fx, fy, cx, cy);
for (int i = 0; i < realPoints.length; i++) {
setPoint(i, realPoints[i]);
}
return this;
}
@Override
public BezierCurve scale(double fx, double fy, Point center) {
return scale(fx, fy, center.x, center.y);
}
@Override
public BezierCurve scale(double factor, Point center) {
return scale(factor, factor, center.x, center.y);
}
/**
* Sets the start {@link Point} of this {@link BezierCurve} to the given
* {@link Point}.
*
* @param p1
* the new start {@link Point} of this {@link BezierCurve}
* @return <code>this</code> for convenience
*/
public BezierCurve setP1(Point p1) {
setPoint(0, p1);
return this;
}
/**
* Sets the end {@link Point} of this {@link BezierCurve} to the given
* {@link Point}.
*
* @param p2
* the new end {@link Point} of this {@link BezierCurve}
* @return <code>this</code> for convenience
*/
public BezierCurve setP2(Point p2) {
setPoint(points.length - 1, p2);
return this;
}
/**
* Sets the <i>i</i>th control {@link Point} of this {@link BezierCurve}.
* The start {@link Point} is at index <code>0</code>, the first handle-
* {@link Point} is at index <code>1</code>, etc.
*
* @param i
* the index of the control {@link Point} of this
* {@link BezierCurve} to set
* @param p
* the new control {@link Point} at the given index
* @return <code>this</code> for convenience
*/
public BezierCurve setPoint(int i, Point p) {
if (i < 0 || i >= points.length) {
throw new IllegalArgumentException("setPoint(" + i + ", " + p
+ "): You can only index this BezierCurve's points from 0 to "
+ (points.length - 1) + ".");
}
points[i] = new Vector3D(p);
return this;
}
/**
* Subdivides this {@link BezierCurve} at the given parameter value <i>t</i>
* into two new {@link BezierCurve}s. The first one is the
* {@link BezierCurve} over the parameter {@link Interval}
* <code>[0;t]</code> and the second one is the {@link BezierCurve} over the
* parameter {@link Interval} <code>[t;1]</code>.
*
* @param t
* the parameter value at which this {@link BezierCurve} is
* subdivided
* @return an array of two {@link BezierCurve}s, the left (
* <code>[0;t]</code>) and the right (<code>[t;1]</code>)
*/
public BezierCurve[] split(double t) {
Vector3D[] leftPoints = new Vector3D[points.length];
Vector3D[] rightPoints = new Vector3D[points.length];
Vector3D[] ratioPoints = getPointsCopy();
for (int i = 0; i < points.length; i++) {
leftPoints[i] = ratioPoints[0];
rightPoints[points.length - 1 - i] = ratioPoints[points.length - 1
- i];
for (int j = 0; j < points.length - i - 1; j++) {
ratioPoints[j] = ratioPoints[j].getRatio(ratioPoints[j + 1], t);
}
}
return new BezierCurve[] { new BezierCurve(leftPoints),
new BezierCurve(rightPoints) };
}
@Override
public BezierCurve[] toBezier() {
return new BezierCurve[] { this };
}
/**
* Returns a hard approximation of this {@link BezierCurve} as a
* {@link CubicCurve}. The new {@link CubicCurve} is constructed from the
* start {@link Point}, the first two handle {@link Point}s and the end
* {@link Point} of this {@link BezierCurve}. If this {@link BezierCurve} is
* not of degree four or higher, i.e. it does not have four or more control
* {@link Point}s (including start and end {@link Point}), <code>null</code>
* is returned.
*
* @return a new {@link CubicCurve} that is constructed from the start
* {@link Point}, the first two handle {@link Point}s and the end
* {@link Point} of this {@link BezierCurve} or <code>null</code> if
* this {@link BezierCurve} does not have at least four control
* {@link Point}s
*/
public CubicCurve toCubic() {
if (points.length > 3) {
return new CubicCurve(points[0].toPoint(), points[1].toPoint(),
points[2].toPoint(), points[points.length - 1].toPoint());
}
return null;
}
/**
* Returns a hard approximation of this {@link BezierCurve} as a
* {@link Line}. The {@link Line} is constructed from the start and end
* {@link Point} of this {@link BezierCurve}.
*
* @return a {@link Line} from the start {@link Point} to the end
* {@link Point} of this {@link BezierCurve} or <code>null</code> if
* this {@link BezierCurve} does only have one control {@link Point}
*/
public Line toLine() {
if (points.length > 1) {
return new Line(points[0].toPoint(),
points[points.length - 1].toPoint());
}
return null;
}
/**
* Computes an approximation of this {@link BezierCurve} by a strip of
* {@link Line}s. For detailed information on how the approximation is
* computed, see {@link BezierCurve#toLineStrip(double, Interval)}.
*
* @param lineSimilarity
* the threshold for the sum of the distances of the control
* {@link Point}s to the baseline ({@link #toLine()}) of this
* {@link BezierCurve}
* @return an approximation of this {@link BezierCurve} by a strip of
* {@link Line}s
* @see BezierCurve#toLineStrip(double, Interval)
*/
public Line[] toLineStrip(double lineSimilarity) {
return toLineStrip(lineSimilarity, Interval.getFull());
}
/**
* <p>
* Computes an approximation of this {@link BezierCurve} by a strip of
* {@link Line}s.
* </p>
* <p>
* The {@link BezierCurve} is recursively subdivided until it is "similar"
* to a straight {@link Line}. The similarity check computes the sum of the
* distances of the control {@link Point}s to the baseline (
* {@link #toLine()}) of this {@link BezierCurve}. If this sum is smaller
* than the given <i>lineSimilarity</i>, the {@link BezierCurve} is assumed
* to be "similar" to a straight line.
* </p>
*
* @param lineSimilarity
* the threshold for the sum of the distances of the control
* points to the baseline of this {@link BezierCurve}
* @param startInterval
* the {@link Interval} of this {@link BezierCurve} that has to
* be approximated by a strip of {@link Line}s
* @return {@link Line} segments approximating this {@link BezierCurve}
*/
public Line[] toLineStrip(double lineSimilarity, Interval startInterval) {
ArrayList<Line> lines = new ArrayList<>();
Point startPoint = getHC(startInterval.a).toPoint();
Stack<Interval> parts = new Stack<>();
parts.push(startInterval);
while (!parts.isEmpty()) {
Interval i = parts.pop();
BezierCurve part = getClipped(i.a, i.b);
if (distanceToBaseLine(part) < lineSimilarity) {
Point endPoint = getHC(i.b).toPoint();
lines.add(new Line(startPoint, endPoint));
startPoint = endPoint;
} else {
double im = i.getMid();
parts.push(new Interval(im, i.b));
parts.push(new Interval(i.a, im));
}
}
return lines.toArray(new Line[] {});
}
/**
* Returns a {@link Path} approximating this {@link BezierCurve} using
* {@link Line} segments.
*
* @return a {@link Path} approximating this {@link BezierCurve} using
* {@link Line} segments
*/
@Override
public Path toPath() {
Path path = new Path();
Point startPoint = points[0].toPoint();
path.moveTo(startPoint.x, startPoint.y);
for (Line seg : toLineStrip(0.25d)) {
path.lineTo(seg.getX2(), seg.getY2());
}
return path;
}
/**
* Computes {@link Point}s on this {@link BezierCurve} over the given
* {@link Interval}. Consecutive returned {@link Point}s are required to be
* {@link Point#equals(Object) equal} to each other.
*
* @param startInterval
* the {@link Interval} of this {@link BezierCurve} to calculate
* {@link Point}s for
* @return {@link Point}s on this {@link BezierCurve} over the given
* parameter {@link Interval} where consecutive {@link Point}s are
* {@link Point#equals(Object) equal} to each other
*/
public Point[] toPoints(Interval startInterval) {
ArrayList<Point> points = new ArrayList<>();
points.add(getHC(startInterval.a).toPoint());
Stack<Interval> parts = new Stack<>();
parts.push(startInterval);
while (!parts.isEmpty()) {
Interval i = parts.pop();
BezierCurve part = getClipped(i.a, i.b);
Point[] partPoints = part.getPoints();
boolean allTogether = true;
for (int j = 1; j < partPoints.length; j++) {
if (!partPoints[0].equals(partPoints[j])) {
allTogether = false;
break;
}
}
if (allTogether) {
points.add(partPoints[partPoints.length - 1]);
} else {
double im = i.getMid();
parts.push(new Interval(im, i.b));
parts.push(new Interval(i.a, im));
}
}
return points.toArray(new Point[] {});
}
/**
* Returns a hard approximation of this {@link BezierCurve} as a
* {@link QuadraticCurve}. The new {@link QuadraticCurve} is constructed
* from the start {@link Point}, the first handle {@link Point} and the end
* {@link Point} of this {@link BezierCurve}. If this {@link BezierCurve} is
* not of degree three or higher, i.e. it does not have three or more
* control {@link Point}s (including start and end {@link Point}),
* <code>null</code> is returned.
*
* @return a new {@link QuadraticCurve} that is constructed from the start
* {@link Point}, the first handle {@link Point} and the end
* {@link Point} of this {@link BezierCurve} or <code>null</code> if
* this {@link BezierCurve} does not have at least three control
* {@link Point}s
*/
public QuadraticCurve toQuadratic() {
if (points.length > 2) {
return new QuadraticCurve(points[0].toPoint(), points[1].toPoint(),
points[points.length - 1].toPoint());
}
return null;
}
@Override
public String toString() {
StringBuffer str = new StringBuffer();
str.append("BezierCurve(");
for (int i = 0; i < points.length; i++) {
Vector3D v = points[i];
str.append(v);
if (i < points.length - 1) {
str.append(", ");
}
}
str.append(")");
return str.toString();
}
@Override
public BezierCurve translate(double dx, double dy) {
Point[] realPoints = getPoints();
Point.translate(realPoints, dx, dy);
for (int i = 0; i < realPoints.length; i++) {
setPoint(i, realPoints[i]);
}
return this;
}
@Override
public BezierCurve translate(Point d) {
return translate(d.x, d.y);
}
}