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