Skip to content

Commit 4bdf437

Browse files
committed
add TestOffsetOrientation and TestOffsets
1 parent e07383f commit 4bdf437

File tree

2 files changed

+391
-0
lines changed

2 files changed

+391
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package clipper2;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.util.List;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import clipper2.core.Path64;
10+
import clipper2.core.Paths64;
11+
import clipper2.offset.ClipperOffset;
12+
import clipper2.offset.EndType;
13+
import clipper2.offset.JoinType;
14+
15+
class TestOffsetOrientation {
16+
17+
@Test
18+
void TestOffsettingOrientation1() {
19+
Paths64 subject = new Paths64(Clipper.MakePath(new int[] { 0, 0, 0, 5, 5, 5, 5, 0 }));
20+
21+
Paths64 solution = Clipper.InflatePaths(subject, 1, JoinType.Round, EndType.Polygon);
22+
23+
assertEquals(1, solution.size());
24+
// when offsetting, output orientation should match input
25+
assertTrue(Clipper.IsPositive(subject.get(0)) == Clipper.IsPositive(solution.get(0)));
26+
}
27+
28+
@Test
29+
void TestOffsettingOrientation2() {
30+
Path64 s1 = Clipper.MakePath(new int[] { 20, 220, 280, 220, 280, 280, 20, 280 });
31+
Path64 s2 = Clipper.MakePath(new int[] { 0, 200, 0, 300, 300, 300, 300, 200 });
32+
Paths64 subject = new Paths64(List.of(s1, s2));
33+
34+
ClipperOffset co = new ClipperOffset();
35+
co.setReverseSolution(true);
36+
co.AddPaths(subject, JoinType.Round, EndType.Polygon);
37+
38+
Paths64 solution = new Paths64();
39+
co.Execute(5, solution);
40+
41+
assertEquals(2, solution.size());
42+
/*
43+
* When offsetting, output orientation should match input EXCEPT when
44+
* ReverseSolution == true However, input path ORDER may not match output path
45+
* order. For example, order will change whenever inner paths (holes) are
46+
* defined before their container outer paths (as above). And when offsetting
47+
* multiple outer paths, their order will likely change too. Due to the
48+
* sweep-line algorithm used, paths with larger Y coordinates will likely be
49+
* listed first.
50+
*/
51+
assertTrue(Clipper.IsPositive(subject.get(1)) != Clipper.IsPositive(solution.get(0)));
52+
53+
}
54+
55+
}
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
package clipper2;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.util.Collections;
6+
import java.util.List;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
import clipper2.core.Path64;
11+
import clipper2.core.Paths64;
12+
import clipper2.core.Point64;
13+
import clipper2.core.PointD;
14+
import clipper2.offset.ClipperOffset;
15+
import clipper2.offset.EndType;
16+
import clipper2.offset.JoinType;
17+
18+
public class TestOffsets {
19+
20+
@Test
21+
void TestOffsets2() { // see #448 & #456
22+
double scale = 10, delta = 10 * scale, arc_tol = 0.25 * scale;
23+
24+
Paths64 subject = new Paths64();
25+
Paths64 solution = new Paths64();
26+
ClipperOffset c = new ClipperOffset();
27+
subject.add(Clipper.MakePath(new long[] { 50, 50, 100, 50, 100, 150, 50, 150, 0, 100 }));
28+
29+
subject = Clipper.ScalePaths(subject, scale);
30+
31+
c.AddPaths(subject, JoinType.Round, EndType.Polygon);
32+
c.setArcTolerance(arc_tol);
33+
c.Execute(delta, solution);
34+
35+
double min_dist = delta * 2;
36+
double max_dist = 0;
37+
38+
for (Point64 subjPt : subject.get(0)) {
39+
Point64 prevPt = solution.get(0).get(solution.get(0).size() - 1);
40+
for (Point64 pt : solution.get(0)) {
41+
Point64 mp = midPoint(prevPt, pt);
42+
double d = distance(mp, subjPt);
43+
if (d < delta * 2) {
44+
if (d < min_dist)
45+
min_dist = d;
46+
if (d > max_dist)
47+
max_dist = d;
48+
}
49+
prevPt = pt;
50+
}
51+
}
52+
53+
assertTrue(min_dist + 1 >= delta - arc_tol); // +1 for rounding errors
54+
assertTrue(solution.get(0).size() <= 21);
55+
}
56+
57+
@Test
58+
void TestOffsets3() { // see #424
59+
Paths64 subjects = new Paths64(List.of(Clipper.MakePath(new long[] { 1525311078, 1352369439, 1526632284, 1366692987, 1519397110, 1367437476, 1520246456,
60+
1380177674, 1520613458, 1385913385, 1517383844, 1386238444, 1517771817, 1392099983, 1518233190, 1398758441, 1518421934, 1401883197, 1518694564,
61+
1406612275, 1520267428, 1430289121, 1520770744, 1438027612, 1521148232, 1443438264, 1521441833, 1448964260, 1521683005, 1452518932, 1521819320,
62+
1454374912, 1527943004, 1454154711, 1527649403, 1448523858, 1535901696, 1447989084, 1535524209, 1442788147, 1538953052, 1442463089, 1541553521,
63+
1442242888, 1541459149, 1438855987, 1538764308, 1439076188, 1538575565, 1436832236, 1538764308, 1436832236, 1536509870, 1405374956, 1550497874,
64+
1404347351, 1550214758, 1402428457, 1543818445, 1402868859, 1543734559, 1402124370, 1540672717, 1402344571, 1540473487, 1399995761, 1524996506,
65+
1400981422, 1524807762, 1398223667, 1530092585, 1397898609, 1531675935, 1397783265, 1531392819, 1394920653, 1529809469, 1395025510, 1529348096,
66+
1388880855, 1531099218, 1388660654, 1530826588, 1385158410, 1532955197, 1384938209, 1532661596, 1379003269, 1532472852, 1376235028, 1531277476,
67+
1376350372, 1530050642, 1361806623, 1599487345, 1352704983, 1602758902, 1378489467, 1618990858, 1376350372, 1615058698, 1344085688, 1603230761,
68+
1345700495, 1598648484, 1346329641, 1598931599, 1348667965, 1596698132, 1348993024, 1595775386, 1342722540 })));
69+
70+
Paths64 solution = Clipper.InflatePaths(subjects, -209715, JoinType.Miter, EndType.Polygon);
71+
assertTrue(solution.get(0).size() - subjects.get(0).size() <= 1);
72+
}
73+
74+
@Test
75+
void TestOffsets4() { // see #482
76+
Paths64 paths = new Paths64(List.of(Clipper.MakePath(new long[] { 0, 0, 20000, 200, 40000, 0, 40000, 50000, 0, 50000, 0, 0 })));
77+
Paths64 solution = Clipper.InflatePaths(paths, -5000, JoinType.Square, EndType.Polygon);
78+
assertEquals(5, solution.get(0).size());
79+
80+
paths = new Paths64(List.of(Clipper.MakePath(new long[] { 0, 0, 20000, 400, 40000, 0, 40000, 50000, 0, 50000, 0, 0 })));
81+
solution = Clipper.InflatePaths(paths, -5000, JoinType.Square, EndType.Polygon);
82+
assertEquals(5, solution.get(0).size());
83+
84+
paths = new Paths64(List.of(Clipper.MakePath(new long[] { 0, 0, 20000, 400, 40000, 0, 40000, 50000, 0, 50000, 0, 0 })));
85+
solution = Clipper.InflatePaths(paths, -5000, JoinType.Round, EndType.Polygon, 2, 100);
86+
assertTrue(solution.get(0).size() > 5);
87+
88+
paths = new Paths64(List.of(Clipper.MakePath(new long[] { 0, 0, 20000, 1500, 40000, 0, 40000, 50000, 0, 50000, 0, 0 })));
89+
solution = Clipper.InflatePaths(paths, -5000, JoinType.Round, EndType.Polygon, 2, 100);
90+
assertTrue(solution.get(0).size() > 5);
91+
}
92+
93+
@Test
94+
void TestOffsets6() {
95+
Path64 squarePath = Clipper.MakePath(new long[] { 620, 620, -620, 620, -620, -620, 620, -620 });
96+
97+
Path64 complexPath = Clipper.MakePath(new long[] { 20, -277, 42, -275, 59, -272, 80, -266, 97, -261, 114, -254, 135, -243, 149, -235, 167, -222, 182,
98+
-211, 197, -197, 212, -181, 223, -167, 234, -150, 244, -133, 253, -116, 260, -99, 267, -78, 272, -61, 275, -40, 278, -18, 276, -39, 272, -61,
99+
267, -79, 260, -99, 253, -116, 245, -133, 235, -150, 223, -167, 212, -181, 197, -197, 182, -211, 168, -222, 152, -233, 135, -243, 114, -254, 97,
100+
-261, 80, -267, 59, -272, 42, -275, 20, -278 });
101+
102+
Paths64 subjects = new Paths64(List.of(squarePath, complexPath));
103+
104+
final double offset = -50;
105+
ClipperOffset offseter = new ClipperOffset();
106+
107+
offseter.AddPaths(subjects, JoinType.Round, EndType.Polygon);
108+
Paths64 solution = new Paths64();
109+
offseter.Execute(offset, solution);
110+
111+
assertEquals(2, solution.size());
112+
113+
double area = Clipper.Area(solution.get(1));
114+
assertTrue(area < -47500);
115+
}
116+
117+
@Test
118+
void TestOffsets7() { // (#593 & #715)
119+
Paths64 solution;
120+
Paths64 subject = new Paths64(List.of(Clipper.MakePath(new long[] { 0, 0, 100, 0, 100, 100, 0, 100 })));
121+
122+
solution = Clipper.InflatePaths(subject, -50, JoinType.Miter, EndType.Polygon);
123+
assertEquals(0, solution.size());
124+
125+
subject.add(Clipper.MakePath(new long[] { 40, 60, 60, 60, 60, 40, 40, 40 }));
126+
solution = Clipper.InflatePaths(subject, 10, JoinType.Miter, EndType.Polygon);
127+
assertEquals(1, solution.size());
128+
129+
Collections.reverse(subject.get(0));
130+
Collections.reverse(subject.get(1));
131+
solution = Clipper.InflatePaths(subject, 10, JoinType.Miter, EndType.Polygon);
132+
assertEquals(1, solution.size());
133+
134+
subject = new Paths64(List.of(subject.get(0)));
135+
solution = Clipper.InflatePaths(subject, -50, JoinType.Miter, EndType.Polygon);
136+
assertEquals(0, solution.size());
137+
}
138+
139+
@Test
140+
void TestOffsets8() { // (#724)
141+
Paths64 subject = new Paths64(List.of(Clipper.MakePath(new long[] { 91759700, -49711991, 83886095, -50331657, -872415388, -50331657, -880288993,
142+
-49711991, -887968725, -47868251, -895265482, -44845834, -901999593, -40719165, -908005244, -35589856, -913134553, -29584205, -917261224,
143+
-22850094, -920283639, -15553337, -922127379, -7873605, -922747045, 0, -922747045, 1434498600, -922160557, 1442159790, -920414763, 1449642437,
144+
-917550346, 1456772156, -913634061, 1463382794, -908757180, 1469320287, -903033355, 1474446264, -896595982, 1478641262, -889595081, 1481807519,
145+
-882193810, 1483871245, -876133965, 1484596521, -876145751, 1484713389, -875781839, 1485061090, -874690056, 1485191762, -874447580, 1485237014,
146+
-874341490, 1485264094, -874171960, 1485309394, -873612294, 1485570372, -873201878, 1485980788, -872941042, 1486540152, -872893274, 1486720070,
147+
-872835064, 1487162210, -872834788, 1487185500, -872769052, 1487406000, -872297948, 1487583168, -871995958, 1487180514, -871995958, 1486914040,
148+
-871908872, 1486364208, -871671308, 1485897962, -871301302, 1485527956, -870835066, 1485290396, -870285226, 1485203310, -868659019, 1485203310,
149+
-868548443, 1485188472, -868239649, 1484791011, -868239527, 1484783879, -838860950, 1484783879, -830987345, 1484164215, -823307613, 1482320475,
150+
-816010856, 1479298059, -809276745, 1475171390, -803271094, 1470042081, -752939437, 1419710424, -747810128, 1413704773, -743683459, 1406970662,
151+
-740661042, 1399673904, -738817302, 1391994173, -738197636, 1384120567, -738197636, 1244148246, -738622462, 1237622613, -739889768, 1231207140,
152+
-802710260, 995094494, -802599822, 995052810, -802411513, 994586048, -802820028, 993050638, -802879992, 992592029, -802827240, 992175479,
153+
-802662144, 991759637, -802578556, 991608039, -802511951, 991496499, -801973473, 990661435, -801899365, 990554757, -801842657, 990478841,
154+
-801770997, 990326371, -801946911, 989917545, -801636397, 989501855, -801546099, 989389271, -800888669, 988625013, -800790843, 988518907,
155+
-800082405, 987801675, -799977513, 987702547, -799221423, 987035738, -799109961, 986944060, -798309801, 986330832, -798192297, 986247036,
156+
-797351857, 985690294, -797228867, 985614778, -796352124, 985117160, -796224232, 985050280, -795315342, 984614140, -795183152, 984556216,
157+
-794246418, 984183618, -794110558, 984134924, -793150414, 983827634, -793011528, 983788398, -792032522, 983547874, -791891266, 983518284,
158+
-790898035, 983345662, -790755079, 983325856, -789752329, 983221956, -789608349, 983212030, -787698545, 983146276, -787626385, 983145034,
159+
-536871008, 983145034, -528997403, 982525368, -521317671, 980681627, -514020914, 977659211, -507286803, 973532542, -501281152, 968403233,
160+
-496151843, 962397582, -492025174, 955663471, -489002757, 948366714, -487159017, 940686982, -486539351, 932813377, -486539351, 667455555,
161+
-486537885, 667377141, -486460249, 665302309, -486448529, 665145917, -486325921, 664057737, -486302547, 663902657, -486098961, 662826683,
162+
-486064063, 662673784, -485780639, 661616030, -485734413, 661466168, -485372735, 660432552, -485315439, 660286564, -484877531, 659282866,
163+
-484809485, 659141568, -484297795, 658173402, -484219379, 658037584, -483636768, 657110363, -483548422, 656980785, -482898150, 656099697,
164+
-482800368, 655977081, -482086070, 655147053, -481979398, 655032087, -481205068, 654257759, -481090104, 654151087, -480260074, 653436789,
165+
-480137460, 653339007, -479256372, 652688735, -479126794, 652600389, -478199574, 652017779, -478063753, 651939363, -477095589, 651427672,
166+
-476954289, 651359626, -475950593, 650921718, -475804605, 650864422, -474770989, 650502744, -474621127, 650456518, -473563373, 650173094,
167+
-473410475, 650138196, -472334498, 649934610, -472179420, 649911236, -471091240, 649788626, -470934848, 649776906, -468860016, 649699272,
168+
-468781602, 649697806, -385876037, 649697806, -378002432, 649078140, -370322700, 647234400, -363025943, 644211983, -356291832, 640085314,
169+
-350286181, 634956006, -345156872, 628950354, -341030203, 622216243, -338007786, 614919486, -336164046, 607239755, -335544380, 599366149,
170+
-335544380, 571247184, -335426942, 571236100, -335124952, 570833446, -335124952, 569200164, -335037864, 568650330, -334800300, 568184084,
171+
-334430294, 567814078, -333964058, 567576517, -333414218, 567489431, -331787995, 567489431, -331677419, 567474593, -331368625, 567077133,
172+
-331368503, 567070001, -142068459, 567070001, -136247086, 566711605, -136220070, 566848475, -135783414, 567098791, -135024220, 567004957,
173+
-134451560, 566929159, -134217752, 566913755, -133983942, 566929159, -133411282, 567004957, -132665482, 567097135, -132530294, 567091859,
174+
-132196038, 566715561, -132195672, 566711157, -126367045, 567070001, -33554438, 567070001, -27048611, 566647761, -20651940, 565388127,
175+
-14471751, 563312231, -8611738, 560454902, 36793963, 534548454, 43059832, 530319881, 48621743, 525200596, 53354240, 519306071, 57150572,
176+
512769270, 59925109, 505737634, 61615265, 498369779, 62182919, 490831896, 62182919, 474237629, 62300359, 474226543, 62602349, 473823889,
177+
62602349, 472190590, 62689435, 471640752, 62926995, 471174516, 63297005, 470804506, 63763241, 470566946, 64313081, 470479860, 65939308,
178+
470479860, 66049884, 470465022, 66358678, 470067562, 66358800, 470060430, 134217752, 470060430, 134217752, 0, 133598086, -7873605, 131754346,
179+
-15553337, 128731929, -22850094, 124605260, -29584205, 119475951, -35589856, 113470300, -40719165, 106736189, -44845834, 99439432, -47868251,
180+
91759700, -49711991
181+
})));
182+
183+
double offset = -50329979.277800001;
184+
double arc_tol = 5000;
185+
186+
Paths64 solution = Clipper.InflatePaths(subject, offset, JoinType.Round, EndType.Polygon, 2, arc_tol);
187+
OffsetQual oq = getOffsetQuality(subject.get(0), solution.get(0), offset);
188+
double smallestDist = distance(oq.smallestInSub, oq.smallestInSol);
189+
double largestDist = distance(oq.largestInSub, oq.largestInSol);
190+
final double rounding_tolerance = 1.0;
191+
offset = Math.abs(offset);
192+
193+
assertTrue(offset - smallestDist - rounding_tolerance <= arc_tol);
194+
assertTrue(largestDist - offset - rounding_tolerance <= arc_tol);
195+
}
196+
197+
@Test
198+
void TestOffsets9() { // (#733)
199+
// solution orientations should match subject orientations UNLESS
200+
// reverse_solution is set true in ClipperOffset's constructor
201+
202+
// start subject's orientation positive ...
203+
Paths64 subject = new Paths64(Clipper.MakePath(new long[] { 100, 100, 200, 100, 200, 400, 100, 400 }));
204+
Paths64 solution = Clipper.InflatePaths(subject, 50, JoinType.Miter, EndType.Polygon);
205+
assertEquals(1, solution.size());
206+
assertTrue(Clipper.IsPositive(solution.get(0)));
207+
208+
// reversing subject's orientation should not affect delta direction
209+
// (ie where positive deltas inflate).
210+
Collections.reverse(subject.get(0));
211+
solution = Clipper.InflatePaths(subject, 50, JoinType.Miter, EndType.Polygon);
212+
assertEquals(1, solution.size());
213+
assertTrue(Math.abs(Clipper.Area(solution.get(0))) > Math.abs(Clipper.Area(subject.get(0))));
214+
assertFalse(Clipper.IsPositive(solution.get(0)));
215+
216+
ClipperOffset co = new ClipperOffset(2, 0, false, true); // last param. reverses solution
217+
co.AddPaths(subject, JoinType.Miter, EndType.Polygon);
218+
co.Execute(50, solution);
219+
assertEquals(1, solution.size());
220+
assertTrue(Math.abs(Clipper.Area(solution.get(0))) > Math.abs(Clipper.Area(subject.get(0))));
221+
assertTrue(Clipper.IsPositive(solution.get(0)));
222+
223+
// add a hole (ie has reverse orientation to outer path)
224+
subject.add(Clipper.MakePath(new long[] { 130, 130, 170, 130, 170, 370, 130, 370 }));
225+
solution = Clipper.InflatePaths(subject, 30, JoinType.Miter, EndType.Polygon);
226+
assertEquals(1, solution.size());
227+
assertFalse(Clipper.IsPositive(solution.get(0)));
228+
229+
co.Clear(); // should still reverse solution orientation
230+
co.AddPaths(subject, JoinType.Miter, EndType.Polygon);
231+
co.Execute(30, solution);
232+
assertEquals(1, solution.size());
233+
assertTrue(Math.abs(Clipper.Area(solution.get(0))) > Math.abs(Clipper.Area(subject.get(0))));
234+
assertTrue(Clipper.IsPositive(solution.get(0)));
235+
236+
solution = Clipper.InflatePaths(subject, -15, JoinType.Miter, EndType.Polygon);
237+
assertEquals(0, solution.size());
238+
}
239+
240+
private static Point64 midPoint(Point64 p1, Point64 p2) {
241+
Point64 result = new Point64();
242+
result.setX((p1.x + p2.x) / 2);
243+
result.setY((p1.y + p2.y) / 2);
244+
return result;
245+
}
246+
247+
private static double distance(Point64 pt1, Point64 pt2) {
248+
long dx = pt1.x - pt2.x;
249+
long dy = pt1.y - pt2.y;
250+
return Math.sqrt(dx * dx + dy * dy);
251+
}
252+
253+
static class OffsetQual {
254+
PointD smallestInSub;
255+
PointD smallestInSol;
256+
PointD largestInSub;
257+
PointD largestInSol;
258+
}
259+
260+
private static OffsetQual getOffsetQuality(Path64 subject, Path64 solution, double delta) {
261+
if (subject.size() == 0 || solution.size() == 0)
262+
return new OffsetQual();
263+
264+
double desiredDistSqr = delta * delta;
265+
double smallestSqr = desiredDistSqr;
266+
double largestSqr = desiredDistSqr;
267+
OffsetQual oq = new OffsetQual();
268+
269+
final int subVertexCount = 4; // 1 .. 100 :)
270+
final double subVertexFrac = 1.0 / subVertexCount;
271+
Point64 solPrev = solution.get(solution.size() - 1);
272+
273+
for (Point64 solPt0 : solution) {
274+
for (int i = 0; i < subVertexCount; ++i) {
275+
// divide each edge in solution into series of sub-vertices (solPt)
276+
PointD solPt = new PointD(solPrev.x + (solPt0.x - solPrev.x) * subVertexFrac * i, solPrev.y + (solPt0.y - solPrev.y) * subVertexFrac * i);
277+
278+
// now find the closest point in subject to each of these solPt
279+
PointD closestToSolPt = new PointD(0, 0);
280+
double closestDistSqr = Double.POSITIVE_INFINITY;
281+
Point64 subPrev = subject.get(subject.size() - 1);
282+
283+
for (Point64 subPt : subject) {
284+
PointD closestPt = getClosestPointOnSegment(solPt, subPt, subPrev);
285+
subPrev = subPt;
286+
double sqrDist = distanceSqr(closestPt, solPt);
287+
if (sqrDist < closestDistSqr) {
288+
closestDistSqr = sqrDist;
289+
closestToSolPt = closestPt;
290+
}
291+
}
292+
293+
// see how this distance compares with every other solPt
294+
if (closestDistSqr < smallestSqr) {
295+
smallestSqr = closestDistSqr;
296+
oq.smallestInSub = closestToSolPt;
297+
oq.smallestInSol = solPt;
298+
}
299+
if (closestDistSqr > largestSqr) {
300+
largestSqr = closestDistSqr;
301+
oq.largestInSub = closestToSolPt;
302+
oq.largestInSol = solPt;
303+
}
304+
}
305+
solPrev = solPt0;
306+
}
307+
return oq;
308+
}
309+
310+
private static PointD getClosestPointOnSegment(PointD offPt, Point64 seg1, Point64 seg2) {
311+
// Handle case where segment is actually a point
312+
if (seg1.x == seg2.x && seg1.y == seg2.y) {
313+
return new PointD(seg1.x, seg1.y);
314+
}
315+
316+
double dx = seg2.x - seg1.x;
317+
double dy = seg2.y - seg1.y;
318+
319+
double q = ((offPt.x - seg1.x) * dx + (offPt.y - seg1.y) * dy) / (dx * dx + dy * dy);
320+
321+
// Clamp q between 0 and 1
322+
q = Math.max(0, Math.min(1, q));
323+
324+
return new PointD(seg1.x + q * dx, seg1.y + q * dy);
325+
}
326+
private static double distanceSqr(PointD pt1, PointD pt2) {
327+
double dx = pt1.x - pt2.x;
328+
double dy = pt1.y - pt2.y;
329+
return dx * dx + dy * dy;
330+
}
331+
332+
private static double distance(PointD pt1, PointD pt2) {
333+
return Math.sqrt(distanceSqr(pt1, pt2));
334+
}
335+
336+
}

0 commit comments

Comments
 (0)