package nodebox.graphics; import com.google.common.base.Function; import java.awt.*; import java.awt.geom.*; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; /** * Base class for all geometric (vector) data. */ public class Path extends AbstractGeometry implements Colorizable, Iterable<Point> { // Simulate a quarter of a circle. private static final double ONE_MINUS_QUARTER = 1.0 - 0.552; private Color fillColor = null; private Color strokeColor = null; private double strokeWidth = 1; private ArrayList<Contour> contours; private transient Contour currentContour = null; private transient boolean pathDirty = true; private transient boolean lengthDirty = true; private transient java.awt.geom.GeneralPath awtPath; private transient Rect bounds; private transient ArrayList<Double> contourLengths; private transient double pathLength = -1; public Path() { fillColor = Color.BLACK; strokeColor = null; strokeWidth = 0; contours = new ArrayList<Contour>(); currentContour = null; } public Path(Path other) { this(other, true); } public Path(Path other, boolean cloneContours) { fillColor = other.fillColor == null ? null : other.fillColor.clone(); strokeColor = other.strokeColor == null ? null : other.strokeColor.clone(); strokeWidth = other.strokeWidth; if (cloneContours) { contours = new ArrayList<Contour>(other.contours.size()); extend(other); if (!contours.isEmpty()) { // Set the current contour to the last contour. currentContour = contours.get(contours.size() - 1); } } else { contours = new ArrayList<Contour>(); currentContour = null; } } public Path(Shape s) { this(); extend(s); } public Path(Contour c) { this(); add(c); } /** * Wrap the current path in a geometry object. * * @return a Geometry object */ public Geometry asGeometry() { Geometry g = new Geometry(); g.add(this); return g; } //// Color operations //// public Color getFillColor() { return fillColor; } public Color getFill() { return fillColor; } public void setFillColor(Color fillColor) { this.fillColor = fillColor; } public void setFill(Color c) { setFillColor(c); } public Color getStrokeColor() { return strokeColor; } public Color getStroke() { return strokeColor; } public void setStrokeColor(Color strokeColor) { this.strokeColor = strokeColor; } public void setStroke(Color c) { setStrokeColor(c); } public double getStrokeWidth() { return strokeWidth; } public void setStrokeWidth(double strokeWidth) { this.strokeWidth = strokeWidth; } //// Point operations //// public int getPointCount() { if (contours == null) return 0; int pointCount = 0; for (Contour c : contours) { pointCount += c.getPointCount(); } return pointCount; } /** * Get the points for this geometry. * <p/> * This returns a live reference to the points of the geometry. Changing the points will change the geometry. * * @return a list of Points. */ public java.util.List<Point> getPoints() { if (contours.isEmpty()) return new ArrayList<Point>(0); ArrayList<Point> points = new ArrayList<Point>(); for (Contour c : contours) { points.addAll(c.getPoints()); } return points; } //// Primitives //// public void moveto(double x, double y) { // Stop using the current contour. addPoint will automatically create a new contour. currentContour = null; addPoint(x, y); } public void lineto(double x, double y) { if (currentContour == null) throw new RuntimeException("Lineto without moveto first."); addPoint(x, y); } public void curveto(double x1, double y1, double x2, double y2, double x3, double y3) { if (currentContour == null) throw new RuntimeException("Curveto without moveto first."); addPoint(new Point(x1, y1, Point.CURVE_DATA)); addPoint(new Point(x2, y2, Point.CURVE_DATA)); addPoint(new Point(x3, y3, Point.CURVE_TO)); } public void close() { if (currentContour != null) currentContour.close(); currentContour = null; invalidate(false); } /** * Start a new contour without closing the current contour first. * <p/> * You can call this method even when there is no current contour. */ public void newContour() { currentContour = null; } public void addPoint(Point pt) { ensureCurrentContour(); currentContour.addPoint(pt); invalidate(false); } public void addPoint(double x, double y) { ensureCurrentContour(); currentContour.addPoint(x, y); invalidate(false); } /** * Invalidates the cache. Querying the path length or asking for getGeneralPath will return an up-to-date result. * <p/> * This operation recursively invalidates all underlying geometry. * <p/> * Cache invalidation happens automatically when using the Path methods, such as rect/ellipse, * or container operations such as add/extend/clear. You should invalidate the cache when manually changing the * point positions or adding points to the underlying contours. * <p/> * Invalidating the cache is a lightweight operation; it doesn't recalculate anything. Only when querying the * new length will the values be recalculated. */ public void invalidate() { invalidate(true); } private void invalidate(boolean recursive) { pathDirty = true; lengthDirty = true; if (recursive) { for (Contour c : contours) { c.invalidate(); } } } /** * Ensure that there is a contour available. */ private void ensureCurrentContour() { if (currentContour != null) return; currentContour = new Contour(); add(currentContour); } //// Basic shapes //// public void rect(Rect r) { rect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } /** * Add a rectangle shape to the path. The rectangle will be centered around the x,y coordinates. * * @param cx the horizontal center of the rectangle * @param cy the vertical center of the rectangle * @param width the width * @param height the height */ public void rect(double cx, double cy, double width, double height) { double w2 = width / 2; double h2 = height / 2; addPoint(cx - w2, cy - h2); addPoint(cx + w2, cy - h2); addPoint(cx + w2, cy + h2); addPoint(cx - w2, cy + h2); close(); } public void rect(Rect r, double roundness) { roundedRect(r.getX(), r.getY(), r.getWidth(), r.getHeight(), roundness); } public void rect(Rect r, double rx, double ry) { roundedRect(r.getX(), r.getY(), r.getWidth(), r.getHeight(), rx, ry); } public void rect(double cx, double cy, double width, double height, double r) { roundedRect(cx, cy, width, height, r); } public void rect(double cx, double cy, double width, double height, double rx, double ry) { roundedRect(cx, cy, width, height, rx, ry); } public void cornerRect(double x, double y, double width, double height) { addPoint(x, y); addPoint(x + width, y); addPoint(x + width, y + height); addPoint(x, y + height); close(); } public void cornerRect(Rect r) { cornerRect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } public void cornerRect(Rect r, double roundness) { roundedRect(Rect.corneredRect(r), roundness); } public void cornerRect(Rect r, double rx, double ry) { roundedRect(Rect.corneredRect(r), rx, ry); } public void cornerRect(double cx, double cy, double width, double height, double r) { roundedRect(Rect.corneredRect(cx, cy, width, height), r); } public void cornerRect(double cx, double cy, double width, double height, double rx, double ry) { roundedRect(Rect.corneredRect(cx, cy, width, height), rx, ry); } public void roundedRect(Rect r, double roundness) { roundedRect(r, roundness, roundness); } public void roundedRect(Rect r, double rx, double ry) { roundedRect(r.getX(), r.getY(), r.getWidth(), r.getHeight(), rx, ry); } public void roundedRect(double cx, double cy, double width, double height, double r) { roundedRect(cx, cy, width, height, r, r); } public void roundedRect(double cx, double cy, double width, double height, double rx, double ry) { double halfWidth = width / 2; double halfHeight = height / 2; double dx = rx; double dy = ry; double left = cx - halfWidth; double right = cx + halfWidth; double top = cy - halfHeight; double bottom = cy + halfHeight; // rx/ry cannot be greater than half of the width of the rectangle // (required by SVG spec) dx = Math.min(dx, width * 0.5); dy = Math.min(dy, height * 0.5); moveto(left + dx, top); if (dx < width * 0.5) lineto(right - rx, top); curveto(right - dx * ONE_MINUS_QUARTER, top, right, top + dy * ONE_MINUS_QUARTER, right, top + dy); if (dy < height * 0.5) lineto(right, bottom - dy); curveto(right, bottom - dy * ONE_MINUS_QUARTER, right - dx * ONE_MINUS_QUARTER, bottom, right - dx, bottom); if (dx < width * 0.5) lineto(left + dx, bottom); curveto(left + dx * ONE_MINUS_QUARTER, bottom, left, bottom - dy * ONE_MINUS_QUARTER, left, bottom - dy); if (dy < height * 0.5) lineto(left, top + dy); curveto(left, top + dy * ONE_MINUS_QUARTER, left + dx * ONE_MINUS_QUARTER, top, left + dx, top); close(); } /** * Add an ellipse shape to the path. The ellipse will be centered around the x,y coordinates. * * @param cx the horizontal center of the ellipse * @param cy the vertical center of the ellipse * @param width the width * @param height the height */ public void ellipse(double cx, double cy, double width, double height) { Ellipse2D.Double e = new Ellipse2D.Double(cx - width / 2, cy - height / 2, width, height); extend(e); } public void cornerEllipse(double x, double y, double width, double height) { Ellipse2D.Double e = new Ellipse2D.Double(x, y, width, height); extend(e); } public void line(double x1, double y1, double x2, double y2) { moveto(x1, y1); lineto(x2, y2); } public void text(Text t) { extend(t.getPath()); } //// Container operations //// /** * Add the given contour. This will also make it active, * so all new drawing operations will operate on the given contour. * <p/> * The given contour is not cloned. * * @param c the contour to add. */ public void add(Contour c) { contours.add(c); currentContour = c; invalidate(false); } public int size() { return contours.size(); } public boolean isEmpty() { return getPointCount() == 0; } public void clear() { contours.clear(); currentContour = null; invalidate(false); } public void extend(Path p) { for (Contour c : p.contours) { contours.add(c.clone()); } invalidate(false); } public void extend(Shape s) { PathIterator pi = s.getPathIterator(new AffineTransform()); double px = 0; double py = 0; while (!pi.isDone()) { double[] points = new double[6]; int cmd = pi.currentSegment(points); if (cmd == PathIterator.SEG_MOVETO) { px = points[0]; py = points[1]; moveto(px, py); } else if (cmd == PathIterator.SEG_LINETO) { px = points[0]; py = points[1]; lineto(px, py); } else if (cmd == PathIterator.SEG_QUADTO) { // Convert the quadratic bezier to a cubic bezier. double c1x = px + (points[0] - px) * 2 / 3; double c1y = py + (points[1] - py) * 2 / 3; double c2x = points[0] + (points[2] - points[0]) / 3; double c2y = points[1] + (points[3] - points[1]) / 3; curveto(c1x, c1y, c2x, c2y, points[2], points[3]); px = points[2]; py = points[3]; } else if (cmd == PathIterator.SEG_CUBICTO) { px = points[4]; py = points[5]; curveto(points[0], points[1], points[2], points[3], px, py); } else if (cmd == PathIterator.SEG_CLOSE) { px = py = 0; close(); } else { throw new AssertionError("Unknown path command " + cmd); } pi.next(); } invalidate(false); } /** * Get the contours of a geometry object. * <p/> * This method returns live references to the geometric objects. * Changing them will change the original geometry. * * @return a list of contours */ public java.util.List<Contour> getContours() { return contours; } /** * Check if the last contour on this path is closed. * <p/> * A path can't technically be called "closed", only specific contours in the path can. * This method provides a reasonable heuristic for a "closed" path by checking the closed state * of the last contour. It returns false if this path contains no contours. * * @return true if the last contour is closed. */ public boolean isClosed() { if (isEmpty()) return false; Contour lastContour = contours.get(contours.size() - 1); return lastContour.isClosed(); } //// Geometric math //// /** * Returns the length of the line. * * @param x0 X start coordinate * @param y0 Y start coordinate * @param x1 X end coordinate * @param y1 Y end coordinate * @return the length of the line */ public static double lineLength(double x0, double y0, double x1, double y1) { x0 = Math.abs(x0 - x1); x0 *= x0; y0 = Math.abs(y0 - y1); y0 *= y0; return Math.sqrt(x0 + y0); } /** * Returns coordinates for point at t on the line. * <p/> * Calculates the coordinates of x and y for a point * at t on a straight line. * <p/> * The t port is a number between 0.0 and 1.0, * x0 and y0 define the starting point of the line, * x1 and y1 the ending point of the line, * * @param t a number between 0.0 and 1.0 defining the position on the path. * @param x0 X start coordinate * @param y0 Y start coordinate * @param x1 X end coordinate * @param y1 Y end coordinate * @return a Point at position t on the line. */ public static Point linePoint(double t, double x0, double y0, double x1, double y1) { return new Point( x0 + t * (x1 - x0), y0 + t * (y1 - y0)); } /** * Returns the length of the spline. * <p/> * Integrates the estimated length of the cubic bezier spline * defined by x0, y0, ... x3, y3, by adding the lengths of * linear lines between points at t. * <p/> * The number of points is defined by n * (n=10 would add the lengths of lines between 0.0 and 0.1, * between 0.1 and 0.2, and so on). * <p/> * This will use a default accuracy of 20, which is fine for most cases, usually * resulting in a deviation of less than 0.01. * * @param x0 X start coordinate * @param y0 Y start coordinate * @param x1 X control point 1 * @param y1 Y control point 1 * @param x2 X control point 2 * @param y2 Y control point 2 * @param x3 X end coordinate * @param y3 Y end coordinate * @return the length of the spline. */ public static double curveLength(double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) { return curveLength(x0, y0, x1, y1, x2, y2, x3, y3, 20); } /** * Returns the length of the spline. * <p/> * Integrates the estimated length of the cubic bezier spline * defined by x0, y0, ... x3, y3, by adding the lengths of * linear lines between points at t. * <p/> * The number of points is defined by n * (n=10 would add the lengths of lines between 0.0 and 0.1, * between 0.1 and 0.2, and so on). * * @param x0 X start coordinate * @param y0 Y start coordinate * @param x1 X control point 1 * @param y1 Y control point 1 * @param x2 X control point 2 * @param y2 Y control point 2 * @param x3 X end coordinate * @param y3 Y end coordinate * @param n accuracy * @return the length of the spline. */ public static double curveLength(double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3, int n) { double length = 0; double xi = x0; double yi = y0; double t; double px, py; double tmpX, tmpY; for (int i = 0; i < n; i++) { t = (i + 1) / (double) n; Point pt = curvePoint(t, x0, y0, x1, y1, x2, y2, x3, y3); px = pt.getX(); py = pt.getY(); tmpX = Math.abs(xi - px); tmpX *= tmpX; tmpY = Math.abs(yi - py); tmpY *= tmpY; length += Math.sqrt(tmpX + tmpY); xi = px; yi = py; } return length; } /** * Returns coordinates for point at t on the spline. * <p/> * Calculates the coordinates of x and y for a point * at t on the cubic bezier spline, and its control points, * based on the de Casteljau interpolation algorithm. * * @param t a number between 0.0 and 1.0 defining the position on the path. * @param x0 X start coordinate * @param y0 Y start coordinate * @param x1 X control point 1 * @param y1 Y control point 1 * @param x2 X control point 2 * @param y2 Y control point 2 * @param x3 X end coordinate * @param y3 Y end coordinate * @return a Point at position t on the spline. */ public static Point curvePoint(double t, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) { double mint = 1 - t; double x01 = x0 * mint + x1 * t; double y01 = y0 * mint + y1 * t; double x12 = x1 * mint + x2 * t; double y12 = y1 * mint + y2 * t; double x23 = x2 * mint + x3 * t; double y23 = y2 * mint + y3 * t; double out_c1x = x01 * mint + x12 * t; double out_c1y = y01 * mint + y12 * t; double out_c2x = x12 * mint + x23 * t; double out_c2y = y12 * mint + y23 * t; double out_x = out_c1x * mint + out_c2x * t; double out_y = out_c1y * mint + out_c2y * t; return new Point(out_x, out_y); } /** * Calculate the length of the path. This is not the number of segments, but rather the sum of all segment lengths. * * @return the length of the path. */ public double getLength() { if (lengthDirty) { updateContourLengths(); } return pathLength; } private void updateContourLengths() { contourLengths = new ArrayList<Double>(contours.size()); pathLength = 0; double length; for (Contour c : contours) { length = c.getLength(); contourLengths.add(length); pathLength += length; } lengthDirty = false; } public Contour contourAt(double t) { // Since t is relative, convert it to the absolute length. double absT = t * getLength(); // Find the contour that contains t. double cLength; for (Contour c : contours) { cLength = c.getLength(); if (absT <= cLength) return c; absT -= cLength; } return null; } /** * Returns coordinates for point at t on the path. * <p/> * Gets the length of the path, based on the length * of each curve and line in the path. * Determines in what segment t falls. * Gets the point on that segment. * * @param t relative coordinate of the point (between 0.0 and 1.0) * Results outside of this range are undefined. * @return coordinates for point at t. */ public Point pointAt(double t) { double length = getLength(); // Since t is relative, convert it to the absolute length. double absT = t * length; // The resT is what remains of t after we traversed all segments. double resT = t; // Find the contour that contains t. double cLength; Contour currentContour = null; for (Contour c : contours) { currentContour = c; cLength = c.getLength(); if (absT <= cLength) break; absT -= cLength; resT -= cLength / length; } if (currentContour == null) return new Point(); resT /= (currentContour.getLength() / length); return currentContour.pointAt(resT); } /** * Same as pointAt(t). * <p/> * This method is here for compatibility with NodeBox 1. * * @param t relative coordinate of the point. * @return coordinates for point at t. * @see #pointAt(double) */ public Point point(double t) { return pointAt(t); } //// Geometric operations //// /** * Make new points along the contours of the existing path. * <p/> * Points are evenly distributed according to the length of each contour. * * @param amount the number of points to create. * @param perContour if true, the amount of points is generated per contour, otherwise the amount * is for the entire path. * @return a list of Points. */ public Point[] makePoints(int amount, boolean perContour) { if (perContour) { Point[] points = new Point[amount * contours.size()]; int index = 0; for (Contour c : contours) { Point[] pointsFromContour = c.makePoints(amount); System.arraycopy(pointsFromContour, 0, points, index, amount); index += amount; } return points; } else { // Distribute all points evenly along the combined length of the contours. double delta = pointDelta(amount, isClosed()); Point[] points = new Point[amount]; for (int i = 0; i < amount; i++) { points[i] = pointAt(delta * i); } return points; } } public Path resampleByAmount(int amount, boolean perContour) { if (perContour) { Path p = cloneAndClear(); for (Contour c : contours) { p.add(c.resampleByAmount(amount)); } return p; } else { Path p = cloneAndClear(); double delta = pointDelta(amount, isClosed()); for (int i = 0; i < amount; i++) { p.addPoint(pointAt(delta * i)); } if (isClosed()) p.close(); return p; } } public Path resampleByLength(double segmentLength) { Path p = cloneAndClear(); for (Contour c : contours) { p.add(c.resampleByLength(segmentLength)); } return p; } public static Path findPath(java.util.List<Point> points) { Point[] pts = new Point[points.size()]; points.toArray(pts); return findPath(pts, 1); } public static Path findPath(java.util.List<Point> points, double curvature) { Point[] pts = new Point[points.size()]; points.toArray(pts); return findPath(pts, curvature); } public static Path findPath(Point[] points) { return findPath(points, 1); } /** * Constructs a path between the given list of points. * </p> * Interpolates the list of points and determines * a smooth bezier path betweem them. * Curvature is only useful if the path has more than three points. * </p> * * @param points the points of which to construct the path from. * @param curvature the smoothness of the generated path (0: straight, 1: smooth) * @return a new Path. */ public static Path findPath(Point[] points, double curvature) { if (points.length == 0) return null; if (points.length == 1) { Path path = new Path(); path.moveto(points[0].x, points[0].y); return path; } if (points.length == 2) { Path path = new Path(); path.moveto(points[0].x, points[0].y); path.lineto(points[1].x, points[1].y); return path; } // Zero curvature means straight lines. curvature = Math.max(0, Math.min(1, curvature)); if (curvature == 0) { Path path = new Path(); path.moveto(points[0].x, points[0].y); for (Point point : points) path.lineto(point.x, point.y); return path; } curvature = 4 + (1.0 - curvature) * 40; HashMap<Integer, Double> dx, dy, bi, ax, ay; dx = new HashMap<Integer, Double>(); dy = new HashMap<Integer, Double>(); bi = new HashMap<Integer, Double>(); ax = new HashMap<Integer, Double>(); ay = new HashMap<Integer, Double>(); dx.put(0, 0.0); dx.put(points.length - 1, 0.0); dy.put(0, 0.0); dy.put(points.length - 1, 0.0); bi.put(1, 1 / curvature); ax.put(1, (points[2].x - points[0].x - dx.get(0)) * bi.get(1)); ay.put(1, (points[2].y - points[0].y - dy.get(0)) * bi.get(1)); for (int i = 2; i < points.length - 1; i++) { bi.put(i, -1 / (curvature + bi.get(i - 1))); ax.put(i, -(points[i + 1].x - points[i - 1].x - ax.get(i - 1)) * bi.get(i)); ay.put(i, -(points[i + 1].y - points[i - 1].y - ay.get(i - 1)) * bi.get(i)); } for (int i = points.length - 2; i >= 1; i--) { dx.put(i, ax.get(i) + dx.get(i + 1) * bi.get(i)); dy.put(i, ay.get(i) + dy.get(i + 1) * bi.get(i)); } Path path = new Path(); path.moveto(points[0].x, points[0].y); for (int i = 0; i < points.length - 1; i++) { path.curveto(points[i].x + dx.get(i), points[i].y + dy.get(i), points[i + 1].x - dx.get(i + 1), points[i + 1].y - dy.get(i + 1), points[i + 1].x, points[i + 1].y); } return path; } //// Geometric queries //// public boolean contains(Point p) { return getGeneralPath().contains(p.toPoint2D()); } public boolean contains(double x, double y) { return getGeneralPath().contains(x, y); } public boolean contains(Rect r) { return getGeneralPath().contains(r.getRectangle2D()); } //// Boolean operations //// public boolean intersects(Rect r) { return getGeneralPath().intersects(r.getRectangle2D()); } public boolean intersects(Path p) { Area a1 = new Area(getGeneralPath()); Area a2 = new Area(p.getGeneralPath()); a1.intersect(a2); return !a1.isEmpty(); } public Path intersected(Path p) { Area a1 = new Area(getGeneralPath()); Area a2 = new Area(p.getGeneralPath()); a1.intersect(a2); return new Path(a1); } public Path subtracted(Path p) { Area a1 = new Area(getGeneralPath()); Area a2 = new Area(p.getGeneralPath()); a1.subtract(a2); return new Path(a1); } public Path united(Path p) { Area a1 = new Area(getGeneralPath()); Area a2 = new Area(p.getGeneralPath()); a1.add(a2); return new Path(a1); } //// Path //// public java.awt.geom.GeneralPath getGeneralPath() { if (!pathDirty) return awtPath; GeneralPath gp = new GeneralPath(GeneralPath.WIND_NON_ZERO); for (Contour c : contours) { c._extendPath(gp); } awtPath = gp; pathDirty = false; return gp; } public Rect getBounds() { if (!pathDirty && bounds != null) return bounds; if (isEmpty()) { bounds = new Rect(); } else { double minX = Double.MAX_VALUE; double minY = Double.MAX_VALUE; double maxX = -Double.MAX_VALUE; double maxY = -Double.MAX_VALUE; double px, py; ArrayList<Point> points = (ArrayList<Point>) getPoints(); for (int i = 0; i < getPointCount(); i++) { Point p = points.get(i); if (p.getType() == Point.LINE_TO) { px = p.getX(); py = p.getY(); if (px < minX) minX = px; if (py < minY) minY = py; if (px > maxX) maxX = px; if (py > maxY) maxY = py; } else if (p.getType() == Point.CURVE_TO) { Bezier b = new Bezier(points.get(i - 3), points.get(i - 2), points.get(i - 1), p); Rect r = b.extrema(); double right = r.getX() + r.getWidth(); double bottom = r.getY() + r.getHeight(); if (r.getX() < minX) minX = r.getX(); if (right > maxX) maxX = right; if (r.getY() < minY) minY = r.getY(); if (bottom > maxY) maxY = bottom; } } bounds = new Rect(minX, minY, maxX - minX, maxY - minY); } return bounds; } //// Transformations //// public void transform(Transform t) { for (Contour c : contours) { c.setPoints(t.map(c.getPoints())); } invalidate(true); } //// Path math //// /** * Flatten the geometry. */ public void flatten() { throw new UnsupportedOperationException("Not implemented."); } /** * Make a flattened copy of the geometry. * * @return a flattened copy. */ public Path flattened() { throw new UnsupportedOperationException("Not implemented."); } //// Operations on the current context. //// public void draw(Graphics2D g) { // If we can't fill or stroke the path, there's nothing to draw. if (fillColor == null && strokeColor == null) return; GeneralPath gp = getGeneralPath(); if (fillColor != null) { fillColor.set(g); g.fill(gp); } if (strokeWidth > 0 && strokeColor != null) { try { strokeColor.set(g); g.setStroke(new BasicStroke((float) strokeWidth)); g.draw(gp); } catch (Exception e) { // Invalid transformations can cause the pen to not display. // Catch the exception and throw it away. // The path would be too small to be displayed anyway. } } } public Path clone() { return new Path(this); } public Path cloneAndClear() { return new Path(this, false); } //// Functional operations //// public AbstractGeometry mapPoints(Function<Point, Point> pointFunction) { Path newPath = this.cloneAndClear(); for (Contour c : getContours()) { Contour newContour = (Contour) c.mapPoints(pointFunction); newPath.add(newContour); } return newPath; } //// Iterator implementation public Iterator<Point> iterator() { return getPoints().iterator(); } private class Bezier { private double x1, y1, x2, y2, x3, y3, x4, y4; private double minx, maxx, miny, maxy; public Bezier(Point p1, Point p2, Point p3, Point p4) { x1 = p1.getX(); y1 = p1.getY(); x2 = p2.getX(); y2 = p2.getY(); x3 = p3.getX(); y3 = p3.getY(); x4 = p4.getX(); y4 = p4.getY(); } private boolean fuzzyCompare(double p1, double p2) { return Math.abs(p1 - p2) <= (0.000000000001 * Math.min(Math.abs(p1), Math.abs(p2))); } public Point pointAt(double t) { double coeff[], a, b, c, d; coeff = coefficients(t); a = coeff[0]; b = coeff[1]; c = coeff[2]; d = coeff[3]; return new Point(a * x1 + b * x2 + c * x3 + d * x4, a * y1 + b * y2 + c * y3 + d * y4); } private double[] coefficients(double t) { double m_t, a, b, c, d; m_t = 1 - t; b = m_t * m_t; c = t * t; d = c * t; a = b * m_t; b *= (3. * t); c *= (3. * m_t); return new double[]{a, b, c, d}; } private void bezierCheck(double t) { if (t >= 0 && t <= 1) { Point p = pointAt(t); if (p.getX() < minx) minx = p.getX(); else if (p.getX() > maxx) maxx = p.getX(); if (p.getY() < miny) miny = p.getY(); else if (p.getY() > maxy) maxy = p.getY(); } } public Rect extrema() { double ax, bx, cx, ay, by, cy; if (x1 < x4) { minx = x1; maxx = x4; } else { minx = x4; maxx = x1; } if (y1 < y4) { miny = y1; maxy = y4; } else { miny = y4; maxy = y1; } ax = 3 * (-x1 + 3 * x2 - 3 * x3 + x4); bx = 6 * (x1 - 2 * x2 + x3); cx = 3 * (-x1 + x2); if (fuzzyCompare(ax + 1, 1)) { if (!fuzzyCompare(bx + 1, 1)) { double t = -cx / bx; bezierCheck(t); } } else { double tx = bx * bx - 4 * ax * cx; if (tx >= 0) { double temp, rcp, t1, t2; temp = (double) Math.sqrt(tx); rcp = 1 / (2 * ax); t1 = (-bx + temp) * rcp; bezierCheck(t1); t2 = (-bx - temp) * rcp; bezierCheck(t2); } } ay = 3 * (-y1 + 3 * y2 - 3 * y3 + y4); by = 6 * (y1 - 2 * y2 + y3); cy = 3 * (-y1 + y2); if (fuzzyCompare(ay + 1, 1)) { if (!fuzzyCompare(by + 1, 1)) { double t = -cy / by; bezierCheck(t); } } else { double ty = by * by - 4 * ay * cy; if (ty > 0) { double temp, rcp, t1, t2; temp = (double) Math.sqrt(ty); rcp = 1 / (2 * ay); t1 = (-by + temp) * rcp; bezierCheck(t1); t2 = (-by - temp) * rcp; bezierCheck(t2); } } return new Rect(minx, miny, maxx - minx, maxy - miny); } } @Override public String toString() { return "<Path>"; } }