Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
9e679da90This file is pinned to the selected release. Download the original to open it in your design tool.
package com.marginallyclever.makelangelo.makeart.turtletool;
import com.marginallyclever.convenience.LineCollection;
import com.marginallyclever.convenience.LineSegment2D;
import com.marginallyclever.convenience.Point2D;
import com.marginallyclever.makelangelo.turtle.Turtle;
import javax.vecmath.Vector2d;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* Take an existing drawing, scan across it horizontally. Add new lines between every pair of lines found.
* It may sometimes make mistakes if it hits the very end of a line.
* @author Dan Royer
* @since 7.31.0
*/
public class InfillTurtle {
public static final double MINIMUM_PEN_DIAMETER = 0.1;
private double penDiameter = 0.8;
/**
* Direction of lines to draw. 0 is horizontal. 90 is vertical.
*/
private double angle = 90.0;
public InfillTurtle() {}
public Turtle run(Turtle input) throws Exception {
// confirmTurtleIsClosedLoop(input);
Turtle result = new Turtle();
result.history.clear();
List<Turtle> list = input.splitByToolChange();
for(Turtle t : list) {
LineCollection segments = infillFromTurtle(t);
result.addLineSegments(segments);
}
return result;
}
@SuppressWarnings("unused")
private void confirmTurtleIsClosedLoop(Turtle input) throws Exception {
throw new Exception("I cannot confirm this Turtle path is a closed loop.");
}
private LineCollection infillFromTurtle(Turtle input) {
// make sure line segments don't start on another line, leading to an odd number
// of intersections.
Rectangle2D.Double bounds = addPaddingToBounds(input.getBounds(), 2.0);
LineCollection results = new LineCollection();
// do this once here instead of once per line.
LineCollection convertedPath = input.getAsLineSegments();
// working variable
LineSegment2D line = new LineSegment2D(new Point2D(), new Point2D(), input.getColor());
double size = Math.max(bounds.getHeight(), bounds.getWidth());
Vector2d majorDir = new Vector2d(Math.cos(Math.toRadians(angle )), Math.sin(Math.toRadians(angle )));
Vector2d minorDir = new Vector2d(Math.cos(Math.toRadians(angle+90)), Math.sin(Math.toRadians(angle+90)));
Vector2d minorStart = new Vector2d(bounds.getCenterX(),bounds.getCenterY());
minorStart.scaleAdd(-size,minorDir,minorStart);
Vector2d majorStart = new Vector2d();
Vector2d majorEnd = new Vector2d();
for(double i=0;i<size*2;i+=penDiameter) {
majorStart.scaleAdd(-size,majorDir,minorStart);
majorEnd.scaleAdd(size,majorDir,minorStart);
line.start.set(majorStart.x,majorStart.y);
line.end.set(majorEnd.x,majorEnd.y);
results.addAll(trimLineToPath(line, convertedPath));
minorStart.scaleAdd(penDiameter,minorDir,minorStart);
}
return results;
}
/**
* Add padding to a {@link Rectangle2D.Double} bounding rectangle.
*
* @param before the original rectangle
* @param percent the added percentage.
* @return the larger bounds
*/
private Rectangle2D.Double addPaddingToBounds(Rectangle2D.Double before, double percent) {
percent*=0.01;
Rectangle2D.Double after = new Rectangle2D.Double();
after.x = before.x - before.width * percent/2.0;
after.y = before.y - before.height * percent/2.0;
after.height = before.height * (1.0 + percent);
after.width = before.width * (1.0 + percent);
return after;
}
/**
* Trim a {@link LineSegment2D} against a path and return a list
* of remaining line segments.
* <p>
* If the polygon is convex, there will be two intersection points. These two
* points are the end points of the trimmed version of the line.
* </p>
* <p>
* If the polygon is not convex, there will be an even number of intersection
* points ≥2. Sort these intersection points (by increasing x value, for
* example). Then, taken in pairs, they give you the end points of the segments
* of the line that lie inside the polygon.
* </p>
*
* @param line A {@link LineSegment2D} to clip
* @param convertedPath The boundary line, which must be a closed loop
* @return a list of remaining {@link LineSegment2D}.
*/
private LineCollection trimLineToPath(LineSegment2D line, LineCollection convertedPath) {
List<Point2D> intersections = new ArrayList<>();
for (LineSegment2D s : convertedPath) {
Point2D p = getIntersection(line, s);
if (p != null) intersections.add(p);
}
LineCollection results = new LineCollection();
int size = intersections.size();
if(size%2==0) {
if (size == 2) {
results.add(new LineSegment2D(intersections.get(0), intersections.get(1), line.color));
} else if (size > 2) {
results.addAll(sortIntersectionsIntoSegments(intersections, line.color));
}
}
return results;
}
/**
* @param intersections A list of intersections. guaranteed to be 2 or more even
* number of intersections.
* @param color Color to assign to line
* @return return Intersections sorted by ascending x value. If x values match,
* sort by ascending y value.
*/
private LineCollection sortIntersectionsIntoSegments(List<Point2D> intersections, Color color) {
Point2D first = intersections.get(0);
Point2D second = intersections.get(1);
if (Double.compare(first.x, second.x) == 0) {
intersections.sort(new ComparePointsByY());
} else {
intersections.sort(new ComparePointsByX());
}
LineCollection results = new LineCollection();
int i = 0;
while (i < intersections.size()-1) {
results.add(new LineSegment2D(intersections.get(i), intersections.get(i + 1), color));
i += 2;
}
return results;
}
static class ComparePointsByY implements Comparator<Point2D> {
@Override
public int compare(Point2D o1, Point2D o2) {
return Double.compare(o1.y, o2.y);
}
}
static class ComparePointsByX implements Comparator<Point2D> {
@Override
public int compare(Point2D o1, Point2D o2) {
return Double.compare(o1.x, o2.x);
}
}
/**
* It is based on an algorithm in Andre LaMothe's "Tricks of the Windows Game Programming Gurus". See
* <a href="https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect">Stackoverflow</a>
* TODO move this to com.marginallyclever.convenience.LineHelper?
* @param alpha first line segment
* @param beta second line segment
* @return intersection {@link Point2D} or null
*/
private Point2D getIntersection(LineSegment2D alpha, LineSegment2D beta) {
double s1_x = alpha.end.x - alpha.start.x;
double s1_y = alpha.end.y - alpha.start.y;
double s2_x = beta.end.x - beta.start.x;
double s2_y = beta.end.y - beta.start.y;
double denominator = (-s2_x * s1_y + s1_x * s2_y);
double s = (-s1_y * (alpha.start.x - beta.start.x) + s1_x * (alpha.start.y - beta.start.y)) / denominator;
double t = ( s2_x * (alpha.start.y - beta.start.y) - s2_y * (alpha.start.x - beta.start.x)) / denominator;
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
// hit!
return new Point2D(alpha.start.x + (t * s1_x), alpha.start.y + (t * s1_y));
}
return null;
}
public double getPenDiameter() {
return penDiameter;
}
public void setPenDiameter(double penDiameter) {
this.penDiameter = Math.max(penDiameter, MINIMUM_PEN_DIAMETER);
}
public double getAngle() {
return angle;
}
/**
* Direction of lines to draw. 0 is horizontal. 90 is vertical.
*/
public void setAngle(double angle) {
this.angle = angle;
}
}