All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nodebox.graphics.Contour Maven / Gradle / Ivy

package nodebox.graphics;

import com.google.common.base.Function;

import java.awt.*;
import java.awt.geom.GeneralPath;
import java.util.ArrayList;
import java.util.List;

public class Contour extends AbstractGeometry {

    private static final BasicStroke DEFAULT_STROKE = new BasicStroke(1);
    private static final int SEGMENT_ACCURACY = 20;

    private List points;
    private boolean closed;
    private transient ArrayList segmentLengths;
    private transient double length = -1;

    public Contour() {
        points = new ArrayList();
        closed = false;
    }

    public Contour(Contour other) {
        points = new ArrayList(other.points.size());
        for (Point p : other.points) {
            points.add(p);
        }
        closed = other.closed;
    }

    public Contour(Iterable points, boolean closed) {
        this.points = new ArrayList();
        for (Point p : points) {
            this.points.add(p);
        }
        this.closed = closed;
    }

    //// Point operations ////

    public int getPointCount() {
        return points.size();
    }

    public java.util.List getPoints() {
        return points;
    }

    void setPoints(List points) {
        this.points = points;
    }

    public void addPoint(Point pt) {
        points.add(pt);
        invalidate();
    }

    public void addPoint(double x, double y) {
        points.add(new Point(x, y));
        invalidate();
    }

    //// Close ////

    public boolean isClosed() {
        return closed;
    }

    public void setClosed(boolean closed) {
        this.closed = closed;
        invalidate();
    }

    public void close() {
        this.closed = true;
        invalidate();
    }

    //// Geometric queries ////

    public boolean isEmpty() {
        return points.isEmpty();
    }

    public Rect getBounds() {
        if (points.isEmpty()) {
            return new Rect();
        }
        double minX = Double.MAX_VALUE;
        double minY = Double.MAX_VALUE;
        double maxX = Double.MIN_VALUE;
        double maxY = Double.MIN_VALUE;
        double px, py;
        for (Point p : points) {
            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;
        }
        return new Rect(minX, minY, maxX - minX, maxY - minY);
    }

    /**
     * Invalidates the cache. Querying the contour length or calling makePoints/resample will an up-to-date result.
     * 

* Cache invalidation happens automatically when using the Contour methods, such as addPoint/close. You should * invalidate the cache only after manually changing the point positions. *

* 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() { segmentLengths = null; } public double updateSegmentLengths() { java.util.List points = getPoints(); segmentLengths = new ArrayList(); double totalLength = 0; // We cannot form a line or curve with the first point. // Since the algorithm looks back at previous points, we // start looking from the first useful point, which is // the second (index of 1). for (int pi = 1; pi < points.size(); pi++) { Point pt = points.get(pi); if (pt.isLineTo()) { Point pt0 = points.get(pi - 1); double length = Path.lineLength(pt0.x, pt0.y, pt.x, pt.y); segmentLengths.add(length); totalLength += length; } else if (pt.isCurveTo()) { Point pt0 = points.get(pi - 3); Point c1 = points.get(pi - 2); Point c2 = points.get(pi - 1); double length = Path.curveLength(pt0.x, pt0.y, c1.x, c1.y, c2.x, c2.y, pt.x, pt.y, SEGMENT_ACCURACY); segmentLengths.add(length); totalLength += length; } } // If the path is closed, add the closing segment. if (closed && !points.isEmpty()) { Point pt0 = points.get(points.size() - 1); Point pt1 = points.get(0); double length = Path.lineLength(pt0.x, pt0.y, pt1.x, pt1.y); segmentLengths.add(length); totalLength += length; } this.length = totalLength; return totalLength; } /** * Calculate the length of the contour. This is not the number of segments, but rather the sum of all segment lengths. * * @return the length of the contour */ public double getLength() { if (segmentLengths == null) updateSegmentLengths(); assert (length != -1); return length; } /** * Returns coordinates for point at t on the path. *

* 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) { if (segmentLengths == null) updateSegmentLengths(); // Check if there is a path. if (points.isEmpty()) throw new NodeBoxError("The path is empty."); // If the path has no length, return the position of the first point. if (length == 0) return points.get(0); // 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 segment that contains t. int segnum = -1; for (Double seglength : segmentLengths) { segnum++; if (absT <= seglength || segnum == segmentLengths.size() - 1) break; absT -= seglength; resT -= seglength / length; } resT /= (segmentLengths.get(segnum) / length); // Find the point index for the segment. int pi = pointIndexForSegment(segnum + 1); Point pt1 = points.get(pi); // If the path is closed, the point index is set to zero. // Set the index to the last point to get the one-but-last point for pt0. if (pi == 0) { pi = points.size(); } if (pt1.isLineTo()) { Point pt0 = points.get(pi - 1); return Path.linePoint(resT, pt0.x, pt0.y, pt1.x, pt1.y); } else if (pt1.isCurveTo()) { Point pt0 = points.get(pi - 3); Point c1 = points.get(pi - 2); Point c2 = points.get(pi - 1); return Path.curvePoint(resT, pt0.x, pt0.y, c1.x, c1.y, c2.x, c2.y, pt1.x, pt1.y); } else { throw new AssertionError("Incorrect point."); } } /** * Same as pointAt(t). *

* 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); } /** * Calculate the point index for the segment number. Segments lie between points. The first point of the segment * is returned. The point index will be a valid index, even if the segment number doesn't exist. * * @param segnum the segment index * @return the index of the point. */ private int pointIndexForSegment(int segnum) { int pointIndex = 0; for (Point pt : points) { if (pt.isCurveTo() || pt.isLineTo()) { if (segnum == 0) break; segnum--; } pointIndex++; } int pointCount = points.size(); if (pointIndex < pointCount) { return pointIndex; } else if (closed) { return 0; } else { return pointCount - 1; } } //// Geometric operations //// /** * Make new points along the contours of the existing path. * * @param amount the number of points to create. * @return a list with "amount" points or zero points if the contour is empty. */ public Point[] makePoints(int amount) { // If the contour is empty, pointAt will fail. Return an empty array. if (points.isEmpty()) return new Point[0]; Point[] points = new Point[amount]; double delta = 1; if (closed) { if (amount > 0) { delta = 1.0 / amount; } } else { // The delta value is divided by amount - 1, because we also want the last point (t=1.0) // If I wouldn't use amount - 1, I fall one point short of the end. // E.g. if amount = 4, I want point at t 0.0, 0.33, 0.66 and 1.0, // if amount = 2, I want point at t 0.0 and t 1.0 if (amount > 2) { delta = 1.0 / (amount - 1.0); } } for (int i = 0; i < amount; i++) { points[i] = pointAt(delta * i); } return points; } /** * Make new points along the contours of the existing path. * * @param amount the amount of points to distribute. * @param perContour this port was added to comply with the IGeometry interface, but is ignored since * we're at the contour level. * @return a list with "amount" points or zero points if the contour is empty. */ public Point[] makePoints(int amount, boolean perContour) { return makePoints(amount); } /** * Generate new geometry with the given amount of points along the shape of the original geometry. *

* The length of each segment is not given and will be determined based on the required number of points. * * @param amount the number of points to generate. * @param perContour this port is ignored since we're at the contour level. * @return a new Contour with the given number of points. */ public Contour resampleByAmount(int amount, boolean perContour) { return resampleByAmount(amount); } /** * Generate new geometry with the given amount of points along the shape of the original geometry. *

* The length of each segment is not given and will be determined based on the required number of points. * * @param amount the number of points to generate. * @return a new Contour with the given number of points. */ public Contour resampleByAmount(int amount) { Contour c = new Contour(); c.extend(makePoints(amount)); c.setClosed(closed); return c; } /** * Generate new geometry with points along the shape of the original geometry, spaced at the given length. *

* The number of points is not given and will be determined by the system based on the segment length. * Note that the last segment may be shorter than the given segment length. * * @param segmentLength the maximum length of each resampled segment. * @return a new Contour with segments of the given length. */ public Contour resampleByLength(double segmentLength) { if (segmentLength <= 0.0000001) { throw new IllegalArgumentException("Segment length must be greater than zero."); } double contourLength = getLength(); int amount = (int) Math.ceil(contourLength / segmentLength); if (closed) { return resampleByAmount(amount); } else { return resampleByAmount(amount + 1); } } public void flatten() { throw new UnsupportedOperationException(); } public IGeometry flattened() { throw new UnsupportedOperationException(); } //// Graphics //// public void draw(Graphics2D g) { if (getPointCount() < 2) return; // Since a contour has no fill or stroke information, draw it in black. // We save the current color so as not to disrupt the context. java.awt.Color savedColor = g.getColor(); Stroke savedStroke = g.getStroke(); GeneralPath gp = new GeneralPath(GeneralPath.WIND_EVEN_ODD, getPointCount()); _extendPath(gp); g.setColor(java.awt.Color.BLACK); g.setStroke(DEFAULT_STROKE); g.draw(gp); g.setColor(savedColor); g.setStroke(savedStroke); } /* package private */ void _extendPath(GeneralPath gp) { if (points.size() == 0) return; Point pt = points.get(0); Point ctrl1, ctrl2; gp.moveTo(pt.x, pt.y); int pointCount = getPointCount(); for (int i = 1; i < pointCount; i++) { pt = points.get(i); if (pt.isLineTo()) { gp.lineTo(pt.x, pt.y); } else if (pt.isCurveTo()) { ctrl1 = points.get(i - 2); ctrl2 = points.get(i - 1); gp.curveTo(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, pt.x, pt.y); } } if (closed) gp.closePath(); } public void transform(Transform t) { this.points = t.map(getPoints()); invalidate(); } //// Conversions //// public Path toPath() { return new Path(this); } //// Functional operations //// public AbstractGeometry mapPoints(Function pointFunction) { Contour c = new Contour(); c.setClosed(isClosed()); for (Point point : getPoints()) { Point newPoint = pointFunction.apply(point); c.addPoint(newPoint); } return c; } //// Object operations //// public Contour clone() { return new Contour(this); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy