KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_poly_triangulation.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.TXT for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 */
11
16#include <trigo.h>
17#include <thread>
18#include <chrono>
19#include <future>
20#include <filesystem>
21#include <fstream>
22
24#include <qa_utils/numeric.h>
26
27#include "geom_test_utils.h"
28
29BOOST_AUTO_TEST_SUITE( PolygonTriangulation )
30
32{
33 static std::vector<double> PartitionAreaFractions( POLYGON_TRIANGULATION& aTriangulator,
34 const SHAPE_LINE_CHAIN& aPoly,
35 size_t aTargetLeaves )
36 {
37 return aTriangulator.PartitionAreaFractionsForTesting( aPoly, aTargetLeaves );
38 }
39};
40
41namespace fs = std::filesystem;
42
43// Helper class to properly manage TRIANGULATED_POLYGON lifecycle
45{
46public:
47 TRIANGULATION_TEST_FIXTURE() : m_result( std::make_unique<SHAPE_POLY_SET::TRIANGULATED_POLYGON>(0) )
48 {
49 }
50
52
53 std::unique_ptr<POLYGON_TRIANGULATION> CreateTriangulator()
54 {
55 return std::make_unique<POLYGON_TRIANGULATION>( *m_result );
56 }
57
58private:
59 std::unique_ptr<SHAPE_POLY_SET::TRIANGULATED_POLYGON> m_result;
60};
61
62// Helper function to create a simple square
63SHAPE_LINE_CHAIN createSquare( int size = 100, VECTOR2I offset = VECTOR2I(0, 0) )
64{
66 chain.Append( offset.x, offset.y );
67 chain.Append( offset.x + size, offset.y );
68 chain.Append( offset.x + size, offset.y + size );
69 chain.Append( offset.x, offset.y + size );
70 chain.SetClosed( true );
71 return chain;
72}
73
74// Helper function to create a triangle
75SHAPE_LINE_CHAIN createTriangle( int size = 100, VECTOR2I offset = VECTOR2I(0, 0) )
76{
78 chain.Append( offset.x, offset.y );
79 chain.Append( offset.x + size, offset.y );
80 chain.Append( offset.x + size/2, offset.y + size );
81 chain.SetClosed( true );
82 return chain;
83}
84
85// Helper function to create a complex concave polygon
87{
89 chain.Append( 0, 0 );
90 chain.Append( size, 0 );
91 chain.Append( size, size/2 );
92 chain.Append( size/2, size/2 ); // Create concave section
93 chain.Append( size/2, size );
94 chain.Append( 0, size );
95 chain.SetClosed( true );
96 return chain;
97}
98
99SHAPE_LINE_CHAIN createSerpentinePolygon( int step = 20000, int teeth = 16 )
100{
102 chain.Append( 0, 0 );
103
104 int x = 0;
105
106 for( int ii = 0; ii < teeth; ++ii )
107 {
108 x += step;
109 chain.Append( x, 0 );
110 chain.Append( x, step * 3 );
111 x += step;
112 chain.Append( x, step * 3 );
113 chain.Append( x, step * 4 );
114 }
115
116 chain.Append( 0, step * 4 );
117 chain.SetClosed( true );
118 return chain;
119}
120
121// Helper function to validate triangulation result with comprehensive checks
123 const SHAPE_LINE_CHAIN& original, bool strict = true )
124{
125 // Basic validation
126 if( result.GetVertexCount() == 0 )
127 return false;
128
129 size_t triangleCount = result.GetTriangleCount();
130 if( triangleCount == 0 )
131 return false;
132
133 // Validate triangle topology
134 for( size_t i = 0; i < triangleCount; i++ )
135 {
136 const auto& triangle = result.Triangles()[i];
137
138 // Check valid vertex indices
139 if( triangle.a >= (int)result.GetVertexCount() ||
140 triangle.b >= (int)result.GetVertexCount() ||
141 triangle.c >= (int)result.GetVertexCount() )
142 {
143 return false;
144 }
145
146 // Triangle vertices should not be the same
147 if( triangle.a == triangle.b || triangle.b == triangle.c || triangle.a == triangle.c )
148 return false;
149
150 // Check triangle area is positive (counter-clockwise orientation)
151 if( strict && triangle.Area() <= 0 )
152 return false;
153 }
154
155 // Validate that original vertices are preserved
156 if( strict && result.GetVertexCount() >= original.PointCount() )
157 {
158 const auto& vertices = result.Vertices();
159 for( int i = 0; i < original.PointCount(); i++ )
160 {
161 bool found = false;
162 for( size_t j = 0; j < vertices.size(); j++ )
163 {
164 if( vertices[j] == original.CPoint( i ) )
165 {
166 found = true;
167 break;
168 }
169 }
170 if( !found )
171 return false;
172 }
173 }
174
175 return true;
176}
177
179{
180 int count = 0;
181
182 for( const auto& tri : aResult.Triangles() )
183 {
184 VECTOR2I pa = tri.GetPoint( 0 );
185 VECTOR2I pb = tri.GetPoint( 1 );
186 VECTOR2I pc = tri.GetPoint( 2 );
187
188 double ab = pa.Distance( pb );
189 double bc = pb.Distance( pc );
190 double ca = pc.Distance( pa );
191
192 double longest = std::max( { ab, bc, ca } );
193 double shortest = std::min( { ab, bc, ca } );
194
195 if( shortest > 0.0 && longest / shortest > 10.0 )
196 ++count;
197 }
198
199 return count;
200}
201
202bool parsePolyFileForTest( const fs::path& aPath, std::vector<SHAPE_POLY_SET>& aZones )
203{
204 std::ifstream file( aPath );
205
206 if( !file.is_open() )
207 return false;
208
209 std::string content( ( std::istreambuf_iterator<char>( file ) ),
210 std::istreambuf_iterator<char>() );
211
212 size_t zonePos = 0;
213
214 while( ( zonePos = content.find( "(zone (layer \"", zonePos ) ) != std::string::npos )
215 {
216 size_t polysetStart = content.find( "polyset ", zonePos );
217
218 if( polysetStart != std::string::npos )
219 {
220 SHAPE_POLY_SET polySet;
221 std::string remainder = content.substr( polysetStart );
222 std::stringstream ss( remainder );
223
224 if( polySet.Parse( ss ) )
225 aZones.push_back( std::move( polySet ) );
226 }
227
228 size_t layerEnd = content.find( "\")", zonePos + 14 );
229
230 if( layerEnd == std::string::npos )
231 break;
232
233 zonePos = layerEnd + 1;
234 }
235
236 return !aZones.empty();
237}
238
239double computeBoardSpikeyRatio( const fs::path& aPath )
240{
241 std::vector<SHAPE_POLY_SET> zones;
242
243 if( !parsePolyFileForTest( aPath, zones ) )
244 return 1.0;
245
246 int totalTriangles = 0;
247 int totalSpikey = 0;
248
249 for( SHAPE_POLY_SET& polySet : zones )
250 {
251 polySet.CacheTriangulation();
252
253 for( unsigned int i = 0; i < polySet.TriangulatedPolyCount(); ++i )
254 {
255 const auto* triPoly = polySet.TriangulatedPolygon( static_cast<int>( i ) );
256 totalTriangles += triPoly->GetTriangleCount();
257 totalSpikey += countSpikeyTriangles( *triPoly );
258 }
259 }
260
261 return totalTriangles > 0 ? static_cast<double>( totalSpikey ) / totalTriangles : 0.0;
262}
263
264// Core functionality tests
265BOOST_AUTO_TEST_CASE( BasicTriangleTriangulation )
266{
268 auto triangulator = fixture.CreateTriangulator();
269
270 SHAPE_LINE_CHAIN triangle = createTriangle();
271
272 bool success = triangulator->TesselatePolygon( triangle, nullptr );
273
274 BOOST_TEST( success );
275 BOOST_TEST( fixture.GetResult().GetVertexCount() == 3 );
276 BOOST_TEST( fixture.GetResult().GetTriangleCount() == 1 );
277 BOOST_TEST( validateTriangulation( fixture.GetResult(), triangle ) );
278}
279
280BOOST_AUTO_TEST_CASE( BasicSquareTriangulation )
281{
283 auto triangulator = fixture.CreateTriangulator();
284
286
287 bool success = triangulator->TesselatePolygon( square, nullptr );
288
289 BOOST_TEST( success );
290 BOOST_TEST( fixture.GetResult().GetVertexCount() == 4 );
291 BOOST_TEST( fixture.GetResult().GetTriangleCount() == 2 );
293}
294
295BOOST_AUTO_TEST_CASE( SplitFirstFracturePartitionProducesMultipleLeaves )
296{
298 auto triangulator = fixture.CreateTriangulator();
300 std::vector<double> fractions =
302 serpentine, 4 );
303
304 BOOST_TEST( fractions.size() == 4 );
305
306 for( double fraction : fractions )
307 BOOST_TEST( fraction > 0.15 );
308}
309
310BOOST_AUTO_TEST_CASE( EarLookaheadImprovesBadTriangulationCase )
311{
312 fs::path polyPath = fs::path( __FILE__ ).parent_path().parent_path().parent_path().parent_path()
313 .parent_path() / "data/triangulation/bad_triangulation_case.kicad_polys";
314
315 BOOST_TEST( fs::exists( polyPath ) );
316 BOOST_TEST( computeBoardSpikeyRatio( polyPath ) < 0.47 );
317}
318
319BOOST_AUTO_TEST_CASE( ConcavePolygonTriangulation )
320{
322 auto triangulator = fixture.CreateTriangulator();
323
324 SHAPE_LINE_CHAIN concave = createConcavePolygon(100000);
325
326 bool success = triangulator->TesselatePolygon( concave, nullptr );
327
328 BOOST_TEST( success );
329
330 const auto& result = fixture.GetResult();
331 bool isValid = validateTriangulation( result, concave );
332 size_t triangleCount = result.GetTriangleCount();
333
334 // Print diagnostic information if validation fails or triangle count is unexpected
335 if( !success || !isValid || triangleCount < 4 )
336 {
337 std::cout << "\n=== ConcavePolygonTriangulation Diagnostic Output ===" << std::endl;
338 std::cout << "Success: " << (success ? "true" : "false") << std::endl;
339 std::cout << "Validation: " << (isValid ? "true" : "false") << std::endl;
340 std::cout << "Triangle count: " << triangleCount << " (expected >= 4)" << std::endl;
341 std::cout << "Vertex count: " << result.GetVertexCount() << std::endl;
342
343 // Print input polygon vertices
344 std::cout << "\nInput polygon vertices (" << concave.PointCount() << " points):" << std::endl;
345 for( int i = 0; i < concave.PointCount(); i++ )
346 {
347 VECTOR2I pt = concave.CPoint( i );
348 std::cout << " [" << i << "]: (" << pt.x << ", " << pt.y << ")" << std::endl;
349 }
350
351 // Print result vertices
352 std::cout << "\nResult vertices (" << result.GetVertexCount() << " points):" << std::endl;
353 const auto& vertices = result.Vertices();
354 for( size_t i = 0; i < vertices.size(); i++ )
355 {
356 std::cout << " [" << i << "]: (" << vertices[i].x << ", " << vertices[i].y << ")" << std::endl;
357 }
358
359 // Print triangles
360 std::cout << "\nTriangles found (" << triangleCount << " triangles):" << std::endl;
361 const auto& triangles = result.Triangles();
362 for( size_t i = 0; i < triangles.size(); i++ )
363 {
364 const auto& tri = triangles[i];
365 VECTOR2I va = vertices[tri.a];
366 VECTOR2I vb = vertices[tri.b];
367 VECTOR2I vc = vertices[tri.c];
368 double area = tri.Area();
369
370 std::cout << " Triangle[" << i << "]: indices(" << tri.a << "," << tri.b << "," << tri.c << ")" << std::endl;
371 std::cout << " A: (" << va.x << ", " << va.y << ")" << std::endl;
372 std::cout << " B: (" << vb.x << ", " << vb.y << ")" << std::endl;
373 std::cout << " C: (" << vc.x << ", " << vc.y << ")" << std::endl;
374 std::cout << " Area: " << area << std::endl;
375
376 // Check for degenerate triangles
377 if( area <= 0 )
378 std::cout << " *** DEGENERATE TRIANGLE (area <= 0) ***" << std::endl;
379 if( tri.a == tri.b || tri.b == tri.c || tri.a == tri.c )
380 std::cout << " *** INVALID TRIANGLE (duplicate vertex indices) ***" << std::endl;
381 }
382
383 // Additional diagnostic information
384 if( triangleCount > 0 )
385 {
386 double totalArea = 0.0;
387 for( const auto& tri : triangles )
388 totalArea += tri.Area();
389 std::cout << "\nTotal triangulated area: " << totalArea << std::endl;
390
391 // Calculate expected area of concave polygon for comparison
392 double originalArea = std::abs( concave.Area() );
393 std::cout << "Original polygon area: " << originalArea << std::endl;
394 std::cout << "Area difference: " << std::abs( totalArea - originalArea ) << std::endl;
395 }
396
397 std::cout << "================================================\n" << std::endl;
398 }
399
400 BOOST_TEST( success );
401 BOOST_TEST( isValid );
402 // L-shaped concave polygons should have 4 triangles
403 BOOST_TEST( triangleCount == 4 );
404}
405
406
407BOOST_AUTO_TEST_CASE( HintDataOptimization )
408{
409 // First triangulation without hint
411 auto triangulator1 = fixture1.CreateTriangulator();
413
414 bool success1 = triangulator1->TesselatePolygon( square, nullptr );
415 BOOST_TEST( success1 );
416
417 // Second triangulation with hint data from first
419 auto triangulator2 = fixture2.CreateTriangulator();
420
421 bool success2 = triangulator2->TesselatePolygon( square, &fixture1.GetResult() );
422 BOOST_TEST( success2 );
423
424 // Results should be identical when hint is applicable
425 BOOST_TEST( fixture1.GetResult().GetVertexCount() == fixture2.GetResult().GetVertexCount() );
426 BOOST_TEST( fixture1.GetResult().GetTriangleCount() == fixture2.GetResult().GetTriangleCount() );
427}
428
429BOOST_AUTO_TEST_CASE( HintDataOptimizationWithSimplifiedInput )
430{
431 SHAPE_LINE_CHAIN noisySquare;
432 noisySquare.Append( 0, 0 );
433 noisySquare.Append( 100, 0 );
434 noisySquare.Append( 100, 10 );
435 noisySquare.Append( 100, 100 );
436 noisySquare.Append( 0, 100 );
437 noisySquare.SetClosed( true );
438
439 TRIANGULATION_TEST_FIXTURE hintFixture;
440 auto hintTriangulator = hintFixture.CreateTriangulator();
441
442 bool success1 = hintTriangulator->TesselatePolygon( noisySquare, nullptr );
443 BOOST_TEST( success1 );
444
445 auto poisonedTriangles = hintFixture.GetResult().Triangles();
446 BOOST_REQUIRE_GE( poisonedTriangles.size(), 2U );
447 std::reverse( poisonedTriangles.begin(), poisonedTriangles.end() );
448 hintFixture.GetResult().SetTriangles( poisonedTriangles );
449
451 auto triangulator = fixture.CreateTriangulator();
452
453 bool success2 = triangulator->TesselatePolygon( noisySquare, &hintFixture.GetResult() );
454 BOOST_TEST( success2 );
455 BOOST_TEST( fixture.GetResult().GetTriangleCount() == poisonedTriangles.size() );
456
457 for( size_t i = 0; i < poisonedTriangles.size(); ++i )
458 {
459 BOOST_TEST( fixture.GetResult().Triangles()[i].a == poisonedTriangles[i].a );
460 BOOST_TEST( fixture.GetResult().Triangles()[i].b == poisonedTriangles[i].b );
461 BOOST_TEST( fixture.GetResult().Triangles()[i].c == poisonedTriangles[i].c );
462 }
463}
464
465BOOST_AUTO_TEST_CASE( HintDataInvalidation )
466{
467 // Create hint data with different vertex count
468 TRIANGULATION_TEST_FIXTURE hintFixture;
469 auto hintTriangulator = hintFixture.CreateTriangulator();
470 SHAPE_LINE_CHAIN triangle = createTriangle();
471 hintTriangulator->TesselatePolygon( triangle, nullptr );
472
473 // Try to use hint with different polygon (should ignore hint)
475 auto triangulator = fixture.CreateTriangulator();
477
478 bool success = triangulator->TesselatePolygon( square, &hintFixture.GetResult() );
479 BOOST_TEST( success );
481}
482
483// Degenerate case handling
484BOOST_AUTO_TEST_CASE( DegeneratePolygons )
485{
487 auto triangulator = fixture.CreateTriangulator();
488
489 // Test empty polygon
491 bool success = triangulator->TesselatePolygon( empty, nullptr );
492 BOOST_TEST( success ); // Should handle gracefully
493
494 // Test single point
496 auto triangulator2 = fixture2.CreateTriangulator();
497 SHAPE_LINE_CHAIN singlePoint;
498 singlePoint.Append( 0, 0 );
499 singlePoint.SetClosed( true );
500 success = triangulator2->TesselatePolygon( singlePoint, nullptr );
501 BOOST_TEST( success ); // Should handle gracefully
502
503 // Test two points (line segment)
505 auto triangulator3 = fixture3.CreateTriangulator();
506 SHAPE_LINE_CHAIN line;
507 line.Append( 0, 0 );
508 line.Append( 100, 0 );
509 line.SetClosed( true );
510 success = triangulator3->TesselatePolygon( line, nullptr );
511 BOOST_TEST( success ); // Should handle gracefully
512}
513
514BOOST_AUTO_TEST_CASE( ZeroAreaPolygon )
515{
517 auto triangulator = fixture.CreateTriangulator();
518
519 // Create a polygon with zero area (all points collinear)
520 SHAPE_LINE_CHAIN zeroArea;
521 zeroArea.Append( 0, 0 );
522 zeroArea.Append( 100, 0 );
523 zeroArea.Append( 50, 0 );
524 zeroArea.Append( 25, 0 );
525 zeroArea.SetClosed( true );
526
527 bool success = triangulator->TesselatePolygon( zeroArea, nullptr );
528
529 BOOST_TEST( success ); // Should handle gracefully without crashing
530}
531
532// Memory management and lifecycle tests
533BOOST_AUTO_TEST_CASE( MemoryManagement )
534{
535 // Test that multiple triangulations properly manage memory
536 for( int i = 0; i < 100; i++ )
537 {
539 auto triangulator = fixture.CreateTriangulator();
540
541 SHAPE_LINE_CHAIN poly = createSquare( 100 + i, VECTOR2I( i, i ) );
542 bool success = triangulator->TesselatePolygon( poly, nullptr );
543
544 BOOST_TEST( success );
545 BOOST_TEST( validateTriangulation( fixture.GetResult(), poly, false ) );
546 }
547}
548
549BOOST_AUTO_TEST_CASE( LargePolygonStressTest )
550{
552 auto triangulator = fixture.CreateTriangulator();
553
554 // Create a large polygon (regular polygon with many vertices)
555 SHAPE_LINE_CHAIN largePoly;
556 int numVertices = 1000;
557 int radius = 10000;
558
559 for( int i = 0; i < numVertices; i++ )
560 {
561 double angle = 2.0 * M_PI * i / numVertices;
562 int x = static_cast<int>( radius * cos( angle ) );
563 int y = static_cast<int>( radius * sin( angle ) );
564 largePoly.Append( x, y );
565 }
566 largePoly.SetClosed( true );
567
568 auto start = std::chrono::high_resolution_clock::now();
569 bool success = triangulator->TesselatePolygon( largePoly, nullptr );
570 auto end = std::chrono::high_resolution_clock::now();
571
572 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
573
574 BOOST_TEST( success );
575 if( success )
576 {
577 BOOST_TEST( validateTriangulation( fixture.GetResult(), largePoly, false ) );
578 BOOST_TEST( fixture.GetResult().GetTriangleCount() > 0 );
579 }
580
581 // Sanitizer instrumentation changes execution cost, not the triangulation contract
582#if defined( KICAD_SANITIZE_THREADS ) || defined( KICAD_SANITIZE_ADDRESS )
583 BOOST_TEST_MESSAGE( "Instrumented triangulation: " << duration.count() << " ms" );
584#else
585 BOOST_TEST( duration.count() < 10000 ); // Less than 10 seconds
586#endif
587}
588
589// Thread safety tests (following SHAPE_POLY_SET patterns)
590BOOST_AUTO_TEST_CASE( ConcurrentTriangulation )
591{
592 const int numThreads = 4;
593 const int numTriangulationsPerThread = 10;
594
595 std::vector<std::future<bool>> futures;
596
597 for( int t = 0; t < numThreads; t++ )
598 {
599 futures.push_back( std::async( std::launch::async, [t, numTriangulationsPerThread]()
600 {
601 for( int i = 0; i < numTriangulationsPerThread; i++ )
602 {
604 auto triangulator = fixture.CreateTriangulator();
605
606 // Create unique polygon for each thread/iteration
607 SHAPE_LINE_CHAIN poly = createSquare( 100 + t * 10 + i, VECTOR2I( t * 100, i * 100 ) );
608
609 bool success = triangulator->TesselatePolygon( poly, nullptr );
610 if( !success || !validateTriangulation( fixture.GetResult(), poly, false ) )
611 return false;
612 }
613 return true;
614 }));
615 }
616
617 // Wait for all threads and check results
618 for( auto& future : futures )
619 {
620 BOOST_TEST( future.get() );
621 }
622}
623
624// Edge case and robustness tests
625BOOST_AUTO_TEST_CASE( SelfIntersectingPolygon )
626{
628 auto triangulator = fixture.CreateTriangulator();
629
630 // Create a bowtie (self-intersecting polygon)
631 SHAPE_LINE_CHAIN bowtie;
632 bowtie.Append( 0, 0 );
633 bowtie.Append( 100, 100 );
634 bowtie.Append( 100, 0 );
635 bowtie.Append( 0, 100 );
636 bowtie.SetClosed( true );
637
638 bool success = triangulator->TesselatePolygon( bowtie, nullptr );
639
640 // Algorithm should handle self-intersecting polygons
641 BOOST_TEST( success );
642 if( success )
643 {
644 BOOST_TEST( validateTriangulation( fixture.GetResult(), bowtie, false ) );
645 }
646}
647
648
661BOOST_AUTO_TEST_CASE( Issue18083_SelfIntersectingPolygonArea )
662{
663 SHAPE_POLY_SET polySet;
664 SHAPE_LINE_CHAIN outline;
665
666 // Coordinates from the issue (converted to internal units: 1mm = 1000000)
667 const int SCALE = 1000000;
668 outline.Append( 165 * SCALE, 87 * SCALE );
669 outline.Append( 179 * SCALE, 87 * SCALE );
670 outline.Append( 174 * SCALE, 94 * SCALE );
671 outline.Append( 169 * SCALE, 87 * SCALE );
672 outline.Append( 167 * SCALE, 94 * SCALE );
673 outline.SetClosed( true );
674
675 polySet.AddOutline( outline );
676
677 // Verify the polygon is detected as self-intersecting
678 BOOST_TEST( polySet.IsSelfIntersecting() );
679
680 // Triangulate via SHAPE_POLY_SET
681 polySet.CacheTriangulation();
683
684 // Calculate the triangulated area
685 double triangulatedArea = 0.0;
686
687 for( int ii = 0; ii < polySet.TriangulatedPolyCount(); ii++ )
688 {
689 const auto triPoly = polySet.TriangulatedPolygon( ii );
690
691 for( const auto& tri : triPoly->Triangles() )
692 triangulatedArea += std::abs( tri.Area() );
693 }
694
695 // The expected total area is 49 mm² (14 mm² + 35 mm² for the two triangular lobes)
696 // Triangle 1: (165,87) - (169,87) - (167,94) = base 4mm, height 7mm = 14 mm²
697 // Triangle 2: (169,87) - (179,87) - (174,94) = base 10mm, height 7mm = 35 mm²
698 double expectedAreaMmSq = 49.0 * SCALE * SCALE;
699
700 // The triangulated area should match the expected area
701 BOOST_TEST( std::abs( triangulatedArea - expectedAreaMmSq ) < expectedAreaMmSq * 0.01,
702 "Triangulated area should match expected area of 49 mm²" );
703}
704
705BOOST_AUTO_TEST_CASE( Issue25141_FractureCorridorIsNotAPinchPoint )
706{
707 // Vertex 6 sits 1nm off vertex 1 and the corridor runs just under 45 degrees, so it rounds
708 // onto segment 1-2. The folded lobe makes the direct pass over-cover, which is what routes
709 // this through splitSelfTouchingOutlines() at all.
710 SHAPE_LINE_CHAIN outline( { 0, 0,
711 20000, 0, // corridor mouth
712 60001, 40000, // hole ring
713 60001, 60000,
714 80000, 50000,
715 60001, 40000,
716 20001, 0, // corridor return
717 100000, 0,
718 100000, 100000,
719 30000, 20000, // fold
720 90000, 90000,
721 0, 100000 } );
722 outline.SetClosed( true );
723
724 SHAPE_POLY_SET polySet;
725 polySet.AddOutline( outline );
726
727 polySet.CacheTriangulation( false );
729
730 const VECTOR2I inHole( 64000, 50000 );
731 std::vector<const SHAPE*> triangles;
732
733 polySet.GetIndexableSubshapes( triangles );
734 BOOST_TEST( !polySet.Contains( inHole, -1, 0, false ) );
735
736 for( const SHAPE* tri : triangles )
737 BOOST_TEST( !tri->Collide( inHole, 0 ) );
738}
739
740BOOST_AUTO_TEST_CASE( NearlyCollinearVertices )
741{
743 auto triangulator = fixture.CreateTriangulator();
744
745 // Create a polygon with vertices that are nearly collinear
746 SHAPE_LINE_CHAIN nearlyCollinear;
747 nearlyCollinear.Append( 0, 0 );
748 nearlyCollinear.Append( 1000000, 0 );
749 nearlyCollinear.Append( 2000000, 1 ); // Very small deviation
750 nearlyCollinear.Append( 3000000, 0 );
751 nearlyCollinear.Append( 1500000, 1000000 );
752 nearlyCollinear.SetClosed( true );
753
754 bool success = triangulator->TesselatePolygon( nearlyCollinear, nullptr );
755
756 BOOST_TEST( success );
757 if( success )
758 {
759 BOOST_TEST( validateTriangulation( fixture.GetResult(), nearlyCollinear, false ) );
760 }
761}
762
763BOOST_AUTO_TEST_CASE( DuplicateVertices )
764{
766 auto triangulator = fixture.CreateTriangulator();
767
768 // Create a square with duplicate vertices
770 duplicate.Append( 0, 0 );
771 duplicate.Append( 0, 0 ); // Duplicate
772 duplicate.Append( 100, 0 );
773 duplicate.Append( 100, 0 ); // Duplicate
774 duplicate.Append( 100, 100 );
775 duplicate.Append( 100, 100 ); // Duplicate
776 duplicate.Append( 0, 100 );
777 duplicate.Append( 0, 100 ); // Duplicate
778 duplicate.SetClosed( true );
779
780 bool success = triangulator->TesselatePolygon( duplicate, nullptr );
781
782 BOOST_TEST( success );
783 if( success )
784 {
785 BOOST_TEST( validateTriangulation( fixture.GetResult(), duplicate, false ) );
786 }
787}
788
789BOOST_AUTO_TEST_CASE( ExtremeCoordinates )
790{
792 auto triangulator = fixture.CreateTriangulator();
793
794 // Test with very large coordinates
795 SHAPE_LINE_CHAIN extreme;
796 int large = 1000000000; // 1 billion
797 extreme.Append( 0, 0 );
798 extreme.Append( large, 0 );
799 extreme.Append( large, large );
800 extreme.Append( 0, large );
801 extreme.SetClosed( true );
802
803 bool success = triangulator->TesselatePolygon( extreme, nullptr );
804
805 BOOST_TEST( success );
806 if( success )
807 {
808 BOOST_TEST( validateTriangulation( fixture.GetResult(), extreme, false ) );
809 }
810}
811
812// Error recovery and cleanup tests
813BOOST_AUTO_TEST_CASE( ErrorRecoveryAndCleanup )
814{
816 auto triangulator = fixture.CreateTriangulator();
817
818 // Try a series of operations, some of which might fail
819 std::vector<SHAPE_LINE_CHAIN> testPolygons;
820
821 // Valid polygon
822 testPolygons.push_back( createSquare() );
823
824 // Degenerate polygon
825 SHAPE_LINE_CHAIN degenerate;
826 degenerate.Append( 0, 0 );
827 degenerate.SetClosed( true );
828 testPolygons.push_back( degenerate );
829
830 // Another valid polygon
831 testPolygons.push_back( createTriangle() );
832
833 for( const auto& poly : testPolygons )
834 {
835 // Each triangulation should start with a clean state
836 bool success = triangulator->TesselatePolygon( poly, nullptr );
837 // Even if triangulation fails, it should not crash
838 success |= fixture.GetResult().GetTriangleCount() > 0;
839 BOOST_TEST( success );
840 }
841}
842
843// Integration tests with TRIANGULATED_POLYGON interface
844BOOST_AUTO_TEST_CASE( TriangulatedPolygonInterface )
845{
847 auto triangulator = fixture.CreateTriangulator();
848
850 bool success = triangulator->TesselatePolygon( square, nullptr );
851
852 BOOST_TEST( success );
853
854 const auto& result = fixture.GetResult();
855
856 // Test GetTriangle method
857 if( result.GetTriangleCount() > 0 )
858 {
859 VECTOR2I a, b, c;
860 result.GetTriangle( 0, a, b, c );
861
862 // Vertices should be valid points from the square
863 BOOST_TEST( (a.x >= 0 && a.x <= 100 && a.y >= 0 && a.y <= 100) );
864 BOOST_TEST( (b.x >= 0 && b.x <= 100 && b.y >= 0 && b.y <= 100) );
865 BOOST_TEST( (c.x >= 0 && c.x <= 100 && c.y >= 0 && c.y <= 100) );
866 }
867
868 // Test triangle iteration
869 for( const auto& tri : result.Triangles() )
870 {
871 BOOST_TEST( tri.GetPointCount() == 3 );
872 BOOST_TEST( tri.GetSegmentCount() == 3 );
873 BOOST_TEST( tri.Area() > 0 );
874 BOOST_TEST( tri.IsClosed() );
875 BOOST_TEST( tri.IsSolid() );
876 }
877}
878
879BOOST_AUTO_TEST_CASE( SourceOutlineIndexTracking )
880{
882 auto triangulator = fixture.CreateTriangulator();
883
884 // Test that source outline index is properly maintained
885 const int expectedOutlineIndex = 5;
886
887 // Create triangulated polygon with specific source index
888 SHAPE_POLY_SET::TRIANGULATED_POLYGON result( expectedOutlineIndex );
889 POLYGON_TRIANGULATION localTriangulator( result );
890
891 SHAPE_LINE_CHAIN triangle = createTriangle();
892 bool success = localTriangulator.TesselatePolygon( triangle, nullptr );
893
894 BOOST_TEST( success );
895 BOOST_TEST( result.GetSourceOutlineIndex() == expectedOutlineIndex );
896}
897
898// Performance regression tests
899BOOST_AUTO_TEST_CASE( PerformanceRegression )
900{
901 // Test various polygon sizes to ensure performance scales reasonably
902 std::vector<int> testSizes = { 10, 50, 100, 500, 1000 };
903 std::vector<long long> durations;
904
905 for( int size : testSizes )
906 {
908 auto triangulator = fixture.CreateTriangulator();
909
910 // Create regular polygon
911 SHAPE_LINE_CHAIN poly;
912 for( int i = 0; i < size; i++ )
913 {
914 double angle = 2.0 * M_PI * i / size;
915 int x = static_cast<int>( 1000 * cos( angle ) );
916 int y = static_cast<int>( 1000 * sin( angle ) );
917 poly.Append( x, y );
918 }
919 poly.SetClosed( true );
920
921 auto start = std::chrono::high_resolution_clock::now();
922 bool success = triangulator->TesselatePolygon( poly, nullptr );
923 auto end = std::chrono::high_resolution_clock::now();
924
925 BOOST_TEST( success );
926
927 auto duration = std::chrono::duration_cast<std::chrono::microseconds>( end - start );
928 durations.push_back( duration.count() );
929 }
930
931 // Check that performance scales reasonably (shouldn't be exponential)
932 // This is a basic sanity check - actual performance will vary by hardware
933 for( size_t i = 1; i < durations.size(); i++ )
934 {
935 double scaleFactor = static_cast<double>( durations[i] ) / durations[i-1];
936 double sizeFactor = static_cast<double>( testSizes[i] ) / testSizes[i-1];
937
938 // Performance shouldn't be worse than O(n^2) in most cases
939 BOOST_TEST( scaleFactor < sizeFactor * sizeFactor * 2 );
940 }
941}
942
948BOOST_AUTO_TEST_CASE( ParallelPartitionTriangulation )
949{
950 SHAPE_POLY_SET polySet;
951 SHAPE_LINE_CHAIN outline;
952 constexpr int vertexCount = 120000;
953 constexpr int centerX = 5000000;
954 constexpr int centerY = 5000000;
955 constexpr int radius = 4000000;
956
957 for( int i = 0; i < vertexCount; ++i )
958 {
959 double angle = 2.0 * M_PI * i / vertexCount;
960 int x = centerX + static_cast<int>( radius * cos( angle ) );
961 int y = centerY + static_cast<int>( radius * sin( angle ) );
962 outline.Append( x, y );
963 }
964
965 outline.SetClosed( true );
966 polySet.AddOutline( outline );
967
968 std::atomic<int> tasksSubmitted( 0 );
969
971 [&tasksSubmitted]( std::function<void()> aTask )
972 {
973 tasksSubmitted++;
974 std::thread( std::move( aTask ) ).detach();
975 };
976
977 polySet.CacheTriangulation( false, submitter );
978
980 BOOST_TEST( tasksSubmitted.load() > 0 );
981
982 double originalArea = std::abs( outline.Area() );
983 double triArea = 0.0;
984
985 for( unsigned int i = 0; i < polySet.TriangulatedPolyCount(); i++ )
986 {
987 const auto* triPoly = polySet.TriangulatedPolygon( static_cast<int>( i ) );
988
989 for( const auto& tri : triPoly->Triangles() )
990 triArea += std::abs( tri.Area() );
991 }
992
993 // Partitioning may clip edges, so allow a few percent tolerance
994 if( originalArea > 0.0 )
995 {
996 double coverage = triArea / originalArea;
997 BOOST_TEST( coverage > 0.90 );
998 BOOST_TEST( coverage < 1.10 );
999 }
1000}
1001
1011BOOST_AUTO_TEST_CASE( Issue24059_HoleEliminationSentinelRemoval )
1012{
1013 SHAPE_LINE_CHAIN outer;
1014 outer.Append( 0, 0 );
1015 outer.Append( 10000, 0 );
1016 outer.Append( 10000, 10000 );
1017 outer.Append( 0, 10000 );
1018 outer.SetClosed( true );
1019
1020 SHAPE_LINE_CHAIN hole;
1021 hole.Append( 0, 0 );
1022 hole.Append( 1000, 1000 );
1023 hole.Append( 0, 2000 );
1024 hole.SetClosed( true );
1025
1027 polygon.push_back( outer );
1028 polygon.push_back( hole );
1029
1031 auto triangulator = fixture.CreateTriangulator();
1032
1033 std::atomic<bool> finished( false );
1034 bool result = false;
1035
1036 std::thread worker( [&]()
1037 {
1038 result = triangulator->TesselatePolygon( polygon, nullptr );
1039 finished.store( true );
1040 } );
1041
1042 worker.detach();
1043
1044 auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 5 );
1045
1046 while( !finished.load() && std::chrono::steady_clock::now() < deadline )
1047 std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) );
1048
1049 BOOST_CHECK_MESSAGE( finished.load(), "TesselatePolygon hung (issue #24059)" );
1050
1051 if( finished.load() )
1052 {
1053 BOOST_TEST( result );
1054 BOOST_TEST( fixture.GetResult().GetTriangleCount() > 0 );
1055 }
1056}
1057
1075BOOST_AUTO_TEST_CASE( Issue24121_DegenerateHoleAllDuplicates )
1076{
1077 SHAPE_LINE_CHAIN outer;
1078 outer.Append( 0, 0 );
1079 outer.Append( 10000, 0 );
1080 outer.Append( 10000, 10000 );
1081 outer.Append( 0, 10000 );
1082 outer.SetClosed( true );
1083
1084 // Hole with all coincident points: simplification leaves one vertex, then the
1085 // duplicate-tail cleanup formerly self-removed it and produced a nullptr-next
1086 // ring that crashed eliminateHoles().
1087 SHAPE_LINE_CHAIN hole;
1088 hole.Append( 5000, 5000 );
1089 hole.Append( 5000, 5000 );
1090 hole.Append( 5000, 5000 );
1091 hole.Append( 5000, 5000 );
1092 hole.SetClosed( true );
1093
1095 polygon.push_back( outer );
1096 polygon.push_back( hole );
1097
1099 auto triangulator = fixture.CreateTriangulator();
1100
1101 bool result = triangulator->TesselatePolygon( polygon, nullptr );
1102
1103 BOOST_TEST( result );
1104 BOOST_TEST( fixture.GetResult().GetTriangleCount() > 0 );
1105}
1106
1113BOOST_AUTO_TEST_CASE( Issue24121_DegenerateHoleBelowSimplification )
1114{
1115 SHAPE_LINE_CHAIN outer;
1116 outer.Append( 0, 0 );
1117 outer.Append( 10000, 0 );
1118 outer.Append( 10000, 10000 );
1119 outer.Append( 0, 10000 );
1120 outer.SetClosed( true );
1121
1122 // All four points lie within a 4-unit box, well below the default 50-unit
1123 // simplification threshold, so addVertex() in createRing() collapses them
1124 // to a single vertex.
1125 SHAPE_LINE_CHAIN tinyHole;
1126 tinyHole.Append( 5000, 5000 );
1127 tinyHole.Append( 5002, 5000 );
1128 tinyHole.Append( 5002, 5002 );
1129 tinyHole.Append( 5000, 5002 );
1130 tinyHole.SetClosed( true );
1131
1133 polygon.push_back( outer );
1134 polygon.push_back( tinyHole );
1135
1137 auto triangulator = fixture.CreateTriangulator();
1138
1139 bool result = triangulator->TesselatePolygon( polygon, nullptr );
1140
1141 BOOST_TEST( result );
1142 BOOST_TEST( fixture.GetResult().GetTriangleCount() > 0 );
1143}
1144
1145BOOST_AUTO_TEST_CASE( CacheTriangulation_AfterMove_FreshPoly )
1146{
1147 SHAPE_LINE_CHAIN outline;
1148 outline.Append( 0, 0 );
1149 outline.Append( 10000, 0 );
1150 outline.Append( 10000, 10000 );
1151 outline.Append( 0, 10000 );
1152 outline.SetClosed( true );
1153
1154 SHAPE_POLY_SET polySet;
1155 polySet.AddOutline( outline );
1156
1157 BOOST_TEST( !polySet.IsTriangulationUpToDate() );
1158
1159 // Move() sets m_hashValid=true without triangulating
1160 polySet.Move( { 1, 1 } );
1161 polySet.CacheTriangulation();
1162
1164 BOOST_TEST( polySet.TriangulatedPolyCount() > 0 );
1165}
1166
1167BOOST_AUTO_TEST_CASE( CacheTriangulation_AfterUpdateTriangulationDataHash )
1168{
1169 SHAPE_LINE_CHAIN outline;
1170 outline.Append( 0, 0 );
1171 outline.Append( 10000, 0 );
1172 outline.Append( 10000, 10000 );
1173 outline.Append( 0, 10000 );
1174 outline.SetClosed( true );
1175
1176 SHAPE_POLY_SET polySet;
1177 polySet.AddOutline( outline );
1178
1180 polySet.CacheTriangulation();
1181
1183 BOOST_TEST( polySet.TriangulatedPolyCount() > 0 );
1184}
1185
1186namespace
1187{
1188double meshMinAngleDeg( const SHAPE_POLY_SET::TRIANGULATED_POLYGON& aTri )
1189{
1190 double minAngle = 180.0;
1191
1192 for( const auto& tri : aTri.Triangles() )
1193 {
1194 minAngle = std::min( minAngle, GEOM_TEST::TriangleMinAngleDeg( tri.GetPoint( 0 ),
1195 tri.GetPoint( 1 ),
1196 tri.GetPoint( 2 ) ) );
1197 }
1198
1199 return minAngle;
1200}
1201
1202
1203double meshArea( const SHAPE_POLY_SET::TRIANGULATED_POLYGON& aTri )
1204{
1205 double area = 0.0;
1206
1207 for( const auto& tri : aTri.Triangles() )
1208 area += tri.Area();
1209
1210 return area;
1211}
1212
1213
1214bool meshHasEdge( const SHAPE_POLY_SET::TRIANGULATED_POLYGON& aTri, int aU, int aV )
1215{
1216 for( const auto& tri : aTri.Triangles() )
1217 {
1218 bool hasU = tri.a == aU || tri.b == aU || tri.c == aU;
1219 bool hasV = tri.a == aV || tri.b == aV || tri.c == aV;
1220
1221 if( hasU && hasV )
1222 return true;
1223 }
1224
1225 return false;
1226}
1227
1228
1229int meshSpikeyCount( const SHAPE_POLY_SET::TRIANGULATED_POLYGON& aTri )
1230{
1231 int count = 0;
1232
1233 for( const auto& tri : aTri.Triangles() )
1234 {
1235 VECTOR2I a = tri.GetPoint( 0 ), b = tri.GetPoint( 1 ), c = tri.GetPoint( 2 );
1236
1237 if( KIGEOM::IsSliverTriangle( a, b, c ) )
1238 count++;
1239 }
1240
1241 return count;
1242}
1243} // namespace
1244
1245
1246BOOST_AUTO_TEST_CASE( RefineFlipsNonDelaunayDiagonal )
1247{
1248 // Vertex 3 lies inside the circumcircle of 0-1-2, so diagonal 0-2 is non-Delaunay and must
1249 // flip to 1-3.
1251 tp.AddVertex( VECTOR2I( 0, 0 ) );
1252 tp.AddVertex( VECTOR2I( 10000, 0 ) );
1253 tp.AddVertex( VECTOR2I( 10000, 10000 ) );
1254 tp.AddVertex( VECTOR2I( 2000, 8000 ) );
1255 tp.AddTriangle( 0, 1, 2 );
1256 tp.AddTriangle( 0, 2, 3 );
1257
1258 double beforeAngle = meshMinAngleDeg( tp );
1259 double beforeArea = meshArea( tp );
1260
1261 tp.Refine();
1262
1263 BOOST_CHECK_EQUAL( tp.GetTriangleCount(), 2u );
1264 BOOST_CHECK_CLOSE( meshArea( tp ), beforeArea, 0.001 );
1265 BOOST_CHECK_GT( meshMinAngleDeg( tp ), beforeAngle );
1266
1267 BOOST_CHECK( meshHasEdge( tp, 0, 1 ) );
1268 BOOST_CHECK( meshHasEdge( tp, 1, 2 ) );
1269 BOOST_CHECK( meshHasEdge( tp, 2, 3 ) );
1270 BOOST_CHECK( meshHasEdge( tp, 0, 3 ) );
1271
1272 BOOST_CHECK( !meshHasEdge( tp, 0, 2 ) );
1273 BOOST_CHECK( meshHasEdge( tp, 1, 3 ) );
1274}
1275
1276
1277BOOST_AUTO_TEST_CASE( RefinePrefersFewerSliversOverDelaunay )
1278{
1279 // Diagonal 0-2 is Delaunay-legal but produces a sliver; 1-3 produces none. Refine must prefer
1280 // the fewer-sliver diagonal over the Delaunay one.
1282 tp.AddVertex( VECTOR2I( 0, 0 ) );
1283 tp.AddVertex( VECTOR2I( 284000, 42000 ) );
1284 tp.AddVertex( VECTOR2I( 276000, 68000 ) );
1285 tp.AddVertex( VECTOR2I( 38000, 126000 ) );
1286 tp.AddTriangle( 0, 1, 2 );
1287 tp.AddTriangle( 0, 2, 3 );
1288
1289 // Precondition: 0-2 is Delaunay-legal, so a Delaunay-only refine would not touch it.
1291 VECTOR2I( 276000, 68000 ),
1292 VECTOR2I( 38000, 126000 ) ) );
1293 BOOST_REQUIRE_EQUAL( meshSpikeyCount( tp ), 1 );
1294
1295 double beforeArea = meshArea( tp );
1296
1297 tp.Refine();
1298
1299 BOOST_CHECK_EQUAL( tp.GetTriangleCount(), 2u );
1300 BOOST_CHECK_CLOSE( meshArea( tp ), beforeArea, 0.001 );
1301 BOOST_CHECK_EQUAL( meshSpikeyCount( tp ), 0 );
1302 BOOST_CHECK( !meshHasEdge( tp, 0, 2 ) );
1303 BOOST_CHECK( meshHasEdge( tp, 1, 3 ) );
1304}
1305
1306
1307BOOST_AUTO_TEST_CASE( RefineLeavesDelaunayMeshUnchanged )
1308{
1309 // An already-Delaunay square triangulation must be a fixed point: no spurious flips that
1310 // would churn the mesh or, worse, oscillate.
1312 tp.AddVertex( VECTOR2I( 0, 0 ) );
1313 tp.AddVertex( VECTOR2I( 10000, 0 ) );
1314 tp.AddVertex( VECTOR2I( 10000, 10000 ) );
1315 tp.AddVertex( VECTOR2I( 0, 10000 ) );
1316 tp.AddTriangle( 0, 1, 2 );
1317 tp.AddTriangle( 0, 2, 3 );
1318
1319 double beforeArea = meshArea( tp );
1320
1321 tp.Refine();
1322
1323 BOOST_CHECK_EQUAL( tp.GetTriangleCount(), 2u );
1324 BOOST_CHECK_CLOSE( meshArea( tp ), beforeArea, 0.001 );
1325 BOOST_CHECK( meshHasEdge( tp, 0, 2 ) );
1326}
1327
1328
1329BOOST_AUTO_TEST_CASE( PredicatesHandleFullCoordinateRange )
1330{
1331 // The 4e9 nm width exceeds a 32-bit difference; a->b->c is counter-clockwise and must read so.
1332 VECTOR2I a( -2000000000, 0 );
1333 VECTOR2I b( 2000000000, 0 );
1334 VECTOR2I c( 2000000000, 1000 );
1335
1338
1339 // The same triangle is a needle; the sliver must still register at this span.
1340 BOOST_CHECK( KIGEOM::IsSliverTriangle( a, b, c ) );
1341}
1342
1343
1344BOOST_AUTO_TEST_CASE( RefinePreservesNonManifoldEdge )
1345{
1346 // Three triangles share edge 0-1, as a hole-bridge pinch produces. That non-manifold edge must
1347 // act as a boundary and never flip.
1349 tp.AddVertex( VECTOR2I( 0, 0 ) );
1350 tp.AddVertex( VECTOR2I( 100000, 0 ) );
1351 tp.AddVertex( VECTOR2I( 50000, 30000 ) );
1352 tp.AddVertex( VECTOR2I( 50000, -30000 ) );
1353 tp.AddVertex( VECTOR2I( 50000, 60000 ) );
1354 tp.AddTriangle( 0, 1, 2 );
1355 tp.AddTriangle( 0, 1, 3 );
1356 tp.AddTriangle( 0, 1, 4 );
1357
1358 std::vector<int> before;
1359
1360 for( const auto& tri : tp.Triangles() )
1361 {
1362 before.push_back( tri.a );
1363 before.push_back( tri.b );
1364 before.push_back( tri.c );
1365 }
1366
1367 tp.Refine();
1368
1369 std::vector<int> after;
1370
1371 for( const auto& tri : tp.Triangles() )
1372 {
1373 after.push_back( tri.a );
1374 after.push_back( tri.b );
1375 after.push_back( tri.c );
1376 }
1377
1378 BOOST_CHECK( before == after );
1379 BOOST_CHECK( meshHasEdge( tp, 0, 1 ) );
1380}
1381
1382
1383BOOST_AUTO_TEST_CASE( DecimateRemovesCollinearRun )
1384{
1385 // A square subdivided into many exactly-collinear points must triangulate down to the two
1386 // triangles the plain square produces; the subdivision points carry no geometry.
1388 const int size = 1000000;
1389 const int steps = 20;
1390
1391 for( int i = 0; i < steps; i++ )
1392 chain.Append( i * size / steps, 0 );
1393
1394 for( int i = 0; i < steps; i++ )
1395 chain.Append( size, i * size / steps );
1396
1397 for( int i = 0; i < steps; i++ )
1398 chain.Append( size - i * size / steps, size );
1399
1400 for( int i = 0; i < steps; i++ )
1401 chain.Append( 0, size - i * size / steps );
1402
1403 chain.SetClosed( true );
1404
1406 auto triangulator = fixture.CreateTriangulator();
1407 BOOST_REQUIRE( triangulator->TesselatePolygon( chain, nullptr ) );
1408
1409 BOOST_CHECK_EQUAL( fixture.GetResult().GetTriangleCount(), 2u );
1410 BOOST_CHECK_CLOSE( meshArea( fixture.GetResult() ), (double) size * size, 1e-6 );
1411}
1412
1413
1414BOOST_AUTO_TEST_CASE( DecimateKeepsVerticesBeyondBand )
1415{
1416 // Teeth of amplitude just past the simplification level must all survive. The teeth are
1417 // tiny and the valley deep, so the area budget alone would let every one go: only the band
1418 // test can hold them, which is what this pins.
1420 const int amp = TRIANGULATESIMPLIFICATIONLEVEL * 2;
1421 const int halfPitch = 25000;
1422 const int teeth = 10;
1423
1424 for( int i = 0; i <= teeth * 2; i++ )
1425 chain.Append( i * halfPitch, ( i % 2 ) ? amp : 0 );
1426
1427 chain.Append( teeth * 2 * halfPitch, -5000000 );
1428 chain.Append( 0, -5000000 );
1429 chain.SetClosed( true );
1430
1432 auto triangulator = fixture.CreateTriangulator();
1433 BOOST_REQUIRE( triangulator->TesselatePolygon( chain, nullptr ) );
1434
1436 static_cast<size_t>( chain.PointCount() ) - 2 );
1437}
1438
1439
1440BOOST_AUTO_TEST_CASE( DecimateKeepsFractureBridges )
1441{
1442 // Fracturing bakes the hole into the outline through a zero-width corridor whose feet
1443 // split an outline edge into exactly-collinear pieces. With the outline edges subdivided
1444 // there is a real run to collapse, so decimation runs on the fractured ring: it must fold
1445 // the subdivided edges away (triangle count near the plain four-corner result) yet never
1446 // cross the corridor and seal the hole (area preserved).
1447 const int size = 1000000;
1448 const int steps = 25;
1449 SHAPE_LINE_CHAIN outline;
1450
1451 for( int i = 0; i < steps; i++ )
1452 outline.Append( i * size / steps, 0 );
1453
1454 for( int i = 0; i < steps; i++ )
1455 outline.Append( size, i * size / steps );
1456
1457 for( int i = 0; i < steps; i++ )
1458 outline.Append( size - i * size / steps, size );
1459
1460 for( int i = 0; i < steps; i++ )
1461 outline.Append( 0, size - i * size / steps );
1462
1463 outline.SetClosed( true );
1464
1465 SHAPE_LINE_CHAIN hole;
1466 hole.Append( 400000, 400000 );
1467 hole.Append( 400000, 600000 );
1468 hole.Append( 600000, 600000 );
1469 hole.Append( 600000, 400000 );
1470 hole.SetClosed( true );
1471
1472 SHAPE_POLY_SET poly;
1473 poly.AddOutline( outline );
1474 poly.AddHole( hole );
1475
1476 const double area = poly.Area();
1477
1478 poly.Fracture();
1479 poly.CacheTriangulation( false );
1480
1481 double meshTotal = 0.0;
1482 size_t triangles = 0;
1483
1484 for( unsigned i = 0; i < poly.TriangulatedPolyCount(); i++ )
1485 {
1486 triangles += poly.TriangulatedPolygon( i )->GetTriangleCount();
1487
1488 for( const auto& tri : poly.TriangulatedPolygon( i )->Triangles() )
1489 meshTotal += tri.Area();
1490 }
1491
1492 BOOST_CHECK_CLOSE( meshTotal, area, 1e-6 );
1493
1494 // The 100 subdivision points carry no geometry; without decimation the fractured ring
1495 // keeps them all and the mesh is an order of magnitude larger.
1496 BOOST_CHECK_LT( triangles, 20u );
1497}
1498
double square(double x)
bool TesselatePolygon(const SHAPE_POLY_SET::POLYGON &aPolygon, SHAPE_POLY_SET::TRIANGULATED_POLYGON *aHintData)
Triangulate a polygon with holes by bridging holes directly into the outer ring's VERTEX linked list,...
std::vector< double > PartitionAreaFractionsForTesting(const SHAPE_LINE_CHAIN &aPoly, size_t aTargetLeaves) const
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
void SetClosed(bool aClosed)
Mark the line chain as closed (i.e.
int PointCount() const
Return the number of points (vertices) in this line chain.
double Area(bool aAbsolute=true) const
Return the area of this chain.
void Append(int aX, int aY, bool aAllowDuplication=false)
Append a new point at the end of the line chain.
const VECTOR2I & CPoint(int aIndex) const
Return a reference to a given point in the line chain.
const std::deque< TRI > & Triangles() const
void SetTriangles(const std::deque< TRI > &aTriangles)
Represent a set of closed polygons.
virtual void GetIndexableSubshapes(std::vector< const SHAPE * > &aSubshapes) const override
bool IsTriangulationUpToDate() const
int AddOutline(const SHAPE_LINE_CHAIN &aOutline)
Adds a new outline to the set and returns its index.
double Area()
Return the area of this poly set.
bool Parse(std::stringstream &aStream) override
virtual void CacheTriangulation(bool aSimplify=false, const TASK_SUBMITTER &aSubmitter={})
Build a polygon triangulation, needed to draw a polygon on OpenGL and in some other calculations.
std::vector< SHAPE_LINE_CHAIN > POLYGON
represents a single polygon outline with holes.
int AddHole(const SHAPE_LINE_CHAIN &aHole, int aOutline=-1)
Adds a new hole to the given outline (default: last) and returns its index.
const TRIANGULATED_POLYGON * TriangulatedPolygon(int aIndex) const
unsigned int TriangulatedPolyCount() const
Return the number of triangulated polygons.
void UpdateTriangulationDataHash()
void Move(const VECTOR2I &aVector) override
void Fracture(bool aSimplify=true)
Convert a set of polygons with holes to a single outline with "slits"/"fractures" connecting the oute...
bool Contains(const VECTOR2I &aP, int aSubpolyIndex=-1, int aAccuracy=0, bool aUseBBoxCaches=false) const
Return true if a given subpolygon contains the point aP.
std::function< void(std::function< void()>)> TASK_SUBMITTER
Callback that submits a unit of work for asynchronous execution.
bool IsSelfIntersecting() const
Check whether any of the polygons in the set is self intersecting.
An abstract shape on 2D plane.
Definition shape.h:124
SHAPE_POLY_SET::TRIANGULATED_POLYGON & GetResult()
std::unique_ptr< POLYGON_TRIANGULATION > CreateTriangulator()
std::unique_ptr< SHAPE_POLY_SET::TRIANGULATED_POLYGON > m_result
double Distance(const VECTOR2< extended_type > &aVector) const
Compute the distance between two vectors.
Definition vector2d.h:549
static bool empty(const wxTextEntryBase *aCtrl)
Exact orientation and in-circle predicates over integer coordinates.
double TriangleMinAngleDeg(const VECTOR2I &a, const VECTOR2I &b, const VECTOR2I &c)
The smallest interior angle of a triangle, in degrees; near zero for a sliver.
int OrientationSign(const VECTOR2I &a, const VECTOR2I &b, const VECTOR2I &c)
Orientation of triangle (a, b, c): +1 counter-clockwise, -1 clockwise, 0 collinear.
bool InCircleDelaunayLegal(const VECTOR2I &a, const VECTOR2I &b, const VECTOR2I &c, const VECTOR2I &p)
True when p is outside the circumcircle of CCW triangle (a, b, c): the shared edge is already Delauna...
bool IsSliverTriangle(const VECTOR2I &a, const VECTOR2I &b, const VECTOR2I &c)
A triangle is a sliver when its longest edge exceeds ten times its shortest.
STL namespace.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
Numerical test predicates.
#define TRIANGULATESIMPLIFICATIONLEVEL
static std::vector< double > PartitionAreaFractions(POLYGON_TRIANGULATION &aTriangulator, const SHAPE_LINE_CHAIN &aPoly, size_t aTargetLeaves)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_TEST(netlist.find("R_G1 ARM_OUT1 DIE_B R='0.001 / ((SW_STATE)") !=std::string::npos)
bool parsePolyFileForTest(const fs::path &aPath, std::vector< SHAPE_POLY_SET > &aZones)
SHAPE_LINE_CHAIN createConcavePolygon(int size=100)
int countSpikeyTriangles(const SHAPE_POLY_SET::TRIANGULATED_POLYGON &aResult)
SHAPE_LINE_CHAIN createSquare(int size=100, VECTOR2I offset=VECTOR2I(0, 0))
double computeBoardSpikeyRatio(const fs::path &aPath)
bool validateTriangulation(const SHAPE_POLY_SET::TRIANGULATED_POLYGON &result, const SHAPE_LINE_CHAIN &original, bool strict=true)
SHAPE_LINE_CHAIN createTriangle(int size=100, VECTOR2I offset=VECTOR2I(0, 0))
BOOST_AUTO_TEST_CASE(BasicTriangleTriangulation)
SHAPE_LINE_CHAIN createSerpentinePolygon(int step=20000, int teeth=16)
const SHAPE_LINE_CHAIN chain
int radius
VECTOR2I end
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
#define M_PI
static thread_pool * tp
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683