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 <advanced_config.h>
17#include <trigo.h>
18#include <thread>
19#include <chrono>
20#include <future>
21#include <filesystem>
22#include <fstream>
23#include <array>
24
25#include <wx/log.h>
26
28#include <qa_utils/numeric.h>
30
31#include "geom_test_utils.h"
33
34BOOST_AUTO_TEST_SUITE( PolygonTriangulation )
35
37{
38 static std::vector<double> PartitionAreaFractions( POLYGON_TRIANGULATION& aTriangulator,
39 const SHAPE_LINE_CHAIN& aPoly,
40 size_t aTargetLeaves )
41 {
42 return aTriangulator.PartitionAreaFractionsForTesting( aPoly, aTargetLeaves );
43 }
44};
45
46namespace fs = std::filesystem;
47
48class TRACE_LOG : public wxLog
49{
50public:
51 bool Contains( const wxString& aText ) const { return m_text.Contains( aText ); }
52
53protected:
54 void DoLogTextAtLevel( wxLogLevel, const wxString& aText ) override { m_text += aText; }
55
56private:
57 wxString m_text;
58};
59
61{
62public:
63 explicit SCOPED_TRACE_CAPTURE( const wxString& aMask ) :
64 m_log( new TRACE_LOG ),
65 m_chain( m_log ),
66 m_mask( aMask ),
67 m_maskWasAllowed( wxLog::IsAllowedTraceMask( aMask ) )
68 {
69 m_chain.PassMessages( false );
70
71 if( !m_maskWasAllowed )
72 wxLog::AddTraceMask( m_mask );
73 }
74
76 {
77 if( !m_maskWasAllowed )
78 wxLog::RemoveTraceMask( m_mask );
79 }
80
81 bool Contains( const wxString& aText ) const { return m_log->Contains( aText ); }
82
83private:
85 wxLogChain m_chain;
86 wxString m_mask;
88};
89
90
91// Helper class to properly manage TRIANGULATED_POLYGON lifecycle
93{
94public:
95 TRIANGULATION_TEST_FIXTURE() : m_result( std::make_unique<SHAPE_POLY_SET::TRIANGULATED_POLYGON>(0) )
96 {
97 }
98
100
101 std::unique_ptr<POLYGON_TRIANGULATION> CreateTriangulator()
102 {
103 return std::make_unique<POLYGON_TRIANGULATION>( *m_result );
104 }
105
106private:
107 std::unique_ptr<SHAPE_POLY_SET::TRIANGULATED_POLYGON> m_result;
108};
109
110// Helper function to create a simple square
111SHAPE_LINE_CHAIN createSquare( int size = 100, VECTOR2I offset = VECTOR2I(0, 0) )
112{
114 chain.Append( offset.x, offset.y );
115 chain.Append( offset.x + size, offset.y );
116 chain.Append( offset.x + size, offset.y + size );
117 chain.Append( offset.x, offset.y + size );
118 chain.SetClosed( true );
119 return chain;
120}
121
122// Helper function to create a triangle
123SHAPE_LINE_CHAIN createTriangle( int size = 100, VECTOR2I offset = VECTOR2I(0, 0) )
124{
126 chain.Append( offset.x, offset.y );
127 chain.Append( offset.x + size, offset.y );
128 chain.Append( offset.x + size/2, offset.y + size );
129 chain.SetClosed( true );
130 return chain;
131}
132
133// Helper function to create a complex concave polygon
135{
137 chain.Append( 0, 0 );
138 chain.Append( size, 0 );
139 chain.Append( size, size/2 );
140 chain.Append( size/2, size/2 ); // Create concave section
141 chain.Append( size/2, size );
142 chain.Append( 0, size );
143 chain.SetClosed( true );
144 return chain;
145}
146
147SHAPE_LINE_CHAIN createSerpentinePolygon( int step = 20000, int teeth = 16 )
148{
150 chain.Append( 0, 0 );
151
152 int x = 0;
153
154 for( int ii = 0; ii < teeth; ++ii )
155 {
156 x += step;
157 chain.Append( x, 0 );
158 chain.Append( x, step * 3 );
159 x += step;
160 chain.Append( x, step * 3 );
161 chain.Append( x, step * 4 );
162 }
163
164 chain.Append( 0, step * 4 );
165 chain.SetClosed( true );
166 return chain;
167}
168
169// Helper function to validate triangulation result with comprehensive checks
171 const SHAPE_LINE_CHAIN& original, bool strict = true )
172{
173 // Basic validation
174 if( result.GetVertexCount() == 0 )
175 return false;
176
177 size_t triangleCount = result.GetTriangleCount();
178 if( triangleCount == 0 )
179 return false;
180
181 // Validate triangle topology
182 for( size_t i = 0; i < triangleCount; i++ )
183 {
184 const auto& triangle = result.Triangles()[i];
185
186 // Check valid vertex indices
187 if( triangle.a >= (int)result.GetVertexCount() ||
188 triangle.b >= (int)result.GetVertexCount() ||
189 triangle.c >= (int)result.GetVertexCount() )
190 {
191 return false;
192 }
193
194 // Triangle vertices should not be the same
195 if( triangle.a == triangle.b || triangle.b == triangle.c || triangle.a == triangle.c )
196 return false;
197
198 // Check triangle area is positive (counter-clockwise orientation)
199 if( strict && triangle.Area() <= 0 )
200 return false;
201 }
202
203 // Validate that original vertices are preserved
204 if( strict && result.GetVertexCount() >= original.PointCount() )
205 {
206 const auto& vertices = result.Vertices();
207 for( int i = 0; i < original.PointCount(); i++ )
208 {
209 bool found = false;
210 for( size_t j = 0; j < vertices.size(); j++ )
211 {
212 if( vertices[j] == original.CPoint( i ) )
213 {
214 found = true;
215 break;
216 }
217 }
218 if( !found )
219 return false;
220 }
221 }
222
223 return true;
224}
225
227{
228 int count = 0;
229
230 for( const auto& tri : aResult.Triangles() )
231 {
232 VECTOR2I pa = tri.GetPoint( 0 );
233 VECTOR2I pb = tri.GetPoint( 1 );
234 VECTOR2I pc = tri.GetPoint( 2 );
235
236 double ab = pa.Distance( pb );
237 double bc = pb.Distance( pc );
238 double ca = pc.Distance( pa );
239
240 double longest = std::max( { ab, bc, ca } );
241 double shortest = std::min( { ab, bc, ca } );
242
243 if( shortest > 0.0 && longest / shortest > 10.0 )
244 ++count;
245 }
246
247 return count;
248}
249
250bool parsePolyFileForTest( const fs::path& aPath, std::vector<SHAPE_POLY_SET>& aZones )
251{
252 std::ifstream file( aPath );
253
254 if( !file.is_open() )
255 return false;
256
257 std::string content( ( std::istreambuf_iterator<char>( file ) ),
258 std::istreambuf_iterator<char>() );
259
260 size_t zonePos = 0;
261
262 while( ( zonePos = content.find( "(zone (layer \"", zonePos ) ) != std::string::npos )
263 {
264 size_t polysetStart = content.find( "polyset ", zonePos );
265
266 if( polysetStart != std::string::npos )
267 {
268 SHAPE_POLY_SET polySet;
269 std::string remainder = content.substr( polysetStart );
270 std::stringstream ss( remainder );
271
272 if( polySet.Parse( ss ) )
273 aZones.push_back( std::move( polySet ) );
274 }
275
276 size_t layerEnd = content.find( "\")", zonePos + 14 );
277
278 if( layerEnd == std::string::npos )
279 break;
280
281 zonePos = layerEnd + 1;
282 }
283
284 return !aZones.empty();
285}
286
287// Forces the fracture implementation under test regardless of the developer's kicad_advanced
289{
290public:
291 explicit SCOPED_FRACTURE_CFG( bool aUseIndex ) :
292 m_cfg( const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() ) ),
293 m_cacheFriendly( m_cfg.m_EnableCacheFriendlyFracture ),
294 m_edgeIndex( m_cfg.m_EnableFractureEdgeIndex )
295 {
296 m_cfg.m_EnableCacheFriendlyFracture = true;
297 m_cfg.m_EnableFractureEdgeIndex = aUseIndex;
298 }
299
301 {
302 m_cfg.m_EnableCacheFriendlyFracture = m_cacheFriendly;
303 m_cfg.m_EnableFractureEdgeIndex = m_edgeIndex;
304 }
305
306private:
310};
311
312
313BOOST_AUTO_TEST_CASE( FractureEdgeIndexMatchesLinearScan )
314{
315#if !defined( __MINGW32__ )
316 const fs::path polyPath = KI_TEST::GetTestDataRootDir() + "triangulation/vme-wren.kicad_polys";
317 std::vector<SHAPE_POLY_SET> zones;
318
319 BOOST_REQUIRE( parsePolyFileForTest( polyPath, zones ) );
320
321 int deepest = 0;
322
323 for( SHAPE_POLY_SET& zone : zones )
324 {
325 zone.Unfracture();
326
327 for( int polygon = 0; polygon < zone.OutlineCount(); ++polygon )
328 deepest = std::max( deepest, zone.HoleCount( polygon ) );
329 }
330
331 // The index only engages past its hole and visit thresholds, so a fixture that shrank would
332 // leave this comparing two linear runs
333 BOOST_REQUIRE_GE( deepest, 100 );
334
335 std::vector<SHAPE_POLY_SET> linear = zones;
336 std::vector<SHAPE_POLY_SET> indexed = zones;
337
338 {
339 SCOPED_FRACTURE_CFG cfg( false );
340
341 for( SHAPE_POLY_SET& zone : linear )
342 zone.Fracture( false );
343 }
344
345 SCOPED_TRACE_CAPTURE trace( wxS( "KICAD_FRACTURE_INDEX" ) );
346
347 {
348 SCOPED_FRACTURE_CFG cfg( true );
349
350 for( SHAPE_POLY_SET& zone : indexed )
351 zone.Fracture( false );
352 }
353
354 BOOST_REQUIRE( trace.Contains( wxS( "Using fracture edge index" ) ) );
355
356 for( size_t zone = 0; zone < linear.size(); ++zone )
357 {
358 BOOST_REQUIRE_EQUAL( indexed[zone].OutlineCount(), linear[zone].OutlineCount() );
359
360 for( int polygon = 0; polygon < linear[zone].OutlineCount(); ++polygon )
361 {
362 const SHAPE_LINE_CHAIN& a = linear[zone].COutline( polygon );
363 const SHAPE_LINE_CHAIN& b = indexed[zone].COutline( polygon );
364 BOOST_REQUIRE_EQUAL( a.PointCount(), b.PointCount() );
365
366 for( int point = 0; point < a.PointCount(); ++point )
367 BOOST_CHECK_EQUAL( a.CPoint( point ), b.CPoint( point ) );
368 }
369 }
370#endif
371}
372
373
374double computeBoardSpikeyRatio( const fs::path& aPath )
375{
376 std::vector<SHAPE_POLY_SET> zones;
377
378 if( !parsePolyFileForTest( aPath, zones ) )
379 return 1.0;
380
381 int totalTriangles = 0;
382 int totalSpikey = 0;
383
384 for( SHAPE_POLY_SET& polySet : zones )
385 {
386 polySet.CacheTriangulation();
387
388 for( unsigned int i = 0; i < polySet.TriangulatedPolyCount(); ++i )
389 {
390 const auto* triPoly = polySet.TriangulatedPolygon( static_cast<int>( i ) );
391 totalTriangles += triPoly->GetTriangleCount();
392 totalSpikey += countSpikeyTriangles( *triPoly );
393 }
394 }
395
396 return totalTriangles > 0 ? static_cast<double>( totalSpikey ) / totalTriangles : 0.0;
397}
398
399// Core functionality tests
400BOOST_AUTO_TEST_CASE( BasicTriangleTriangulation )
401{
403 auto triangulator = fixture.CreateTriangulator();
404
405 SHAPE_LINE_CHAIN triangle = createTriangle();
406
407 bool success = triangulator->TesselatePolygon( triangle, nullptr );
408
409 BOOST_TEST( success );
410 BOOST_TEST( fixture.GetResult().GetVertexCount() == 3 );
411 BOOST_TEST( fixture.GetResult().GetTriangleCount() == 1 );
412 BOOST_TEST( validateTriangulation( fixture.GetResult(), triangle ) );
413}
414
415BOOST_AUTO_TEST_CASE( BasicSquareTriangulation )
416{
418 auto triangulator = fixture.CreateTriangulator();
419
421
422 bool success = triangulator->TesselatePolygon( square, nullptr );
423
424 BOOST_TEST( success );
425 BOOST_TEST( fixture.GetResult().GetVertexCount() == 4 );
426 BOOST_TEST( fixture.GetResult().GetTriangleCount() == 2 );
428}
429
430BOOST_AUTO_TEST_CASE( SplitFirstFracturePartitionProducesMultipleLeaves )
431{
433 auto triangulator = fixture.CreateTriangulator();
435 std::vector<double> fractions =
437 serpentine, 4 );
438
439 BOOST_TEST( fractions.size() == 4 );
440
441 for( double fraction : fractions )
442 BOOST_TEST( fraction > 0.15 );
443}
444
445BOOST_AUTO_TEST_CASE( EarLookaheadImprovesBadTriangulationCase )
446{
447 fs::path polyPath = KI_TEST::GetTestDataRootDir() + "triangulation/bad_triangulation_case.kicad_polys";
448
449 BOOST_TEST( fs::exists( polyPath ) );
450 BOOST_TEST( computeBoardSpikeyRatio( polyPath ) < 0.47 );
451}
452
453BOOST_AUTO_TEST_CASE( ConcavePolygonTriangulation )
454{
456 auto triangulator = fixture.CreateTriangulator();
457
458 SHAPE_LINE_CHAIN concave = createConcavePolygon(100000);
459
460 bool success = triangulator->TesselatePolygon( concave, nullptr );
461
462 BOOST_TEST( success );
463
464 const auto& result = fixture.GetResult();
465 bool isValid = validateTriangulation( result, concave );
466 size_t triangleCount = result.GetTriangleCount();
467
468 // Print diagnostic information if validation fails or triangle count is unexpected
469 if( !success || !isValid || triangleCount < 4 )
470 {
471 std::cout << "\n=== ConcavePolygonTriangulation Diagnostic Output ===" << std::endl;
472 std::cout << "Success: " << (success ? "true" : "false") << std::endl;
473 std::cout << "Validation: " << (isValid ? "true" : "false") << std::endl;
474 std::cout << "Triangle count: " << triangleCount << " (expected >= 4)" << std::endl;
475 std::cout << "Vertex count: " << result.GetVertexCount() << std::endl;
476
477 // Print input polygon vertices
478 std::cout << "\nInput polygon vertices (" << concave.PointCount() << " points):" << std::endl;
479 for( int i = 0; i < concave.PointCount(); i++ )
480 {
481 VECTOR2I pt = concave.CPoint( i );
482 std::cout << " [" << i << "]: (" << pt.x << ", " << pt.y << ")" << std::endl;
483 }
484
485 // Print result vertices
486 std::cout << "\nResult vertices (" << result.GetVertexCount() << " points):" << std::endl;
487 const auto& vertices = result.Vertices();
488 for( size_t i = 0; i < vertices.size(); i++ )
489 {
490 std::cout << " [" << i << "]: (" << vertices[i].x << ", " << vertices[i].y << ")" << std::endl;
491 }
492
493 // Print triangles
494 std::cout << "\nTriangles found (" << triangleCount << " triangles):" << std::endl;
495 const auto& triangles = result.Triangles();
496 for( size_t i = 0; i < triangles.size(); i++ )
497 {
498 const auto& tri = triangles[i];
499 VECTOR2I va = vertices[tri.a];
500 VECTOR2I vb = vertices[tri.b];
501 VECTOR2I vc = vertices[tri.c];
502 double area = tri.Area();
503
504 std::cout << " Triangle[" << i << "]: indices(" << tri.a << "," << tri.b << "," << tri.c << ")" << std::endl;
505 std::cout << " A: (" << va.x << ", " << va.y << ")" << std::endl;
506 std::cout << " B: (" << vb.x << ", " << vb.y << ")" << std::endl;
507 std::cout << " C: (" << vc.x << ", " << vc.y << ")" << std::endl;
508 std::cout << " Area: " << area << std::endl;
509
510 // Check for degenerate triangles
511 if( area <= 0 )
512 std::cout << " *** DEGENERATE TRIANGLE (area <= 0) ***" << std::endl;
513 if( tri.a == tri.b || tri.b == tri.c || tri.a == tri.c )
514 std::cout << " *** INVALID TRIANGLE (duplicate vertex indices) ***" << std::endl;
515 }
516
517 // Additional diagnostic information
518 if( triangleCount > 0 )
519 {
520 double totalArea = 0.0;
521 for( const auto& tri : triangles )
522 totalArea += tri.Area();
523 std::cout << "\nTotal triangulated area: " << totalArea << std::endl;
524
525 // Calculate expected area of concave polygon for comparison
526 double originalArea = std::abs( concave.Area() );
527 std::cout << "Original polygon area: " << originalArea << std::endl;
528 std::cout << "Area difference: " << std::abs( totalArea - originalArea ) << std::endl;
529 }
530
531 std::cout << "================================================\n" << std::endl;
532 }
533
534 BOOST_TEST( success );
535 BOOST_TEST( isValid );
536 // L-shaped concave polygons should have 4 triangles
537 BOOST_TEST( triangleCount == 4 );
538}
539
540
541BOOST_AUTO_TEST_CASE( HintDataOptimization )
542{
543 // First triangulation without hint
545 auto triangulator1 = fixture1.CreateTriangulator();
547
548 bool success1 = triangulator1->TesselatePolygon( square, nullptr );
549 BOOST_TEST( success1 );
550
551 // Second triangulation with hint data from first
553 auto triangulator2 = fixture2.CreateTriangulator();
554
555 bool success2 = triangulator2->TesselatePolygon( square, &fixture1.GetResult() );
556 BOOST_TEST( success2 );
557
558 // Results should be identical when hint is applicable
559 BOOST_TEST( fixture1.GetResult().GetVertexCount() == fixture2.GetResult().GetVertexCount() );
560 BOOST_TEST( fixture1.GetResult().GetTriangleCount() == fixture2.GetResult().GetTriangleCount() );
561}
562
563BOOST_AUTO_TEST_CASE( HintDataOptimizationWithSimplifiedInput )
564{
565 SHAPE_LINE_CHAIN noisySquare;
566 noisySquare.Append( 0, 0 );
567 noisySquare.Append( 100, 0 );
568 noisySquare.Append( 100, 10 );
569 noisySquare.Append( 100, 100 );
570 noisySquare.Append( 0, 100 );
571 noisySquare.SetClosed( true );
572
573 TRIANGULATION_TEST_FIXTURE hintFixture;
574 auto hintTriangulator = hintFixture.CreateTriangulator();
575
576 bool success1 = hintTriangulator->TesselatePolygon( noisySquare, nullptr );
577 BOOST_TEST( success1 );
578
579 auto poisonedTriangles = hintFixture.GetResult().Triangles();
580 BOOST_REQUIRE_GE( poisonedTriangles.size(), 2U );
581 std::reverse( poisonedTriangles.begin(), poisonedTriangles.end() );
582 hintFixture.GetResult().SetTriangles( poisonedTriangles );
583
585 auto triangulator = fixture.CreateTriangulator();
586
587 bool success2 = triangulator->TesselatePolygon( noisySquare, &hintFixture.GetResult() );
588 BOOST_TEST( success2 );
589 BOOST_TEST( fixture.GetResult().GetTriangleCount() == poisonedTriangles.size() );
590
591 for( size_t i = 0; i < poisonedTriangles.size(); ++i )
592 {
593 BOOST_TEST( fixture.GetResult().Triangles()[i].a == poisonedTriangles[i].a );
594 BOOST_TEST( fixture.GetResult().Triangles()[i].b == poisonedTriangles[i].b );
595 BOOST_TEST( fixture.GetResult().Triangles()[i].c == poisonedTriangles[i].c );
596 }
597}
598
599BOOST_AUTO_TEST_CASE( HintDataInvalidation )
600{
601 // Create hint data with different vertex count
602 TRIANGULATION_TEST_FIXTURE hintFixture;
603 auto hintTriangulator = hintFixture.CreateTriangulator();
604 SHAPE_LINE_CHAIN triangle = createTriangle();
605 hintTriangulator->TesselatePolygon( triangle, nullptr );
606
607 // Try to use hint with different polygon (should ignore hint)
609 auto triangulator = fixture.CreateTriangulator();
611
612 bool success = triangulator->TesselatePolygon( square, &hintFixture.GetResult() );
613 BOOST_TEST( success );
615}
616
617// Degenerate case handling
618BOOST_AUTO_TEST_CASE( DegeneratePolygons )
619{
621 auto triangulator = fixture.CreateTriangulator();
622
623 // Test empty polygon
625 bool success = triangulator->TesselatePolygon( empty, nullptr );
626 BOOST_TEST( success ); // Should handle gracefully
627
628 // Test single point
630 auto triangulator2 = fixture2.CreateTriangulator();
631 SHAPE_LINE_CHAIN singlePoint;
632 singlePoint.Append( 0, 0 );
633 singlePoint.SetClosed( true );
634 success = triangulator2->TesselatePolygon( singlePoint, nullptr );
635 BOOST_TEST( success ); // Should handle gracefully
636
637 // Test two points (line segment)
639 auto triangulator3 = fixture3.CreateTriangulator();
640 SHAPE_LINE_CHAIN line;
641 line.Append( 0, 0 );
642 line.Append( 100, 0 );
643 line.SetClosed( true );
644 success = triangulator3->TesselatePolygon( line, nullptr );
645 BOOST_TEST( success ); // Should handle gracefully
646}
647
648BOOST_AUTO_TEST_CASE( ZeroAreaPolygon )
649{
651 auto triangulator = fixture.CreateTriangulator();
652
653 // Create a polygon with zero area (all points collinear)
654 SHAPE_LINE_CHAIN zeroArea;
655 zeroArea.Append( 0, 0 );
656 zeroArea.Append( 100, 0 );
657 zeroArea.Append( 50, 0 );
658 zeroArea.Append( 25, 0 );
659 zeroArea.SetClosed( true );
660
661 bool success = triangulator->TesselatePolygon( zeroArea, nullptr );
662
663 BOOST_TEST( success ); // Should handle gracefully without crashing
664}
665
666// Memory management and lifecycle tests
667BOOST_AUTO_TEST_CASE( MemoryManagement )
668{
669 // Test that multiple triangulations properly manage memory
670 for( int i = 0; i < 100; i++ )
671 {
673 auto triangulator = fixture.CreateTriangulator();
674
675 SHAPE_LINE_CHAIN poly = createSquare( 100 + i, VECTOR2I( i, i ) );
676 bool success = triangulator->TesselatePolygon( poly, nullptr );
677
678 BOOST_TEST( success );
679 BOOST_TEST( validateTriangulation( fixture.GetResult(), poly, false ) );
680 }
681}
682
683BOOST_AUTO_TEST_CASE( LargePolygonStressTest )
684{
686 auto triangulator = fixture.CreateTriangulator();
687
688 // Create a large polygon (regular polygon with many vertices)
689 SHAPE_LINE_CHAIN largePoly;
690 int numVertices = 1000;
691 int radius = 10000;
692
693 for( int i = 0; i < numVertices; i++ )
694 {
695 double angle = 2.0 * M_PI * i / numVertices;
696 int x = static_cast<int>( radius * cos( angle ) );
697 int y = static_cast<int>( radius * sin( angle ) );
698 largePoly.Append( x, y );
699 }
700 largePoly.SetClosed( true );
701
702 auto start = std::chrono::high_resolution_clock::now();
703 bool success = triangulator->TesselatePolygon( largePoly, nullptr );
704 auto end = std::chrono::high_resolution_clock::now();
705
706 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
707
708 BOOST_TEST( success );
709 if( success )
710 {
711 BOOST_TEST( validateTriangulation( fixture.GetResult(), largePoly, false ) );
712 BOOST_TEST( fixture.GetResult().GetTriangleCount() > 0 );
713 }
714
715 // Sanitizer instrumentation changes execution cost, not the triangulation contract
716#if defined( KICAD_SANITIZE_THREADS ) || defined( KICAD_SANITIZE_ADDRESS )
717 BOOST_TEST_MESSAGE( "Instrumented triangulation: " << duration.count() << " ms" );
718#else
719 BOOST_TEST( duration.count() < 10000 ); // Less than 10 seconds
720#endif
721}
722
723// Thread safety tests (following SHAPE_POLY_SET patterns)
724BOOST_AUTO_TEST_CASE( ConcurrentTriangulation )
725{
726 const int numThreads = 4;
727 const int numTriangulationsPerThread = 10;
728
729 std::vector<std::future<bool>> futures;
730
731 for( int t = 0; t < numThreads; t++ )
732 {
733 futures.push_back( std::async( std::launch::async, [t, numTriangulationsPerThread]()
734 {
735 for( int i = 0; i < numTriangulationsPerThread; i++ )
736 {
738 auto triangulator = fixture.CreateTriangulator();
739
740 // Create unique polygon for each thread/iteration
741 SHAPE_LINE_CHAIN poly = createSquare( 100 + t * 10 + i, VECTOR2I( t * 100, i * 100 ) );
742
743 bool success = triangulator->TesselatePolygon( poly, nullptr );
744 if( !success || !validateTriangulation( fixture.GetResult(), poly, false ) )
745 return false;
746 }
747 return true;
748 }));
749 }
750
751 // Wait for all threads and check results
752 for( auto& future : futures )
753 {
754 BOOST_TEST( future.get() );
755 }
756}
757
758// Edge case and robustness tests
759BOOST_AUTO_TEST_CASE( SelfIntersectingPolygon )
760{
762 auto triangulator = fixture.CreateTriangulator();
763
764 // Create a bowtie (self-intersecting polygon)
765 SHAPE_LINE_CHAIN bowtie;
766 bowtie.Append( 0, 0 );
767 bowtie.Append( 100, 100 );
768 bowtie.Append( 100, 0 );
769 bowtie.Append( 0, 100 );
770 bowtie.SetClosed( true );
771
772 bool success = triangulator->TesselatePolygon( bowtie, nullptr );
773
774 // Algorithm should handle self-intersecting polygons
775 BOOST_TEST( success );
776 if( success )
777 {
778 BOOST_TEST( validateTriangulation( fixture.GetResult(), bowtie, false ) );
779 }
780}
781
782
795BOOST_AUTO_TEST_CASE( Issue18083_SelfIntersectingPolygonArea )
796{
797 SHAPE_POLY_SET polySet;
798 SHAPE_LINE_CHAIN outline;
799
800 // Coordinates from the issue (converted to internal units: 1mm = 1000000)
801 const int SCALE = 1000000;
802 outline.Append( 165 * SCALE, 87 * SCALE );
803 outline.Append( 179 * SCALE, 87 * SCALE );
804 outline.Append( 174 * SCALE, 94 * SCALE );
805 outline.Append( 169 * SCALE, 87 * SCALE );
806 outline.Append( 167 * SCALE, 94 * SCALE );
807 outline.SetClosed( true );
808
809 polySet.AddOutline( outline );
810
811 // Verify the polygon is detected as self-intersecting
812 BOOST_TEST( polySet.IsSelfIntersecting() );
813
814 // Triangulate via SHAPE_POLY_SET
815 polySet.CacheTriangulation();
817
818 // Calculate the triangulated area
819 double triangulatedArea = 0.0;
820
821 for( int ii = 0; ii < polySet.TriangulatedPolyCount(); ii++ )
822 {
823 const auto triPoly = polySet.TriangulatedPolygon( ii );
824
825 for( const auto& tri : triPoly->Triangles() )
826 triangulatedArea += std::abs( tri.Area() );
827 }
828
829 // The expected total area is 49 mm² (14 mm² + 35 mm² for the two triangular lobes)
830 // Triangle 1: (165,87) - (169,87) - (167,94) = base 4mm, height 7mm = 14 mm²
831 // Triangle 2: (169,87) - (179,87) - (174,94) = base 10mm, height 7mm = 35 mm²
832 double expectedAreaMmSq = 49.0 * SCALE * SCALE;
833
834 // The triangulated area should match the expected area
835 BOOST_TEST( std::abs( triangulatedArea - expectedAreaMmSq ) < expectedAreaMmSq * 0.01,
836 "Triangulated area should match expected area of 49 mm²" );
837}
838
839BOOST_AUTO_TEST_CASE( Issue18083_IndexedSelfTouchingOutlineArea )
840{
841 const int SCALE = 1000000;
842 const std::array<VECTOR2I, 5> corners = { VECTOR2I( 165 * SCALE, 87 * SCALE ),
843 VECTOR2I( 179 * SCALE, 87 * SCALE ),
844 VECTOR2I( 174 * SCALE, 94 * SCALE ),
845 VECTOR2I( 169 * SCALE, 87 * SCALE ),
846 VECTOR2I( 167 * SCALE, 94 * SCALE ) };
847 SHAPE_LINE_CHAIN outline;
848
849 // Subdivision preserves the pinch and its area while exercising the indexed split
850 for( size_t i = 0; i < corners.size(); ++i )
851 {
852 const VECTOR2I& a = corners[i];
853 const VECTOR2I& b = corners[( i + 1 ) % corners.size()];
854
855 for( int step = 0; step < 8; ++step )
856 outline.Append( a.x + ( b.x - a.x ) * step / 8, a.y + ( b.y - a.y ) * step / 8 );
857 }
858
859 outline.SetClosed( true );
860 BOOST_REQUIRE_EQUAL( outline.PointCount(), 40 );
861
862 SHAPE_POLY_SET polySet;
863 polySet.AddOutline( outline );
864 const double outlineArea = polySet.Area();
865
866 polySet.CacheTriangulation( false );
868
869 double triangulatedArea = 0.0;
870
871 for( int i = 0; i < polySet.TriangulatedPolyCount(); ++i )
872 {
873 const auto triPoly = polySet.TriangulatedPolygon( i );
874
875 for( const auto& tri : triPoly->Triangles() )
876 triangulatedArea += std::abs( tri.Area() );
877 }
878
879 BOOST_TEST( std::abs( triangulatedArea - outlineArea ) < outlineArea * 0.01 );
880}
881
882BOOST_AUTO_TEST_CASE( Issue25141_FractureCorridorIsNotAPinchPoint )
883{
884 // Vertex 6 sits 1nm off vertex 1 and the corridor runs just under 45 degrees, so it rounds
885 // onto segment 1-2. The folded lobe makes the direct pass over-cover, which is what routes
886 // this through splitSelfTouchingOutlines() at all.
887 SHAPE_LINE_CHAIN outline( { 0, 0,
888 20000, 0, // corridor mouth
889 60001, 40000, // hole ring
890 60001, 60000,
891 80000, 50000,
892 60001, 40000,
893 20001, 0, // corridor return
894 100000, 0,
895 100000, 100000,
896 30000, 20000, // fold
897 90000, 90000,
898 0, 100000 } );
899 outline.SetClosed( true );
900
901 SHAPE_POLY_SET polySet;
902 polySet.AddOutline( outline );
903
904 polySet.CacheTriangulation( false );
906
907 const VECTOR2I inHole( 64000, 50000 );
908 std::vector<const SHAPE*> triangles;
909
910 polySet.GetIndexableSubshapes( triangles );
911 BOOST_TEST( !polySet.Contains( inHole, -1, 0, false ) );
912
913 for( const SHAPE* tri : triangles )
914 BOOST_TEST( !tri->Collide( inHole, 0 ) );
915}
916
917BOOST_AUTO_TEST_CASE( NearlyCollinearVertices )
918{
920 auto triangulator = fixture.CreateTriangulator();
921
922 // Create a polygon with vertices that are nearly collinear
923 SHAPE_LINE_CHAIN nearlyCollinear;
924 nearlyCollinear.Append( 0, 0 );
925 nearlyCollinear.Append( 1000000, 0 );
926 nearlyCollinear.Append( 2000000, 1 ); // Very small deviation
927 nearlyCollinear.Append( 3000000, 0 );
928 nearlyCollinear.Append( 1500000, 1000000 );
929 nearlyCollinear.SetClosed( true );
930
931 bool success = triangulator->TesselatePolygon( nearlyCollinear, nullptr );
932
933 BOOST_TEST( success );
934 if( success )
935 {
936 BOOST_TEST( validateTriangulation( fixture.GetResult(), nearlyCollinear, false ) );
937 }
938}
939
940BOOST_AUTO_TEST_CASE( DuplicateVertices )
941{
943 auto triangulator = fixture.CreateTriangulator();
944
945 // Create a square with duplicate vertices
947 duplicate.Append( 0, 0 );
948 duplicate.Append( 0, 0 ); // Duplicate
949 duplicate.Append( 100, 0 );
950 duplicate.Append( 100, 0 ); // Duplicate
951 duplicate.Append( 100, 100 );
952 duplicate.Append( 100, 100 ); // Duplicate
953 duplicate.Append( 0, 100 );
954 duplicate.Append( 0, 100 ); // Duplicate
955 duplicate.SetClosed( true );
956
957 bool success = triangulator->TesselatePolygon( duplicate, nullptr );
958
959 BOOST_TEST( success );
960 if( success )
961 {
962 BOOST_TEST( validateTriangulation( fixture.GetResult(), duplicate, false ) );
963 }
964}
965
966BOOST_AUTO_TEST_CASE( ExtremeCoordinates )
967{
969 auto triangulator = fixture.CreateTriangulator();
970
971 // Test with very large coordinates
972 SHAPE_LINE_CHAIN extreme;
973 int large = 1000000000; // 1 billion
974 extreme.Append( 0, 0 );
975 extreme.Append( large, 0 );
976 extreme.Append( large, large );
977 extreme.Append( 0, large );
978 extreme.SetClosed( true );
979
980 bool success = triangulator->TesselatePolygon( extreme, nullptr );
981
982 BOOST_TEST( success );
983 if( success )
984 {
985 BOOST_TEST( validateTriangulation( fixture.GetResult(), extreme, false ) );
986 }
987}
988
989// Error recovery and cleanup tests
990BOOST_AUTO_TEST_CASE( ErrorRecoveryAndCleanup )
991{
993 auto triangulator = fixture.CreateTriangulator();
994
995 // Try a series of operations, some of which might fail
996 std::vector<SHAPE_LINE_CHAIN> testPolygons;
997
998 // Valid polygon
999 testPolygons.push_back( createSquare() );
1000
1001 // Degenerate polygon
1002 SHAPE_LINE_CHAIN degenerate;
1003 degenerate.Append( 0, 0 );
1004 degenerate.SetClosed( true );
1005 testPolygons.push_back( degenerate );
1006
1007 // Another valid polygon
1008 testPolygons.push_back( createTriangle() );
1009
1010 for( const auto& poly : testPolygons )
1011 {
1012 // Each triangulation should start with a clean state
1013 bool success = triangulator->TesselatePolygon( poly, nullptr );
1014 // Even if triangulation fails, it should not crash
1015 success |= fixture.GetResult().GetTriangleCount() > 0;
1016 BOOST_TEST( success );
1017 }
1018}
1019
1020// Integration tests with TRIANGULATED_POLYGON interface
1021BOOST_AUTO_TEST_CASE( TriangulatedPolygonInterface )
1022{
1024 auto triangulator = fixture.CreateTriangulator();
1025
1027 bool success = triangulator->TesselatePolygon( square, nullptr );
1028
1029 BOOST_TEST( success );
1030
1031 const auto& result = fixture.GetResult();
1032
1033 // Test GetTriangle method
1034 if( result.GetTriangleCount() > 0 )
1035 {
1036 VECTOR2I a, b, c;
1037 result.GetTriangle( 0, a, b, c );
1038
1039 // Vertices should be valid points from the square
1040 BOOST_TEST( (a.x >= 0 && a.x <= 100 && a.y >= 0 && a.y <= 100) );
1041 BOOST_TEST( (b.x >= 0 && b.x <= 100 && b.y >= 0 && b.y <= 100) );
1042 BOOST_TEST( (c.x >= 0 && c.x <= 100 && c.y >= 0 && c.y <= 100) );
1043 }
1044
1045 // Test triangle iteration
1046 for( const auto& tri : result.Triangles() )
1047 {
1048 BOOST_TEST( tri.GetPointCount() == 3 );
1049 BOOST_TEST( tri.GetSegmentCount() == 3 );
1050 BOOST_TEST( tri.Area() > 0 );
1051 BOOST_TEST( tri.IsClosed() );
1052 BOOST_TEST( tri.IsSolid() );
1053 }
1054}
1055
1056BOOST_AUTO_TEST_CASE( SourceOutlineIndexTracking )
1057{
1059 auto triangulator = fixture.CreateTriangulator();
1060
1061 // Test that source outline index is properly maintained
1062 const int expectedOutlineIndex = 5;
1063
1064 // Create triangulated polygon with specific source index
1065 SHAPE_POLY_SET::TRIANGULATED_POLYGON result( expectedOutlineIndex );
1066 POLYGON_TRIANGULATION localTriangulator( result );
1067
1068 SHAPE_LINE_CHAIN triangle = createTriangle();
1069 bool success = localTriangulator.TesselatePolygon( triangle, nullptr );
1070
1071 BOOST_TEST( success );
1072 BOOST_TEST( result.GetSourceOutlineIndex() == expectedOutlineIndex );
1073}
1074
1075// Performance regression tests
1076BOOST_AUTO_TEST_CASE( PerformanceRegression )
1077{
1078 // Test various polygon sizes to ensure performance scales reasonably
1079 std::vector<int> testSizes = { 10, 50, 100, 500, 1000 };
1080 std::vector<long long> durations;
1081
1082 for( int size : testSizes )
1083 {
1085 auto triangulator = fixture.CreateTriangulator();
1086
1087 // Create regular polygon
1088 SHAPE_LINE_CHAIN poly;
1089 for( int i = 0; i < size; i++ )
1090 {
1091 double angle = 2.0 * M_PI * i / size;
1092 int x = static_cast<int>( 1000 * cos( angle ) );
1093 int y = static_cast<int>( 1000 * sin( angle ) );
1094 poly.Append( x, y );
1095 }
1096 poly.SetClosed( true );
1097
1098 auto start = std::chrono::high_resolution_clock::now();
1099 bool success = triangulator->TesselatePolygon( poly, nullptr );
1100 auto end = std::chrono::high_resolution_clock::now();
1101
1102 BOOST_TEST( success );
1103
1104 auto duration = std::chrono::duration_cast<std::chrono::microseconds>( end - start );
1105 durations.push_back( duration.count() );
1106 }
1107
1108 // Check that performance scales reasonably (shouldn't be exponential)
1109 // This is a basic sanity check - actual performance will vary by hardware
1110 for( size_t i = 1; i < durations.size(); i++ )
1111 {
1112 double scaleFactor = static_cast<double>( durations[i] ) / durations[i-1];
1113 double sizeFactor = static_cast<double>( testSizes[i] ) / testSizes[i-1];
1114
1115 // Performance shouldn't be worse than O(n^2) in most cases
1116 BOOST_TEST( scaleFactor < sizeFactor * sizeFactor * 2 );
1117 }
1118}
1119
1125BOOST_AUTO_TEST_CASE( ParallelPartitionTriangulation )
1126{
1127 SHAPE_POLY_SET polySet;
1128 SHAPE_LINE_CHAIN outline;
1129 constexpr int vertexCount = 120000;
1130 constexpr int centerX = 5000000;
1131 constexpr int centerY = 5000000;
1132 constexpr int radius = 4000000;
1133
1134 for( int i = 0; i < vertexCount; ++i )
1135 {
1136 double angle = 2.0 * M_PI * i / vertexCount;
1137 int x = centerX + static_cast<int>( radius * cos( angle ) );
1138 int y = centerY + static_cast<int>( radius * sin( angle ) );
1139 outline.Append( x, y );
1140 }
1141
1142 outline.SetClosed( true );
1143 polySet.AddOutline( outline );
1144
1145 std::atomic<int> tasksSubmitted( 0 );
1146
1148 [&tasksSubmitted]( std::function<void()> aTask )
1149 {
1150 tasksSubmitted++;
1151 std::thread( std::move( aTask ) ).detach();
1152 };
1153
1154 polySet.CacheTriangulation( false, submitter );
1155
1157 BOOST_TEST( tasksSubmitted.load() > 0 );
1158
1159 double originalArea = std::abs( outline.Area() );
1160 double triArea = 0.0;
1161
1162 for( unsigned int i = 0; i < polySet.TriangulatedPolyCount(); i++ )
1163 {
1164 const auto* triPoly = polySet.TriangulatedPolygon( static_cast<int>( i ) );
1165
1166 for( const auto& tri : triPoly->Triangles() )
1167 triArea += std::abs( tri.Area() );
1168 }
1169
1170 // Partitioning may clip edges, so allow a few percent tolerance
1171 if( originalArea > 0.0 )
1172 {
1173 double coverage = triArea / originalArea;
1174 BOOST_TEST( coverage > 0.90 );
1175 BOOST_TEST( coverage < 1.10 );
1176 }
1177}
1178
1188BOOST_AUTO_TEST_CASE( Issue24059_HoleEliminationSentinelRemoval )
1189{
1190 SHAPE_LINE_CHAIN outer;
1191 outer.Append( 0, 0 );
1192 outer.Append( 10000, 0 );
1193 outer.Append( 10000, 10000 );
1194 outer.Append( 0, 10000 );
1195 outer.SetClosed( true );
1196
1197 SHAPE_LINE_CHAIN hole;
1198 hole.Append( 0, 0 );
1199 hole.Append( 1000, 1000 );
1200 hole.Append( 0, 2000 );
1201 hole.SetClosed( true );
1202
1204 polygon.push_back( outer );
1205 polygon.push_back( hole );
1206
1208 auto triangulator = fixture.CreateTriangulator();
1209
1210 std::atomic<bool> finished( false );
1211 bool result = false;
1212
1213 std::thread worker( [&]()
1214 {
1215 result = triangulator->TesselatePolygon( polygon, nullptr );
1216 finished.store( true );
1217 } );
1218
1219 worker.detach();
1220
1221 auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 5 );
1222
1223 while( !finished.load() && std::chrono::steady_clock::now() < deadline )
1224 std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) );
1225
1226 BOOST_CHECK_MESSAGE( finished.load(), "TesselatePolygon hung (issue #24059)" );
1227
1228 if( finished.load() )
1229 {
1230 BOOST_TEST( result );
1231 BOOST_TEST( fixture.GetResult().GetTriangleCount() > 0 );
1232 }
1233}
1234
1252BOOST_AUTO_TEST_CASE( Issue24121_DegenerateHoleAllDuplicates )
1253{
1254 SHAPE_LINE_CHAIN outer;
1255 outer.Append( 0, 0 );
1256 outer.Append( 10000, 0 );
1257 outer.Append( 10000, 10000 );
1258 outer.Append( 0, 10000 );
1259 outer.SetClosed( true );
1260
1261 // Hole with all coincident points: simplification leaves one vertex, then the
1262 // duplicate-tail cleanup formerly self-removed it and produced a nullptr-next
1263 // ring that crashed eliminateHoles().
1264 SHAPE_LINE_CHAIN hole;
1265 hole.Append( 5000, 5000 );
1266 hole.Append( 5000, 5000 );
1267 hole.Append( 5000, 5000 );
1268 hole.Append( 5000, 5000 );
1269 hole.SetClosed( true );
1270
1272 polygon.push_back( outer );
1273 polygon.push_back( hole );
1274
1276 auto triangulator = fixture.CreateTriangulator();
1277
1278 bool result = triangulator->TesselatePolygon( polygon, nullptr );
1279
1280 BOOST_TEST( result );
1281 BOOST_TEST( fixture.GetResult().GetTriangleCount() > 0 );
1282}
1283
1290BOOST_AUTO_TEST_CASE( Issue24121_DegenerateHoleBelowSimplification )
1291{
1292 SHAPE_LINE_CHAIN outer;
1293 outer.Append( 0, 0 );
1294 outer.Append( 10000, 0 );
1295 outer.Append( 10000, 10000 );
1296 outer.Append( 0, 10000 );
1297 outer.SetClosed( true );
1298
1299 // All four points lie within a 4-unit box, well below the default 50-unit
1300 // simplification threshold, so addVertex() in createRing() collapses them
1301 // to a single vertex.
1302 SHAPE_LINE_CHAIN tinyHole;
1303 tinyHole.Append( 5000, 5000 );
1304 tinyHole.Append( 5002, 5000 );
1305 tinyHole.Append( 5002, 5002 );
1306 tinyHole.Append( 5000, 5002 );
1307 tinyHole.SetClosed( true );
1308
1310 polygon.push_back( outer );
1311 polygon.push_back( tinyHole );
1312
1314 auto triangulator = fixture.CreateTriangulator();
1315
1316 bool result = triangulator->TesselatePolygon( polygon, nullptr );
1317
1318 BOOST_TEST( result );
1319 BOOST_TEST( fixture.GetResult().GetTriangleCount() > 0 );
1320}
1321
1322BOOST_AUTO_TEST_CASE( CacheTriangulation_AfterMove_FreshPoly )
1323{
1324 SHAPE_LINE_CHAIN outline;
1325 outline.Append( 0, 0 );
1326 outline.Append( 10000, 0 );
1327 outline.Append( 10000, 10000 );
1328 outline.Append( 0, 10000 );
1329 outline.SetClosed( true );
1330
1331 SHAPE_POLY_SET polySet;
1332 polySet.AddOutline( outline );
1333
1334 BOOST_TEST( !polySet.IsTriangulationUpToDate() );
1335
1336 // Move() sets m_hashValid=true without triangulating
1337 polySet.Move( { 1, 1 } );
1338 polySet.CacheTriangulation();
1339
1341 BOOST_TEST( polySet.TriangulatedPolyCount() > 0 );
1342}
1343
1344BOOST_AUTO_TEST_CASE( CacheTriangulation_AfterUpdateTriangulationDataHash )
1345{
1346 SHAPE_LINE_CHAIN outline;
1347 outline.Append( 0, 0 );
1348 outline.Append( 10000, 0 );
1349 outline.Append( 10000, 10000 );
1350 outline.Append( 0, 10000 );
1351 outline.SetClosed( true );
1352
1353 SHAPE_POLY_SET polySet;
1354 polySet.AddOutline( outline );
1355
1357 polySet.CacheTriangulation();
1358
1360 BOOST_TEST( polySet.TriangulatedPolyCount() > 0 );
1361}
1362
1363namespace
1364{
1365double meshMinAngleDeg( const SHAPE_POLY_SET::TRIANGULATED_POLYGON& aTri )
1366{
1367 double minAngle = 180.0;
1368
1369 for( const auto& tri : aTri.Triangles() )
1370 {
1371 minAngle = std::min( minAngle, GEOM_TEST::TriangleMinAngleDeg( tri.GetPoint( 0 ),
1372 tri.GetPoint( 1 ),
1373 tri.GetPoint( 2 ) ) );
1374 }
1375
1376 return minAngle;
1377}
1378
1379
1380double meshArea( const SHAPE_POLY_SET::TRIANGULATED_POLYGON& aTri )
1381{
1382 double area = 0.0;
1383
1384 for( const auto& tri : aTri.Triangles() )
1385 area += tri.Area();
1386
1387 return area;
1388}
1389
1390
1391bool meshHasEdge( const SHAPE_POLY_SET::TRIANGULATED_POLYGON& aTri, int aU, int aV )
1392{
1393 for( const auto& tri : aTri.Triangles() )
1394 {
1395 bool hasU = tri.a == aU || tri.b == aU || tri.c == aU;
1396 bool hasV = tri.a == aV || tri.b == aV || tri.c == aV;
1397
1398 if( hasU && hasV )
1399 return true;
1400 }
1401
1402 return false;
1403}
1404
1405
1406int meshSpikeyCount( const SHAPE_POLY_SET::TRIANGULATED_POLYGON& aTri )
1407{
1408 int count = 0;
1409
1410 for( const auto& tri : aTri.Triangles() )
1411 {
1412 VECTOR2I a = tri.GetPoint( 0 ), b = tri.GetPoint( 1 ), c = tri.GetPoint( 2 );
1413
1414 if( KIGEOM::IsSliverTriangle( a, b, c ) )
1415 count++;
1416 }
1417
1418 return count;
1419}
1420} // namespace
1421
1422
1423BOOST_AUTO_TEST_CASE( RefineFlipsNonDelaunayDiagonal )
1424{
1425 // Vertex 3 lies inside the circumcircle of 0-1-2, so diagonal 0-2 is non-Delaunay and must
1426 // flip to 1-3.
1428 tp.AddVertex( VECTOR2I( 0, 0 ) );
1429 tp.AddVertex( VECTOR2I( 10000, 0 ) );
1430 tp.AddVertex( VECTOR2I( 10000, 10000 ) );
1431 tp.AddVertex( VECTOR2I( 2000, 8000 ) );
1432 tp.AddTriangle( 0, 1, 2 );
1433 tp.AddTriangle( 0, 2, 3 );
1434
1435 double beforeAngle = meshMinAngleDeg( tp );
1436 double beforeArea = meshArea( tp );
1437
1438 tp.Refine();
1439
1440 BOOST_CHECK_EQUAL( tp.GetTriangleCount(), 2u );
1441 BOOST_CHECK_CLOSE( meshArea( tp ), beforeArea, 0.001 );
1442 BOOST_CHECK_GT( meshMinAngleDeg( tp ), beforeAngle );
1443
1444 BOOST_CHECK( meshHasEdge( tp, 0, 1 ) );
1445 BOOST_CHECK( meshHasEdge( tp, 1, 2 ) );
1446 BOOST_CHECK( meshHasEdge( tp, 2, 3 ) );
1447 BOOST_CHECK( meshHasEdge( tp, 0, 3 ) );
1448
1449 BOOST_CHECK( !meshHasEdge( tp, 0, 2 ) );
1450 BOOST_CHECK( meshHasEdge( tp, 1, 3 ) );
1451}
1452
1453
1454BOOST_AUTO_TEST_CASE( RefinePrefersFewerSliversOverDelaunay )
1455{
1456 // Diagonal 0-2 is Delaunay-legal but produces a sliver; 1-3 produces none. Refine must prefer
1457 // the fewer-sliver diagonal over the Delaunay one.
1459 tp.AddVertex( VECTOR2I( 0, 0 ) );
1460 tp.AddVertex( VECTOR2I( 284000, 42000 ) );
1461 tp.AddVertex( VECTOR2I( 276000, 68000 ) );
1462 tp.AddVertex( VECTOR2I( 38000, 126000 ) );
1463 tp.AddTriangle( 0, 1, 2 );
1464 tp.AddTriangle( 0, 2, 3 );
1465
1466 // Precondition: 0-2 is Delaunay-legal, so a Delaunay-only refine would not touch it.
1468 VECTOR2I( 276000, 68000 ),
1469 VECTOR2I( 38000, 126000 ) ) );
1470 BOOST_REQUIRE_EQUAL( meshSpikeyCount( tp ), 1 );
1471
1472 double beforeArea = meshArea( tp );
1473
1474 tp.Refine();
1475
1476 BOOST_CHECK_EQUAL( tp.GetTriangleCount(), 2u );
1477 BOOST_CHECK_CLOSE( meshArea( tp ), beforeArea, 0.001 );
1478 BOOST_CHECK_EQUAL( meshSpikeyCount( tp ), 0 );
1479 BOOST_CHECK( !meshHasEdge( tp, 0, 2 ) );
1480 BOOST_CHECK( meshHasEdge( tp, 1, 3 ) );
1481}
1482
1483
1484BOOST_AUTO_TEST_CASE( RefineLeavesDelaunayMeshUnchanged )
1485{
1486 // An already-Delaunay square triangulation must be a fixed point: no spurious flips that
1487 // would churn the mesh or, worse, oscillate.
1489 tp.AddVertex( VECTOR2I( 0, 0 ) );
1490 tp.AddVertex( VECTOR2I( 10000, 0 ) );
1491 tp.AddVertex( VECTOR2I( 10000, 10000 ) );
1492 tp.AddVertex( VECTOR2I( 0, 10000 ) );
1493 tp.AddTriangle( 0, 1, 2 );
1494 tp.AddTriangle( 0, 2, 3 );
1495
1496 double beforeArea = meshArea( tp );
1497
1498 tp.Refine();
1499
1500 BOOST_CHECK_EQUAL( tp.GetTriangleCount(), 2u );
1501 BOOST_CHECK_CLOSE( meshArea( tp ), beforeArea, 0.001 );
1502 BOOST_CHECK( meshHasEdge( tp, 0, 2 ) );
1503}
1504
1505
1506BOOST_AUTO_TEST_CASE( PredicatesHandleFullCoordinateRange )
1507{
1508 // The 4e9 nm width exceeds a 32-bit difference; a->b->c is counter-clockwise and must read so.
1509 VECTOR2I a( -2000000000, 0 );
1510 VECTOR2I b( 2000000000, 0 );
1511 VECTOR2I c( 2000000000, 1000 );
1512
1515
1516 // The same triangle is a needle; the sliver must still register at this span.
1517 BOOST_CHECK( KIGEOM::IsSliverTriangle( a, b, c ) );
1518}
1519
1520
1521BOOST_AUTO_TEST_CASE( RefinePreservesNonManifoldEdge )
1522{
1523 // Three triangles share edge 0-1, as a hole-bridge pinch produces. That non-manifold edge must
1524 // act as a boundary and never flip.
1526 tp.AddVertex( VECTOR2I( 0, 0 ) );
1527 tp.AddVertex( VECTOR2I( 100000, 0 ) );
1528 tp.AddVertex( VECTOR2I( 50000, 30000 ) );
1529 tp.AddVertex( VECTOR2I( 50000, -30000 ) );
1530 tp.AddVertex( VECTOR2I( 50000, 60000 ) );
1531 tp.AddTriangle( 0, 1, 2 );
1532 tp.AddTriangle( 0, 1, 3 );
1533 tp.AddTriangle( 0, 1, 4 );
1534
1535 std::vector<int> before;
1536
1537 for( const auto& tri : tp.Triangles() )
1538 {
1539 before.push_back( tri.a );
1540 before.push_back( tri.b );
1541 before.push_back( tri.c );
1542 }
1543
1544 tp.Refine();
1545
1546 std::vector<int> after;
1547
1548 for( const auto& tri : tp.Triangles() )
1549 {
1550 after.push_back( tri.a );
1551 after.push_back( tri.b );
1552 after.push_back( tri.c );
1553 }
1554
1555 BOOST_CHECK( before == after );
1556 BOOST_CHECK( meshHasEdge( tp, 0, 1 ) );
1557}
1558
1559
1560BOOST_AUTO_TEST_CASE( DecimateRemovesCollinearRun )
1561{
1562 // A square subdivided into many exactly-collinear points must triangulate down to the two
1563 // triangles the plain square produces; the subdivision points carry no geometry.
1565 const int size = 1000000;
1566 const int steps = 20;
1567
1568 for( int i = 0; i < steps; i++ )
1569 chain.Append( i * size / steps, 0 );
1570
1571 for( int i = 0; i < steps; i++ )
1572 chain.Append( size, i * size / steps );
1573
1574 for( int i = 0; i < steps; i++ )
1575 chain.Append( size - i * size / steps, size );
1576
1577 for( int i = 0; i < steps; i++ )
1578 chain.Append( 0, size - i * size / steps );
1579
1580 chain.SetClosed( true );
1581
1583 auto triangulator = fixture.CreateTriangulator();
1584 BOOST_REQUIRE( triangulator->TesselatePolygon( chain, nullptr ) );
1585
1586 BOOST_CHECK_EQUAL( fixture.GetResult().GetTriangleCount(), 2u );
1587 BOOST_CHECK_CLOSE( meshArea( fixture.GetResult() ), (double) size * size, 1e-6 );
1588}
1589
1590
1591BOOST_AUTO_TEST_CASE( DecimateKeepsVerticesBeyondBand )
1592{
1593 // Teeth of amplitude just past the simplification level must all survive. The teeth are
1594 // tiny and the valley deep, so the area budget alone would let every one go: only the band
1595 // test can hold them, which is what this pins.
1597 const int amp = TRIANGULATESIMPLIFICATIONLEVEL * 2;
1598 const int halfPitch = 25000;
1599 const int teeth = 10;
1600
1601 for( int i = 0; i <= teeth * 2; i++ )
1602 chain.Append( i * halfPitch, ( i % 2 ) ? amp : 0 );
1603
1604 chain.Append( teeth * 2 * halfPitch, -5000000 );
1605 chain.Append( 0, -5000000 );
1606 chain.SetClosed( true );
1607
1609 auto triangulator = fixture.CreateTriangulator();
1610 BOOST_REQUIRE( triangulator->TesselatePolygon( chain, nullptr ) );
1611
1613 static_cast<size_t>( chain.PointCount() ) - 2 );
1614}
1615
1616
1617BOOST_AUTO_TEST_CASE( DecimateKeepsFractureBridges )
1618{
1619 // Fracturing bakes the hole into the outline through a zero-width corridor whose feet
1620 // split an outline edge into exactly-collinear pieces. With the outline edges subdivided
1621 // there is a real run to collapse, so decimation runs on the fractured ring: it must fold
1622 // the subdivided edges away (triangle count near the plain four-corner result) yet never
1623 // cross the corridor and seal the hole (area preserved).
1624 const int size = 1000000;
1625 const int steps = 25;
1626 SHAPE_LINE_CHAIN outline;
1627
1628 for( int i = 0; i < steps; i++ )
1629 outline.Append( i * size / steps, 0 );
1630
1631 for( int i = 0; i < steps; i++ )
1632 outline.Append( size, i * size / steps );
1633
1634 for( int i = 0; i < steps; i++ )
1635 outline.Append( size - i * size / steps, size );
1636
1637 for( int i = 0; i < steps; i++ )
1638 outline.Append( 0, size - i * size / steps );
1639
1640 outline.SetClosed( true );
1641
1642 SHAPE_LINE_CHAIN hole;
1643 hole.Append( 400000, 400000 );
1644 hole.Append( 400000, 600000 );
1645 hole.Append( 600000, 600000 );
1646 hole.Append( 600000, 400000 );
1647 hole.SetClosed( true );
1648
1649 SHAPE_POLY_SET poly;
1650 poly.AddOutline( outline );
1651 poly.AddHole( hole );
1652
1653 const double area = poly.Area();
1654
1655 poly.Fracture();
1656 poly.CacheTriangulation( false );
1657
1658 double meshTotal = 0.0;
1659 size_t triangles = 0;
1660
1661 for( unsigned i = 0; i < poly.TriangulatedPolyCount(); i++ )
1662 {
1663 triangles += poly.TriangulatedPolygon( i )->GetTriangleCount();
1664
1665 for( const auto& tri : poly.TriangulatedPolygon( i )->Triangles() )
1666 meshTotal += tri.Area();
1667 }
1668
1669 BOOST_CHECK_CLOSE( meshTotal, area, 1e-6 );
1670
1671 // The 100 subdivision points carry no geometry; without decimation the fractured ring
1672 // keeps them all and the mesh is an order of magnitude larger.
1673 BOOST_CHECK_LT( triangles, 20u );
1674}
1675
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
SCOPED_TRACE_CAPTURE(const wxString &aMask)
bool Contains(const wxString &aText) 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
bool Contains(const wxString &aText) const
void DoLogTextAtLevel(wxLogLevel, const wxString &aText) override
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:574
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.
std::string GetTestDataRootDir()
STL namespace.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:437
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)
BOOST_AUTO_TEST_CASE(FractureEdgeIndexMatchesLinearScan)
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))
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:708