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
15#include <trigo.h>
16#include <thread>
17#include <chrono>
18#include <future>
19#include <filesystem>
20#include <fstream>
21
23#include <qa_utils/numeric.h>
25
26#include "geom_test_utils.h"
27
28BOOST_AUTO_TEST_SUITE( PolygonTriangulation )
29
31{
32 static std::vector<double> PartitionAreaFractions( POLYGON_TRIANGULATION& aTriangulator,
33 const SHAPE_LINE_CHAIN& aPoly,
34 size_t aTargetLeaves )
35 {
36 return aTriangulator.PartitionAreaFractionsForTesting( aPoly, aTargetLeaves );
37 }
38};
39
40namespace fs = std::filesystem;
41
42// Helper class to properly manage TRIANGULATED_POLYGON lifecycle
44{
45public:
46 TRIANGULATION_TEST_FIXTURE() : m_result( std::make_unique<SHAPE_POLY_SET::TRIANGULATED_POLYGON>(0) )
47 {
48 }
49
51
52 std::unique_ptr<POLYGON_TRIANGULATION> CreateTriangulator()
53 {
54 return std::make_unique<POLYGON_TRIANGULATION>( *m_result );
55 }
56
57private:
58 std::unique_ptr<SHAPE_POLY_SET::TRIANGULATED_POLYGON> m_result;
59};
60
61// Helper function to create a simple square
62SHAPE_LINE_CHAIN createSquare( int size = 100, VECTOR2I offset = VECTOR2I(0, 0) )
63{
65 chain.Append( offset.x, offset.y );
66 chain.Append( offset.x + size, offset.y );
67 chain.Append( offset.x + size, offset.y + size );
68 chain.Append( offset.x, offset.y + size );
69 chain.SetClosed( true );
70 return chain;
71}
72
73// Helper function to create a triangle
74SHAPE_LINE_CHAIN createTriangle( int size = 100, VECTOR2I offset = VECTOR2I(0, 0) )
75{
77 chain.Append( offset.x, offset.y );
78 chain.Append( offset.x + size, offset.y );
79 chain.Append( offset.x + size/2, offset.y + size );
80 chain.SetClosed( true );
81 return chain;
82}
83
84// Helper function to create a complex concave polygon
86{
88 chain.Append( 0, 0 );
89 chain.Append( size, 0 );
90 chain.Append( size, size/2 );
91 chain.Append( size/2, size/2 ); // Create concave section
92 chain.Append( size/2, size );
93 chain.Append( 0, size );
94 chain.SetClosed( true );
95 return chain;
96}
97
98SHAPE_LINE_CHAIN createSerpentinePolygon( int step = 20000, int teeth = 16 )
99{
101 chain.Append( 0, 0 );
102
103 int x = 0;
104
105 for( int ii = 0; ii < teeth; ++ii )
106 {
107 x += step;
108 chain.Append( x, 0 );
109 chain.Append( x, step * 3 );
110 x += step;
111 chain.Append( x, step * 3 );
112 chain.Append( x, step * 4 );
113 }
114
115 chain.Append( 0, step * 4 );
116 chain.SetClosed( true );
117 return chain;
118}
119
120// Helper function to validate triangulation result with comprehensive checks
122 const SHAPE_LINE_CHAIN& original, bool strict = true )
123{
124 // Basic validation
125 if( result.GetVertexCount() == 0 )
126 return false;
127
128 size_t triangleCount = result.GetTriangleCount();
129 if( triangleCount == 0 )
130 return false;
131
132 // Validate triangle topology
133 for( size_t i = 0; i < triangleCount; i++ )
134 {
135 const auto& triangle = result.Triangles()[i];
136
137 // Check valid vertex indices
138 if( triangle.a >= (int)result.GetVertexCount() ||
139 triangle.b >= (int)result.GetVertexCount() ||
140 triangle.c >= (int)result.GetVertexCount() )
141 {
142 return false;
143 }
144
145 // Triangle vertices should not be the same
146 if( triangle.a == triangle.b || triangle.b == triangle.c || triangle.a == triangle.c )
147 return false;
148
149 // Check triangle area is positive (counter-clockwise orientation)
150 if( strict && triangle.Area() <= 0 )
151 return false;
152 }
153
154 // Validate that original vertices are preserved
155 if( strict && result.GetVertexCount() >= original.PointCount() )
156 {
157 const auto& vertices = result.Vertices();
158 for( int i = 0; i < original.PointCount(); i++ )
159 {
160 bool found = false;
161 for( size_t j = 0; j < vertices.size(); j++ )
162 {
163 if( vertices[j] == original.CPoint( i ) )
164 {
165 found = true;
166 break;
167 }
168 }
169 if( !found )
170 return false;
171 }
172 }
173
174 return true;
175}
176
178{
179 int count = 0;
180
181 for( const auto& tri : aResult.Triangles() )
182 {
183 VECTOR2I pa = tri.GetPoint( 0 );
184 VECTOR2I pb = tri.GetPoint( 1 );
185 VECTOR2I pc = tri.GetPoint( 2 );
186
187 double ab = pa.Distance( pb );
188 double bc = pb.Distance( pc );
189 double ca = pc.Distance( pa );
190
191 double longest = std::max( { ab, bc, ca } );
192 double shortest = std::min( { ab, bc, ca } );
193
194 if( shortest > 0.0 && longest / shortest > 10.0 )
195 ++count;
196 }
197
198 return count;
199}
200
201bool parsePolyFileForTest( const fs::path& aPath, std::vector<SHAPE_POLY_SET>& aZones )
202{
203 std::ifstream file( aPath );
204
205 if( !file.is_open() )
206 return false;
207
208 std::string content( ( std::istreambuf_iterator<char>( file ) ),
209 std::istreambuf_iterator<char>() );
210
211 size_t zonePos = 0;
212
213 while( ( zonePos = content.find( "(zone (layer \"", zonePos ) ) != std::string::npos )
214 {
215 size_t polysetStart = content.find( "polyset ", zonePos );
216
217 if( polysetStart != std::string::npos )
218 {
219 SHAPE_POLY_SET polySet;
220 std::string remainder = content.substr( polysetStart );
221 std::stringstream ss( remainder );
222
223 if( polySet.Parse( ss ) )
224 aZones.push_back( std::move( polySet ) );
225 }
226
227 size_t layerEnd = content.find( "\")", zonePos + 14 );
228
229 if( layerEnd == std::string::npos )
230 break;
231
232 zonePos = layerEnd + 1;
233 }
234
235 return !aZones.empty();
236}
237
238double computeBoardSpikeyRatio( const fs::path& aPath )
239{
240 std::vector<SHAPE_POLY_SET> zones;
241
242 if( !parsePolyFileForTest( aPath, zones ) )
243 return 1.0;
244
245 int totalTriangles = 0;
246 int totalSpikey = 0;
247
248 for( SHAPE_POLY_SET& polySet : zones )
249 {
250 polySet.CacheTriangulation();
251
252 for( unsigned int i = 0; i < polySet.TriangulatedPolyCount(); ++i )
253 {
254 const auto* triPoly = polySet.TriangulatedPolygon( static_cast<int>( i ) );
255 totalTriangles += triPoly->GetTriangleCount();
256 totalSpikey += countSpikeyTriangles( *triPoly );
257 }
258 }
259
260 return totalTriangles > 0 ? static_cast<double>( totalSpikey ) / totalTriangles : 0.0;
261}
262
263// Core functionality tests
264BOOST_AUTO_TEST_CASE( BasicTriangleTriangulation )
265{
267 auto triangulator = fixture.CreateTriangulator();
268
269 SHAPE_LINE_CHAIN triangle = createTriangle();
270
271 bool success = triangulator->TesselatePolygon( triangle, nullptr );
272
273 BOOST_TEST( success );
274 BOOST_TEST( fixture.GetResult().GetVertexCount() == 3 );
275 BOOST_TEST( fixture.GetResult().GetTriangleCount() == 1 );
276 BOOST_TEST( validateTriangulation( fixture.GetResult(), triangle ) );
277}
278
279BOOST_AUTO_TEST_CASE( BasicSquareTriangulation )
280{
282 auto triangulator = fixture.CreateTriangulator();
283
285
286 bool success = triangulator->TesselatePolygon( square, nullptr );
287
288 BOOST_TEST( success );
289 BOOST_TEST( fixture.GetResult().GetVertexCount() == 4 );
290 BOOST_TEST( fixture.GetResult().GetTriangleCount() == 2 );
292}
293
294BOOST_AUTO_TEST_CASE( SplitFirstFracturePartitionProducesMultipleLeaves )
295{
297 auto triangulator = fixture.CreateTriangulator();
299 std::vector<double> fractions =
301 serpentine, 4 );
302
303 BOOST_TEST( fractions.size() == 4 );
304
305 for( double fraction : fractions )
306 BOOST_TEST( fraction > 0.15 );
307}
308
309BOOST_AUTO_TEST_CASE( EarLookaheadImprovesBadTriangulationCase )
310{
311 fs::path polyPath = fs::path( __FILE__ ).parent_path().parent_path().parent_path().parent_path()
312 .parent_path() / "data/triangulation/bad_triangulation_case.kicad_polys";
313
314 BOOST_TEST( fs::exists( polyPath ) );
315 BOOST_TEST( computeBoardSpikeyRatio( polyPath ) < 0.47 );
316}
317
318BOOST_AUTO_TEST_CASE( ConcavePolygonTriangulation )
319{
321 auto triangulator = fixture.CreateTriangulator();
322
323 SHAPE_LINE_CHAIN concave = createConcavePolygon(100000);
324
325 bool success = triangulator->TesselatePolygon( concave, nullptr );
326
327 BOOST_TEST( success );
328
329 const auto& result = fixture.GetResult();
330 bool isValid = validateTriangulation( result, concave );
331 size_t triangleCount = result.GetTriangleCount();
332
333 // Print diagnostic information if validation fails or triangle count is unexpected
334 if( !success || !isValid || triangleCount < 4 )
335 {
336 std::cout << "\n=== ConcavePolygonTriangulation Diagnostic Output ===" << std::endl;
337 std::cout << "Success: " << (success ? "true" : "false") << std::endl;
338 std::cout << "Validation: " << (isValid ? "true" : "false") << std::endl;
339 std::cout << "Triangle count: " << triangleCount << " (expected >= 4)" << std::endl;
340 std::cout << "Vertex count: " << result.GetVertexCount() << std::endl;
341
342 // Print input polygon vertices
343 std::cout << "\nInput polygon vertices (" << concave.PointCount() << " points):" << std::endl;
344 for( int i = 0; i < concave.PointCount(); i++ )
345 {
346 VECTOR2I pt = concave.CPoint( i );
347 std::cout << " [" << i << "]: (" << pt.x << ", " << pt.y << ")" << std::endl;
348 }
349
350 // Print result vertices
351 std::cout << "\nResult vertices (" << result.GetVertexCount() << " points):" << std::endl;
352 const auto& vertices = result.Vertices();
353 for( size_t i = 0; i < vertices.size(); i++ )
354 {
355 std::cout << " [" << i << "]: (" << vertices[i].x << ", " << vertices[i].y << ")" << std::endl;
356 }
357
358 // Print triangles
359 std::cout << "\nTriangles found (" << triangleCount << " triangles):" << std::endl;
360 const auto& triangles = result.Triangles();
361 for( size_t i = 0; i < triangles.size(); i++ )
362 {
363 const auto& tri = triangles[i];
364 VECTOR2I va = vertices[tri.a];
365 VECTOR2I vb = vertices[tri.b];
366 VECTOR2I vc = vertices[tri.c];
367 double area = tri.Area();
368
369 std::cout << " Triangle[" << i << "]: indices(" << tri.a << "," << tri.b << "," << tri.c << ")" << std::endl;
370 std::cout << " A: (" << va.x << ", " << va.y << ")" << std::endl;
371 std::cout << " B: (" << vb.x << ", " << vb.y << ")" << std::endl;
372 std::cout << " C: (" << vc.x << ", " << vc.y << ")" << std::endl;
373 std::cout << " Area: " << area << std::endl;
374
375 // Check for degenerate triangles
376 if( area <= 0 )
377 std::cout << " *** DEGENERATE TRIANGLE (area <= 0) ***" << std::endl;
378 if( tri.a == tri.b || tri.b == tri.c || tri.a == tri.c )
379 std::cout << " *** INVALID TRIANGLE (duplicate vertex indices) ***" << std::endl;
380 }
381
382 // Additional diagnostic information
383 if( triangleCount > 0 )
384 {
385 double totalArea = 0.0;
386 for( const auto& tri : triangles )
387 totalArea += tri.Area();
388 std::cout << "\nTotal triangulated area: " << totalArea << std::endl;
389
390 // Calculate expected area of concave polygon for comparison
391 double originalArea = std::abs( concave.Area() );
392 std::cout << "Original polygon area: " << originalArea << std::endl;
393 std::cout << "Area difference: " << std::abs( totalArea - originalArea ) << std::endl;
394 }
395
396 std::cout << "================================================\n" << std::endl;
397 }
398
399 BOOST_TEST( success );
400 BOOST_TEST( isValid );
401 // L-shaped concave polygons should have 4 triangles
402 BOOST_TEST( triangleCount == 4 );
403}
404
405
406BOOST_AUTO_TEST_CASE( HintDataOptimization )
407{
408 // First triangulation without hint
410 auto triangulator1 = fixture1.CreateTriangulator();
412
413 bool success1 = triangulator1->TesselatePolygon( square, nullptr );
414 BOOST_TEST( success1 );
415
416 // Second triangulation with hint data from first
418 auto triangulator2 = fixture2.CreateTriangulator();
419
420 bool success2 = triangulator2->TesselatePolygon( square, &fixture1.GetResult() );
421 BOOST_TEST( success2 );
422
423 // Results should be identical when hint is applicable
424 BOOST_TEST( fixture1.GetResult().GetVertexCount() == fixture2.GetResult().GetVertexCount() );
425 BOOST_TEST( fixture1.GetResult().GetTriangleCount() == fixture2.GetResult().GetTriangleCount() );
426}
427
428BOOST_AUTO_TEST_CASE( HintDataOptimizationWithSimplifiedInput )
429{
430 SHAPE_LINE_CHAIN noisySquare;
431 noisySquare.Append( 0, 0 );
432 noisySquare.Append( 100, 0 );
433 noisySquare.Append( 100, 10 );
434 noisySquare.Append( 100, 100 );
435 noisySquare.Append( 0, 100 );
436 noisySquare.SetClosed( true );
437
438 TRIANGULATION_TEST_FIXTURE hintFixture;
439 auto hintTriangulator = hintFixture.CreateTriangulator();
440
441 bool success1 = hintTriangulator->TesselatePolygon( noisySquare, nullptr );
442 BOOST_TEST( success1 );
443
444 auto poisonedTriangles = hintFixture.GetResult().Triangles();
445 BOOST_REQUIRE_GE( poisonedTriangles.size(), 2U );
446 std::reverse( poisonedTriangles.begin(), poisonedTriangles.end() );
447 hintFixture.GetResult().SetTriangles( poisonedTriangles );
448
450 auto triangulator = fixture.CreateTriangulator();
451
452 bool success2 = triangulator->TesselatePolygon( noisySquare, &hintFixture.GetResult() );
453 BOOST_TEST( success2 );
454 BOOST_TEST( fixture.GetResult().GetTriangleCount() == poisonedTriangles.size() );
455
456 for( size_t i = 0; i < poisonedTriangles.size(); ++i )
457 {
458 BOOST_TEST( fixture.GetResult().Triangles()[i].a == poisonedTriangles[i].a );
459 BOOST_TEST( fixture.GetResult().Triangles()[i].b == poisonedTriangles[i].b );
460 BOOST_TEST( fixture.GetResult().Triangles()[i].c == poisonedTriangles[i].c );
461 }
462}
463
464BOOST_AUTO_TEST_CASE( HintDataInvalidation )
465{
466 // Create hint data with different vertex count
467 TRIANGULATION_TEST_FIXTURE hintFixture;
468 auto hintTriangulator = hintFixture.CreateTriangulator();
469 SHAPE_LINE_CHAIN triangle = createTriangle();
470 hintTriangulator->TesselatePolygon( triangle, nullptr );
471
472 // Try to use hint with different polygon (should ignore hint)
474 auto triangulator = fixture.CreateTriangulator();
476
477 bool success = triangulator->TesselatePolygon( square, &hintFixture.GetResult() );
478 BOOST_TEST( success );
480}
481
482// Degenerate case handling
483BOOST_AUTO_TEST_CASE( DegeneratePolygons )
484{
486 auto triangulator = fixture.CreateTriangulator();
487
488 // Test empty polygon
490 bool success = triangulator->TesselatePolygon( empty, nullptr );
491 BOOST_TEST( success ); // Should handle gracefully
492
493 // Test single point
495 auto triangulator2 = fixture2.CreateTriangulator();
496 SHAPE_LINE_CHAIN singlePoint;
497 singlePoint.Append( 0, 0 );
498 singlePoint.SetClosed( true );
499 success = triangulator2->TesselatePolygon( singlePoint, nullptr );
500 BOOST_TEST( success ); // Should handle gracefully
501
502 // Test two points (line segment)
504 auto triangulator3 = fixture3.CreateTriangulator();
505 SHAPE_LINE_CHAIN line;
506 line.Append( 0, 0 );
507 line.Append( 100, 0 );
508 line.SetClosed( true );
509 success = triangulator3->TesselatePolygon( line, nullptr );
510 BOOST_TEST( success ); // Should handle gracefully
511}
512
513BOOST_AUTO_TEST_CASE( ZeroAreaPolygon )
514{
516 auto triangulator = fixture.CreateTriangulator();
517
518 // Create a polygon with zero area (all points collinear)
519 SHAPE_LINE_CHAIN zeroArea;
520 zeroArea.Append( 0, 0 );
521 zeroArea.Append( 100, 0 );
522 zeroArea.Append( 50, 0 );
523 zeroArea.Append( 25, 0 );
524 zeroArea.SetClosed( true );
525
526 bool success = triangulator->TesselatePolygon( zeroArea, nullptr );
527
528 BOOST_TEST( success ); // Should handle gracefully without crashing
529}
530
531// Memory management and lifecycle tests
532BOOST_AUTO_TEST_CASE( MemoryManagement )
533{
534 // Test that multiple triangulations properly manage memory
535 for( int i = 0; i < 100; i++ )
536 {
538 auto triangulator = fixture.CreateTriangulator();
539
540 SHAPE_LINE_CHAIN poly = createSquare( 100 + i, VECTOR2I( i, i ) );
541 bool success = triangulator->TesselatePolygon( poly, nullptr );
542
543 BOOST_TEST( success );
544 BOOST_TEST( validateTriangulation( fixture.GetResult(), poly, false ) );
545 }
546}
547
548BOOST_AUTO_TEST_CASE( LargePolygonStressTest )
549{
551 auto triangulator = fixture.CreateTriangulator();
552
553 // Create a large polygon (regular polygon with many vertices)
554 SHAPE_LINE_CHAIN largePoly;
555 int numVertices = 1000;
556 int radius = 10000;
557
558 for( int i = 0; i < numVertices; i++ )
559 {
560 double angle = 2.0 * M_PI * i / numVertices;
561 int x = static_cast<int>( radius * cos( angle ) );
562 int y = static_cast<int>( radius * sin( angle ) );
563 largePoly.Append( x, y );
564 }
565 largePoly.SetClosed( true );
566
567 auto start = std::chrono::high_resolution_clock::now();
568 bool success = triangulator->TesselatePolygon( largePoly, nullptr );
569 auto end = std::chrono::high_resolution_clock::now();
570
571 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
572
573 BOOST_TEST( success );
574 if( success )
575 {
576 BOOST_TEST( validateTriangulation( fixture.GetResult(), largePoly, false ) );
577 BOOST_TEST( fixture.GetResult().GetTriangleCount() > 0 );
578 }
579
580 // Performance check - should complete in reasonable time
581 BOOST_TEST( duration.count() < 10000 ); // Less than 10 seconds
582}
583
584// Thread safety tests (following SHAPE_POLY_SET patterns)
585BOOST_AUTO_TEST_CASE( ConcurrentTriangulation )
586{
587 const int numThreads = 4;
588 const int numTriangulationsPerThread = 10;
589
590 std::vector<std::future<bool>> futures;
591
592 for( int t = 0; t < numThreads; t++ )
593 {
594 futures.push_back( std::async( std::launch::async, [t, numTriangulationsPerThread]()
595 {
596 for( int i = 0; i < numTriangulationsPerThread; i++ )
597 {
599 auto triangulator = fixture.CreateTriangulator();
600
601 // Create unique polygon for each thread/iteration
602 SHAPE_LINE_CHAIN poly = createSquare( 100 + t * 10 + i, VECTOR2I( t * 100, i * 100 ) );
603
604 bool success = triangulator->TesselatePolygon( poly, nullptr );
605 if( !success || !validateTriangulation( fixture.GetResult(), poly, false ) )
606 return false;
607 }
608 return true;
609 }));
610 }
611
612 // Wait for all threads and check results
613 for( auto& future : futures )
614 {
615 BOOST_TEST( future.get() );
616 }
617}
618
619// Edge case and robustness tests
620BOOST_AUTO_TEST_CASE( SelfIntersectingPolygon )
621{
623 auto triangulator = fixture.CreateTriangulator();
624
625 // Create a bowtie (self-intersecting polygon)
626 SHAPE_LINE_CHAIN bowtie;
627 bowtie.Append( 0, 0 );
628 bowtie.Append( 100, 100 );
629 bowtie.Append( 100, 0 );
630 bowtie.Append( 0, 100 );
631 bowtie.SetClosed( true );
632
633 bool success = triangulator->TesselatePolygon( bowtie, nullptr );
634
635 // Algorithm should handle self-intersecting polygons
636 BOOST_TEST( success );
637 if( success )
638 {
639 BOOST_TEST( validateTriangulation( fixture.GetResult(), bowtie, false ) );
640 }
641}
642
643
656BOOST_AUTO_TEST_CASE( Issue18083_SelfIntersectingPolygonArea )
657{
658 SHAPE_POLY_SET polySet;
659 SHAPE_LINE_CHAIN outline;
660
661 // Coordinates from the issue (converted to internal units: 1mm = 1000000)
662 const int SCALE = 1000000;
663 outline.Append( 165 * SCALE, 87 * SCALE );
664 outline.Append( 179 * SCALE, 87 * SCALE );
665 outline.Append( 174 * SCALE, 94 * SCALE );
666 outline.Append( 169 * SCALE, 87 * SCALE );
667 outline.Append( 167 * SCALE, 94 * SCALE );
668 outline.SetClosed( true );
669
670 polySet.AddOutline( outline );
671
672 // Verify the polygon is detected as self-intersecting
673 BOOST_TEST( polySet.IsSelfIntersecting() );
674
675 // Triangulate via SHAPE_POLY_SET
676 polySet.CacheTriangulation();
678
679 // Calculate the triangulated area
680 double triangulatedArea = 0.0;
681
682 for( int ii = 0; ii < polySet.TriangulatedPolyCount(); ii++ )
683 {
684 const auto triPoly = polySet.TriangulatedPolygon( ii );
685
686 for( const auto& tri : triPoly->Triangles() )
687 triangulatedArea += std::abs( tri.Area() );
688 }
689
690 // The expected total area is 49 mm² (14 mm² + 35 mm² for the two triangular lobes)
691 // Triangle 1: (165,87) - (169,87) - (167,94) = base 4mm, height 7mm = 14 mm²
692 // Triangle 2: (169,87) - (179,87) - (174,94) = base 10mm, height 7mm = 35 mm²
693 double expectedAreaMmSq = 49.0 * SCALE * SCALE;
694
695 // The triangulated area should match the expected area
696 BOOST_TEST( std::abs( triangulatedArea - expectedAreaMmSq ) < expectedAreaMmSq * 0.01,
697 "Triangulated area should match expected area of 49 mm²" );
698}
699
700BOOST_AUTO_TEST_CASE( NearlyCollinearVertices )
701{
703 auto triangulator = fixture.CreateTriangulator();
704
705 // Create a polygon with vertices that are nearly collinear
706 SHAPE_LINE_CHAIN nearlyCollinear;
707 nearlyCollinear.Append( 0, 0 );
708 nearlyCollinear.Append( 1000000, 0 );
709 nearlyCollinear.Append( 2000000, 1 ); // Very small deviation
710 nearlyCollinear.Append( 3000000, 0 );
711 nearlyCollinear.Append( 1500000, 1000000 );
712 nearlyCollinear.SetClosed( true );
713
714 bool success = triangulator->TesselatePolygon( nearlyCollinear, nullptr );
715
716 BOOST_TEST( success );
717 if( success )
718 {
719 BOOST_TEST( validateTriangulation( fixture.GetResult(), nearlyCollinear, false ) );
720 }
721}
722
723BOOST_AUTO_TEST_CASE( DuplicateVertices )
724{
726 auto triangulator = fixture.CreateTriangulator();
727
728 // Create a square with duplicate vertices
730 duplicate.Append( 0, 0 );
731 duplicate.Append( 0, 0 ); // Duplicate
732 duplicate.Append( 100, 0 );
733 duplicate.Append( 100, 0 ); // Duplicate
734 duplicate.Append( 100, 100 );
735 duplicate.Append( 100, 100 ); // Duplicate
736 duplicate.Append( 0, 100 );
737 duplicate.Append( 0, 100 ); // Duplicate
738 duplicate.SetClosed( true );
739
740 bool success = triangulator->TesselatePolygon( duplicate, nullptr );
741
742 BOOST_TEST( success );
743 if( success )
744 {
745 BOOST_TEST( validateTriangulation( fixture.GetResult(), duplicate, false ) );
746 }
747}
748
749BOOST_AUTO_TEST_CASE( ExtremeCoordinates )
750{
752 auto triangulator = fixture.CreateTriangulator();
753
754 // Test with very large coordinates
755 SHAPE_LINE_CHAIN extreme;
756 int large = 1000000000; // 1 billion
757 extreme.Append( 0, 0 );
758 extreme.Append( large, 0 );
759 extreme.Append( large, large );
760 extreme.Append( 0, large );
761 extreme.SetClosed( true );
762
763 bool success = triangulator->TesselatePolygon( extreme, nullptr );
764
765 BOOST_TEST( success );
766 if( success )
767 {
768 BOOST_TEST( validateTriangulation( fixture.GetResult(), extreme, false ) );
769 }
770}
771
772// Error recovery and cleanup tests
773BOOST_AUTO_TEST_CASE( ErrorRecoveryAndCleanup )
774{
776 auto triangulator = fixture.CreateTriangulator();
777
778 // Try a series of operations, some of which might fail
779 std::vector<SHAPE_LINE_CHAIN> testPolygons;
780
781 // Valid polygon
782 testPolygons.push_back( createSquare() );
783
784 // Degenerate polygon
785 SHAPE_LINE_CHAIN degenerate;
786 degenerate.Append( 0, 0 );
787 degenerate.SetClosed( true );
788 testPolygons.push_back( degenerate );
789
790 // Another valid polygon
791 testPolygons.push_back( createTriangle() );
792
793 for( const auto& poly : testPolygons )
794 {
795 // Each triangulation should start with a clean state
796 bool success = triangulator->TesselatePolygon( poly, nullptr );
797 // Even if triangulation fails, it should not crash
798 success |= fixture.GetResult().GetTriangleCount() > 0;
799 BOOST_TEST( success );
800 }
801}
802
803// Integration tests with TRIANGULATED_POLYGON interface
804BOOST_AUTO_TEST_CASE( TriangulatedPolygonInterface )
805{
807 auto triangulator = fixture.CreateTriangulator();
808
810 bool success = triangulator->TesselatePolygon( square, nullptr );
811
812 BOOST_TEST( success );
813
814 const auto& result = fixture.GetResult();
815
816 // Test GetTriangle method
817 if( result.GetTriangleCount() > 0 )
818 {
819 VECTOR2I a, b, c;
820 result.GetTriangle( 0, a, b, c );
821
822 // Vertices should be valid points from the square
823 BOOST_TEST( (a.x >= 0 && a.x <= 100 && a.y >= 0 && a.y <= 100) );
824 BOOST_TEST( (b.x >= 0 && b.x <= 100 && b.y >= 0 && b.y <= 100) );
825 BOOST_TEST( (c.x >= 0 && c.x <= 100 && c.y >= 0 && c.y <= 100) );
826 }
827
828 // Test triangle iteration
829 for( const auto& tri : result.Triangles() )
830 {
831 BOOST_TEST( tri.GetPointCount() == 3 );
832 BOOST_TEST( tri.GetSegmentCount() == 3 );
833 BOOST_TEST( tri.Area() > 0 );
834 BOOST_TEST( tri.IsClosed() );
835 BOOST_TEST( tri.IsSolid() );
836 }
837}
838
839BOOST_AUTO_TEST_CASE( SourceOutlineIndexTracking )
840{
842 auto triangulator = fixture.CreateTriangulator();
843
844 // Test that source outline index is properly maintained
845 const int expectedOutlineIndex = 5;
846
847 // Create triangulated polygon with specific source index
848 SHAPE_POLY_SET::TRIANGULATED_POLYGON result( expectedOutlineIndex );
849 POLYGON_TRIANGULATION localTriangulator( result );
850
851 SHAPE_LINE_CHAIN triangle = createTriangle();
852 bool success = localTriangulator.TesselatePolygon( triangle, nullptr );
853
854 BOOST_TEST( success );
855 BOOST_TEST( result.GetSourceOutlineIndex() == expectedOutlineIndex );
856}
857
858// Performance regression tests
859BOOST_AUTO_TEST_CASE( PerformanceRegression )
860{
861 // Test various polygon sizes to ensure performance scales reasonably
862 std::vector<int> testSizes = { 10, 50, 100, 500, 1000 };
863 std::vector<long long> durations;
864
865 for( int size : testSizes )
866 {
868 auto triangulator = fixture.CreateTriangulator();
869
870 // Create regular polygon
871 SHAPE_LINE_CHAIN poly;
872 for( int i = 0; i < size; i++ )
873 {
874 double angle = 2.0 * M_PI * i / size;
875 int x = static_cast<int>( 1000 * cos( angle ) );
876 int y = static_cast<int>( 1000 * sin( angle ) );
877 poly.Append( x, y );
878 }
879 poly.SetClosed( true );
880
881 auto start = std::chrono::high_resolution_clock::now();
882 bool success = triangulator->TesselatePolygon( poly, nullptr );
883 auto end = std::chrono::high_resolution_clock::now();
884
885 BOOST_TEST( success );
886
887 auto duration = std::chrono::duration_cast<std::chrono::microseconds>( end - start );
888 durations.push_back( duration.count() );
889 }
890
891 // Check that performance scales reasonably (shouldn't be exponential)
892 // This is a basic sanity check - actual performance will vary by hardware
893 for( size_t i = 1; i < durations.size(); i++ )
894 {
895 double scaleFactor = static_cast<double>( durations[i] ) / durations[i-1];
896 double sizeFactor = static_cast<double>( testSizes[i] ) / testSizes[i-1];
897
898 // Performance shouldn't be worse than O(n^2) in most cases
899 BOOST_TEST( scaleFactor < sizeFactor * sizeFactor * 2 );
900 }
901}
902
908BOOST_AUTO_TEST_CASE( ParallelPartitionTriangulation )
909{
910 SHAPE_POLY_SET polySet;
911 SHAPE_LINE_CHAIN outline;
912 constexpr int vertexCount = 120000;
913 constexpr int centerX = 5000000;
914 constexpr int centerY = 5000000;
915 constexpr int radius = 4000000;
916
917 for( int i = 0; i < vertexCount; ++i )
918 {
919 double angle = 2.0 * M_PI * i / vertexCount;
920 int x = centerX + static_cast<int>( radius * cos( angle ) );
921 int y = centerY + static_cast<int>( radius * sin( angle ) );
922 outline.Append( x, y );
923 }
924
925 outline.SetClosed( true );
926 polySet.AddOutline( outline );
927
928 std::atomic<int> tasksSubmitted( 0 );
929
931 [&tasksSubmitted]( std::function<void()> aTask )
932 {
933 tasksSubmitted++;
934 std::thread( std::move( aTask ) ).detach();
935 };
936
937 polySet.CacheTriangulation( false, submitter );
938
940 BOOST_TEST( tasksSubmitted.load() > 0 );
941
942 double originalArea = std::abs( outline.Area() );
943 double triArea = 0.0;
944
945 for( unsigned int i = 0; i < polySet.TriangulatedPolyCount(); i++ )
946 {
947 const auto* triPoly = polySet.TriangulatedPolygon( static_cast<int>( i ) );
948
949 for( const auto& tri : triPoly->Triangles() )
950 triArea += std::abs( tri.Area() );
951 }
952
953 // Partitioning may clip edges, so allow a few percent tolerance
954 if( originalArea > 0.0 )
955 {
956 double coverage = triArea / originalArea;
957 BOOST_TEST( coverage > 0.90 );
958 BOOST_TEST( coverage < 1.10 );
959 }
960}
961
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.
bool IsTriangulationUpToDate() const
int AddOutline(const SHAPE_LINE_CHAIN &aOutline)
Adds a new outline to the set and returns its index.
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.
const TRIANGULATED_POLYGON * TriangulatedPolygon(int aIndex) const
unsigned int TriangulatedPolyCount() const
Return the number of triangulated polygons.
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.
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:553
static bool empty(const wxTextEntryBase *aCtrl)
STL namespace.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
Numerical test predicates.
static std::vector< double > PartitionAreaFractions(POLYGON_TRIANGULATION &aTriangulator, const SHAPE_LINE_CHAIN &aPoly, size_t aTargetLeaves)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_TEST(contains==c.ExpectedContains)
BOOST_AUTO_TEST_SUITE_END()
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
wxString result
Test unit parsing edge cases and error handling.
#define M_PI
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:687