11package com .marginallyclever .makelangelo .makeart .turtletool ;
22
3- import com .marginallyclever .convenience .linecollection .LineCollection ;
43import com .marginallyclever .convenience .LineSegment2D ;
4+ import com .marginallyclever .convenience .linecollection .LineCollection ;
55import com .marginallyclever .makelangelo .turtle .Turtle ;
6+ import org .slf4j .Logger ;
7+ import org .slf4j .LoggerFactory ;
68
7- import javax .vecmath .Vector2d ;
89import javax .vecmath .Point2d ;
9- import java . awt .* ;
10+ import javax . vecmath . Vector2d ;
1011import java .awt .geom .Rectangle2D ;
1112import java .util .ArrayList ;
12- import java .util .Comparator ;
1313import java .util .List ;
1414
1515/**
1919 * @since 7.31.0
2020 */
2121public class InfillTurtle {
22+ private static final Logger logger = LoggerFactory .getLogger (InfillTurtle .class );
23+
2224 public static final double MINIMUM_PEN_DIAMETER = 0.1 ;
2325
2426 private double penDiameter = 0.8 ;
@@ -38,10 +40,11 @@ public Turtle run(Turtle input) {
3840 List <Turtle > list = input .splitByToolChange ();
3941 for (Turtle t : list ) {
4042 LineCollection segments = infillFromTurtle (t );
41- result .setStroke (t .getColor (), t .getDiameter ());
43+ // result.setStroke(t.getColor(), t.getDiameter());
4244 result .addLineSegments (segments );
4345 }
4446
47+
4548 return result ;
4649 }
4750
@@ -68,16 +71,19 @@ private LineCollection infillFromTurtle(Turtle input) {
6871 Vector2d minorDir = new Vector2d (Math .cos (Math .toRadians (angle +90 )), Math .sin (Math .toRadians (angle +90 )));
6972 Vector2d minorStart = new Vector2d (bounds .getCenterX (),bounds .getCenterY ());
7073 minorStart .scaleAdd (-size ,minorDir ,minorStart );
74+ Vector2d minorTemp = new Vector2d ();
7175 Vector2d majorStart = new Vector2d ();
7276 Vector2d majorEnd = new Vector2d ();
7377
74- for (double i =0 ;i <size *2 ;i +=penDiameter ) {
75- majorStart .scaleAdd (-size ,majorDir ,minorStart );
76- majorEnd .scaleAdd (size ,majorDir ,minorStart );
78+ var end = size *2 ;
79+
80+ for (double i =0 ;i <end ;i +=penDiameter ) {
81+ minorTemp .scaleAdd (i ,minorDir ,minorStart );
82+ majorStart .scaleAdd (-size ,majorDir ,minorTemp );
83+ majorEnd .scaleAdd (size ,majorDir ,minorTemp );
7784 line .start .set (majorStart .x ,majorStart .y );
7885 line .end .set (majorEnd .x ,majorEnd .y );
7986 results .addAll (trimLineToPath (line , convertedPath ));
80- minorStart .scaleAdd (penDiameter ,minorDir ,minorStart );
8187 }
8288
8389 return results ;
@@ -87,7 +93,7 @@ private LineCollection infillFromTurtle(Turtle input) {
8793 * Add padding to a {@link Rectangle2D.Double} bounding rectangle.
8894 *
8995 * @param before the original rectangle
90- * @param percent the added percentage.
96+ * @param percent the added percentage. 0...100
9197 * @return the larger bounds
9298 */
9399 private Rectangle2D .Double addPaddingToBounds (Rectangle2D .Double before , double percent ) {
@@ -113,72 +119,59 @@ private Rectangle2D.Double addPaddingToBounds(Rectangle2D.Double before, double
113119 * example). Then, taken in pairs, they give you the end points of the segments
114120 * of the line that lie inside the polygon.
115121 * </p>
122+ * <p>Collect intersections and compute their parametric 't' along the infill line.
123+ * Sorting by projection (t) is more robust than sorting by x or y, and we
124+ * remove nearly-duplicate intersections caused by shared segment endpoints
125+ * or floating point noise.</p>
116126 *
117127 * @param line A {@link LineSegment2D} to clip
118128 * @param convertedPath The boundary line, which must be a closed loop
119129 * @return a list of remaining {@link LineSegment2D}.
120130 */
121131 private LineCollection trimLineToPath (LineSegment2D line , LineCollection convertedPath ) {
122- List <Point2d > intersections = new ArrayList <>();
132+ record Hit (Point2d p ,double t ) {}
133+ List <Hit > hits = new ArrayList <>();
134+ Vector2d s1 = new Vector2d (line .end );
135+ s1 .sub (line .start );
136+ double s1_len2 = s1 .lengthSquared ();
123137
124138 for (LineSegment2D s : convertedPath ) {
125139 Point2d p = getIntersection (line , s );
126- if (p != null ) intersections .add (p );
140+ if (p != null ) {
141+ double t = 0.0 ;
142+ if (s1_len2 > 0 ) {
143+ t = ((p .x - line .start .x ) * s1 .x + (p .y - line .start .y ) * s1 .y ) / s1_len2 ;
144+ }
145+ hits .add (new Hit (p , t ));
146+ }
127147 }
128148
129149 LineCollection results = new LineCollection ();
130- int size = intersections .size ();
131- if (size %2 ==0 ) {
132- if (size == 2 ) {
133- results .add (new LineSegment2D (intersections .get (0 ), intersections .get (1 ), line .color ));
134- } else if (size > 2 ) {
135- results .addAll (sortIntersectionsIntoSegments (intersections , line .color ));
150+ if (hits .isEmpty ()) return results ;
151+
152+ // sort by t and deduplicate near-equal t values
153+ hits .sort ((a ,b ) -> Double .compare (a .t ,b .t ));
154+ List <Hit > unique = new ArrayList <>();
155+ double EPS = 1e-8 ;
156+ for (Hit h : hits ) {
157+ if (unique .isEmpty () || Math .abs (h .t - unique .getLast ().t ) > EPS ) {
158+ unique .add (h );
136159 }
137160 }
138161
139- return results ;
140- }
141-
142- /**
143- * @param intersections A list of intersections. guaranteed to be 2 or more even
144- * number of intersections.
145- * @param color Color to assign to line
146- * @return return Intersections sorted by ascending x value. If x values match,
147- * sort by ascending y value.
148- */
149- private LineCollection sortIntersectionsIntoSegments (List <Point2d > intersections , Color color ) {
150- Point2d first = intersections .get (0 );
151- Point2d second = intersections .get (1 );
152- if (Double .compare (first .x , second .x ) == 0 ) {
153- intersections .sort (new ComparePointsByY ());
162+ int size = unique .size ();
163+ if (size % 2 == 0 ) {
164+ for (int i = 0 ; i < size - 1 ; i += 2 ) {
165+ results .add (new LineSegment2D (unique .get (i ).p , unique .get (i +1 ).p , line .color ));
166+ }
154167 } else {
155- intersections .sort (new ComparePointsByX ());
156- }
157-
158- LineCollection results = new LineCollection ();
159- int i = 0 ;
160- while (i < intersections .size ()-1 ) {
161- results .add (new LineSegment2D (intersections .get (i ), intersections .get (i + 1 ), color ));
162- i += 2 ;
168+ // odd number of intersections — something unexpected; log for debugging
169+ logger .error ("infill: odd intersection count=" + size );
163170 }
164171
165172 return results ;
166173 }
167174
168- static class ComparePointsByY implements Comparator <Point2d > {
169- @ Override
170- public int compare (Point2d o1 , Point2d o2 ) {
171- return Double .compare (o1 .y , o2 .y );
172- }
173- }
174-
175- static class ComparePointsByX implements Comparator <Point2d > {
176- @ Override
177- public int compare (Point2d o1 , Point2d o2 ) {
178- return Double .compare (o1 .x , o2 .x );
179- }
180- }
181-
182175 /**
183176 * It is based on an algorithm in Andre LaMothe's "Tricks of the Windows Game Programming Gurus". See
184177 * <a href="https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect">Stackoverflow</a>
@@ -194,6 +187,9 @@ private Point2d getIntersection(LineSegment2D alpha, LineSegment2D beta) {
194187 double s2_y = beta .end .y - beta .start .y ;
195188
196189 double denominator = (-s2_x * s1_y + s1_x * s2_y );
190+ // avoid division by (near) zero -> parallel or nearly-parallel segments
191+ if (Math .abs (denominator ) < 1e-12 ) return null ;
192+
197193 double s = (-s1_y * (alpha .start .x - beta .start .x ) + s1_x * (alpha .start .y - beta .start .y )) / denominator ;
198194 double t = ( s2_x * (alpha .start .y - beta .start .y ) - s2_y * (alpha .start .x - beta .start .x )) / denominator ;
199195
0 commit comments