/*
* @(#)BezierPath.java
*
* Copyright (c) 1996-2010 The authors and contributors of JHotDraw.
* You may not use, copy or modify this file, except in compliance with the
* accompanying license terms.
*/
package org.jhotdraw.geom;
import javax.annotation.Nullable;
import java.awt.*;
import java.awt.geom.*;
import java.io.Serializable;
import java.util.*;
/**
* BezierPath allows the construction of paths consisting of straight lines,
* quadratic curves and cubic curves.
* <p>
* A BezierPath is defined by its nodes. Each node has three control points:
* C0, C1, C2. A mask defines which control points are in use. At a node,
* the path passes through C0. C1 controls the curve going towards C0. C2
* controls the curve going away from C0.
*
* @author Werner Randelshofer
* @version $Id$
*/
public class BezierPath extends ArrayList<BezierPath.Node>
implements Shape, Serializable, Cloneable {
private static final long serialVersionUID=1L;
/** Constant for having only control point C0 in effect. C0 is the point
* through whitch the curve passes. */
public static final int C0_MASK = 0;
/** Constant for having control point C1 in effect (in addition
* to C0). C1 controls the curve going towards C0.
* */
public static final int C1_MASK = 1;
/** Constant for having control point C2 in effect (in addition to C0).
* C2 controls the curve going away from C0.
*/
public static final int C2_MASK = 2;
/** Constant for having control points C1 and C2 in effect (in addition to C0). */
public static final int C1C2_MASK = C1_MASK | C2_MASK;
/**
* We cache a Path2D.Double instance to speed up Shape operations.
*/
@Nullable private transient Path2D.Double generalPath;
/**
* We cache a Rectangle2D.Double instance to speed up getBounds operations.
*/
@Nullable private transient Rectangle2D.Double bounds;
/**
* We cache the index of the outermost node to speed up method indexOfOutermostNode();
*/
private int outer = -1;
/**
* If this value is set to true, closes the bezier path.
*/
private boolean isClosed;
/**
* The winding rule for filling the bezier path.
*/
private int windingRule = Path2D.Double.WIND_EVEN_ODD;
/**
* Defines a vertex (node) of the bezier path.
* <p>
* A vertex consists of three control points: C0, C1 and C2.
* <ul>
* <li>The bezier path always passes through C0.</li>
* <li>C1 is used to control the curve towards C0.
* </li>
* <li>C2 is used to control the curve going away from C0.</li>
* </ul>
*/
public static class Node implements Cloneable, Serializable {
private static final long serialVersionUID=1L;
/**
* This mask is used to describe which control points in addition to
* C0 are in effect.
*/
public int mask = 0;
/** Control point x coordinates. */
public double[] x = new double[3];
/** Control point y coordinates. */
public double[] y = new double[3];
/** This is a hint for editing tools. If this is set to true,
* the editing tools shall keep all control points on the same
* line.
*/
public boolean keepColinear = true;
public Node() {
}
public Node(Node that) {
setTo(that);
}
public void setTo(Node that) {
this.mask = that.mask;
this.keepColinear = that.keepColinear;
System.arraycopy(that.x, 0, this.x, 0, 3);
System.arraycopy(that.y, 0, this.y, 0, 3);
}
public Node(Point2D.Double c0) {
this.mask = 0;
x[0] = c0.x;
y[0] = c0.y;
x[1] = c0.x;
y[1] = c0.y;
x[2] = c0.x;
y[2] = c0.y;
}
public Node(int mask, Point2D.Double c0, Point2D.Double c1, Point2D.Double c2) {
this.mask = mask;
x[0] = c0.x;
y[0] = c0.y;
x[1] = c1.x;
y[1] = c1.y;
x[2] = c2.x;
y[2] = c2.y;
}
public Node(double x0, double y0) {
this.mask = 0;
x[0] = x0;
y[0] = y0;
x[1] = x0;
y[1] = y0;
x[2] = x0;
y[2] = y0;
}
public Node(int mask, double x0, double y0, double x1, double y1, double x2, double y2) {
this.mask = mask;
x[0] = x0;
y[0] = y0;
x[1] = x1;
y[1] = y1;
x[2] = x2;
y[2] = y2;
}
public int getMask() {
return mask;
}
public void setMask(int newValue) {
mask = newValue;
}
public void setControlPoint(int index, Point2D.Double p) {
x[index] = p.x;
y[index] = p.y;
}
public Point2D.Double getControlPoint(int index) {
return new Point2D.Double(x[index], y[index]);
}
public void moveTo(Point2D.Double p) {
moveBy(p.x - x[0], p.y - y[0]);
}
public void moveTo(double x, double y) {
moveBy(x - this.x[0], y - this.y[0]);
}
public void moveBy(double dx, double dy) {
for (int i = 0; i < 3; i++) {
x[i] += dx;
y[i] += dy;
}
}
@Override
public Object clone() {
try {
Node that = (Node) super.clone();
that.x = this.x.clone();
that.y = this.y.clone();
return that;
} catch (CloneNotSupportedException e) {
InternalError error = new InternalError();
error.initCause(e);
throw error;
}
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append(super.toString());
buf.append('[');
for (int i = 0; i < 3; i++) {
if (i != 0) {
if ((mask & i) == i) {
buf.append(',');
} else {
continue;
}
}
buf.append('x');
buf.append(i);
buf.append('=');
buf.append(x[i]);
buf.append(",y");
buf.append(i);
buf.append('=');
buf.append(y[i]);
}
buf.append(']');
return buf.toString();
}
@Override
public int hashCode() {
return (mask & 0x3) << 29
| (Arrays.hashCode(x) & 0x3fff0000)
| (Arrays.hashCode(y) & 0xffff);
}
@Override
public boolean equals(Object o) {
if (o instanceof BezierPath.Node) {
BezierPath.Node that = (BezierPath.Node) o;
return that.mask == this.mask
&& Arrays.equals(that.x, this.x)
&& Arrays.equals(that.y, this.y);
}
return false;
}
}
/** Creates a new instance. */
public BezierPath() {
}
/**
* Adds a node to the path.
* <p>
* This is a convenience method for adding a node with a single control
* point C0 to the path.
*/
public void add(Point2D.Double c0) {
add(new Node(0, c0, c0, c0));
}
/**
* Adds a node to the path.
* <p>
* This is a convenience method for adding a node with a single control
* point C0 to the path.
*/
public void add(double x, double y) {
add(new Node(0, x, y, x, y, x, y));
}
/**
* Adds a node to the path.
* <p>
* This is a convenience method for adding a node with three control points
* C0, C1 and C2, and a mask.
*
* @param ctrlMask An or-combination of C0_MASK,C1_MASK and C2_MASK.
* @param c0 The coordinates of the C0 control point.
* @param c1 The coordinates of the C1 control point.
* @param c2 The coordinates of the C2 control point.
*/
public void add(int ctrlMask, Point2D.Double c0, Point2D.Double c1, Point2D.Double c2) {
add(new Node(ctrlMask, c0, c1, c2));
}
/**
* Adds a set of nodes to the path.
* <p>
* Convenience method for adding multiple nodes with a single control point
* C0.
*/
public void addPolyline(Collection<Point2D.Double> points) {
for (Point2D.Double c0 : points) {
add(new Node(0, c0, c0, c0));
}
}
/**
* Convenience method for changing a single control point of a node.
*
* @param nodeIndex The index of the node.
* @param ctrlIndex Either C0_MASK, C1_MASK or C2_MASK.
* @param p The control point. The coordinates will be cloned.
*/
public void set(int nodeIndex, int ctrlIndex, Point2D.Double p) {
Node c = get(nodeIndex);
c.x[ctrlIndex] = p.x;
c.y[ctrlIndex] = p.y;
}
/**
* Convenience method for getting a single control point of a node.
*
* @param nodeIndex The index of the node.
* @param ctrlIndex Either C0_MASK, C1_MASK or C2_MASK.
* @return Returns a clone of the control point.
*/
public Point2D.Double get(int nodeIndex, int ctrlIndex) {
Node c = get(nodeIndex);
return new Point2D.Double(
c.x[ctrlIndex],
c.y[ctrlIndex]);
}
/**
* This must be called after the BezierPath has been changed.
*/
public void invalidatePath() {
generalPath = null;
bounds = null;
outer = -1;
}
/**
* Recomputes the BezierPath, if it is invalid.
*/
public void validatePath() {
if (generalPath == null) {
generalPath = toGeneralPath();
}
}
/** Converts the BezierPath into a Path2D.Double. */
public Path2D.Double toGeneralPath() {
Path2D.Double gp = new Path2D.Double();
gp.setWindingRule(windingRule);
if (size() == 0) {
gp.moveTo(0, 0);
gp.lineTo(0, 0 + 1);
} else if (size() == 1) {
Node current = get(0);
gp.moveTo(current.x[0], current.y[0]);
gp.lineTo(current.x[0], current.y[0] + 1);
} else {
Node previous;
Node current;
previous = current = get(0);
gp.moveTo(current.x[0], current.y[0]);
for (int i = 1, n = size(); i < n; i++) {
previous = current;
current = get(i);
if ((previous.mask & C2_MASK) == 0) {
if ((current.mask & C1_MASK) == 0) {
gp.lineTo(
current.x[0], current.y[0]);
} else {
gp.quadTo(
current.x[1], current.y[1],
current.x[0], current.y[0]);
}
} else {
if ((current.mask & C1_MASK) == 0) {
gp.quadTo(
previous.x[2], previous.y[2],
current.x[0], current.y[0]);
} else {
gp.curveTo(
previous.x[2], previous.y[2],
current.x[1], current.y[1],
current.x[0], current.y[0]);
}
}
}
if (isClosed) {
if (size() > 1) {
previous = get(size() - 1);
current = get(0);
if ((previous.mask & C2_MASK) == 0) {
if ((current.mask & C1_MASK) == 0) {
gp.lineTo(
current.x[0], current.y[0]);
} else {
gp.quadTo(
current.x[1], current.y[1],
current.x[0], current.y[0]);
}
} else {
if ((current.mask & C1_MASK) == 0) {
gp.quadTo(
previous.x[2], previous.y[2],
current.x[0], current.y[0]);
} else {
gp.curveTo(
previous.x[2], previous.y[2],
current.x[1], current.y[1],
current.x[0], current.y[0]);
}
}
}
gp.closePath();
}
}
return gp;
}
@Override
public boolean contains(Point2D p) {
validatePath();
return generalPath.contains(p);
}
;
/**
* Returns true, if the outline of this bezier path contains the specified
* point.
*
* @param p The point to be tested.
* @param tolerance The tolerance for the test.
*/
public boolean outlineContains(Point2D.Double p, double tolerance) {
return Shapes.outlineContains(this, p, tolerance);
}
@Override
public boolean intersects(Rectangle2D r) {
validatePath();
return generalPath.intersects(r);
}
@Override
public PathIterator getPathIterator(AffineTransform at) {
return new BezierPathIterator(this, at);
}
@Override
public PathIterator getPathIterator(AffineTransform at, double flatness) {
return new FlatteningPathIterator(new BezierPathIterator(this, at), flatness);
}
@Override
public boolean contains(Rectangle2D r) {
validatePath();
return generalPath.contains(r);
}
@Override
public boolean intersects(double x, double y, double w, double h) {
validatePath();
return generalPath.intersects(x, y, w, h);
}
@Override
public Rectangle2D.Double getBounds2D() {
if (bounds == null) {
double x1, y1, x2, y2;
int size = size();
if (size == 0) {
x1 = y1 = x2 = y2 = 0.0f;
} else {
double x, y;
// handle first node
Node node = get(0);
y1 = y2 = node.y[0];
x1 = x2 = node.x[0];
if (isClosed && (node.mask & C1_MASK) != 0) {
y = node.y[1];
x = node.x[1];
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
}
if ((node.mask & C2_MASK) != 0) {
y = node.y[2];
x = node.x[2];
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
}
// handle last node
node = get(size - 1);
y = node.y[0];
x = node.x[0];
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
if ((node.mask & C1_MASK) != 0) {
y = node.y[1];
x = node.x[1];
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
}
if (isClosed && (node.mask & C2_MASK) != 0) {
y = node.y[2];
x = node.x[2];
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
}
// handle all other nodes
for (int i = 1, n = size - 1; i < n; i++) {
node = get(i);
y = node.y[0];
x = node.x[0];
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
if ((node.mask & C1_MASK) != 0) {
y = node.y[1];
x = node.x[1];
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
}
if ((node.mask & C2_MASK) != 0) {
y = node.y[2];
x = node.x[2];
if (x < x1) {
x1 = x;
}
if (y < y1) {
y1 = y;
}
if (x > x2) {
x2 = x;
}
if (y > y2) {
y2 = y;
}
}
}
}
bounds = new Rectangle2D.Double(x1, y1, x2 - x1, y2 - y1);
}
return (Rectangle2D.Double) bounds.clone();
}
@Override
public Rectangle getBounds() {
return getBounds2D().getBounds();
}
@Override
public boolean contains(double x, double y, double w, double h) {
validatePath();
return generalPath.contains(x, y, w, h);
}
@Override
public boolean contains(double x, double y) {
validatePath();
return generalPath.contains(x, y);
}
public void setClosed(boolean newValue) {
if (isClosed != newValue) {
isClosed = newValue;
invalidatePath();
}
}
public boolean isClosed() {
return isClosed;
}
/** Creates a deep copy of the BezierPath. */
@Override
public BezierPath clone() {
BezierPath that = (BezierPath) super.clone();
for (int i = 0, n = this.size(); i < n; i++) {
that.set(i, (Node) this.get(i).clone());
}
return that;
}
/**
* Transforms the BezierPath.
* @param tx the transformation.
*/
public void transform(AffineTransform tx) {
Point2D.Double p = new Point2D.Double();
for (Node cp : this) {
for (int i = 0; i < 3; i++) {
p.x = cp.x[i];
p.y = cp.y[i];
tx.transform(p, p);
cp.x[i] = p.x;
cp.y[i] = p.y;
}
}
invalidatePath();
}
/**
* Sets all values of this bezier path to that bezier path, so that this
* path becomes identical to that path.
*/
public void setTo(BezierPath that) {
while (that.size() < size()) {
remove(size() - 1);
}
for (int i = 0, n = size(); i < n; i++) {
get(i).setTo(that.get(i));
}
while (size() < that.size()) {
add((Node) that.get(size()).clone());
}
}
/**
* Returns the point at the center of the bezier path.
*/
public Point2D.Double getCenter() {
double sx = 0;
double sy = 0;
for (Node p : this) {
sx += p.x[0];
sy += p.y[0];
}
int n = size();
return new Point2D.Double(sx / n, sy / n);
}
/**
* Returns a point on the edge of the bezier path which crosses the line
* from the center of the bezier path to the specified point.
* If no edge crosses the line, the nearest C0 control point is returned.
*/
public Point2D.Double chop(Point2D.Double p) {
return Geom.chop(this, p);
}
/**
* Return the index of the node that is the furthest away from the center
**/
public int indexOfOutermostNode() {
if (outer == -1) {
Point2D.Double ctr = getCenter();
outer = 0;
double dist = 0;
for (int i = 0, n = size(); i < n; i++) {
Node cp = get(i);
double d = Geom.length2(ctr.x, ctr.y,
cp.x[0],
cp.y[0]);
if (d > dist) {
dist = d;
outer = i;
}
}
}
return outer;
}
/**
* Returns a relative point on the path.
* Where 0 is the start point of the path and 1 is the end point of the
* path.
*
* @param relative a value between 0 and 1.
*/
@Nullable
public Point2D.Double getPointOnPath(double relative, double flatness) {
// This method works only for straight lines
if (size() == 0) {
return null;
} else if (size() == 1) {
return get(0).getControlPoint(0);
}
if (relative <= 0) {
return get(0).getControlPoint(0);
} else if (relative >= 1) {
return get(size() - 1).getControlPoint(0);
}
validatePath();
// Compute the relative point on the path
double len = getLengthOfPath(flatness);
double relativeLen = len * relative;
double pos = 0;
double[] coords = new double[6];
PathIterator i = generalPath.getPathIterator(new AffineTransform(), flatness);
double prevX = coords[0];
double prevY = coords[1];
i.next();
for (; !i.isDone(); i.next()) {
i.currentSegment(coords);
double segLen = Geom.length(prevX, prevY, coords[0], coords[1]);
if (pos + segLen >= relativeLen) {
//if (true) return new Point2D.Double(coords[0], coords[1]);
// Compute the relative Point2D.Double on the line
/*
return new Point2D.Double(
prevX * pos / len + coords[0] * (pos + segLen) / len,
prevY * pos / len + coords[1] * (pos + segLen) / len
);*/
double factor = (relativeLen - pos) / segLen;
return new Point2D.Double(
prevX * (1 - factor) + coords[0] * factor,
prevY * (1 - factor) + coords[1] * factor);
}
pos += segLen;
prevX = coords[0];
prevY = coords[1];
}
throw new InternalError("We should never get here");
}
/**
* Returns the length of the path.
*
* @param flatness the flatness used to approximate the length.
*/
public double getLengthOfPath(double flatness) {
double len = 0;
PathIterator i = generalPath.getPathIterator(new AffineTransform(), flatness);
double[] coords = new double[6];
double prevX = coords[0];
double prevY = coords[1];
i.next();
for (; !i.isDone(); i.next()) {
i.currentSegment(coords);
len += Geom.length(prevX, prevY, coords[0], coords[1]);
prevX = coords[0];
prevY = coords[1];
}
return len;
}
/**
* Returns the relative position of the specified point on the path.
*
* @param flatness the flatness used to approximate the length.
*
* @return relative position on path, this is a number between 0 and 1.
* Returns -1, if the point is not on the path.
*/
public double getRelativePositionOnPath(Point2D.Double find, double flatness) {
// XXX - This method works only for straight lines!
double len = getLengthOfPath(flatness);
double relativeLen = 0d;
Node v1, v2;
BezierPath tempPath = new BezierPath();
Node t1, t2;
tempPath.add(t1 = new Node());
tempPath.add(t2 = new Node());
for (int i = 0, n = size() - 1; i < n; i++) {
v1 = get(i);
v2 = get(i + 1);
if (v1.mask == 0 && v2.mask == 0) {
if (Geom.lineContainsPoint(v1.x[0], v1.y[0], v2.x[0], v2.y[0], find.x, find.y, flatness)) {
relativeLen += Geom.length(v1.x[0], v1.y[0], find.x, find.y);
return relativeLen / len;
} else {
relativeLen += Geom.length(v1.x[0], v1.y[0], v2.x[0], v2.y[0]);
}
} else {
t1.setTo(v1);
t2.setTo(v2);
tempPath.invalidatePath();
if (tempPath.outlineContains(find, flatness)) {
relativeLen += Geom.length(v1.x[0], v1.y[0], find.x, find.y);
return relativeLen / len;
} else {
relativeLen += Geom.length(v1.x[0], v1.y[0], v2.x[0], v2.y[0]);
}
}
}
if (isClosed && size() > 1) {
v1 = get(size() - 1);
v2 = get(0);
if (v1.mask == 0 && v2.mask == 0) {
if (Geom.lineContainsPoint(v1.x[0], v1.y[0], v2.x[0], v2.y[0], find.x, find.y, flatness)) {
relativeLen += Geom.length(v1.x[0], v1.y[0], find.x, find.y);
return relativeLen / len;
}
} else {
t1.setTo(v1);
t2.setTo(v2);
tempPath.invalidatePath();
if (tempPath.outlineContains(find, flatness)) {
relativeLen += Geom.length(v1.x[0], v1.y[0], find.x, find.y);
return relativeLen / len;
}
}
}
return -1;
}
/**
* Gets the segment of the polyline that is hit by
* the given Point2D.Double.
* @return the index of the segment or -1 if no segment was hit.
*/
public int findSegment(Point2D.Double find, double tolerance) {
// XXX - This works only for straight lines!
Node v1, v2;
BezierPath tempPath = new BezierPath();
Node t1, t2;
tempPath.add(t1 = new Node());
tempPath.add(t2 = new Node());
for (int i = 0, n = size() - 1; i < n; i++) {
v1 = get(i);
v2 = get(i + 1);
if (v1.mask == 0 && v2.mask == 0) {
if (Geom.lineContainsPoint(v1.x[0], v1.y[0], v2.x[0], v2.y[0], find.x, find.y, tolerance)) {
return i;
}
} else {
t1.setTo(v1);
t2.setTo(v2);
tempPath.invalidatePath();
if (tempPath.outlineContains(find, tolerance)) {
return i;
}
}
}
if (isClosed && size() > 1) {
v1 = get(size() - 1);
v2 = get(0);
if (v1.mask == 0 && v2.mask == 0) {
if (Geom.lineContainsPoint(v1.x[0], v1.y[0], v2.x[0], v2.y[0], find.x, find.y, tolerance)) {
return size() - 1;
}
} else {
t1.setTo(v1);
t2.setTo(v2);
tempPath.invalidatePath();
if (tempPath.outlineContains(find, tolerance)) {
return size() - 1;
}
}
}
return -1;
}
/**
* Joins two segments into one if the given Point2D.Double hits a node
* of the bezier path.
* @return the index of the joined segment or -1 if no segment was joined.
*/
public int joinSegments(Point2D.Double join, double tolerance) {
for (int i = 0; i < size(); i++) {
Node p = get(i);
if (Geom.length(p.x[0], p.y[0], join.x, join.y) < tolerance) {
remove(i);
return i;
}
}
return -1;
}
/**
* Splits the segment at the given Point2D.Double if a segment was hit.
* @return the index of the segment or -1 if no segment was hit.
*/
public int splitSegment(Point2D.Double split, double tolerance) {
int i = findSegment(split, tolerance);
int nextI = (i + 1) % size();
if (i != -1) {
if ((get(i).mask & C2_MASK) == C2_MASK
&& (get(nextI).mask & C1_MASK) == 0) {
// quadto
add(i + 1, new Node(C2_MASK, split, split, split));
} else if ((get(i).mask & C2_MASK) == 0
&& (get(nextI).mask & C1_MASK) == C1_MASK) {
// quadto
add(i + 1, new Node(C1_MASK, split, split, split));
} else if ((get(i).mask & C2_MASK) == C2_MASK
&& (get(nextI).mask & C1_MASK) == C1_MASK) {
// cubicto
add(i + 1, new Node(C1_MASK | C2_MASK, split, split, split));
} else {
// lineto
add(i + 1, new Node(split));
}
}
return i + 1;
}
/**
* Adds the first node to the bezier path.
* <p>
* This is a convenience method for adding the first node with a single
* control point C0 to the bezier path.
*/
public void moveTo(double x1, double y1) {
if (size() != 0) {
throw new IllegalPathStateException("moveTo only allowed when empty");
}
Node node = new Node(x1, y1);
node.keepColinear = false;
add(node);
}
/**
* Adds a (at least) linear 'curve' to the bezier path.
* <p>
* If the previous node has no C2 control point the line will be straight
* (linear), otherwise the line will be quadratic.
* <p>
* This is a convenience method for adding a node with a single control
* point C0.
* <p>
* The bezier path must already have at least one node.
*/
public void lineTo(double x1, double y1) {
if (size() == 0) {
throw new IllegalPathStateException("lineTo only allowed when not empty");
}
get(size() - 1).keepColinear = false;
add(new Node(x1, y1));
}
/**
* Adds a (at least) quadratic curve to the bezier path.
* <p>
* If the previous node has no C2 control point the line will be quadratic
* otherwise the line will be cubic.
* <p>
* This is a convenience method for adding a node with control point C0 and
* C1 (incoming curve) to the bezier path.
* <p>
* The bezier path must already have at least one node.
*/
public void quadTo(double x1, double y1,
double x2, double y2) {
if (size() == 0) {
throw new IllegalPathStateException("quadTo only allowed when not empty");
}
add(new Node(C1_MASK, x2, y2, x1, y1, x2, y2));
}
/**
* Adds a cubic curve to the bezier path.
* <p>
* This is a convenience method for adding a node with control point C0 and
* C1 (incoming curve) to the bezier path, and also specifying the control
* point C2 (outgoing curve) of the previous node.
* <p>
* The bezier path must already have at least one node.
*/
public void curveTo(double x1, double y1,
double x2, double y2,
double x3, double y3) {
if (size() == 0) {
throw new IllegalPathStateException("curveTo only allowed when not empty");
}
Node lastPoint = get(size() - 1);
lastPoint.mask |= C2_MASK;
lastPoint.x[2] = x1;
lastPoint.y[2] = y1;
if ((lastPoint.mask & C1C2_MASK) == C1C2_MASK) {
lastPoint.keepColinear = Math.abs(
Geom.angle(lastPoint.x[0], lastPoint.y[0],
lastPoint.x[1], lastPoint.y[1])
- Geom.angle(lastPoint.x[2], lastPoint.y[2],
lastPoint.x[0], lastPoint.y[0])) < 0.001;
}
add(new Node(C1_MASK, x3, y3, x2, y2, x3, y3));
}
/**
* Adds an elliptical arc, defined by two radii, an angle from the
* x-axis, a flag to choose the large arc or not, a flag to
* indicate if we increase or decrease the angles and the final
* point of the arc.
* <p>
* As specified in http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
* <p>
* The implementation of this method has been derived from
* Apache Batik class org.apache.batik.ext.awt.geom.ExtendedGeneralPath#computArc
*
* @param rx the x radius of the ellipse
* @param ry the y radius of the ellipse
*
* @param xAxisRotation the angle from the x-axis of the current
* coordinate system to the x-axis of the ellipse in degrees.
*
* @param largeArcFlag the large arc flag. If true the arc
* spanning less than or equal to 180 degrees is chosen, otherwise
* the arc spanning greater than 180 degrees is chosen
*
* @param sweepFlag the sweep flag. If true the line joining
* center to arc sweeps through decreasing angles otherwise it
* sweeps through increasing angles
*
* @param x the absolute x coordinate of the final point of the arc.
* @param y the absolute y coordinate of the final point of the arc.
*/
public void arcTo(double rx, double ry,
double xAxisRotation,
boolean largeArcFlag, boolean sweepFlag,
double x, double y) {
// Ensure radii are valid
if (rx == 0 || ry == 0) {
lineTo(x, y);
return;
}
// Get the current (x, y) coordinates of the path
Node lastPoint = get(size() - 1);
double x0 = ((lastPoint.mask & C2_MASK) == C2_MASK) ? lastPoint.x[2] : lastPoint.x[0];
double y0 = ((lastPoint.mask & C2_MASK) == C2_MASK) ? lastPoint.y[2] : lastPoint.y[0];
if (x0 == x && y0 == y) {
// If the endpoints (x, y) and (x0, y0) are identical, then this
// is equivalent to omitting the elliptical arc segment entirely.
return;
}
// Compute the half distance between the current and the final point
double dx2 = (x0 - x) / 2d;
double dy2 = (y0 - y) / 2d;
// Convert angle from degrees to radians
double angle = Math.toRadians(xAxisRotation);
double cosAngle = Math.cos(angle);
double sinAngle = Math.sin(angle);
//
// Step 1 : Compute (x1, y1)
//
double x1 = (cosAngle * dx2 + sinAngle * dy2);
double y1 = (-sinAngle * dx2 + cosAngle * dy2);
// Ensure radii are large enough
rx = Math.abs(rx);
ry = Math.abs(ry);
double Prx = rx * rx;
double Pry = ry * ry;
double Px1 = x1 * x1;
double Py1 = y1 * y1;
// check that radii are large enough
double radiiCheck = Px1 / Prx + Py1 / Pry;
if (radiiCheck > 1) {
rx = Math.sqrt(radiiCheck) * rx;
ry = Math.sqrt(radiiCheck) * ry;
Prx = rx * rx;
Pry = ry * ry;
}
//
// Step 2 : Compute (cx1, cy1)
//
double sign = (largeArcFlag == sweepFlag) ? -1 : 1;
double sq = ((Prx * Pry) - (Prx * Py1) - (Pry * Px1)) / ((Prx * Py1) + (Pry * Px1));
sq = (sq < 0) ? 0 : sq;
double coef = (sign * Math.sqrt(sq));
double cx1 = coef * ((rx * y1) / ry);
double cy1 = coef * -((ry * x1) / rx);
//
// Step 3 : Compute (cx, cy) from (cx1, cy1)
//
double sx2 = (x0 + x) / 2.0;
double sy2 = (y0 + y) / 2.0;
double cx = sx2 + (cosAngle * cx1 - sinAngle * cy1);
double cy = sy2 + (sinAngle * cx1 + cosAngle * cy1);
//
// Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle)
//
double ux = (x1 - cx1) / rx;
double uy = (y1 - cy1) / ry;
double vx = (-x1 - cx1) / rx;
double vy = (-y1 - cy1) / ry;
double p, n;
// Compute the angle start
n = Math.sqrt((ux * ux) + (uy * uy));
p = ux; // (1 * ux) + (0 * uy)
sign = (uy < 0) ? -1d : 1d;
double angleStart = Math.toDegrees(sign * Math.acos(p / n));
// Compute the angle extent
n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
p = ux * vx + uy * vy;
sign = (ux * vy - uy * vx < 0) ? -1d : 1d;
double angleExtent = Math.toDegrees(sign * Math.acos(p / n));
if (!sweepFlag && angleExtent > 0) {
angleExtent -= 360f;
} else if (sweepFlag && angleExtent < 0) {
angleExtent += 360f;
}
angleExtent %= 360f;
angleStart %= 360f;
//
// We can now build the resulting Arc2D in double precision
//
Arc2D.Double arc = new Arc2D.Double(
cx - rx, cy - ry,
rx * 2d, ry * 2d,
-angleStart, -angleExtent,
Arc2D.OPEN);
// Create a path iterator of the rotated arc
PathIterator i = arc.getPathIterator(
AffineTransform.getRotateInstance(
angle, arc.getCenterX(), arc.getCenterY()));
// Add the segments to the bezier path
double[] coords = new double[6];
i.next(); // skip first moveto
while (!i.isDone()) {
int type = i.currentSegment(coords);
switch (type) {
case PathIterator.SEG_CLOSE:
// ignore
break;
case PathIterator.SEG_CUBICTO:
curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
break;
case PathIterator.SEG_LINETO:
lineTo(coords[0], coords[1]);
break;
case PathIterator.SEG_MOVETO:
// ignore
break;
case PathIterator.SEG_QUADTO:
quadTo(coords[0], coords[1], coords[2], coords[3]);
break;
}
i.next();
}
}
/**
* Creates a polygon/polyline array of the bezier path which only includes
* the C0 control points of the bezier nodes.
* <p>
* If the bezier path is closed, the array describes a polygon.
* If the bezier path is open, the array describes a polyline.
* <p>
* @return Point array.
*/
public Point2D.Double[] toPolygonArray() {
Point2D.Double[] points = new Point2D.Double[size()];
for (int i = 0, n = size(); i < n; i++) {
points[i] = new Point2D.Double(get(i).x[0], get(i).y[0]);
}
return points;
}
/**
* Sets winding rule for filling the bezier path.
* @param newValue Must be Path2D.Double.WIND_EVEN_ODD or Path2D.Double.WIND_NON_ZERO.
*/
public void setWindingRule(int newValue) {
if (newValue != windingRule) {
invalidatePath();
int oldValue = windingRule;
this.windingRule = newValue;
}
}
/**
* Gets winding rule for filling the bezier path.
* @return Path2D.Double.WIND_EVEN_ODD or Path2D.Double.WIND_NON_ZERO.
*/
public int getWindingRule() {
return windingRule;
}
}