KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_zone_filler.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 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
25#include <boost/test/data/test_case.hpp>
26
27#include <chrono>
28
30#include <board.h>
31#include <board_commit.h>
33#include <drc/drc_engine.h>
34#include <pad.h>
35#include <pcb_track.h>
36#include <footprint.h>
37#include <zone.h>
38#include <drc/drc_engine.h>
39#include <drc/drc_item.h>
42#include <advanced_config.h>
44#include <teardrop/teardrop.h>
45
46
49static void CheckAllOutlineAreasAtLeast( const std::shared_ptr<SHAPE_POLY_SET>& aFill,
50 double aMinArea, const wxString& aLabel )
51{
52 for( int ii = 0; ii < aFill->OutlineCount(); ++ii )
53 {
54 const double area = std::abs( aFill->Outline( ii ).Area() );
55
56 BOOST_CHECK_MESSAGE( area >= aMinArea,
57 wxString::Format( "%s %d area %.0f IU^2 below %.0f IU^2; partial "
58 "stamps should not survive.",
59 aLabel, ii, area, aMinArea ) );
60 }
61}
62
63
72
73
74int delta = KiROUND( 0.006 * pcbIUScale.IU_PER_MM );
75
76
78{
79 KI_TEST::LoadBoard( m_settingsManager, "zone_filler", m_board );
80
81 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
82
83 KI_TEST::FillZones( m_board.get() );
84
85 // Now that the zones are filled we're going to increase the size of -some- pads and
86 // tracks so that they generate DRC errors. The test then makes sure that those errors
87 // are generated, and that the other pads and tracks do -not- generate errors.
88
89 for( PAD* pad : m_board->Footprints()[0]->Pads() )
90 {
91 if( pad->GetNumber() == "2" || pad->GetNumber() == "4" || pad->GetNumber() == "6" )
92 {
93 pad->SetSize( PADSTACK::ALL_LAYERS,
94 pad->GetSize( PADSTACK::ALL_LAYERS ) + VECTOR2I( delta, delta ) );
95 }
96 }
97
98 int ii = 0;
99 KIID arc8;
100 KIID arc12;
101
102 for( PCB_TRACK* track : m_board->Tracks() )
103 {
104 if( track->Type() == PCB_ARC_T )
105 {
106 ii++;
107
108 if( ii == 8 )
109 {
110 arc8 = track->m_Uuid;
111 track->SetWidth( track->GetWidth() + delta + delta );
112 }
113 else if( ii == 12 )
114 {
115 arc12 = track->m_Uuid;
116 track->Move( VECTOR2I( -delta, -delta ) );
117 }
118 }
119 }
120
121 bool foundPad2Error = false;
122 bool foundPad4Error = false;
123 bool foundPad6Error = false;
124 bool foundArc8Error = false;
125 bool foundArc12Error = false;
126 bool foundOtherError = false;
127
128 bds.m_DRCEngine->InitEngine( wxFileName() ); // Just to be sure to be sure
129
131 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
132 const std::function<void( PCB_MARKER* )>& aPathGenerator )
133 {
134 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
135 {
136 BOARD_ITEM* item_a = m_board->ResolveItem( aItem->GetMainItemID() );
137 PAD* pad_a = dynamic_cast<PAD*>( item_a );
138 PCB_TRACK* trk_a = dynamic_cast<PCB_TRACK*>( item_a );
139
140 BOARD_ITEM* item_b = m_board->ResolveItem( aItem->GetAuxItemID() );
141 PAD* pad_b = dynamic_cast<PAD*>( item_b );
142 PCB_TRACK* trk_b = dynamic_cast<PCB_TRACK*>( item_b );
143
144 if( pad_a && pad_a->GetNumber() == "2" ) foundPad2Error = true;
145 else if( pad_a && pad_a->GetNumber() == "4" ) foundPad4Error = true;
146 else if( pad_a && pad_a->GetNumber() == "6" ) foundPad6Error = true;
147 else if( pad_b && pad_b->GetNumber() == "2" ) foundPad2Error = true;
148 else if( pad_b && pad_b->GetNumber() == "4" ) foundPad4Error = true;
149 else if( pad_b && pad_b->GetNumber() == "6" ) foundPad6Error = true;
150 else if( trk_a && trk_a->m_Uuid == arc8 ) foundArc8Error = true;
151 else if( trk_a && trk_a->m_Uuid == arc12 ) foundArc12Error = true;
152 else if( trk_b && trk_b->m_Uuid == arc8 ) foundArc8Error = true;
153 else if( trk_b && trk_b->m_Uuid == arc12 ) foundArc12Error = true;
154 else foundOtherError = true;
155
156 }
157 } );
158
159 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
160
161 BOOST_CHECK_EQUAL( foundPad2Error, true );
162 BOOST_CHECK_EQUAL( foundPad4Error, true );
163 BOOST_CHECK_EQUAL( foundPad6Error, true );
164 BOOST_CHECK_EQUAL( foundArc8Error, true );
165 BOOST_CHECK_EQUAL( foundArc12Error, true );
166 BOOST_CHECK_EQUAL( foundOtherError, false );
167}
168
169
171{
172 KI_TEST::LoadBoard( m_settingsManager, "notched_zones", m_board );
173
174 // Older algorithms had trouble where the filleted zones intersected and left notches.
175 // See:
176 // https://gitlab.com/kicad/code/kicad/-/issues/2737
177 // https://gitlab.com/kicad/code/kicad/-/issues/2752
178 SHAPE_POLY_SET frontCopper;
179
180 KI_TEST::FillZones( m_board.get() );
181
182 frontCopper = SHAPE_POLY_SET();
183
184 for( ZONE* zone : m_board->Zones() )
185 {
186 if( zone->GetLayerSet().Contains( F_Cu ) )
187 {
188 frontCopper.BooleanAdd( *zone->GetFilledPolysList( F_Cu ) );
189 }
190 }
191
192 BOOST_CHECK_EQUAL( frontCopper.OutlineCount(), 2 );
193}
194
195
196static const std::vector<wxString> RegressionZoneFillTests_tests = {
197 "issue18",
198 "issue2568",
199 "issue3812",
200 "issue5102",
201 "issue5313",
202 "issue5320",
203 "issue5567",
204 "issue5830",
205 "issue6039",
206 "issue6260",
207 "issue6284",
208 "issue7086",
209 "issue14294", // Bad Clipper2 fill
210 "fill_bad" // Missing zone clearance expansion
211};
212
213
215 boost::unit_test::data::make( RegressionZoneFillTests_tests ), relPath )
216{
217 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
218
219 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
220
221 KI_TEST::FillZones( m_board.get() );
222
223 std::vector<DRC_ITEM> violations;
224
226 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
227 const std::function<void( PCB_MARKER* )>& aPathGenerator )
228 {
229 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
230 violations.push_back( *aItem );
231 } );
232
233 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
234
235 if( violations.empty() )
236 {
237 BOOST_CHECK_EQUAL( 1, 1 ); // quiet "did not check any assertions" warning
238 BOOST_TEST_MESSAGE( wxString::Format( "Zone fill regression: %s passed", relPath ) );
239 }
240 else
241 {
242 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::INCH );
243
244 std::map<KIID, EDA_ITEM*> itemMap;
245 m_board->FillItemMap( itemMap );
246
247 for( const DRC_ITEM& item : violations )
248 BOOST_TEST_MESSAGE( item.ShowReport( &unitsProvider, RPT_SEVERITY_ERROR, itemMap ) );
249
250 BOOST_ERROR( wxString::Format( "Zone fill regression: %s failed", relPath ) );
251 }
252}
253
254
264BOOST_FIXTURE_TEST_CASE( RegressionZoneClearanceWithIterativeRefill, ZONE_FILL_TEST_FIXTURE )
265{
266 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
267 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
268
269 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
270 guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
271
272 auto runDrcClearanceCheck =
273 [this]( bool aIterative ) -> int
274 {
275 ADVANCED_CFG& innerCfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
276 innerCfg.m_ZoneFillIterativeRefill = aIterative;
277
278 KI_TEST::LoadBoard( m_settingsManager, "issue23053/issue23053", m_board );
279
280 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
281
282 KI_TEST::FillZones( m_board.get() );
283
284 std::vector<DRC_ITEM> violations;
285
286 std::map<KIID, EDA_ITEM*> itemMap;
287 m_board->FillItemMap( itemMap );
288 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::MM );
289
291 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos,
292 int aLayer,
293 const std::function<void( PCB_MARKER* )>& aPathGenerator )
294 {
295 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
296 {
297 BOARD_ITEM* itemA = m_board->ResolveItem( aItem->GetMainItemID() );
298 BOARD_ITEM* itemB = m_board->ResolveItem( aItem->GetAuxItemID() );
299
300 if( dynamic_cast<ZONE*>( itemA ) && dynamic_cast<ZONE*>( itemB ) )
301 {
302 violations.push_back( *aItem );
303
305 aItem->ShowReport( &unitsProvider,
306 RPT_SEVERITY_ERROR, itemMap ) );
307 }
308 }
309 } );
310
311 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
312
313 return static_cast<int>( violations.size() );
314 };
315
316 int iterativeViolations = runDrcClearanceCheck( true );
317
318 BOOST_CHECK_MESSAGE( iterativeViolations == 0,
319 wxString::Format( "Iterative refill produced %d zone-to-zone clearance "
320 "violations (expected 0)", iterativeViolations ) );
321
322 int nonIterativeViolations = runDrcClearanceCheck( false );
323
324 BOOST_CHECK_MESSAGE( nonIterativeViolations == 0,
325 wxString::Format( "Non-iterative refill produced %d zone-to-zone clearance "
326 "violations (expected 0)", nonIterativeViolations ) );
327}
328
329
330static const std::vector<wxString> RegressionSliverZoneFillTests_tests = {
331 "issue16182" // Slivers
332};
333
334
335BOOST_DATA_TEST_CASE_F( ZONE_FILL_TEST_FIXTURE, RegressionSliverZoneFillTests,
336 boost::unit_test::data::make( RegressionSliverZoneFillTests_tests ),
337 relPath )
338{
339 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
340
341 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
342
343 KI_TEST::FillZones( m_board.get() );
344
345 std::vector<DRC_ITEM> violations;
346
348 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
349 const std::function<void( PCB_MARKER* )>& aPathGenerator )
350 {
351 if( aItem->GetErrorCode() == DRCE_COPPER_SLIVER )
352 violations.push_back( *aItem );
353 } );
354
355 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
356
357 if( violations.empty() )
358 {
359 BOOST_CHECK_EQUAL( 1, 1 ); // quiet "did not check any assertions" warning
360 BOOST_TEST_MESSAGE( wxString::Format( "Zone fill copper sliver regression: %s passed", relPath ) );
361 }
362 else
363 {
364 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::INCH );
365
366 std::map<KIID, EDA_ITEM*> itemMap;
367 m_board->FillItemMap( itemMap );
368
369 for( const DRC_ITEM& item : violations )
370 BOOST_TEST_MESSAGE( item.ShowReport( &unitsProvider, RPT_SEVERITY_ERROR, itemMap ) );
371
372 BOOST_ERROR( wxString::Format( "Zone fill copper sliver regression: %s failed", relPath ) );
373 }
374}
375
376
377static const std::vector<std::pair<wxString,int>> RegressionTeardropFill_tests = {
378 { "teardrop_issue_JPC2", 5 }, // Arcs with teardrops connecting to pads
379};
380
381
383 boost::unit_test::data::make( RegressionTeardropFill_tests ), test )
384{
385 const wxString& relPath = test.first;
386 const int count = test.second;
387
388 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
389
390 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
391
392 KI_TEST::FillZones( m_board.get() );
393
394 int zoneCount = 0;
395
396 for( ZONE* zone : m_board->Zones() )
397 {
398 if( zone->IsTeardropArea() )
399 zoneCount++;
400 }
401
402 BOOST_CHECK_MESSAGE( zoneCount == count, "Expected " << count << " teardrop zones in "
403 << relPath << ", found "
404 << zoneCount );
405}
406
407
409{
410
411 std::vector<wxString> tests = { { "issue19956/issue19956" } // Arcs with teardrops connecting to pads
412 };
413
414 for( const wxString& relPath : tests )
415 {
416 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
417 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
418 KI_TEST::FillZones( m_board.get() );
419
420 for( ZONE* zone : m_board->Zones() )
421 {
422 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
423 {
424 std::shared_ptr<SHAPE> a_shape( zone->GetEffectiveShape( layer ) );
425
426 for( PAD* pad : m_board->GetPads() )
427 {
428 std::shared_ptr<SHAPE> pad_shape( pad->GetEffectiveShape( layer ) );
429 int clearance = pad_shape->GetClearance( a_shape.get() );
430 BOOST_CHECK_MESSAGE( pad->GetNetCode() == zone->GetNetCode() || clearance != 0,
431 wxString::Format( "Pad %s from Footprint %s has net code %s and "
432 "is connected to zone with net code %s",
433 pad->GetNumber(),
434 pad->GetParentFootprint()->GetReferenceAsString(),
435 pad->GetNetname(),
436 zone->GetNetname() ) );
437 }
438 }
439 }
440 }
441}
442
443
458BOOST_FIXTURE_TEST_CASE( RegressionZonePriorityIsolatedIslands, ZONE_FILL_TEST_FIXTURE )
459{
460 // Enable iterative refill to fix issue 21746
461 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
462 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
463 cfg.m_ZoneFillIterativeRefill = true;
464
465 // Restore config at end of scope to avoid polluting other tests
466 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
467
468 KI_TEST::LoadBoard( m_settingsManager, "issue21746/issue21746", m_board );
469
470 KI_TEST::FillZones( m_board.get() );
471
472 // Find the GND zone
473 ZONE* gndZone = nullptr;
474
475 for( ZONE* zone : m_board->Zones() )
476 {
477 if( zone->GetNetname() == "GND" )
478 {
479 gndZone = zone;
480 break;
481 }
482 }
483
484 BOOST_REQUIRE_MESSAGE( gndZone != nullptr, "GND zone not found in test board" );
485
486 // Calculate board outline area
487 SHAPE_POLY_SET boardOutline;
488 bool hasOutline = m_board->GetBoardPolygonOutlines( boardOutline, true );
489 BOOST_REQUIRE_MESSAGE( hasOutline, "Board outline not found" );
490
491 double boardArea = 0.0;
492
493 for( int i = 0; i < boardOutline.OutlineCount(); i++ )
494 boardArea += boardOutline.Outline( i ).Area();
495
496 // Get GND zone filled area
497 gndZone->CalculateFilledArea();
498 double gndFilledArea = gndZone->GetFilledArea();
499
500 // The GND zone should fill at least 25% of the board area
501 // With the bug, it fills almost nothing because VDD knocks it out
502 double fillRatio = gndFilledArea / boardArea;
503
504 BOOST_TEST_MESSAGE( wxString::Format( "Board area: %.2f sq mm, GND filled area: %.2f sq mm, "
505 "Fill ratio: %.1f%%",
506 boardArea / 1e6, gndFilledArea / 1e6,
507 fillRatio * 100.0 ) );
508
509 BOOST_CHECK_MESSAGE( fillRatio >= 0.25,
510 wxString::Format( "GND zone fill ratio %.1f%% is less than expected 25%%. "
511 "This indicates issue 21746 - lower priority zones not "
512 "filling areas where higher priority isolated islands "
513 "were removed.",
514 fillRatio * 100.0 ) );
515}
516
517
531BOOST_FIXTURE_TEST_CASE( RegressionViaFlashingUnreachableZone, ZONE_FILL_TEST_FIXTURE )
532{
533 KI_TEST::LoadBoard( m_settingsManager, "issue22010/issue22010", m_board );
534
535 KI_TEST::FillZones( m_board.get() );
536
537 // Find vias with zone_layer_connections set for In1.Cu or In2.Cu
538 // After filling, vias that the zone doesn't actually reach should NOT be flashed
539 int viasWithUnreachableFlashing = 0;
540 int totalConditionalVias = 0;
541
542 PCB_LAYER_ID in1Cu = m_board->GetLayerID( wxT( "In1.Cu" ) );
543 PCB_LAYER_ID in2Cu = m_board->GetLayerID( wxT( "In2.Cu" ) );
544
545 for( PCB_TRACK* track : m_board->Tracks() )
546 {
547 if( track->Type() != PCB_VIA_T )
548 continue;
549
550 PCB_VIA* via = static_cast<PCB_VIA*>( track );
551
552 if( !via->GetRemoveUnconnected() )
553 continue;
554
555 totalConditionalVias++;
556
557 // Check if via is flashed on In1.Cu or In2.Cu
558 bool flashedOnIn1 = via->FlashLayer( in1Cu );
559 bool flashedOnIn2 = via->FlashLayer( in2Cu );
560
561 if( !flashedOnIn1 && !flashedOnIn2 )
562 continue;
563
564 VECTOR2I viaCenter = via->GetPosition();
565 int holeRadius = via->GetDrillValue() / 2;
566
567 // Check if any zone fill actually reaches this via
568 bool zoneReachesVia = false;
569
570 for( ZONE* zone : m_board->Zones() )
571 {
572 if( zone->GetIsRuleArea() )
573 continue;
574
575 if( zone->GetNetCode() != via->GetNetCode() )
576 continue;
577
578 for( PCB_LAYER_ID layer : { in1Cu, in2Cu } )
579 {
580 if( !zone->IsOnLayer( layer ) )
581 continue;
582
583 if( !zone->HasFilledPolysForLayer( layer ) )
584 continue;
585
586 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
587
588 if( fill->Contains( viaCenter, -1, holeRadius ) )
589 {
590 zoneReachesVia = true;
591 break;
592 }
593 }
594
595 if( zoneReachesVia )
596 break;
597 }
598
599 // If via is flashed but zone doesn't reach it, that's the bug
600 if( !zoneReachesVia && ( flashedOnIn1 || flashedOnIn2 ) )
601 viasWithUnreachableFlashing++;
602 }
603
604 BOOST_TEST_MESSAGE( wxString::Format( "Total conditional vias: %d, Vias with unreachable "
605 "flashing: %d", totalConditionalVias,
606 viasWithUnreachableFlashing ) );
607
608 BOOST_CHECK_MESSAGE( viasWithUnreachableFlashing == 0,
609 wxString::Format( "Found %d vias flashed on zone layers where the zone "
610 "fill doesn't actually reach them. This indicates "
611 "issue 22010 is not fixed.",
612 viasWithUnreachableFlashing ) );
613}
614
615
629{
630 KI_TEST::LoadBoard( m_settingsManager, "issue12964/issue12964", m_board );
631
632 KI_TEST::FillZones( m_board.get() );
633
634 int viasShortingZones = 0;
635 int totalConditionalVias = 0;
636
637 for( PCB_TRACK* track : m_board->Tracks() )
638 {
639 if( track->Type() != PCB_VIA_T )
640 continue;
641
642 PCB_VIA* via = static_cast<PCB_VIA*>( track );
643
644 if( !via->GetRemoveUnconnected() )
645 continue;
646
647 totalConditionalVias++;
648
649 VECTOR2I viaCenter = via->GetPosition();
650
651 for( ZONE* zone : m_board->Zones() )
652 {
653 if( zone->GetIsRuleArea() )
654 continue;
655
656 if( zone->GetNetCode() == via->GetNetCode() )
657 continue;
658
659 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
660 {
661 if( !via->FlashLayer( layer ) )
662 continue;
663
664 if( !zone->HasFilledPolysForLayer( layer ) )
665 continue;
666
667 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
668 int viaRadius = via->GetWidth( layer ) / 2;
669
670 if( fill->Contains( viaCenter, -1, viaRadius ) )
671 {
672 BOOST_TEST_MESSAGE( wxString::Format(
673 "Via at (%d, %d) on net %s is flashing on layer %s where zone "
674 "net %s is filled - this creates a short!",
675 viaCenter.x, viaCenter.y, via->GetNetname(),
676 m_board->GetLayerName( layer ), zone->GetNetname() ) );
677 viasShortingZones++;
678 }
679 }
680 }
681 }
682
683 BOOST_TEST_MESSAGE( wxString::Format( "Total conditional vias: %d, Vias shorting zones: %d",
684 totalConditionalVias, viasShortingZones ) );
685
686 BOOST_CHECK_MESSAGE( viasShortingZones == 0,
687 wxString::Format( "Found %d vias flashed on layers where they short to "
688 "zones with different nets. This indicates issue 12964 "
689 "is not fixed.",
690 viasShortingZones ) );
691}
692
693
706BOOST_FIXTURE_TEST_CASE( HatchZoneThermalConnectivity, ZONE_FILL_TEST_FIXTURE )
707{
708 KI_TEST::LoadBoard( m_settingsManager, "hatch_thermal_connectivity/hatch_thermal_connectivity",
709 m_board );
710
711 KI_TEST::FillZones( m_board.get() );
712
713 m_board->BuildConnectivity();
714
715 int unconnectedCount = m_board->GetConnectivity()->GetUnconnectedCount( false );
716
717 BOOST_CHECK_MESSAGE( unconnectedCount == 0,
718 wxString::Format( "Found %d unconnected items after zone fill. "
719 "Hatch zone thermal reliefs should maintain connectivity "
720 "even with large hatch gaps.",
721 unconnectedCount ) );
722}
723
724
741BOOST_FIXTURE_TEST_CASE( RegressionShallowArcZoneFill, ZONE_FILL_TEST_FIXTURE )
742{
743 KI_TEST::LoadBoard( m_settingsManager, "issue22475/issue22475", m_board );
744
745 PCB_LAYER_ID in1Cu = m_board->GetLayerID( wxT( "In1.Cu" ) );
746
747 ZONE* gndZone = nullptr;
748
749 for( ZONE* zone : m_board->Zones() )
750 {
751 if( zone->GetNetname() == "GND" && zone->IsOnLayer( in1Cu ) )
752 {
753 gndZone = zone;
754 break;
755 }
756 }
757
758 BOOST_REQUIRE_MESSAGE( gndZone != nullptr, "GND zone on In1.Cu not found in test board" );
759
760 if( !gndZone )
761 return;
762
763 KI_TEST::FillZones( m_board.get() );
764
765 BOOST_REQUIRE_MESSAGE( gndZone->HasFilledPolysForLayer( in1Cu ),
766 "GND zone has no fill on In1.Cu" );
767
768 const std::shared_ptr<SHAPE_POLY_SET>& fill = gndZone->GetFilledPolysList( in1Cu );
769
770 // The zone fill should produce a single contiguous outline. Multiple outlines
771 // indicate disconnected fill areas caused by malformed clearance holes.
772 BOOST_CHECK_EQUAL( fill->OutlineCount(), 1 );
773
774 double zoneOutlineArea = gndZone->Outline()->Area();
775
776 BOOST_REQUIRE_MESSAGE( zoneOutlineArea > 0.0, "Zone outline area must be positive" );
777
778 double fillArea = 0.0;
779
780 for( int i = 0; i < fill->OutlineCount(); i++ )
781 fillArea += std::abs( fill->Outline( i ).Area() );
782
783 double fillRatio = fillArea / zoneOutlineArea;
784
785 // The zone should be mostly filled. A low fill ratio indicates excessive voids
786 // from malformed clearance holes around shallow arcs.
787 BOOST_CHECK_GE( fillRatio, 0.90 );
788}
789
790
803BOOST_FIXTURE_TEST_CASE( RegressionIterativeRefillRespectsKeepouts, ZONE_FILL_TEST_FIXTURE )
804{
805 // Enable iterative refill
806 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
807 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
808 cfg.m_ZoneFillIterativeRefill = true;
809
810 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
811 guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
812
813 KI_TEST::LoadBoard( m_settingsManager, "issue22809/issue22809", m_board );
814
815 KI_TEST::FillZones( m_board.get() );
816
817 // Find all zone keepouts
818 std::vector<ZONE*> keepouts;
819
820 for( ZONE* zone : m_board->Zones() )
821 {
822 if( zone->GetIsRuleArea() && zone->GetDoNotAllowZoneFills() )
823 keepouts.push_back( zone );
824 }
825
826 BOOST_REQUIRE_MESSAGE( !keepouts.empty(), "No zone keepouts found in test board" );
827
828 // For each keepout, check that no zone fill exists inside it
829 int violationCount = 0;
830
831 for( ZONE* keepout : keepouts )
832 {
833 for( PCB_LAYER_ID layer : keepout->GetLayerSet().Seq() )
834 {
835 SHAPE_POLY_SET keepoutOutline( *keepout->Outline() );
836 keepoutOutline.ClearArcs();
837
838 for( ZONE* zone : m_board->Zones() )
839 {
840 if( zone->GetIsRuleArea() )
841 continue;
842
843 if( !zone->IsOnLayer( layer ) )
844 continue;
845
846 if( !zone->HasFilledPolysForLayer( layer ) )
847 continue;
848
849 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
850
851 // Check if any fill intersects the keepout
852 SHAPE_POLY_SET intersection = *fill;
853 intersection.BooleanIntersection( keepoutOutline );
854
855 if( intersection.OutlineCount() > 0 )
856 {
857 double intersectionArea = 0;
858
859 for( int i = 0; i < intersection.OutlineCount(); i++ )
860 intersectionArea += std::abs( intersection.Outline( i ).Area() );
861
862 // Allow for small numerical errors (less than 1 square mm)
863 if( intersectionArea > 1e6 )
864 {
865 BOOST_TEST_MESSAGE( wxString::Format(
866 "Zone %s fill on layer %s overlaps keepout by %.2f sq mm",
867 zone->GetNetname(),
868 m_board->GetLayerName( layer ),
869 intersectionArea / 1e6 ) );
870 violationCount++;
871 }
872 }
873 }
874 }
875 }
876
877 BOOST_CHECK_MESSAGE( violationCount == 0,
878 wxString::Format( "Found %d zone fills overlapping keepout areas. "
879 "This indicates issue 22809 - iterative refiller "
880 "ignores zone keepouts.", violationCount ) );
881}
882
883
897BOOST_FIXTURE_TEST_CASE( RegressionTHPadInnerLayerFlashing, ZONE_FILL_TEST_FIXTURE )
898{
899 KI_TEST::LoadBoard( m_settingsManager, "issue22826/issue22826", m_board );
900
901 KI_TEST::FillZones( m_board.get() );
902
903 PCB_LAYER_ID in2Cu = m_board->GetLayerID( wxT( "In2.Cu" ) );
904 int padsWithMissingFlashing = 0;
905 int totalConditionalPads = 0;
906
907 for( FOOTPRINT* footprint : m_board->Footprints() )
908 {
909 for( PAD* pad : footprint->Pads() )
910 {
911 if( !pad->GetRemoveUnconnected() )
912 continue;
913
914 if( !pad->HasHole() )
915 continue;
916
917 if( pad->GetNetname() != "VBUS_DUT" && pad->GetNetname() != "VBUS_DBG" )
918 continue;
919
920 totalConditionalPads++;
921
922 // Check if the pad should flash on In2.Cu
923 bool shouldFlash = false;
924
925 for( ZONE* zone : m_board->Zones() )
926 {
927 if( zone->GetIsRuleArea() )
928 continue;
929
930 if( zone->GetNetCode() != pad->GetNetCode() )
931 continue;
932
933 if( !zone->IsOnLayer( in2Cu ) )
934 continue;
935
936 if( zone->Outline()->Contains( pad->GetPosition() ) )
937 {
938 shouldFlash = true;
939 break;
940 }
941 }
942
943 if( shouldFlash && !pad->FlashLayer( in2Cu ) )
944 {
945 BOOST_TEST_MESSAGE( wxString::Format(
946 "Pad %s at (%d, %d) on net %s is inside zone but not flashing on In2.Cu",
947 pad->GetNumber(), pad->GetPosition().x, pad->GetPosition().y,
948 pad->GetNetname() ) );
949 padsWithMissingFlashing++;
950 }
951 }
952 }
953
954 BOOST_TEST_MESSAGE( wxString::Format( "Total conditional pads: %d, Pads with missing "
955 "flashing: %d", totalConditionalPads,
956 padsWithMissingFlashing ) );
957
958 BOOST_CHECK_MESSAGE( padsWithMissingFlashing == 0,
959 wxString::Format( "Found %d TH pads that should flash on inner layers "
960 "but don't. This indicates issue 22826 is not fixed.",
961 padsWithMissingFlashing ) );
962}
963
964
973BOOST_FIXTURE_TEST_CASE( RegressionRoundRectTeardropGeometry, ZONE_FILL_TEST_FIXTURE )
974{
975 KI_TEST::LoadBoard( m_settingsManager, "issue19405_roundrect_teardrop", m_board );
976
977 // Set up tool manager for teardrop generation
978 TOOL_MANAGER toolMgr;
979 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
980
981 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
982 toolMgr.RegisterTool( dummyTool );
983
984 // Generate teardrops
985 BOARD_COMMIT commit( dummyTool );
986 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
987 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
988
989 if( !commit.Empty() )
990 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
991
992 // Find teardrop zones
993 int teardropCount = 0;
994 bool foundBadTeardrop = false;
995
996 for( ZONE* zone : m_board->Zones() )
997 {
998 if( !zone->IsTeardropArea() )
999 continue;
1000
1001 teardropCount++;
1002
1003 // Get the teardrop outline
1004 const SHAPE_POLY_SET* outline = zone->Outline();
1005
1006 if( !outline || outline->OutlineCount() == 0 )
1007 continue;
1008
1009 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1010
1011 // Check that the teardrop polygon is convex or at least doesn't have
1012 // any sharp concave angles that would indicate intersection with the pad corner.
1013 // A well-formed teardrop should have all turns in the same direction
1014 // (or very close to it) except at the pad anchor points.
1015 int concaveCount = 0;
1016
1017 for( int i = 0; i < chain.PointCount(); i++ )
1018 {
1019 int prev = ( i == 0 ) ? chain.PointCount() - 1 : i - 1;
1020 int next = ( i + 1 ) % chain.PointCount();
1021
1022 VECTOR2I v1 = chain.CPoint( i ) - chain.CPoint( prev );
1023 VECTOR2I v2 = chain.CPoint( next ) - chain.CPoint( i );
1024
1025 // Cross product gives handedness of turn
1026 int64_t cross = (int64_t) v1.x * v2.y - (int64_t) v1.y * v2.x;
1027
1028 // Count significant concave turns (negative cross product for CCW polygons)
1029 // Small values are numerical noise
1030 if( cross < -1000 )
1031 concaveCount++;
1032 }
1033
1034 // A teardrop should have at most 2-3 concave points (at the pad anchor points)
1035 // Many concave points indicate the curve is intersecting the pad corner
1036 if( concaveCount > 5 )
1037 {
1038 BOOST_TEST_MESSAGE( wxString::Format( "Teardrop has %d concave vertices, "
1039 "indicating possible corner intersection",
1040 concaveCount ) );
1041 foundBadTeardrop = true;
1042 }
1043 }
1044
1045 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1046
1047 BOOST_CHECK_MESSAGE( !foundBadTeardrop,
1048 "Found teardrop with excessive concave vertices, indicating "
1049 "issue 19405 - teardrop curve intersecting rounded rectangle corner" );
1050}
1051
1052
1059{
1060 KI_TEST::LoadBoard( m_settingsManager, "teardrop_spike", m_board );
1061
1062 TOOL_MANAGER toolMgr;
1063 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1064
1065 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1066 toolMgr.RegisterTool( dummyTool );
1067
1068 BOARD_COMMIT commit( dummyTool );
1069 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1070 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1071
1072 if( !commit.Empty() )
1073 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1074
1075 int teardropCount = 0;
1076 bool foundSpike = false;
1077
1078 const int maxError = m_board->GetDesignSettings().m_MaxError;
1079
1080 for( ZONE* zone : m_board->Zones() )
1081 {
1082 if( !zone->IsTeardropArea() )
1083 continue;
1084
1085 teardropCount++;
1086
1087 PCB_LAYER_ID layer = zone->GetFirstLayer();
1088 int netcode = zone->GetNetCode();
1089
1090 // A well-formed teardrop only ever covers the copper it bridges: the pads/vias it
1091 // anchors on and the track(s) it follows. Build that corridor from all copper on the
1092 // teardrop's net and layer (generously inflated) and require the teardrop to lie
1093 // inside it. A spike sweeps area outside the corridor.
1094 SHAPE_POLY_SET corridor;
1095
1096 for( FOOTPRINT* fp : m_board->Footprints() )
1097 {
1098 for( PAD* pad : fp->Pads() )
1099 {
1100 if( pad->GetNetCode() == netcode && pad->IsOnLayer( layer ) )
1101 pad->TransformShapeToPolygon( corridor, layer, 0, maxError, ERROR_OUTSIDE );
1102 }
1103 }
1104
1105 for( PCB_TRACK* track : m_board->Tracks() )
1106 {
1107 if( track->GetNetCode() == netcode && track->IsOnLayer( layer ) )
1108 track->TransformShapeToPolygon( corridor, layer, 0, maxError, ERROR_OUTSIDE );
1109 }
1110
1111 // Inflate by a full track width so the teardrop's flare toward the pad, which is
1112 // legitimately wider than the bare track, is comfortably inside the corridor.
1113 corridor.Inflate( pcbIUScale.mmToIU( 0.127 ), CORNER_STRATEGY::ROUND_ALL_CORNERS,
1114 maxError );
1115 corridor.Simplify();
1116
1117 SHAPE_POLY_SET outside = *zone->Outline();
1118 outside.BooleanSubtract( corridor );
1119
1120 double tdArea = std::abs( zone->Outline()->Area() );
1121 double outArea = std::abs( outside.Area() );
1122 double ratio = tdArea > 0 ? outArea / tdArea : 0.0;
1123
1124 BOOST_TEST_MESSAGE( wxString::Format(
1125 "Teardrop on layer %d: area %.0f, area outside corridor %.0f (%.1f%%)",
1126 (int) layer, tdArea, outArea, ratio * 100.0 ) );
1127
1128 if( ratio > 0.02 )
1129 {
1130 foundSpike = true;
1131 BOOST_TEST_MESSAGE( wxString::Format(
1132 "Teardrop on layer %d sweeps %.1f%% of its area outside the track/pad "
1133 "corridor (spike)",
1134 (int) layer, ratio * 100.0 ) );
1135 }
1136 }
1137
1138 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1139 BOOST_CHECK_MESSAGE( !foundSpike,
1140 "A teardrop vertex spikes outside the track/pad corridor it should "
1141 "follow" );
1142}
1143
1144
1153{
1154 KI_TEST::LoadBoard( m_settingsManager, "oval_teardrop", m_board );
1155
1156 // Set up tool manager for teardrop generation
1157 TOOL_MANAGER toolMgr;
1158 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1159
1160 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1161 toolMgr.RegisterTool( dummyTool );
1162
1163 // Generate teardrops
1164 BOARD_COMMIT commit( dummyTool );
1165 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1166 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1167
1168 if( !commit.Empty() )
1169 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1170
1171 // Find teardrop zones
1172 int teardropCount = 0;
1173 bool foundBadTeardrop = false;
1174
1175 for( ZONE* zone : m_board->Zones() )
1176 {
1177 if( !zone->IsTeardropArea() )
1178 continue;
1179
1180 teardropCount++;
1181
1182 const SHAPE_POLY_SET* outline = zone->Outline();
1183
1184 if( !outline || outline->OutlineCount() == 0 )
1185 continue;
1186
1187 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1188
1189 // Check for excessive concave vertices that would indicate the teardrop curve
1190 // is not tangent to the oval's semicircular end
1191 int concaveCount = 0;
1192
1193 for( int i = 0; i < chain.PointCount(); i++ )
1194 {
1195 int prev = ( i == 0 ) ? chain.PointCount() - 1 : i - 1;
1196 int next = ( i + 1 ) % chain.PointCount();
1197
1198 VECTOR2I v1 = chain.CPoint( i ) - chain.CPoint( prev );
1199 VECTOR2I v2 = chain.CPoint( next ) - chain.CPoint( i );
1200
1201 int64_t cross = (int64_t) v1.x * v2.y - (int64_t) v1.y * v2.x;
1202
1203 if( cross < -1000 )
1204 concaveCount++;
1205 }
1206
1207 if( concaveCount > 5 )
1208 {
1209 BOOST_TEST_MESSAGE( wxString::Format( "Oval teardrop has %d concave vertices",
1210 concaveCount ) );
1211 foundBadTeardrop = true;
1212 }
1213 }
1214
1215 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1216
1217 BOOST_CHECK_MESSAGE( !foundBadTeardrop,
1218 "Found teardrop with excessive concave vertices on oval pad, "
1219 "indicating curve is not tangent to semicircular end" );
1220}
1221
1222
1231{
1232 KI_TEST::LoadBoard( m_settingsManager, "large_circle_teardrop", m_board );
1233
1234 // Set up tool manager for teardrop generation
1235 TOOL_MANAGER toolMgr;
1236 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1237
1238 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1239 toolMgr.RegisterTool( dummyTool );
1240
1241 // Generate teardrops
1242 BOARD_COMMIT commit( dummyTool );
1243 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1244 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1245
1246 if( !commit.Empty() )
1247 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1248
1249 // Find the pad and its teardrop
1250 PAD* largePad = nullptr;
1251
1252 for( FOOTPRINT* fp : m_board->Footprints() )
1253 {
1254 for( PAD* pad : fp->Pads() )
1255 {
1256 if( pad->GetShape( F_Cu ) == PAD_SHAPE::CIRCLE )
1257 {
1258 largePad = pad;
1259 break;
1260 }
1261 }
1262 }
1263
1264 BOOST_REQUIRE_MESSAGE( largePad != nullptr, "Expected a circular pad in test board" );
1265
1266 int padRadius = largePad->GetSize( F_Cu ).x / 2;
1267 VECTOR2I padCenter = largePad->GetPosition();
1268
1269 // Find teardrop zones
1270 int teardropCount = 0;
1271 bool foundBadTeardrop = false;
1272
1273 for( ZONE* zone : m_board->Zones() )
1274 {
1275 if( !zone->IsTeardropArea() )
1276 continue;
1277
1278 teardropCount++;
1279
1280 const SHAPE_POLY_SET* outline = zone->Outline();
1281
1282 if( !outline || outline->OutlineCount() == 0 )
1283 continue;
1284
1285 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1286
1287 // Check for excessive concave vertices
1288 int concaveCount = 0;
1289
1290 for( int i = 0; i < chain.PointCount(); i++ )
1291 {
1292 int prev = ( i == 0 ) ? chain.PointCount() - 1 : i - 1;
1293 int next = ( i + 1 ) % chain.PointCount();
1294
1295 VECTOR2I v1 = chain.CPoint( i ) - chain.CPoint( prev );
1296 VECTOR2I v2 = chain.CPoint( next ) - chain.CPoint( i );
1297
1298 int64_t cross = (int64_t) v1.x * v2.y - (int64_t) v1.y * v2.x;
1299
1300 if( cross < -1000 )
1301 concaveCount++;
1302 }
1303
1304 if( concaveCount > 5 )
1305 {
1306 BOOST_TEST_MESSAGE( wxString::Format( "Large circle teardrop has %d concave vertices",
1307 concaveCount ) );
1308 foundBadTeardrop = true;
1309 }
1310
1311 // Also verify that the teardrop anchor points near the pad are approximately
1312 // on the circle edge (within tolerance)
1313 int maxError = m_board->GetDesignSettings().m_MaxError;
1314
1315 for( int i = 0; i < chain.PointCount(); i++ )
1316 {
1317 VECTOR2I pt = chain.CPoint( i );
1318 double dist = ( pt - padCenter ).EuclideanNorm();
1319
1320 // Points that are close to the circle should be approximately on it
1321 if( dist > padRadius * 0.5 && dist < padRadius * 1.5 )
1322 {
1323 double deviation = std::abs( dist - padRadius );
1324
1325 // Allow some tolerance for polygon approximation
1326 if( deviation > maxError * 5 && deviation < padRadius * 0.2 )
1327 {
1328 BOOST_TEST_MESSAGE( wxString::Format(
1329 "Teardrop point at distance %.2f from pad center (radius %.2f), "
1330 "deviation %.2f exceeds tolerance",
1331 dist / 1000.0, padRadius / 1000.0, deviation / 1000.0 ) );
1332 }
1333 }
1334 }
1335 }
1336
1337 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1338
1339 BOOST_CHECK_MESSAGE( !foundBadTeardrop,
1340 "Found teardrop with excessive concave vertices on large circle, "
1341 "indicating anchor points may not be on circle edge" );
1342}
1343
1344
1355BOOST_FIXTURE_TEST_CASE( RegressionCoincidentPadClearance, ZONE_FILL_TEST_FIXTURE )
1356{
1357 KI_TEST::LoadBoard( m_settingsManager, "issue23123_minimal", m_board );
1358
1359 KI_TEST::FillZones( m_board.get() );
1360
1361 // After filling, every pad whose net differs from the zone must have clearance.
1362 // Check each zone/pad combination on each shared layer.
1363 int violations = 0;
1364
1365 for( ZONE* zone : m_board->Zones() )
1366 {
1367 if( zone->GetIsRuleArea() )
1368 continue;
1369
1370 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1371 {
1372 if( !zone->HasFilledPolysForLayer( layer ) )
1373 continue;
1374
1375 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
1376
1377 for( PAD* pad : m_board->GetPads() )
1378 {
1379 if( !pad->IsOnLayer( layer ) )
1380 continue;
1381
1382 if( pad->GetNetCode() == zone->GetNetCode() )
1383 continue;
1384
1385 std::shared_ptr<SHAPE> padShape = pad->GetEffectiveShape( layer );
1386 int clearance = padShape->GetClearance( fill.get() );
1387
1388 if( clearance < 1 )
1389 {
1390 BOOST_TEST_MESSAGE( wxString::Format(
1391 "Pad %s (net %s) at (%d, %d) has zero clearance to zone %s "
1392 "on layer %s",
1393 pad->GetNumber(), pad->GetNetname(),
1394 pad->GetPosition().x, pad->GetPosition().y,
1395 zone->GetNetname(), m_board->GetLayerName( layer ) ) );
1396 violations++;
1397 }
1398 }
1399 }
1400 }
1401
1402 BOOST_CHECK_MESSAGE( violations == 0,
1403 wxString::Format( "Found %d pads with missing zone clearance. "
1404 "Coincident pads with different nets must not be "
1405 "deduplicated in zone fill knockout.",
1406 violations ) );
1407}
1408
1409
1420{
1421 KI_TEST::LoadBoard( m_settingsManager, "connect/connect", m_board );
1422
1423 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1424
1425 KI_TEST::FillZones( m_board.get() );
1426
1427 std::vector<DRC_ITEM> violations;
1428
1429 bds.m_DRCEngine->InitEngine( wxFileName() );
1430
1432 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
1433 const std::function<void( PCB_MARKER* )>& aPathGenerator )
1434 {
1435 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
1436 {
1437 BOARD_ITEM* item_a = m_board->ResolveItem( aItem->GetMainItemID() );
1438 BOARD_ITEM* item_b = m_board->ResolveItem( aItem->GetAuxItemID() );
1439
1440 ZONE* zone_a = dynamic_cast<ZONE*>( item_a );
1441 ZONE* zone_b = dynamic_cast<ZONE*>( item_b );
1442
1443 if( zone_a || zone_b )
1444 violations.push_back( *aItem );
1445 }
1446 } );
1447
1448 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
1449
1450 BOOST_CHECK_EQUAL( violations.size(), 0 );
1451}
1452
1453
1465{
1466 KI_TEST::LoadBoard( m_settingsManager, "issue23339_zone_layer_rules", m_board );
1467
1468 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1469
1470 // First verify that EvalRules returns the correct clearance per layer
1471 ZONE* hvZone = nullptr;
1472 ZONE* lvZone = nullptr;
1473
1474 for( ZONE* zone : m_board->Zones() )
1475 {
1476 if( zone->GetNetname() == "HV_NET" )
1477 hvZone = zone;
1478 else if( zone->GetNetname() == "LV_NET" )
1479 lvZone = zone;
1480 }
1481
1482 BOOST_REQUIRE( hvZone );
1483 BOOST_REQUIRE( lvZone );
1484
1485 // Outer layer rule should give 4.6mm clearance on F.Cu
1487 hvZone, lvZone, F_Cu );
1488
1489 BOOST_TEST_MESSAGE( "F.Cu clearance: " << outerConstraint.GetValue().Min()
1490 << " (expected " << pcbIUScale.mmToIU( 4.6 ) << ")" );
1491 BOOST_CHECK_EQUAL( outerConstraint.GetValue().Min(), pcbIUScale.mmToIU( 4.6 ) );
1492
1493 // Inner layer rule should give 2.3mm clearance on In1.Cu
1495 hvZone, lvZone, In1_Cu );
1496
1497 BOOST_TEST_MESSAGE( "In1.Cu clearance: " << innerConstraint.GetValue().Min()
1498 << " (expected " << pcbIUScale.mmToIU( 2.3 ) << ")" );
1499 BOOST_CHECK_EQUAL( innerConstraint.GetValue().Min(), pcbIUScale.mmToIU( 2.3 ) );
1500
1501 // Now fill zones and check that fills actually respect the clearances
1502 KI_TEST::FillZones( m_board.get() );
1503
1504 // Run DRC and verify no clearance violations between zones
1505 std::vector<DRC_ITEM> violations;
1506
1507 bds.m_DRCEngine->InitEngine( wxFileName() );
1508
1510 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
1511 const std::function<void( PCB_MARKER* )>& aPathGenerator )
1512 {
1513 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
1514 {
1515 BOARD_ITEM* item_a = m_board->ResolveItem( aItem->GetMainItemID() );
1516 BOARD_ITEM* item_b = m_board->ResolveItem( aItem->GetAuxItemID() );
1517
1518 ZONE* zone_a = dynamic_cast<ZONE*>( item_a );
1519 ZONE* zone_b = dynamic_cast<ZONE*>( item_b );
1520
1521 if( zone_a && zone_b )
1522 {
1523 BOOST_TEST_MESSAGE( "Zone-to-zone clearance violation on layer "
1524 << aLayer << ": " << aItem->GetErrorMessage( true ) );
1525 violations.push_back( *aItem );
1526 }
1527 }
1528 } );
1529
1530 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
1531
1532 BOOST_CHECK_EQUAL( violations.size(), 0 );
1533}
1534
1535
1536BOOST_FIXTURE_TEST_CASE( RegressionZoneFillMinWidthAfterKnockout, ZONE_FILL_TEST_FIXTURE )
1537{
1538 KI_TEST::LoadBoard( m_settingsManager, "issue23332_min_width/issue23332_min_width", m_board );
1539
1540 KI_TEST::FillZones( m_board.get() );
1541
1542 int epsilon = pcbIUScale.mmToIU( 0.001 );
1543
1544 for( ZONE* zone : m_board->Zones() )
1545 {
1546 int half_min_width = zone->GetMinThickness() / 2;
1547
1548 if( half_min_width - epsilon <= epsilon )
1549 continue;
1550
1551 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1552 {
1553 if( !zone->HasFilledPolysForLayer( layer ) )
1554 continue;
1555
1556 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1557
1558 if( !fill || fill->OutlineCount() == 0 )
1559 continue;
1560
1561 // Check each filled island individually so that a tiny thin sliver
1562 // isn't masked by a large zone's total area
1563 for( int ii = 0; ii < fill->OutlineCount(); ii++ )
1564 {
1565 SHAPE_POLY_SET island;
1566 island.AddOutline( fill->Outline( ii ) );
1567
1568 for( int jj = 0; jj < fill->HoleCount( ii ); jj++ )
1569 island.AddHole( fill->Hole( ii, jj ) );
1570
1571 double originalArea = island.Area();
1572
1573 if( originalArea <= 0 )
1574 continue;
1575
1577
1578 test.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS,
1579 ARC_HIGH_DEF );
1580
1581 test.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS,
1582 ARC_HIGH_DEF, true );
1583
1584 double prunedArea = test.Area();
1585 double areaLoss = ( originalArea - prunedArea ) / originalArea;
1586
1587 BOOST_TEST_MESSAGE( wxString::Format(
1588 "Zone %s layer %d island %d: area=%.0f, loss=%.4f%%",
1589 zone->GetNetname(), static_cast<int>( layer ), ii,
1590 originalArea, areaLoss * 100.0 ) );
1591
1592 BOOST_CHECK_MESSAGE( areaLoss < 0.01,
1593 wxString::Format(
1594 "Zone %s layer %d island %d lost %.2f%% area from "
1595 "min-width pruning (min_width=%.3fmm)",
1596 zone->GetNetname(), static_cast<int>( layer ), ii,
1597 areaLoss * 100.0,
1598 zone->GetMinThickness()
1599 / static_cast<double>( pcbIUScale.IU_PER_MM ) ) );
1600 }
1601 }
1602 }
1603}
1604
1605
1606BOOST_FIXTURE_TEST_CASE( RegressionSameNetOverlappingZones, ZONE_FILL_TEST_FIXTURE )
1607{
1608 KI_TEST::LoadBoard( m_settingsManager, "issue23418/testing", m_board );
1609
1610 KI_TEST::FillZones( m_board.get() );
1611
1612 int epsilon = pcbIUScale.mmToIU( 0.001 );
1613
1614 for( ZONE* zone : m_board->Zones() )
1615 {
1616 int half_min_width = zone->GetMinThickness() / 2;
1617
1618 if( half_min_width - epsilon <= epsilon )
1619 continue;
1620
1621 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1622 {
1623 if( !zone->HasFilledPolysForLayer( layer ) )
1624 continue;
1625
1626 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1627
1628 if( !fill || fill->OutlineCount() == 0 )
1629 continue;
1630
1631 for( int ii = 0; ii < fill->OutlineCount(); ii++ )
1632 {
1633 SHAPE_POLY_SET island;
1634 island.AddOutline( fill->Outline( ii ) );
1635
1636 for( int jj = 0; jj < fill->HoleCount( ii ); jj++ )
1637 island.AddHole( fill->Hole( ii, jj ) );
1638
1639 double originalArea = island.Area();
1640
1641 if( originalArea <= 0 )
1642 continue;
1643
1645
1646 test.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS,
1647 ARC_HIGH_DEF );
1648
1649 test.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS,
1650 ARC_HIGH_DEF, true );
1651
1652 double prunedArea = test.Area();
1653 double areaLoss = ( originalArea - prunedArea ) / originalArea;
1654
1655 BOOST_CHECK_MESSAGE( areaLoss < 0.01,
1656 wxString::Format(
1657 "Zone %s (priority %d) layer %d island %d lost "
1658 "%.2f%% area from min-width pruning, suggesting "
1659 "degenerate geometry from overlapping same-net zones",
1660 zone->GetNetname(),
1661 zone->GetAssignedPriority(),
1662 static_cast<int>( layer ), ii,
1663 areaLoss * 100.0 ) );
1664 }
1665 }
1666 }
1667}
1668
1669
1670BOOST_FIXTURE_TEST_CASE( RegressionDiffNetOverlappingZones, ZONE_FILL_TEST_FIXTURE )
1671{
1672 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
1673 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
1674
1675 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
1676 guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
1677
1678 auto runAreaLossCheck =
1679 [this]( bool aIterative )
1680 {
1681 ADVANCED_CFG& innerCfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
1682 innerCfg.m_ZoneFillIterativeRefill = aIterative;
1683
1684 KI_TEST::LoadBoard( m_settingsManager, "issue23418_diffnet/testing", m_board );
1685 KI_TEST::FillZones( m_board.get() );
1686
1687 int epsilon = pcbIUScale.mmToIU( 0.001 );
1688
1689 for( ZONE* zone : m_board->Zones() )
1690 {
1691 int half_min_width = zone->GetMinThickness() / 2;
1692
1693 if( half_min_width - epsilon <= epsilon )
1694 continue;
1695
1696 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1697 {
1698 if( !zone->HasFilledPolysForLayer( layer ) )
1699 continue;
1700
1701 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1702
1703 if( !fill || fill->OutlineCount() == 0 )
1704 continue;
1705
1706 for( int ii = 0; ii < fill->OutlineCount(); ii++ )
1707 {
1708 SHAPE_POLY_SET island;
1709 island.AddOutline( fill->Outline( ii ) );
1710
1711 for( int jj = 0; jj < fill->HoleCount( ii ); jj++ )
1712 island.AddHole( fill->Hole( ii, jj ) );
1713
1714 double originalArea = island.Area();
1715
1716 if( originalArea <= 0 )
1717 continue;
1718
1720
1721 test.Deflate( half_min_width - epsilon,
1723
1724 test.Inflate( half_min_width - epsilon,
1726
1727 double prunedArea = test.Area();
1728 double areaLoss = ( originalArea - prunedArea ) / originalArea;
1729
1731 areaLoss < 0.01,
1732 wxString::Format(
1733 "Zone %s (priority %d) layer %d island %d lost "
1734 "%.2f%% area (iterative=%d), suggesting degenerate "
1735 "geometry from different-net zone knockouts",
1736 zone->GetNetname(), zone->GetAssignedPriority(),
1737 static_cast<int>( layer ), ii, areaLoss * 100.0,
1738 aIterative ) );
1739 }
1740 }
1741 }
1742 };
1743
1744 runAreaLossCheck( false );
1745 runAreaLossCheck( true );
1746}
1747
1748
1764BOOST_FIXTURE_TEST_CASE( RegressionThermalReliefsToNowhere, ZONE_FILL_TEST_FIXTURE )
1765{
1766 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
1767 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
1768 cfg.m_ZoneFillIterativeRefill = true;
1769
1770 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
1771 guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
1772
1773 KI_TEST::LoadBoard( m_settingsManager, "issue23535_minimal/issue23535_minimal", m_board );
1774
1775 KI_TEST::FillZones( m_board.get() );
1776
1777 ZONE* gndZone = nullptr;
1778
1779 for( ZONE* zone : m_board->Zones() )
1780 {
1781 if( zone->GetNetname() == "GND" )
1782 gndZone = zone;
1783 }
1784
1785 BOOST_REQUIRE( gndZone );
1787
1788 const std::shared_ptr<SHAPE_POLY_SET>& gndFill = gndZone->GetFilledPolysList( F_Cu );
1789
1790 // The pad is at (6.5mm, 5mm) with size 1.5mm and thermal gap 0.5mm.
1791 // The right edge of the pad is at x=7.25mm, thermal gap extends to x=7.75mm.
1792 // After zone knockout, GND fill stops at roughly x=7.5mm.
1793 //
1794 // A thermal-relief-to-nowhere spoke would create copper at a point inside the
1795 // thermal gap but past the zone fill boundary. Check a point at (7.4mm, 5mm)
1796 // which is in the thermal gap (x > 7.25) and near the knockout edge.
1797 VECTOR2I spokeTestPoint( pcbIUScale.mmToIU( 7.4 ), pcbIUScale.mmToIU( 5.0 ) );
1798
1799 bool hasSpokeToNowhere = gndFill->Contains( spokeTestPoint );
1800
1801 BOOST_CHECK_MESSAGE( !hasSpokeToNowhere,
1802 "GND zone fill contains copper at the thermal gap test point (7.4, 5.0), "
1803 "indicating a thermal relief spoke to nowhere (issue 23535)." );
1804
1805 // Also verify that the left-pointing spoke still connects properly.
1806 // A point at (5.6mm, 5mm) is in the thermal gap on the left side and should have
1807 // copper from a valid left-pointing spoke.
1808 VECTOR2I validSpokePoint( pcbIUScale.mmToIU( 5.6 ), pcbIUScale.mmToIU( 5.0 ) );
1809
1810 bool hasValidSpoke = gndFill->Contains( validSpokePoint );
1811
1812 BOOST_CHECK_MESSAGE( hasValidSpoke,
1813 "GND zone fill does not contain copper at the valid spoke test point "
1814 "(5.6, 5.0). The fix may have incorrectly removed valid spokes." );
1815}
1816
1817
1828{
1829 KI_TEST::LoadBoard( m_settingsManager, "off_center_teardrop", m_board );
1830
1831 TOOL_MANAGER toolMgr;
1832 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1833
1834 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1835 toolMgr.RegisterTool( dummyTool );
1836
1837 BOARD_COMMIT commit( dummyTool );
1838 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1839 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1840
1841 if( !commit.Empty() )
1842 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1843
1844 // The test board has a 3mm circle pad at (100, 100) with a 0.25mm track connecting
1845 // at (100.75, 99) heading to (115, 99). The track enters the pad off-center: 1mm above
1846 // and 0.75mm right of center. The teardrop should be approximately symmetric about the
1847 // track's axis (the line from ~(100.75, 99) toward (115, 99), i.e., horizontal).
1848
1849 int teardropCount = 0;
1850
1851 for( ZONE* zone : m_board->Zones() )
1852 {
1853 if( !zone->IsTeardropArea() )
1854 continue;
1855
1856 teardropCount++;
1857
1858 const SHAPE_POLY_SET* outline = zone->Outline();
1859
1860 if( !outline || outline->OutlineCount() == 0 )
1861 continue;
1862
1863 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1864
1865 // The track axis is approximately at Y=99mm (in board coordinates = 99 * 1e6 nm).
1866 // Measure the maximum extent above and below this axis across all teardrop vertices.
1867 int trackY = pcbIUScale.mmToIU( 99 );
1868 int maxAbove = 0;
1869 int maxBelow = 0;
1870
1871 for( int i = 0; i < chain.PointCount(); i++ )
1872 {
1873 int dy = chain.CPoint( i ).y - trackY;
1874
1875 if( dy < 0 )
1876 maxAbove = std::max( maxAbove, -dy );
1877 else
1878 maxBelow = std::max( maxBelow, dy );
1879 }
1880
1881 // Both sides should have some extent (the teardrop flares out on both sides)
1882 BOOST_CHECK_MESSAGE( maxAbove > 0 && maxBelow > 0,
1883 "Teardrop should extend on both sides of the track axis" );
1884
1885 if( maxAbove > 0 && maxBelow > 0 )
1886 {
1887 // The two sides should be approximately equal. Allow 30% asymmetry tolerance
1888 // to account for polygon approximation of the circular pad and convex hull rounding.
1889 double ratio = static_cast<double>( std::min( maxAbove, maxBelow ) )
1890 / static_cast<double>( std::max( maxAbove, maxBelow ) );
1891
1892 BOOST_CHECK_MESSAGE( ratio > 0.7,
1893 wxString::Format( "Teardrop asymmetry ratio %.2f is too low "
1894 "(above=%d, below=%d). Expected roughly "
1895 "symmetric about the track axis.",
1896 ratio, maxAbove, maxBelow ) );
1897 }
1898 }
1899
1900 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone for off-center track" );
1901}
1902
1903
1912BOOST_FIXTURE_TEST_CASE( ElongatedPadTeardropContainment, ZONE_FILL_TEST_FIXTURE )
1913{
1914 KI_TEST::LoadBoard( m_settingsManager, "teardrop_elongated_pad", m_board );
1915
1916 TOOL_MANAGER toolMgr;
1917 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1918
1919 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1920 toolMgr.RegisterTool( dummyTool );
1921
1922 BOARD_COMMIT commit( dummyTool );
1923 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1924 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1925
1926 if( !commit.Empty() )
1927 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1928
1929 // Find the pad to build an expanded outline for containment checking.
1930 // The pad is at board position (136.45, 100.819) with size (3.5, 0.3) rotated 270 deg,
1931 // giving board extents X: [136.3, 136.6], Y: [99.069, 102.569].
1932 PAD* testPad = nullptr;
1933
1934 for( FOOTPRINT* fp : m_board->Footprints() )
1935 {
1936 for( PAD* pad : fp->Pads() )
1937 {
1938 if( pad->GetNumber() == "7" )
1939 {
1940 testPad = pad;
1941 break;
1942 }
1943 }
1944 }
1945
1946 BOOST_REQUIRE_MESSAGE( testPad != nullptr, "Could not find pad 7 in test board" );
1947
1948 // Build the pad outline polygon with a small tolerance for the track half-width
1949 int tolerance = std::max( m_board->GetDesignSettings().m_MaxError,
1950 pcbIUScale.mmToIU( 0.001 ) );
1951 SHAPE_POLY_SET padPoly;
1952 testPad->TransformShapeToPolygon( padPoly, B_Cu, tolerance,
1953 m_board->GetDesignSettings().m_MaxError, ERROR_OUTSIDE );
1954
1955 int teardropCount = 0;
1956
1957 for( ZONE* zone : m_board->Zones() )
1958 {
1959 if( !zone->IsTeardropArea() )
1960 continue;
1961
1962 const SHAPE_POLY_SET* outline = zone->Outline();
1963
1964 BOOST_REQUIRE_MESSAGE( outline && outline->OutlineCount() > 0,
1965 "Teardrop zone has no outline" );
1966
1967 teardropCount++;
1968
1969 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1970
1971 // Check each vertex of the teardrop. Vertices on the pad side (closer to pad center
1972 // than to the track anchor) must be inside the expanded pad outline.
1973 VECTOR2I padCenter = testPad->GetPosition();
1974
1975 // The track anchor region is near (136.45, 99.16) in mm, i.e., outside the pad.
1976 // We only check vertices that are closer to the pad center than to the track anchor.
1977 VECTOR2I trackAnchor( pcbIUScale.mmToIU( 136.45 ), pcbIUScale.mmToIU( 99.16 ) );
1978
1979 for( int i = 0; i < chain.PointCount(); i++ )
1980 {
1981 VECTOR2I pt = chain.CPoint( i );
1982 double distToPad = ( VECTOR2D( pt ) - VECTOR2D( padCenter ) ).EuclideanNorm();
1983 double distToTrack = ( VECTOR2D( pt ) - VECTOR2D( trackAnchor ) ).EuclideanNorm();
1984
1985 // Only check vertices on the pad side of the teardrop
1986 if( distToPad < distToTrack )
1987 {
1989 padPoly.Contains( pt ),
1990 wxString::Format( "Teardrop vertex (%d, %d) is outside the pad "
1991 "outline with %d nm tolerance",
1992 pt.x, pt.y, tolerance ) );
1993 }
1994 }
1995 }
1996
1997 BOOST_CHECK_MESSAGE( teardropCount > 0,
1998 "Expected at least one teardrop zone for elongated pad" );
1999}
2000
2001
2010BOOST_FIXTURE_TEST_CASE( TwoSegmentAngledTeardropNoSelfIntersection, ZONE_FILL_TEST_FIXTURE )
2011{
2012 auto runVariant = [&]( bool aCurvedEdges )
2013 {
2014 KI_TEST::LoadBoard( m_settingsManager, "two_segment_teardrop", m_board );
2015
2016 for( PCB_TRACK* track : m_board->Tracks() )
2017 {
2018 if( track->Type() == PCB_VIA_T )
2019 {
2020 static_cast<PCB_VIA*>( track )->SetTeardropCurved( aCurvedEdges );
2021 break;
2022 }
2023 }
2024
2025 TOOL_MANAGER toolMgr;
2026 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2027
2028 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2029 toolMgr.RegisterTool( dummyTool );
2030
2031 BOARD_COMMIT commit( dummyTool );
2032 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2033 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2034
2035 if( !commit.Empty() )
2036 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2037
2038 int teardropCount = 0;
2039 bool foundSelfIntersection = false;
2040
2041 for( ZONE* zone : m_board->Zones() )
2042 {
2043 if( !zone->IsTeardropArea() )
2044 continue;
2045
2046 teardropCount++;
2047
2048 const SHAPE_POLY_SET* outline = zone->Outline();
2049
2050 if( !outline || outline->OutlineCount() == 0 )
2051 continue;
2052
2053 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2054 int n = chain.PointCount();
2055
2056 for( int i = 0; i < n && !foundSelfIntersection; i++ )
2057 {
2058 SEG segA( chain.CPoint( i ), chain.CPoint( ( i + 1 ) % n ) );
2059
2060 for( int j = i + 2; j < n; j++ )
2061 {
2062 if( i == 0 && j == n - 1 )
2063 continue;
2064
2065 SEG segB( chain.CPoint( j ), chain.CPoint( ( j + 1 ) % n ) );
2066 OPT_VECTOR2I hit = segA.Intersect( segB );
2067
2068 if( hit.has_value() )
2069 {
2070 BOOST_TEST_MESSAGE( wxString::Format(
2071 "Self-intersection at (%d, %d) between edges %d and %d "
2072 "(curved=%s)",
2073 hit->x, hit->y, i, j,
2074 aCurvedEdges ? "yes" : "no" ) );
2075
2076 for( int k = 0; k < n; k++ )
2077 {
2078 BOOST_TEST_MESSAGE( wxString::Format(
2079 " pt[%d] = (%d, %d)", k,
2080 chain.CPoint( k ).x, chain.CPoint( k ).y ) );
2081 }
2082
2083 foundSelfIntersection = true;
2084 break;
2085 }
2086 }
2087 }
2088 }
2089
2090 BOOST_CHECK_MESSAGE( teardropCount > 0,
2091 wxString::Format( "Expected at least one teardrop zone "
2092 "(curved=%s)",
2093 aCurvedEdges ? "yes" : "no" ) );
2094
2095 BOOST_CHECK_MESSAGE( !foundSelfIntersection,
2096 wxString::Format( "Teardrop polygon has self-intersecting "
2097 "edges (curved=%s)",
2098 aCurvedEdges ? "yes" : "no" ) );
2099 };
2100
2101 runVariant( true );
2102 runVariant( false );
2103}
2104
2105
2124BOOST_FIXTURE_TEST_CASE( OffCenterTwoSegmentTeardropNoSpike, ZONE_FILL_TEST_FIXTURE )
2125{
2126 auto runVariant = [&]( bool aCurvedEdges )
2127 {
2128 KI_TEST::LoadBoard( m_settingsManager, "teardrop_offcenter_two_segment", m_board );
2129
2130 VECTOR2I viaPos;
2131 int viaRadius = 0;
2132
2133 for( PCB_TRACK* track : m_board->Tracks() )
2134 {
2135 if( track->Type() == PCB_VIA_T )
2136 {
2137 PCB_VIA* via = static_cast<PCB_VIA*>( track );
2138 via->SetTeardropCurved( aCurvedEdges );
2139 viaPos = via->GetPosition();
2140 viaRadius = via->GetWidth( PADSTACK::ALL_LAYERS ) / 2;
2141 break;
2142 }
2143 }
2144
2145 BOOST_REQUIRE( viaRadius > 0 );
2146
2147 TOOL_MANAGER toolMgr;
2148 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2149
2150 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2151 toolMgr.RegisterTool( dummyTool );
2152
2153 BOARD_COMMIT commit( dummyTool );
2154 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2155 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2156
2157 if( !commit.Empty() )
2158 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2159
2160 // The crafted board's first segment emerges from the via by ~10 um on a 100 um
2161 // track width; the emerging-length filter rejects it and no teardrop is built.
2162 const double maxBackSideDist = viaRadius * 1.2;
2163 int teardropCount = 0;
2164 int spikingPoints = 0;
2165 VECTOR2I worstPoint;
2166 double worstDistance = 0.0;
2167
2168 for( ZONE* zone : m_board->Zones() )
2169 {
2170 if( !zone->IsTeardropArea() )
2171 continue;
2172
2173 teardropCount++;
2174
2175 const SHAPE_POLY_SET* outline = zone->Outline();
2176
2177 if( !outline || outline->OutlineCount() == 0 )
2178 continue;
2179
2180 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2181
2182 for( int i = 0; i < chain.PointCount(); i++ )
2183 {
2184 const VECTOR2I& pt = chain.CPoint( i );
2185 VECTOR2I rel = pt - viaPos;
2186
2187 // Only consider points on the back side (opposite the track entry).
2188 if( rel.x >= 0 )
2189 continue;
2190
2191 double dist = rel.EuclideanNorm();
2192
2193 if( dist > maxBackSideDist )
2194 {
2195 spikingPoints++;
2196
2197 if( dist > worstDistance )
2198 {
2199 worstDistance = dist;
2200 worstPoint = pt;
2201 }
2202 }
2203 }
2204 }
2205
2206 BOOST_CHECK_MESSAGE( teardropCount == 0,
2207 wxString::Format( "Expected no teardrop on grazing-entry "
2208 "track (emergence below track width), got "
2209 "%d (curved=%s)",
2210 teardropCount,
2211 aCurvedEdges ? "yes" : "no" ) );
2212
2213 BOOST_CHECK_MESSAGE( spikingPoints == 0,
2214 wxString::Format( "Found %d teardrop polygon vertex/vertices "
2215 "outside the expected envelope (worst at "
2216 "(%d, %d), %f mm from via center; curved=%s)",
2217 spikingPoints,
2218 worstPoint.x, worstPoint.y,
2219 worstDistance / pcbIUScale.IU_PER_MM,
2220 aCurvedEdges ? "yes" : "no" ) );
2221 };
2222
2223 runVariant( true );
2224 runVariant( false );
2225}
2226
2227
2239BOOST_FIXTURE_TEST_CASE( MultiTrackSharedInsideJunctionNoSelfIntersection,
2241{
2242 auto runVariant = [&]( bool aCurvedEdges )
2243 {
2244 KI_TEST::LoadBoard( m_settingsManager, "teardrop_multi_inside_via", m_board );
2245
2246 for( PCB_TRACK* track : m_board->Tracks() )
2247 {
2248 if( track->Type() == PCB_VIA_T )
2249 {
2250 static_cast<PCB_VIA*>( track )->SetTeardropCurved( aCurvedEdges );
2251 break;
2252 }
2253 }
2254
2255 TOOL_MANAGER toolMgr;
2256 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2257
2258 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2259 toolMgr.RegisterTool( dummyTool );
2260
2261 BOARD_COMMIT commit( dummyTool );
2262 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2263 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2264
2265 if( !commit.Empty() )
2266 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2267
2268 int teardropCount = 0;
2269 int selfIntersectingCount = 0;
2270 VECTOR2I worstPoint;
2271
2272 for( ZONE* zone : m_board->Zones() )
2273 {
2274 if( !zone->IsTeardropArea() )
2275 continue;
2276
2277 teardropCount++;
2278
2279 const SHAPE_POLY_SET* outline = zone->Outline();
2280
2281 if( !outline || outline->OutlineCount() == 0 )
2282 continue;
2283
2284 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2285 int n = chain.PointCount();
2286 bool intersected = false;
2287
2288 for( int i = 0; i < n && !intersected; i++ )
2289 {
2290 SEG segA( chain.CPoint( i ), chain.CPoint( ( i + 1 ) % n ) );
2291
2292 for( int j = i + 2; j < n; j++ )
2293 {
2294 if( i == 0 && j == n - 1 )
2295 continue;
2296
2297 SEG segB( chain.CPoint( j ), chain.CPoint( ( j + 1 ) % n ) );
2298 OPT_VECTOR2I hit = segA.Intersect( segB );
2299
2300 if( hit.has_value() )
2301 {
2302 BOOST_TEST_MESSAGE( wxString::Format(
2303 "Teardrop polygon self-intersection at (%d, %d) "
2304 "between edges %d and %d (curved=%s)",
2305 hit->x, hit->y, i, j, aCurvedEdges ? "yes" : "no" ) );
2306
2307 worstPoint = hit.value();
2308 intersected = true;
2309 break;
2310 }
2311 }
2312 }
2313
2314 if( intersected )
2315 selfIntersectingCount++;
2316 }
2317
2318 BOOST_CHECK_MESSAGE( teardropCount > 0,
2319 wxString::Format( "Expected at least one teardrop zone "
2320 "(curved=%s)",
2321 aCurvedEdges ? "yes" : "no" ) );
2322
2323 BOOST_CHECK_MESSAGE( selfIntersectingCount == 0,
2324 wxString::Format( "%d of %d teardrop polygon(s) self-intersect "
2325 "(worst at (%d, %d); curved=%s)",
2326 selfIntersectingCount, teardropCount,
2327 worstPoint.x, worstPoint.y,
2328 aCurvedEdges ? "yes" : "no" ) );
2329 };
2330
2331 runVariant( true );
2332 runVariant( false );
2333}
2334
2335
2343BOOST_FIXTURE_TEST_CASE( RegressionKeepoutBoundaryMissingFill, ZONE_FILL_TEST_FIXTURE )
2344{
2345 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
2346 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
2347
2348 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
2349 guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
2350
2351 auto getTotalFilledArea =
2352 [this]() -> double
2353 {
2354 double totalArea = 0;
2355
2356 for( ZONE* zone : m_board->Zones() )
2357 {
2358 if( zone->GetIsRuleArea() )
2359 continue;
2360
2361 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
2362 {
2363 if( !zone->HasFilledPolysForLayer( layer ) )
2364 continue;
2365
2366 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
2367
2368 if( fill )
2369 totalArea += std::abs( fill->Area() );
2370 }
2371 }
2372
2373 return totalArea;
2374 };
2375
2376 auto refillAndMeasure =
2377 [this, &cfg, &getTotalFilledArea]( bool aIterative ) -> double
2378 {
2379 cfg.m_ZoneFillIterativeRefill = aIterative;
2380
2381 KI_TEST::LoadBoard( m_settingsManager, "issue23515/issue23515", m_board );
2382
2383 double storedArea = getTotalFilledArea();
2384
2385 BOOST_REQUIRE_MESSAGE( storedArea > 0, "Stored v9 fill has zero area" );
2386
2387 KI_TEST::FillZones( m_board.get() );
2388 return getTotalFilledArea();
2389 };
2390
2391 KI_TEST::LoadBoard( m_settingsManager, "issue23515/issue23515", m_board );
2392
2393 double storedArea = getTotalFilledArea();
2394
2395 BOOST_REQUIRE_MESSAGE( storedArea > 0, "Stored v9 fill has zero area" );
2396
2397 double nonIterativeArea = refillAndMeasure( false );
2398 double iterativeArea = refillAndMeasure( true );
2399 double nonIterativeAreaRatio = nonIterativeArea / storedArea;
2400 double iterativeAreaRatio = iterativeArea / storedArea;
2401
2403 nonIterativeAreaRatio > 0.99999,
2404 wxString::Format(
2405 "Non-iterative refill lost %.4f%% versus stored v9 fill "
2406 "(stored=%.2f mm^2, non-iterative=%.2f mm^2). "
2407 "This suggests missing pieces near keepout boundaries (issue 23515).",
2408 ( 1.0 - nonIterativeAreaRatio ) * 100.0,
2409 storedArea / 1e6, nonIterativeArea / 1e6 ) );
2410
2412 iterativeAreaRatio > 0.99999,
2413 wxString::Format(
2414 "Iterative refill lost %.4f%% versus stored v9 fill "
2415 "(stored=%.2f mm^2, iterative=%.2f mm^2). "
2416 "This suggests missing pieces near keepout boundaries (issue 23515).",
2417 ( 1.0 - iterativeAreaRatio ) * 100.0,
2418 storedArea / 1e6, iterativeArea / 1e6 ) );
2419}
2420
2421
2430BOOST_FIXTURE_TEST_CASE( HatchZoneViaConnectionRespectsSetting, ZONE_FILL_TEST_FIXTURE )
2431{
2432 m_board = std::make_unique<BOARD>();
2433
2434 // Two-layer board is sufficient for this test
2435 m_board->SetCopperLayerCount( 2 );
2436
2437 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
2438 bds.SetCopperLayerCount( 2 );
2439
2440 bds.m_MinClearance = pcbIUScale.mmToIU( 0.2 );
2441
2442 // Add a GND net
2443 NETINFO_ITEM* gndNet = new NETINFO_ITEM( m_board.get(), wxT( "GND" ) );
2444 m_board->Add( gndNet );
2445 int gndNetCode = gndNet->GetNetCode();
2446
2447 // Via dimensions: 2.0mm diameter, 1.0mm drill - large enough to span multiple hatch cells
2448 // so the via always touches webbing lines regardless of position within the hatch grid.
2449 int viaDiam = pcbIUScale.mmToIU( 2.0 );
2450 int viaDrill = pcbIUScale.mmToIU( 1.0 );
2451
2452 // Hatch zone parameters: 0.5mm gap, 0.3mm thickness. The via (radius=1.0mm) is wider
2453 // than the gap, so it will always intersect webbing in FULL mode. The thermal gap
2454 // (0.5mm) makes the knockout circle radius = 1.0+0.5 = 1.5mm.
2455 int hatchGap = pcbIUScale.mmToIU( 0.5 );
2456 int hatchThickness = pcbIUScale.mmToIU( 0.3 );
2457
2458 // Via center at 10mm,10mm (middle of the zone)
2459 VECTOR2I viaPos( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) );
2460
2461 auto makeVia =
2462 [&]() -> PCB_VIA*
2463 {
2464 PCB_VIA* via = new PCB_VIA( m_board.get() );
2465 via->SetPosition( viaPos );
2466 via->SetLayerPair( F_Cu, B_Cu );
2467 via->SetDrill( viaDrill );
2468 via->SetWidth( PADSTACK::ALL_LAYERS, viaDiam );
2469 via->SetNetCode( gndNetCode );
2470 m_board->Add( via );
2471 return via;
2472 };
2473
2474 auto makeHatchZone =
2475 [&]( ZONE_CONNECTION aConnection ) -> ZONE*
2476 {
2477 ZONE* zone = new ZONE( m_board.get() );
2478 zone->SetLayer( F_Cu );
2479 zone->SetNetCode( gndNetCode );
2481 zone->SetHatchGap( hatchGap );
2482 zone->SetHatchThickness( hatchThickness );
2483 zone->SetPadConnection( aConnection );
2484 zone->SetMinThickness( pcbIUScale.mmToIU( 0.2 ) );
2485 zone->SetThermalReliefGap( pcbIUScale.mmToIU( 0.5 ) );
2486 zone->SetThermalReliefSpokeWidth( pcbIUScale.mmToIU( 0.5 ) );
2487
2488 SHAPE_POLY_SET outline;
2489 outline.NewOutline();
2490 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2491 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 19 ), pcbIUScale.mmToIU( 1 ) ) );
2492 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 19 ), pcbIUScale.mmToIU( 19 ) ) );
2493 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 19 ) ) );
2494 zone->AddPolygon( outline.COutline( 0 ) );
2495
2496 m_board->Add( zone );
2497 return zone;
2498 };
2499
2500 auto initDRC =
2501 [&]()
2502 {
2503 m_board->BuildConnectivity();
2504 auto drcEngine = std::make_shared<DRC_ENGINE>( m_board.get(), &bds );
2505 drcEngine->InitEngine( wxFileName() );
2506 bds.m_DRCEngine = drcEngine;
2507 };
2508
2509 // The thermal relief adds a circular ring around the via that covers hatch holes which
2510 // would otherwise be open. With viaRadius=1.0mm, thermalGap=0.5mm, spokeWidth=0.5mm:
2511 // ring outer radius = 1.75mm, inner radius = 1.25mm
2512 // ring area added inside hatch holes > knockout area removed from webbing
2513 // net result: THERMAL fill area > FULL fill area by ~0.4 sq mm
2514 // FULL connection skips both the knockout and the ring addition, so the THERMAL fill
2515 // should be measurably larger than the FULL fill.
2516
2517 double fullFillArea = 0.0;
2518 double thermalFillArea = 0.0;
2519
2520 // Test 1: FULL connection
2521 {
2522 PCB_VIA* via = makeVia();
2523 ZONE* zone = makeHatchZone( ZONE_CONNECTION::FULL );
2524
2525 initDRC();
2526 KI_TEST::FillZones( m_board.get() );
2527
2528 BOOST_REQUIRE_MESSAGE( zone->HasFilledPolysForLayer( F_Cu ),
2529 "Zone should have fill on F.Cu with FULL connection" );
2530
2531 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
2532
2533 for( int i = 0; i < fill->OutlineCount(); i++ )
2534 fullFillArea += std::abs( fill->Outline( i ).Area() );
2535
2536 m_board->Remove( via );
2537 m_board->Remove( zone );
2538 delete via;
2539 delete zone;
2540 }
2541
2542 // Test 2: THERMAL connection
2543 {
2544 PCB_VIA* via = makeVia();
2545 ZONE* zone = makeHatchZone( ZONE_CONNECTION::THERMAL );
2546
2547 initDRC();
2548 KI_TEST::FillZones( m_board.get() );
2549
2550 BOOST_REQUIRE_MESSAGE( zone->HasFilledPolysForLayer( F_Cu ),
2551 "Zone should have fill on F.Cu with THERMAL connection" );
2552
2553 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
2554
2555 for( int i = 0; i < fill->OutlineCount(); i++ )
2556 thermalFillArea += std::abs( fill->Outline( i ).Area() );
2557
2558 m_board->Remove( via );
2559 m_board->Remove( zone );
2560 delete via;
2561 delete zone;
2562 }
2563
2564 // The THERMAL fill should have more area than the FULL fill because a thermal ring was
2565 // added around the via, filling hatch holes that would otherwise be open.
2566 // Use a 0.2 sq mm threshold to avoid sensitivity to small edge effects.
2567 double iuPerMM = pcbIUScale.IU_PER_MM;
2568 double areaThreshold = 0.2 * iuPerMM * iuPerMM; // 0.2 sq mm in IU^2
2569
2570 double areaIU2toMM2 = 1.0 / ( iuPerMM * iuPerMM );
2571
2572 BOOST_CHECK_MESSAGE( thermalFillArea > fullFillArea + areaThreshold,
2573 wxString::Format(
2574 "THERMAL connection fill area (%.2f sq mm) should be larger "
2575 "than FULL fill area (%.2f sq mm) by at least 0.2 sq mm. "
2576 "If they are equal or FULL is larger, thermal ring was not "
2577 "added for THERMAL connection, or thermal ring was incorrectly "
2578 "added for FULL connection (issue 23516 regression).",
2579 thermalFillArea * areaIU2toMM2, fullFillArea * areaIU2toMM2 ) );
2580}
2581
2582
2600BOOST_FIXTURE_TEST_CASE( RegressionCascadingIslandRefill, ZONE_FILL_TEST_FIXTURE )
2601{
2602 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
2603 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
2604 cfg.m_ZoneFillIterativeRefill = true;
2605
2606 struct ScopeGuard
2607 {
2608 bool& ref;
2609 bool orig;
2610 ~ScopeGuard() { ref = orig; }
2611 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
2612
2613 KI_TEST::LoadBoard( m_settingsManager, "zone_refill_cascading_islands", m_board );
2614 KI_TEST::FillZones( m_board.get() );
2615
2616 const std::vector<std::string> checkedNames = { "hi1", "hi2", "hi3", "hi4", "hi5", "hi6", "hi7",
2617 "lo1", "lo2", "lo3", "lo4", "lo5", "lo6" };
2618 std::map<std::string, ZONE*> zoneByName;
2619
2620 for( ZONE* zone : m_board->Zones() )
2621 zoneByName[zone->GetZoneName().ToStdString()] = zone;
2622
2623 for( const std::string& name : checkedNames )
2624 {
2625 BOOST_REQUIRE_MESSAGE( zoneByName.count( name ), "Zone '" + name + "' not found in test board" );
2626 BOOST_REQUIRE_MESSAGE( zoneByName[name]->HasFilledPolysForLayer( F_Cu ),
2627 "Zone '" + name + "' has no fill on F.Cu" );
2628 }
2629
2630 // hi3, hi5, hi7 each split into two copper islands (one standalone, one merged with lo2/lo4/lo6).
2631 for( const std::string& name : { "hi3", "hi5", "hi7" } )
2632 {
2633 int islands = zoneByName[name]->GetFilledPolysList( F_Cu )->OutlineCount();
2634
2635 BOOST_CHECK_MESSAGE( islands == 2, wxString::Format( "Zone '%s' should have 2 filled islands but has %d. "
2636 "Cascading island removal did not converge correctly.",
2637 name, islands ) );
2638 }
2639
2640 // All lo zones and hi2/hi4/hi6 are single zones.
2641 for( const std::string& name : { "lo1", "lo2", "lo3", "lo4", "lo5", "lo6", "hi2", "hi4", "hi6" } )
2642 {
2643 int islands = zoneByName[name]->GetFilledPolysList( F_Cu )->OutlineCount();
2644
2645 BOOST_CHECK_MESSAGE( islands == 1, wxString::Format( "Zone '%s' should have 1 filled island but has %d. "
2646 "Iterative refill may have incorrectly blocked or "
2647 "expanded this zone.",
2648 name, islands ) );
2649 }
2650}
2651
2652
2659BOOST_FIXTURE_TEST_CASE( CopperThievingZone_HatchSurvivesTrackBisection, ZONE_FILL_TEST_FIXTURE )
2660{
2661 KI_TEST::LoadBoard( m_settingsManager, "zone_thieving_track_bisection", m_board );
2662 KI_TEST::FillZones( m_board.get() );
2663
2664 ZONE* thievingZone = nullptr;
2665
2666 for( ZONE* z : m_board->Zones() )
2667 {
2668 if( z->GetFillMode() == ZONE_FILL_MODE::COPPER_THIEVING )
2669 {
2670 thievingZone = z;
2671 break;
2672 }
2673 }
2674
2675 BOOST_REQUIRE( thievingZone );
2676
2677 const std::shared_ptr<SHAPE_POLY_SET>& fill = thievingZone->GetFilledPolysList( F_Cu );
2678 BOOST_REQUIRE( fill );
2679
2680 // The track splits the fill area in two; before the fix the connectivity
2681 // pass classified the narrow side as an isolated island and deleted it.
2682 // Expect at least two outlines covering both halves of the original zone.
2683 BOOST_CHECK_GE( fill->OutlineCount(), 2 );
2684
2685 // The fill must span the full zone width (left edge through right edge).
2686 BOX2I fillBox = fill->BBox();
2687 BOX2I zoneBox = thievingZone->Outline()->BBox();
2688
2689 BOOST_CHECK_LT( fillBox.GetLeft(), zoneBox.GetLeft() + pcbIUScale.mmToIU( 2.0 ) );
2690 BOOST_CHECK_GT( fillBox.GetRight(), zoneBox.GetRight() - pcbIUScale.mmToIU( 2.0 ) );
2691
2692 // The mesh must have real structure on both sides.
2693 BOOST_CHECK_GT( fill->TotalVertices(), 200 );
2694}
2695
2696
2709BOOST_FIXTURE_TEST_CASE( IterativeRefillConvergenceLimit, ZONE_FILL_TEST_FIXTURE )
2710{
2711 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
2712 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
2713 cfg.m_ZoneFillIterativeRefill = true;
2714
2715 struct ScopeGuard
2716 {
2717 bool& ref;
2718 bool orig;
2719 ~ScopeGuard() { ref = orig; }
2720 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
2721
2722 // Capture wxLogWarning calls so we can assert that the iteration cap fires.
2723 class WarningCapture : public wxLog
2724 {
2725 public:
2726 bool m_hadWarning = false;
2727
2728 protected:
2729 void DoLogRecord( wxLogLevel aLevel, const wxString&, const wxLogRecordInfo& ) override
2730 {
2731 if( aLevel == wxLOG_Warning )
2732 m_hadWarning = true;
2733 }
2734 };
2735
2736 auto* capture = new WarningCapture();
2737 wxLog* oldLog = wxLog::SetActiveTarget( capture );
2738
2739 struct LogGuard
2740 {
2741 wxLog* old;
2742 ~LogGuard() { wxLog::SetActiveTarget( old ); }
2743 } logGuard{ oldLog };
2744
2745 KI_TEST::LoadBoard( m_settingsManager, "zone_refill_convergence_limit", m_board );
2746 KI_TEST::FillZones( m_board.get() );
2747
2748 BOOST_CHECK_MESSAGE( capture->m_hadWarning, "Expected a wxLogWarning when iterative refill hits the iteration "
2749 "limit, but none was emitted. The convergence-limit board may no "
2750 "longer trigger the cap, or the warning path has changed." );
2751}
2752
2753
2759BOOST_FIXTURE_TEST_CASE( CopperThievingZone_NonCopperLayerStampsNotSolid, ZONE_FILL_TEST_FIXTURE )
2760{
2761 m_board = std::make_unique<BOARD>();
2762
2763 ZONE* zone = new ZONE( m_board.get() );
2764 zone->SetLayer( F_SilkS );
2765 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
2766 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
2767 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
2768 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
2770
2771 THIEVING_SETTINGS thieving;
2773 thieving.element_size = pcbIUScale.mmToIU( 0.5 );
2774 thieving.gap = pcbIUScale.mmToIU( 1.5 );
2775 zone->SetThievingSettings( thieving );
2777 m_board->Add( zone );
2778
2779 KI_TEST::FillZones( m_board.get() );
2780
2781 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_SilkS );
2782 BOOST_REQUIRE( fill );
2783
2784 // A solid fill would have one outline (the zone polygon); a dots grid
2785 // produces dozens. Lower bound is conservative to avoid edge-clipping flakiness.
2786 BOOST_CHECK_GT( fill->OutlineCount(), 5 );
2787}
2788
2789
2798{
2799 m_board = std::make_unique<BOARD>();
2800 m_board->SetCopperLayerCount( 2 );
2801
2802 // 10 mm x 10 mm zone outline
2803 ZONE* zone = new ZONE( m_board.get() );
2804 zone->SetLayer( F_Cu );
2805 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
2806 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
2807 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
2808 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
2809
2811
2812 THIEVING_SETTINGS thieving;
2814 thieving.element_size = pcbIUScale.mmToIU( 0.5 );
2815 thieving.gap = pcbIUScale.mmToIU( 2.0 );
2816 thieving.line_width = pcbIUScale.mmToIU( 0.3 );
2817 thieving.stagger = false;
2818 thieving.orientation = ANGLE_0;
2819 zone->SetThievingSettings( thieving );
2820
2822
2823 m_board->Add( zone );
2824
2825 KI_TEST::FillZones( m_board.get() );
2826
2827 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
2828 BOOST_REQUIRE( fill );
2829 BOOST_REQUIRE_GT( fill->OutlineCount(), 0 );
2830
2831 // 2.5 mm pitch, 0.5 mm dot. The four positions whose disc touches the
2832 // zone edge (x or y at 0 or 10 mm) are dropped, leaving a 3 x 3 grid.
2833 BOOST_CHECK_GE( fill->OutlineCount(), 6 );
2834 BOOST_CHECK_LE( fill->OutlineCount(), 12 );
2835
2836 // 10% slack covers the polygonal circle approximation plus post-fill corner rounding.
2837 const double fullDotArea = M_PI * std::pow( pcbIUScale.mmToIU( 0.25 ), 2 );
2838 CheckAllOutlineAreasAtLeast( fill, 0.9 * fullDotArea, wxT( "Dot" ) );
2839}
2840
2841
2848BOOST_FIXTURE_TEST_CASE( CopperThievingZone_StaggerProducesDifferentLayout, ZONE_FILL_TEST_FIXTURE )
2849{
2850 auto countDots = []( bool stagger ) -> int
2851 {
2852 auto board = std::make_unique<BOARD>();
2853 board->SetCopperLayerCount( 2 );
2854
2855 ZONE* zone = new ZONE( board.get() );
2856 zone->SetLayer( F_Cu );
2857 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
2858 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 20 ), 0 ), -1 );
2859 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 20 ), pcbIUScale.mmToIU( 20 ) ), -1 );
2860 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 20 ) ), -1 );
2862
2863 THIEVING_SETTINGS thieving;
2865 thieving.element_size = pcbIUScale.mmToIU( 0.5 );
2866 thieving.gap = pcbIUScale.mmToIU( 2.0 );
2867 thieving.stagger = stagger;
2868 zone->SetThievingSettings( thieving );
2870 board->Add( zone );
2871
2872 KI_TEST::FillZones( board.get() );
2873 return zone->GetFilledPolysList( F_Cu )->OutlineCount();
2874 };
2875
2876 int plain = countDots( false );
2877 int staggered = countDots( true );
2878
2879 BOOST_TEST_MESSAGE( "plain dots: " << plain << " staggered dots: " << staggered );
2880
2881 // Within a factor of two — catches the offset walking dots off the board
2882 // (returning ~0) without being brittle about edge-clipping rounding.
2883 BOOST_CHECK_NE( plain, staggered );
2884 BOOST_CHECK_GE( staggered, plain / 2 );
2885 BOOST_CHECK_LE( staggered, plain * 2 );
2886}
2887
2888
2894BOOST_FIXTURE_TEST_CASE( CopperThievingZone_SquaresGrid, ZONE_FILL_TEST_FIXTURE )
2895{
2896 m_board = std::make_unique<BOARD>();
2897 m_board->SetCopperLayerCount( 2 );
2898
2899 ZONE* zone = new ZONE( m_board.get() );
2900 zone->SetLayer( F_Cu );
2901 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
2902 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
2903 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
2904 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
2906
2907 THIEVING_SETTINGS thieving;
2909 thieving.element_size = pcbIUScale.mmToIU( 0.6 );
2910 thieving.gap = pcbIUScale.mmToIU( 2.0 );
2911 zone->SetThievingSettings( thieving );
2913 m_board->Add( zone );
2914
2915 KI_TEST::FillZones( m_board.get() );
2916
2917 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
2918 BOOST_REQUIRE( fill );
2919 BOOST_REQUIRE_GT( fill->OutlineCount(), 0 );
2920
2921 // 2.6 mm pitch, 0.6 mm square. Same edge-drop behavior as the dots test:
2922 // strict-containment leaves a 3 x 3 grid of full squares.
2923 BOOST_CHECK_GE( fill->OutlineCount(), 6 );
2924 BOOST_CHECK_LE( fill->OutlineCount(), 12 );
2925
2926 const double fullSquareArea = std::pow( pcbIUScale.mmToIU( 0.6 ), 2 );
2927 CheckAllOutlineAreasAtLeast( fill, 0.9 * fullSquareArea, wxT( "Square" ) );
2928}
2929
2930
2939BOOST_FIXTURE_TEST_CASE( CopperThievingZone_HighDensityPerformance, ZONE_FILL_TEST_FIXTURE )
2940{
2941 m_board = std::make_unique<BOARD>();
2942 m_board->SetCopperLayerCount( 2 );
2943
2944 ZONE* zone = new ZONE( m_board.get() );
2945 zone->SetLayer( F_Cu );
2946 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
2947 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 100 ), 0 ), -1 );
2948 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 100 ), pcbIUScale.mmToIU( 100 ) ), -1 );
2949 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 100 ) ), -1 );
2951
2952 THIEVING_SETTINGS thieving;
2954 thieving.element_size = pcbIUScale.mmToIU( 0.3 );
2955 thieving.gap = pcbIUScale.mmToIU( 1.0 );
2956 zone->SetThievingSettings( thieving );
2958 m_board->Add( zone );
2959
2960 auto start = std::chrono::steady_clock::now();
2961 KI_TEST::FillZones( m_board.get() );
2962 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
2963 std::chrono::steady_clock::now() - start )
2964 .count();
2965
2966 BOOST_TEST_MESSAGE( "5.9k-dot fill elapsed: " << elapsed << " ms" );
2967
2969 BOOST_CHECK_GT( zone->GetFilledPolysList( F_Cu )->OutlineCount(), 4000 );
2970
2971 // 30 s upper bound on QABUILD with assertions on; current implementation
2972 // measures in low seconds.
2973 BOOST_CHECK_LT( elapsed, 30000 );
2974}
2975
2976
2983BOOST_FIXTURE_TEST_CASE( CopperThievingZone_HatchPattern, ZONE_FILL_TEST_FIXTURE )
2984{
2985 m_board = std::make_unique<BOARD>();
2986 m_board->SetCopperLayerCount( 2 );
2987
2988 ZONE* zone = new ZONE( m_board.get() );
2989 zone->SetLayer( F_Cu );
2990 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
2991 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
2992 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
2993 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
2995
2996 THIEVING_SETTINGS thieving;
2998 thieving.gap = pcbIUScale.mmToIU( 2.0 );
2999 thieving.line_width = pcbIUScale.mmToIU( 0.3 );
3000 zone->SetThievingSettings( thieving );
3002 m_board->Add( zone );
3003
3004 KI_TEST::FillZones( m_board.get() );
3005
3006 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
3007 BOOST_REQUIRE( fill );
3008 BOOST_REQUIRE_GT( fill->TotalVertices(), 0 );
3009
3010 // Subtractive hatch produces a single connected outline after fracturing
3011 // (perimeter border + interior mesh linked through bridges). A dot grid
3012 // in the same outline would have dozens of disconnected pieces.
3013 BOOST_CHECK_EQUAL( fill->OutlineCount(), 1 );
3014
3015 // The fill bounding box must reach the zone corners — the perimeter
3016 // border is what differentiates hatch from a dot grid. Solid would also
3017 // reach the corners; the high vertex count below catches that case.
3018 BOX2I fillBox = fill->BBox();
3019 BOOST_CHECK_LT( fillBox.GetLeft(), pcbIUScale.mmToIU( 0.5 ) );
3020 BOOST_CHECK_GT( fillBox.GetRight(), pcbIUScale.mmToIU( 9.5 ) );
3021 BOOST_CHECK_LT( fillBox.GetTop(), pcbIUScale.mmToIU( 0.5 ) );
3022 BOOST_CHECK_GT( fillBox.GetBottom(), pcbIUScale.mmToIU( 9.5 ) );
3023
3024 // A solid 10x10 mm rectangle would have ~4 vertices. A hatched mesh has
3025 // many vertices because each void cut adds outline segments.
3026 BOOST_CHECK_GT( fill->TotalVertices(), 30 );
3027}
3028
3029
3037BOOST_FIXTURE_TEST_CASE( RegressionNonCopperZoneKeepoutIslands, ZONE_FILL_TEST_FIXTURE )
3038{
3039 KI_TEST::LoadBoard( m_settingsManager, "issue24089/issue24089", m_board );
3040
3041 auto countIslands =
3042 [this]() -> int
3043 {
3044 int total = 0;
3045
3046 for( ZONE* zone : m_board->Zones() )
3047 {
3048 if( zone->GetIsRuleArea() )
3049 continue;
3050
3051 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
3052 {
3053 if( !zone->HasFilledPolysForLayer( layer ) )
3054 continue;
3055
3056 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
3057
3058 if( fill )
3059 total += fill->OutlineCount();
3060 }
3061 }
3062
3063 return total;
3064 };
3065
3066 int storedIslands = countIslands();
3067
3068 BOOST_REQUIRE_MESSAGE( storedIslands >= 3,
3069 wxString::Format( "Stored v9 fill should have at least 3 silk islands; "
3070 "found %d",
3071 storedIslands ) );
3072
3073 KI_TEST::FillZones( m_board.get() );
3074
3075 int refilledIslands = countIslands();
3076
3077 BOOST_CHECK_MESSAGE( refilledIslands == storedIslands,
3078 wxString::Format( "Refill lost silk islands: stored=%d, refilled=%d. "
3079 "Outline 0 of every non-copper multi-island zone "
3080 "was being incorrectly removed (issue 24089).",
3081 storedIslands, refilledIslands ) );
3082}
const char * name
@ ERROR_OUTSIDE
constexpr int ARC_HIGH_DEF
Definition base_units.h:141
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:125
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:990
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
virtual void Push(const wxString &aMessage=wxEmptyString, int aCommitFlags=0) override
Execute the changes.
Container for design settings for a BOARD object.
std::shared_ptr< DRC_ENGINE > m_DRCEngine
void SetCopperLayerCount(int aNewLayerCount)
Set the copper layer count to aNewLayerCount.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
constexpr coord_type GetLeft() const
Definition box2.h:228
constexpr coord_type GetRight() const
Definition box2.h:217
constexpr coord_type GetTop() const
Definition box2.h:229
constexpr coord_type GetBottom() const
Definition box2.h:222
bool Empty() const
Definition commit.h:137
const MINOPTMAX< int > & GetValue() const
Definition drc_rule.h:200
void RunTests(EDA_UNITS aUnits, bool aReportAllTrackErrors, bool aTestFootprints, BOARD_COMMIT *aCommit=nullptr)
Run the DRC tests.
void SetViolationHandler(DRC_VIOLATION_HANDLER aHandler)
Set an optional DRC violation handler (receives DRC_ITEMs and positions).
Definition drc_engine.h:168
DRC_CONSTRAINT EvalRules(DRC_CONSTRAINT_T aConstraintType, const BOARD_ITEM *a, const BOARD_ITEM *b, PCB_LAYER_ID aLayer, REPORTER *aReporter=nullptr)
void InitEngine(const wxFileName &aRulePath)
Initialize the DRC engine.
const KIID m_Uuid
Definition eda_item.h:535
Definition kiid.h:48
T Min() const
Definition minoptmax.h:33
Handle the data for a net.
Definition netinfo.h:50
int GetNetCode() const
Definition netinfo.h:98
static constexpr PCB_LAYER_ID ALL_LAYERS
! Temporary layer identifier to identify code that is not padstack-aware
Definition padstack.h:177
Definition pad.h:65
const wxString & GetNumber() const
Definition pad.h:147
VECTOR2I GetPosition() const override
Definition pad.h:219
void TransformShapeToPolygon(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aMaxError, ERROR_LOC aErrorLoc=ERROR_INSIDE, bool ignoreLineWidth=false) const override
Convert the pad shape to a closed polygon.
Definition pad.cpp:2613
const VECTOR2I & GetSize(PCB_LAYER_ID aLayer) const
Definition pad.h:274
Definition seg.h:42
OPT_VECTOR2I Intersect(const SEG &aSeg, bool aIgnoreEndpoints=false, bool aLines=false) const
Compute intersection point of segment (this) with segment aSeg.
Definition seg.cpp:446
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
double Area(bool aAbsolute=true) const
Return the area of this chain.
Represent a set of closed polygons.
void BooleanAdd(const SHAPE_POLY_SET &b)
Perform boolean polyset union.
void ClearArcs()
Removes all arc references from all the outlines and holes in the polyset.
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.
void Inflate(int aAmount, CORNER_STRATEGY aCornerStrategy, int aMaxError, bool aSimplify=false)
Perform outline inflation/deflation.
int Append(int x, int y, int aOutline=-1, int aHole=-1, bool aAllowDuplication=false)
Appends a vertex at the end of the given outline/hole (default: the last outline)
void Simplify()
Simplify the polyset (merges overlapping polys, eliminates degeneracy/self-intersections)
int AddHole(const SHAPE_LINE_CHAIN &aHole, int aOutline=-1)
Adds a new hole to the given outline (default: last) and returns its index.
SHAPE_LINE_CHAIN & Outline(int aIndex)
Return the reference to aIndex-th outline in the set.
int NewOutline()
Creates a new empty polygon in the set and returns its index.
void BooleanIntersection(const SHAPE_POLY_SET &b)
Perform boolean polyset intersection.
int OutlineCount() const
Return the number of outlines in the set.
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.
SHAPE_POLY_SET CloneDropTriangulation() const
void BooleanSubtract(const SHAPE_POLY_SET &b)
Perform boolean polyset difference.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
TEARDROP_MANAGER manage and build teardrop areas A teardrop area is a polygonal area (a copper ZONE) ...
Definition teardrop.h:94
void UpdateTeardrops(BOARD_COMMIT &aCommit, const std::vector< BOARD_ITEM * > *dirtyPadsAndVias, const std::set< PCB_TRACK * > *dirtyTracks, bool aForceFullUpdate=false)
Update teardrops on a list of items.
Definition teardrop.cpp:229
Master controller class:
void RegisterTool(TOOL_BASE *aTool)
Add a tool to the manager set and sets it up.
void SetEnvironment(EDA_ITEM *aModel, KIGFX::VIEW *aView, KIGFX::VIEW_CONTROLS *aViewControls, APP_SETTINGS_BASE *aSettings, TOOLS_HOLDER *aFrame)
Set the work environment (model, view, view controls and the parent window).
T EuclideanNorm() const
Compute the Euclidean norm of the vector, which is defined as sqrt(x ** 2 + y ** 2).
Definition vector2d.h:283
Handle a list of polygons defining a copper zone.
Definition zone.h:74
void SetHatchThickness(int aThickness)
Definition zone.h:330
void AddPolygon(std::vector< VECTOR2I > &aPolygon)
Add a polygon to the zone outline.
Definition zone.cpp:1263
std::shared_ptr< SHAPE_POLY_SET > GetFilledPolysList(PCB_LAYER_ID aLayer) const
Definition zone.h:688
void SetMinThickness(int aMinThickness)
Definition zone.h:320
double GetFilledArea()
This area is cached from the most recent call to CalculateFilledArea().
Definition zone.h:283
void SetThermalReliefSpokeWidth(int aThermalReliefSpokeWidth)
Definition zone.h:255
virtual void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
Definition zone.cpp:603
SHAPE_POLY_SET * Outline()
Definition zone.h:422
bool SetNetCode(int aNetCode, bool aNoAssert) override
Override that clamps the netcode to 0 when this zone is in copper-thieving fill mode.
Definition zone.cpp:585
void SetFillMode(ZONE_FILL_MODE aFillMode)
Definition zone.cpp:609
bool HasFilledPolysForLayer(PCB_LAYER_ID aLayer) const
Definition zone.h:679
void SetThievingSettings(const THIEVING_SETTINGS &aSettings)
Definition zone.h:356
void SetThermalReliefGap(int aThermalReliefGap)
Definition zone.h:244
bool AppendCorner(VECTOR2I aPosition, int aHoleIdx, bool aAllowDuplication=false)
Add a new corner to the zone outline (to the main outline or a hole)
Definition zone.cpp:1280
double CalculateFilledArea()
Compute the area currently occupied by the zone fill.
Definition zone.cpp:1690
void SetPadConnection(ZONE_CONNECTION aPadConnection)
Definition zone.h:317
void SetIslandRemovalMode(ISLAND_REMOVAL_MODE aRemove)
Definition zone.h:825
void SetHatchGap(int aStep)
Definition zone.h:333
@ CHAMFER_ALL_CORNERS
All angles are chamfered.
@ ROUND_ALL_CORNERS
All angles are rounded.
@ DRCE_CLEARANCE
Definition drc_item.h:44
@ DRCE_COPPER_SLIVER
Definition drc_item.h:93
@ CLEARANCE_CONSTRAINT
Definition drc_rule.h:55
#define _(s)
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:411
bool m_ZoneFillIterativeRefill
Enable iterative zone filling to handle isolated islands in higher priority zones.
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:60
@ B_Cu
Definition layer_ids.h:65
@ F_SilkS
Definition layer_ids.h:100
@ In1_Cu
Definition layer_ids.h:66
@ F_Cu
Definition layer_ids.h:64
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
void FillZones(BOARD *m_board)
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
CITER next(CITER it)
Definition ptree.cpp:124
@ RPT_SEVERITY_ERROR
const double epsilon
#define SKIP_SET_DIRTY
Definition sch_commit.h:42
#define SKIP_UNDO
Definition sch_commit.h:40
std::optional< VECTOR2I > OPT_VECTOR2I
Definition seg.h:39
Parameters that drive copper-thieving fill generation.
EDA_ANGLE orientation
THIEVING_PATTERN pattern
std::unique_ptr< BOARD > m_board
SETTINGS_MANAGER m_settingsManager
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
VECTOR3I v1(5, 5, 5)
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
const SHAPE_LINE_CHAIN chain
int clearance
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2I v2(1, 0)
static const std::vector< wxString > RegressionZoneFillTests_tests
int delta
static const std::vector< std::pair< wxString, int > > RegressionTeardropFill_tests
BOOST_DATA_TEST_CASE_F(ZONE_FILL_TEST_FIXTURE, RegressionZoneFillTests, boost::unit_test::data::make(RegressionZoneFillTests_tests), relPath)
static void CheckAllOutlineAreasAtLeast(const std::shared_ptr< SHAPE_POLY_SET > &aFill, double aMinArea, const wxString &aLabel)
Assert every outline in aFill has at least aMinArea — used to verify thieving stamps survived the fil...
static const std::vector< wxString > RegressionSliverZoneFillTests_tests
BOOST_FIXTURE_TEST_CASE(BasicZoneFills, ZONE_FILL_TEST_FIXTURE)
#define M_PI
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:94
@ PCB_ARC_T
class PCB_ARC, an arc track segment on a copper layer
Definition typeinfo.h:95
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:687
VECTOR2< double > VECTOR2D
Definition vector2d.h:686
ZONE_CONNECTION
How pads are covered by copper in zone.
Definition zones.h:47
@ THERMAL
Use thermal relief for pads.
Definition zones.h:50
@ FULL
pads are covered by copper
Definition zones.h:51