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, see <https://www.gnu.org/licenses/>.
18 */
19
21#include <boost/test/data/test_case.hpp>
22
23#include <chrono>
24#include <cmath>
25
27#include <board.h>
28#include <board_commit.h>
29#include <wx/log.h>
30#include <zone_filler.h>
32#include <drc/drc_engine.h>
33#include <pad.h>
34#include <pcb_track.h>
35#include <footprint.h>
36#include <zone.h>
37#include <drc/drc_engine.h>
38#include <drc/drc_item.h>
41#include <advanced_config.h>
43#include <teardrop/teardrop.h>
45#include <netclass.h>
46#include <netinfo.h>
47
48
51static void CheckAllOutlineAreasAtLeast( const std::shared_ptr<SHAPE_POLY_SET>& aFill,
52 double aMinArea, const wxString& aLabel )
53{
54 for( int ii = 0; ii < aFill->OutlineCount(); ++ii )
55 {
56 const double area = std::abs( aFill->Outline( ii ).Area() );
57
58 BOOST_CHECK_MESSAGE( area >= aMinArea,
59 wxString::Format( "%s %d area %.0f IU^2 below %.0f IU^2; partial "
60 "stamps should not survive.",
61 aLabel, ii, area, aMinArea ) );
62 }
63}
64
65
74
75
76int delta = KiROUND( 0.006 * pcbIUScale.IU_PER_MM );
77
78
80{
81 KI_TEST::LoadBoard( m_settingsManager, "zone_filler", m_board );
82
83 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
84
85 KI_TEST::FillZones( m_board.get() );
86
87 // Now that the zones are filled we're going to increase the size of -some- pads and
88 // tracks so that they generate DRC errors. The test then makes sure that those errors
89 // are generated, and that the other pads and tracks do -not- generate errors.
90
91 for( PAD* pad : m_board->Footprints()[0]->Pads() )
92 {
93 if( pad->GetNumber() == "2" || pad->GetNumber() == "4" || pad->GetNumber() == "6" )
95 }
96
97 int ii = 0;
98 KIID arc8;
99 KIID arc12;
100
101 for( PCB_TRACK* track : m_board->Tracks() )
102 {
103 if( track->Type() == PCB_ARC_T )
104 {
105 ii++;
106
107 if( ii == 8 )
108 {
109 arc8 = track->m_Uuid;
110 track->SetWidth( track->GetWidth() + delta + delta );
111 }
112 else if( ii == 12 )
113 {
114 arc12 = track->m_Uuid;
115 track->Move( VECTOR2I( -delta, -delta ) );
116 }
117 }
118 }
119
120 bool foundPad2Error = false;
121 bool foundPad4Error = false;
122 bool foundPad6Error = false;
123 bool foundArc8Error = false;
124 bool foundArc12Error = false;
125 bool foundOtherError = false;
126
127 bds.m_DRCEngine->InitEngine( wxFileName() ); // Just to be sure to be sure
128
130 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
131 const std::function<void( PCB_MARKER* )>& aPathGenerator )
132 {
133 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
134 {
135 BOARD_ITEM* item_a = m_board->ResolveItem( aItem->GetMainItemID() );
136 PAD* pad_a = dynamic_cast<PAD*>( item_a );
137 PCB_TRACK* trk_a = dynamic_cast<PCB_TRACK*>( item_a );
138
139 BOARD_ITEM* item_b = m_board->ResolveItem( aItem->GetAuxItemID() );
140 PAD* pad_b = dynamic_cast<PAD*>( item_b );
141 PCB_TRACK* trk_b = dynamic_cast<PCB_TRACK*>( item_b );
142
143 if( pad_a && pad_a->GetNumber() == "2" ) foundPad2Error = true;
144 else if( pad_a && pad_a->GetNumber() == "4" ) foundPad4Error = true;
145 else if( pad_a && pad_a->GetNumber() == "6" ) foundPad6Error = true;
146 else if( pad_b && pad_b->GetNumber() == "2" ) foundPad2Error = true;
147 else if( pad_b && pad_b->GetNumber() == "4" ) foundPad4Error = true;
148 else if( pad_b && pad_b->GetNumber() == "6" ) foundPad6Error = true;
149 else if( trk_a && trk_a->m_Uuid == arc8 ) foundArc8Error = true;
150 else if( trk_a && trk_a->m_Uuid == arc12 ) foundArc12Error = true;
151 else if( trk_b && trk_b->m_Uuid == arc8 ) foundArc8Error = true;
152 else if( trk_b && trk_b->m_Uuid == arc12 ) foundArc12Error = true;
153 else foundOtherError = true;
154
155 }
156 } );
157
158 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
159
160 BOOST_CHECK_EQUAL( foundPad2Error, true );
161 BOOST_CHECK_EQUAL( foundPad4Error, true );
162 BOOST_CHECK_EQUAL( foundPad6Error, true );
163 BOOST_CHECK_EQUAL( foundArc8Error, true );
164 BOOST_CHECK_EQUAL( foundArc12Error, true );
165 BOOST_CHECK_EQUAL( foundOtherError, false );
166}
167
168
170{
171 KI_TEST::LoadBoard( m_settingsManager, "notched_zones", m_board );
172
173 // Older algorithms had trouble where the filleted zones intersected and left notches.
174 // See:
175 // https://gitlab.com/kicad/code/kicad/-/issues/2737
176 // https://gitlab.com/kicad/code/kicad/-/issues/2752
177 SHAPE_POLY_SET frontCopper;
178
179 KI_TEST::FillZones( m_board.get() );
180
181 frontCopper = SHAPE_POLY_SET();
182
183 for( ZONE* zone : m_board->Zones() )
184 {
185 if( zone->GetLayerSet().Contains( F_Cu ) )
186 {
187 frontCopper.BooleanAdd( *zone->GetFilledPolysList( F_Cu ) );
188 }
189 }
190
191 BOOST_CHECK_EQUAL( frontCopper.OutlineCount(), 2 );
192}
193
194
195static const std::vector<wxString> RegressionZoneFillTests_tests = {
196 "issue18",
197 "issue2568",
198 "issue3812",
199 "issue5102",
200 "issue5313",
201 "issue5320",
202 "issue5567",
203 "issue5830",
204 "issue6039",
205 "issue6260",
206 "issue6284",
207 "issue7086",
208 "issue14294", // Bad Clipper2 fill
209 "fill_bad" // Missing zone clearance expansion
210};
211
212
214 boost::unit_test::data::make( RegressionZoneFillTests_tests ), relPath )
215{
216 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
217
218 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
219
220 KI_TEST::FillZones( m_board.get() );
221
222 std::vector<DRC_ITEM> violations;
223
225 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
226 const std::function<void( PCB_MARKER* )>& aPathGenerator )
227 {
228 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
229 violations.push_back( *aItem );
230 } );
231
232 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
233
234 if( violations.empty() )
235 {
236 BOOST_CHECK_EQUAL( 1, 1 ); // quiet "did not check any assertions" warning
237 BOOST_TEST_MESSAGE( wxString::Format( "Zone fill regression: %s passed", relPath ) );
238 }
239 else
240 {
241 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::INCH );
242
243 std::map<KIID, EDA_ITEM*> itemMap;
244 m_board->FillItemMap( itemMap );
245
246 for( const DRC_ITEM& item : violations )
247 BOOST_TEST_MESSAGE( item.ShowReport( &unitsProvider, RPT_SEVERITY_ERROR, itemMap ) );
248
249 BOOST_ERROR( wxString::Format( "Zone fill regression: %s failed", relPath ) );
250 }
251}
252
253
255{
256 KI_TEST::LoadBoard( m_settingsManager, "line_ending_zone_flood/line_ending_zone_flood", m_board );
257
258 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
259
260 KI_TEST::FillZones( m_board.get() );
261
262 std::vector<DRC_ITEM> violations;
263
265 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
266 const std::function<void( PCB_MARKER* )>& )
267 {
268 if( aItem->GetErrorCode() == DRCE_CLEARANCE || aItem->GetErrorCode() == DRCE_SHORTING_ITEMS )
269 {
270 violations.push_back( *aItem );
271 }
272 } );
273
274 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
276
277 if( violations.empty() )
278 {
279 BOOST_CHECK_EQUAL( violations.size(), 0 );
280 return;
281 }
282
283 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::INCH );
284 std::map<KIID, EDA_ITEM*> itemMap;
285 m_board->FillItemMap( itemMap );
286
287 for( const DRC_ITEM& item : violations )
288 BOOST_TEST_MESSAGE( item.ShowReport( &unitsProvider, RPT_SEVERITY_ERROR, itemMap ) );
289
290 BOOST_ERROR( wxString::Format( "Zone fill line-ending regression failed with %d clearance/shorting violations",
291 (int) violations.size() ) );
292}
293
294
304BOOST_FIXTURE_TEST_CASE( RegressionZoneClearanceWithIterativeRefill, ZONE_FILL_TEST_FIXTURE )
305{
306 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
307 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
308
309 struct ScopeGuard
310 {
311 bool& ref;
312 bool orig;
313 ~ScopeGuard() { ref = orig; }
314 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
315
316 auto runDrcClearanceCheck =
317 [this]( bool aIterative ) -> int
318 {
319 ADVANCED_CFG& innerCfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
320 innerCfg.m_ZoneFillIterativeRefill = aIterative;
321
322 KI_TEST::LoadBoard( m_settingsManager, "issue23053/issue23053", m_board );
323
324 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
325
326 KI_TEST::FillZones( m_board.get() );
327
328 std::vector<DRC_ITEM> violations;
329
330 std::map<KIID, EDA_ITEM*> itemMap;
331 m_board->FillItemMap( itemMap );
332 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::MM );
333
335 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
336 const std::function<void( PCB_MARKER* )>& aPathGenerator )
337 {
338 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
339 {
340 BOARD_ITEM* itemA = m_board->ResolveItem( aItem->GetMainItemID() );
341 BOARD_ITEM* itemB = m_board->ResolveItem( aItem->GetAuxItemID() );
342
343 if( dynamic_cast<ZONE*>( itemA ) && dynamic_cast<ZONE*>( itemB ) )
344 {
345 violations.push_back( *aItem );
346
347 BOOST_TEST_MESSAGE( aItem->ShowReport( &unitsProvider, RPT_SEVERITY_ERROR,
348 itemMap ) );
349 }
350 }
351 } );
352
353 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
354
355 return static_cast<int>( violations.size() );
356 };
357
358 int iterativeViolations = runDrcClearanceCheck( true );
359
360 BOOST_CHECK_MESSAGE( iterativeViolations == 0,
361 wxString::Format( "Iterative refill produced %d zone-to-zone clearance "
362 "violations (expected 0)", iterativeViolations ) );
363
364 int nonIterativeViolations = runDrcClearanceCheck( false );
365
366 BOOST_CHECK_MESSAGE( nonIterativeViolations == 0,
367 wxString::Format( "Non-iterative refill produced %d zone-to-zone clearance "
368 "violations (expected 0)", nonIterativeViolations ) );
369}
370
371
372static const std::vector<wxString> RegressionSliverZoneFillTests_tests = {
373 "issue16182" // Slivers
374};
375
376
377BOOST_DATA_TEST_CASE_F( ZONE_FILL_TEST_FIXTURE, RegressionSliverZoneFillTests,
378 boost::unit_test::data::make( RegressionSliverZoneFillTests_tests ), relPath )
379{
380 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
381
382 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
383
384 KI_TEST::FillZones( m_board.get() );
385
386 std::vector<DRC_ITEM> violations;
387
389 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
390 const std::function<void( PCB_MARKER* )>& aPathGenerator )
391 {
392 if( aItem->GetErrorCode() == DRCE_COPPER_SLIVER )
393 violations.push_back( *aItem );
394 } );
395
396 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
397
398 if( violations.empty() )
399 {
400 BOOST_CHECK_EQUAL( 1, 1 ); // quiet "did not check any assertions" warning
401 BOOST_TEST_MESSAGE( wxString::Format( "Zone fill copper sliver regression: %s passed", relPath ) );
402 }
403 else
404 {
405 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::INCH );
406
407 std::map<KIID, EDA_ITEM*> itemMap;
408 m_board->FillItemMap( itemMap );
409
410 for( const DRC_ITEM& item : violations )
411 BOOST_TEST_MESSAGE( item.ShowReport( &unitsProvider, RPT_SEVERITY_ERROR, itemMap ) );
412
413 BOOST_ERROR( wxString::Format( "Zone fill copper sliver regression: %s failed", relPath ) );
414 }
415}
416
417
418static const std::vector<std::pair<wxString,int>> RegressionTeardropFill_tests = {
419 { "teardrop_issue_JPC2", 5 }, // Arcs with teardrops connecting to pads
420};
421
422
424 boost::unit_test::data::make( RegressionTeardropFill_tests ), test )
425{
426 const wxString& relPath = test.first;
427 const int count = test.second;
428
429 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
430 KI_TEST::FillZones( m_board.get() );
431
432 int zoneCount = 0;
433
434 for( ZONE* zone : m_board->Zones() )
435 {
436 if( zone->IsTeardropArea() )
437 zoneCount++;
438 }
439
440 BOOST_CHECK_MESSAGE( zoneCount == count, "Expected " << count << " teardrop zones in " << relPath << ", found "
441 << zoneCount );
442}
443
444
446{
447
448 std::vector<wxString> tests = { { "issue19956/issue19956" } // Arcs with teardrops connecting to pads
449 };
450
451 for( const wxString& relPath : tests )
452 {
453 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
454 KI_TEST::FillZones( m_board.get() );
455
456 for( ZONE* zone : m_board->Zones() )
457 {
458 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
459 {
460 std::shared_ptr<SHAPE> a_shape( zone->GetEffectiveShape( layer ) );
461
462 for( PAD* pad : m_board->GetPads() )
463 {
464 std::shared_ptr<SHAPE> pad_shape( pad->GetEffectiveShape( layer ) );
465 int clearance = pad_shape->GetClearance( a_shape.get() );
466 BOOST_CHECK_MESSAGE( pad->GetNetCode() == zone->GetNetCode() || clearance != 0,
467 wxString::Format( "Pad %s from Footprint %s has net code %s and "
468 "is connected to zone with net code %s",
469 pad->GetNumber(),
470 pad->GetParentFootprint()->GetReferenceAsString(),
471 pad->GetNetname(),
472 zone->GetNetname() ) );
473 }
474 }
475 }
476 }
477}
478
479
494BOOST_FIXTURE_TEST_CASE( RegressionZonePriorityIsolatedIslands, ZONE_FILL_TEST_FIXTURE )
495{
496 // Enable iterative refill to fix issue 21746
497 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
498 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
499 cfg.m_ZoneFillIterativeRefill = true;
500
501 // Restore config at end of scope to avoid polluting other tests
502 struct ScopeGuard
503 {
504 bool& ref;
505 bool orig;
506 ~ScopeGuard() { ref = orig; }
507 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
508
509 KI_TEST::LoadBoard( m_settingsManager, "issue21746/issue21746", m_board );
510
511 KI_TEST::FillZones( m_board.get() );
512
513 // Find the GND zone
514 ZONE* gndZone = nullptr;
515
516 for( ZONE* zone : m_board->Zones() )
517 {
518 if( zone->GetNetname() == "GND" )
519 {
520 gndZone = zone;
521 break;
522 }
523 }
524
525 BOOST_REQUIRE_MESSAGE( gndZone != nullptr, "GND zone not found in test board" );
526
527 // Calculate board outline area
528 SHAPE_POLY_SET boardOutline;
529 bool hasOutline = m_board->GetBoardPolygonOutlines( boardOutline, true );
530 BOOST_REQUIRE_MESSAGE( hasOutline, "Board outline not found" );
531
532 double boardArea = 0.0;
533
534 for( int i = 0; i < boardOutline.OutlineCount(); i++ )
535 boardArea += boardOutline.Outline( i ).Area();
536
537 // Get GND zone filled area
538 gndZone->CalculateFilledArea();
539 double gndFilledArea = gndZone->GetFilledArea();
540
541 // The GND zone should fill at least 25% of the board area
542 // With the bug, it fills almost nothing because VDD knocks it out
543 double fillRatio = gndFilledArea / boardArea;
544
545 BOOST_TEST_MESSAGE( wxString::Format( "Board area: %.2f sq mm, GND filled area: %.2f sq mm, Fill ratio: %.1f%%",
546 boardArea / 1e6,
547 gndFilledArea / 1e6,
548 fillRatio * 100.0 ) );
549
550 BOOST_CHECK_MESSAGE( fillRatio >= 0.25,
551 wxString::Format( "GND zone fill ratio %.1f%% is less than expected 25%%. This indicates "
552 "issue 21746 - lower priority zones not filling areas where higher "
553 "priority isolated islands were removed.",
554 fillRatio * 100.0 ) );
555}
556
557
571BOOST_FIXTURE_TEST_CASE( RegressionViaFlashingUnreachableZone, ZONE_FILL_TEST_FIXTURE )
572{
573 KI_TEST::LoadBoard( m_settingsManager, "issue22010/issue22010", m_board );
574
575 KI_TEST::FillZones( m_board.get() );
576
577 // Find vias with zone_layer_connections set for In1.Cu or In2.Cu
578 // After filling, vias that the zone doesn't actually reach should NOT be flashed
579 int viasWithUnreachableFlashing = 0;
580 int totalConditionalVias = 0;
581
582 PCB_LAYER_ID in1Cu = m_board->GetLayerID( wxT( "In1.Cu" ) );
583 PCB_LAYER_ID in2Cu = m_board->GetLayerID( wxT( "In2.Cu" ) );
584
585 for( PCB_TRACK* track : m_board->Tracks() )
586 {
587 if( track->Type() != PCB_VIA_T )
588 continue;
589
590 PCB_VIA* via = static_cast<PCB_VIA*>( track );
591
592 if( !via->GetRemoveUnconnected() )
593 continue;
594
595 totalConditionalVias++;
596
597 // Check if via is flashed on In1.Cu or In2.Cu
598 bool flashedOnIn1 = via->FlashLayer( in1Cu );
599 bool flashedOnIn2 = via->FlashLayer( in2Cu );
600
601 if( !flashedOnIn1 && !flashedOnIn2 )
602 continue;
603
604 VECTOR2I viaCenter = via->GetPosition();
605 int holeRadius = via->GetDrillValue() / 2;
606
607 // Check if any zone fill actually reaches this via
608 bool zoneReachesVia = false;
609
610 for( ZONE* zone : m_board->Zones() )
611 {
612 if( zone->GetIsRuleArea() )
613 continue;
614
615 if( zone->GetNetCode() != via->GetNetCode() )
616 continue;
617
618 for( PCB_LAYER_ID layer : { in1Cu, in2Cu } )
619 {
620 if( !zone->IsOnLayer( layer ) )
621 continue;
622
623 if( !zone->HasFilledPolysForLayer( layer ) )
624 continue;
625
626 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
627
628 if( fill->Contains( viaCenter, -1, holeRadius ) )
629 {
630 zoneReachesVia = true;
631 break;
632 }
633 }
634
635 if( zoneReachesVia )
636 break;
637 }
638
639 // If via is flashed but zone doesn't reach it, that's the bug
640 if( !zoneReachesVia && ( flashedOnIn1 || flashedOnIn2 ) )
641 viasWithUnreachableFlashing++;
642 }
643
644 BOOST_TEST_MESSAGE( wxString::Format( "Total conditional vias: %d, Vias with unreachable flashing: %d",
645 totalConditionalVias,
646 viasWithUnreachableFlashing ) );
647
648 BOOST_CHECK_MESSAGE( viasWithUnreachableFlashing == 0,
649 wxString::Format( "Found %d vias flashed on zone layers where the zone fill doesn't "
650 "actually reach them. This indicates issue 22010 is not fixed.",
651 viasWithUnreachableFlashing ) );
652}
653
654
668{
669 KI_TEST::LoadBoard( m_settingsManager, "issue12964/issue12964", m_board );
670
671 KI_TEST::FillZones( m_board.get() );
672
673 int viasShortingZones = 0;
674 int totalConditionalVias = 0;
675
676 for( PCB_TRACK* track : m_board->Tracks() )
677 {
678 if( track->Type() != PCB_VIA_T )
679 continue;
680
681 PCB_VIA* via = static_cast<PCB_VIA*>( track );
682
683 if( !via->GetRemoveUnconnected() )
684 continue;
685
686 totalConditionalVias++;
687
688 VECTOR2I viaCenter = via->GetPosition();
689
690 for( ZONE* zone : m_board->Zones() )
691 {
692 if( zone->GetIsRuleArea() )
693 continue;
694
695 if( zone->GetNetCode() == via->GetNetCode() )
696 continue;
697
698 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
699 {
700 if( !via->FlashLayer( layer ) )
701 continue;
702
703 if( !zone->HasFilledPolysForLayer( layer ) )
704 continue;
705
706 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
707 int viaRadius = via->GetWidth( layer ) / 2;
708
709 if( fill->Contains( viaCenter, -1, viaRadius ) )
710 {
711 BOOST_TEST_MESSAGE( wxString::Format( "Via at (%d, %d) on net %s is flashing on layer %s where "
712 "zone net %s is filled - this creates a short!",
713 viaCenter.x, viaCenter.y,
714 via->GetNetname(),
715 m_board->GetLayerName( layer ),
716 zone->GetNetname() ) );
717 viasShortingZones++;
718 }
719 }
720 }
721 }
722
723 BOOST_TEST_MESSAGE( wxString::Format( "Total conditional vias: %d, Vias shorting zones: %d",
724 totalConditionalVias,
725 viasShortingZones ) );
726
727 BOOST_CHECK_MESSAGE( viasShortingZones == 0,
728 wxString::Format( "Found %d vias flashed on layers where they short to zones with "
729 "different nets. This indicates issue 12964 is not fixed.",
730 viasShortingZones ) );
731}
732
733
746BOOST_FIXTURE_TEST_CASE( HatchZoneThermalConnectivity, ZONE_FILL_TEST_FIXTURE )
747{
748 KI_TEST::LoadBoard( m_settingsManager, "hatch_thermal_connectivity/hatch_thermal_connectivity",
749 m_board );
750
751 KI_TEST::FillZones( m_board.get() );
752
753 m_board->BuildConnectivity();
754
755 int unconnectedCount = m_board->GetConnectivity()->GetUnconnectedCount( false );
756
757 BOOST_CHECK_MESSAGE( unconnectedCount == 0,
758 wxString::Format( "Found %d unconnected items after zone fill. Hatch zone thermal "
759 "reliefs should maintain connectivity even with large hatch gaps.",
760 unconnectedCount ) );
761}
762
763
780BOOST_FIXTURE_TEST_CASE( RegressionShallowArcZoneFill, ZONE_FILL_TEST_FIXTURE )
781{
782 KI_TEST::LoadBoard( m_settingsManager, "issue22475/issue22475", m_board );
783
784 PCB_LAYER_ID in1Cu = m_board->GetLayerID( wxT( "In1.Cu" ) );
785
786 ZONE* gndZone = nullptr;
787
788 for( ZONE* zone : m_board->Zones() )
789 {
790 if( zone->GetNetname() == "GND" && zone->IsOnLayer( in1Cu ) )
791 {
792 gndZone = zone;
793 break;
794 }
795 }
796
797 BOOST_REQUIRE_MESSAGE( gndZone != nullptr, "GND zone on In1.Cu not found in test board" );
798
799 if( !gndZone )
800 return;
801
802 KI_TEST::FillZones( m_board.get() );
803
804 BOOST_REQUIRE_MESSAGE( gndZone->HasFilledPolysForLayer( in1Cu ), "GND zone has no fill on In1.Cu" );
805
806 const std::shared_ptr<SHAPE_POLY_SET>& fill = gndZone->GetFilledPolysList( in1Cu );
807
808 // The zone fill should produce a single contiguous outline. Multiple outlines
809 // indicate disconnected fill areas caused by malformed clearance holes.
810 BOOST_CHECK_EQUAL( fill->OutlineCount(), 1 );
811
812 double zoneOutlineArea = gndZone->Outline()->Area();
813
814 BOOST_REQUIRE_MESSAGE( zoneOutlineArea > 0.0, "Zone outline area must be positive" );
815
816 double fillArea = 0.0;
817
818 for( int i = 0; i < fill->OutlineCount(); i++ )
819 fillArea += std::abs( fill->Outline( i ).Area() );
820
821 double fillRatio = fillArea / zoneOutlineArea;
822
823 // The zone should be mostly filled. A low fill ratio indicates excessive voids
824 // from malformed clearance holes around shallow arcs.
825 BOOST_CHECK_GE( fillRatio, 0.90 );
826}
827
828
841BOOST_FIXTURE_TEST_CASE( RegressionIterativeRefillRespectsKeepouts, ZONE_FILL_TEST_FIXTURE )
842{
843 // Enable iterative refill
844 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
845 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
846 cfg.m_ZoneFillIterativeRefill = true;
847
848 struct ScopeGuard
849 {
850 bool& ref;
851 bool orig;
852 ~ScopeGuard() { ref = orig; }
853 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
854
855 KI_TEST::LoadBoard( m_settingsManager, "issue22809/issue22809", m_board );
856
857 KI_TEST::FillZones( m_board.get() );
858
859 // Find all zone keepouts
860 std::vector<ZONE*> keepouts;
861
862 for( ZONE* zone : m_board->Zones() )
863 {
864 if( zone->GetIsRuleArea() && zone->GetDoNotAllowZoneFills() )
865 keepouts.push_back( zone );
866 }
867
868 BOOST_REQUIRE_MESSAGE( !keepouts.empty(), "No zone keepouts found in test board" );
869
870 // For each keepout, check that no zone fill exists inside it
871 int violationCount = 0;
872
873 for( ZONE* keepout : keepouts )
874 {
875 for( PCB_LAYER_ID layer : keepout->GetLayerSet().Seq() )
876 {
877 SHAPE_POLY_SET keepoutOutline( *keepout->Outline() );
878 keepoutOutline.ClearArcs();
879
880 for( ZONE* zone : m_board->Zones() )
881 {
882 if( zone->GetIsRuleArea() )
883 continue;
884
885 if( !zone->IsOnLayer( layer ) )
886 continue;
887
888 if( !zone->HasFilledPolysForLayer( layer ) )
889 continue;
890
891 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
892
893 // Check if any fill intersects the keepout
894 SHAPE_POLY_SET intersection = *fill;
895 intersection.BooleanIntersection( keepoutOutline );
896
897 if( intersection.OutlineCount() > 0 )
898 {
899 double intersectionArea = 0;
900
901 for( int i = 0; i < intersection.OutlineCount(); i++ )
902 intersectionArea += std::abs( intersection.Outline( i ).Area() );
903
904 // Allow for small numerical errors (less than 1 square mm)
905 if( intersectionArea > 1e6 )
906 {
907 BOOST_TEST_MESSAGE( wxString::Format( "Zone %s fill on layer %s overlaps keepout by %.2f sq mm",
908 zone->GetNetname(),
909 m_board->GetLayerName( layer ),
910 intersectionArea / 1e6 ) );
911 violationCount++;
912 }
913 }
914 }
915 }
916 }
917
918 BOOST_CHECK_MESSAGE( violationCount == 0,
919 wxString::Format( "Found %d zone fills overlapping keepout areas. This indicates "
920 "issue 22809 - iterative refiller ignores zone keepouts.",
921 violationCount ) );
922}
923
924
938BOOST_FIXTURE_TEST_CASE( RegressionTHPadInnerLayerFlashing, ZONE_FILL_TEST_FIXTURE )
939{
940 KI_TEST::LoadBoard( m_settingsManager, "issue22826/issue22826", m_board );
941
942 KI_TEST::FillZones( m_board.get() );
943
944 PCB_LAYER_ID in2Cu = m_board->GetLayerID( wxT( "In2.Cu" ) );
945 int padsWithMissingFlashing = 0;
946 int totalConditionalPads = 0;
947
948 for( FOOTPRINT* footprint : m_board->Footprints() )
949 {
950 for( PAD* pad : footprint->Pads() )
951 {
952 if( !pad->GetRemoveUnconnected() )
953 continue;
954
955 if( !pad->HasHole() )
956 continue;
957
958 if( pad->GetNetname() != "VBUS_DUT" && pad->GetNetname() != "VBUS_DBG" )
959 continue;
960
961 totalConditionalPads++;
962
963 // Check if the pad should flash on In2.Cu
964 bool shouldFlash = false;
965
966 for( ZONE* zone : m_board->Zones() )
967 {
968 if( zone->GetIsRuleArea() )
969 continue;
970
971 if( zone->GetNetCode() != pad->GetNetCode() )
972 continue;
973
974 if( !zone->IsOnLayer( in2Cu ) )
975 continue;
976
977 if( zone->Outline()->Contains( pad->GetPosition() ) )
978 {
979 shouldFlash = true;
980 break;
981 }
982 }
983
984 if( shouldFlash && !pad->FlashLayer( in2Cu ) )
985 {
986 BOOST_TEST_MESSAGE( wxString::Format( "Pad %s at (%d, %d) on net %s is inside zone but not flashing "
987 "on In2.Cu",
988 pad->GetNumber(),
989 pad->GetPosition().x,
990 pad->GetPosition().y,
991 pad->GetNetname() ) );
992 padsWithMissingFlashing++;
993 }
994 }
995 }
996
997 BOOST_TEST_MESSAGE( wxString::Format( "Total conditional pads: %d, Pads with missing flashing: %d",
998 totalConditionalPads,
999 padsWithMissingFlashing ) );
1000
1001 BOOST_CHECK_MESSAGE( padsWithMissingFlashing == 0,
1002 wxString::Format( "Found %d TH pads that should flash on inner layers but don't. This "
1003 "indicates issue 22826 is not fixed.",
1004 padsWithMissingFlashing ) );
1005}
1006
1007
1017BOOST_FIXTURE_TEST_CASE( RegressionThermalReliefAnnularRing45, ZONE_FILL_TEST_FIXTURE )
1018{
1019 KI_TEST::LoadBoard( m_settingsManager, "issue24865/issue24865", m_board );
1020
1021 KI_TEST::FillZones( m_board.get() );
1022
1023 const PCB_LAYER_ID innerLayers[] = { m_board->GetLayerID( wxT( "In1.Cu" ) ),
1024 m_board->GetLayerID( wxT( "In2.Cu" ) ) };
1025
1026 int padsWithMissingFlashing = 0;
1027 int totalConditionalPads = 0;
1028
1029 for( FOOTPRINT* footprint : m_board->Footprints() )
1030 {
1031 for( PAD* pad : footprint->Pads() )
1032 {
1033 if( !pad->GetRemoveUnconnected() || !pad->HasHole() )
1034 continue;
1035
1036 for( PCB_LAYER_ID layer : innerLayers )
1037 {
1038 bool shouldFlash = false;
1039
1040 for( ZONE* zone : m_board->Zones() )
1041 {
1042 if( zone->GetIsRuleArea() || zone->GetNetCode() != pad->GetNetCode() )
1043 continue;
1044
1045 if( !zone->IsOnLayer( layer ) )
1046 continue;
1047
1048 if( zone->Outline()->Contains( pad->GetPosition() ) )
1049 {
1050 shouldFlash = true;
1051 break;
1052 }
1053 }
1054
1055 if( !shouldFlash )
1056 continue;
1057
1058 totalConditionalPads++;
1059
1060 if( !pad->FlashLayer( layer ) )
1061 {
1063 wxString::Format( "Pad %s (drill %.2f mm, spoke angle %.0f deg) on net %s is inside the "
1064 "zone but not flashing on %s",
1065 pad->GetNumber(),
1066 pcbIUScale.IUTomm( pad->GetDrillSizeX() ),
1067 pad->GetThermalSpokeAngle().AsDegrees(),
1068 pad->GetNetname(),
1069 m_board->GetLayerName( layer ) ) );
1070 padsWithMissingFlashing++;
1071 }
1072 }
1073 }
1074 }
1075
1076 BOOST_TEST_MESSAGE( wxString::Format( "Pads inside inner-layer zones: %d, missing flashing: %d",
1077 totalConditionalPads,
1078 padsWithMissingFlashing ) );
1079
1080 BOOST_CHECK_MESSAGE( padsWithMissingFlashing == 0,
1081 wxString::Format( "Found %d TH pads inside a same-net inner-layer zone that lost their "
1082 "annular ring after fill. This indicates issue 24865 is not fixed.",
1083 padsWithMissingFlashing ) );
1084}
1085
1086
1095BOOST_FIXTURE_TEST_CASE( RegressionRoundRectTeardropGeometry, ZONE_FILL_TEST_FIXTURE )
1096{
1097 KI_TEST::LoadBoard( m_settingsManager, "issue19405_roundrect_teardrop", m_board );
1098
1099 // Set up tool manager for teardrop generation
1100 TOOL_MANAGER toolMgr;
1101 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1102
1103 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1104 toolMgr.RegisterTool( dummyTool );
1105
1106 // Generate teardrops
1107 BOARD_COMMIT commit( dummyTool );
1108 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1109 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1110
1111 if( !commit.Empty() )
1112 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1113
1114 // Find teardrop zones
1115 int teardropCount = 0;
1116 bool foundBadTeardrop = false;
1117
1118 for( ZONE* zone : m_board->Zones() )
1119 {
1120 if( !zone->IsTeardropArea() )
1121 continue;
1122
1123 teardropCount++;
1124
1125 // Get the teardrop outline
1126 const SHAPE_POLY_SET* outline = zone->Outline();
1127
1128 if( !outline || outline->OutlineCount() == 0 )
1129 continue;
1130
1131 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1132
1133 // Check that the teardrop polygon is convex or at least doesn't have
1134 // any sharp concave angles that would indicate intersection with the pad corner.
1135 // A well-formed teardrop should have all turns in the same direction
1136 // (or very close to it) except at the pad anchor points.
1137 int concaveCount = 0;
1138
1139 for( int i = 0; i < chain.PointCount(); i++ )
1140 {
1141 int prev = ( i == 0 ) ? chain.PointCount() - 1 : i - 1;
1142 int next = ( i + 1 ) % chain.PointCount();
1143
1144 VECTOR2I v1 = chain.CPoint( i ) - chain.CPoint( prev );
1145 VECTOR2I v2 = chain.CPoint( next ) - chain.CPoint( i );
1146
1147 // Cross product gives handedness of turn
1148 int64_t cross = (int64_t) v1.x * v2.y - (int64_t) v1.y * v2.x;
1149
1150 // Count significant concave turns (negative cross product for CCW polygons)
1151 // Small values are numerical noise
1152 if( cross < -1000 )
1153 concaveCount++;
1154 }
1155
1156 // A teardrop should have at most 2-3 concave points (at the pad anchor points)
1157 // Many concave points indicate the curve is intersecting the pad corner
1158 if( concaveCount > 5 )
1159 {
1160 BOOST_TEST_MESSAGE( wxString::Format( "Teardrop has %d concave vertices, indicating possible corner "
1161 "intersection",
1162 concaveCount ) );
1163 foundBadTeardrop = true;
1164 }
1165 }
1166
1167 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1168
1169 BOOST_CHECK_MESSAGE( !foundBadTeardrop, "Found teardrop with excessive concave vertices, indicating "
1170 "issue 19405 - teardrop curve intersecting rounded rectangle corner" );
1171}
1172
1173
1180{
1181 KI_TEST::LoadBoard( m_settingsManager, "teardrop_spike", m_board );
1182
1183 TOOL_MANAGER toolMgr;
1184 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1185
1186 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1187 toolMgr.RegisterTool( dummyTool );
1188
1189 BOARD_COMMIT commit( dummyTool );
1190 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1191 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1192
1193 if( !commit.Empty() )
1194 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1195
1196 int teardropCount = 0;
1197 bool foundSpike = false;
1198
1199 const int maxError = m_board->GetDesignSettings().m_MaxError;
1200
1201 for( ZONE* zone : m_board->Zones() )
1202 {
1203 if( !zone->IsTeardropArea() )
1204 continue;
1205
1206 teardropCount++;
1207
1208 PCB_LAYER_ID layer = zone->GetFirstLayer();
1209 int netcode = zone->GetNetCode();
1210
1211 // A well-formed teardrop only ever covers the copper it bridges: the pads/vias it
1212 // anchors on and the track(s) it follows. Build that corridor from all copper on the
1213 // teardrop's net and layer (generously inflated) and require the teardrop to lie
1214 // inside it. A spike sweeps area outside the corridor.
1215 SHAPE_POLY_SET corridor;
1216
1217 for( FOOTPRINT* fp : m_board->Footprints() )
1218 {
1219 for( PAD* pad : fp->Pads() )
1220 {
1221 if( pad->GetNetCode() == netcode && pad->IsOnLayer( layer ) )
1222 pad->TransformShapeToPolygon( corridor, layer, 0, maxError, ERROR_OUTSIDE );
1223 }
1224 }
1225
1226 for( PCB_TRACK* track : m_board->Tracks() )
1227 {
1228 if( track->GetNetCode() == netcode && track->IsOnLayer( layer ) )
1229 track->TransformShapeToPolygon( corridor, layer, 0, maxError, ERROR_OUTSIDE );
1230 }
1231
1232 // Inflate by a full track width so the teardrop's flare toward the pad, which is
1233 // legitimately wider than the bare track, is comfortably inside the corridor.
1234 corridor.Inflate( pcbIUScale.mmToIU( 0.127 ), CORNER_STRATEGY::ROUND_ALL_CORNERS,
1235 maxError );
1236 corridor.Simplify();
1237
1238 SHAPE_POLY_SET outside = *zone->Outline();
1239 outside.BooleanSubtract( corridor );
1240
1241 double tdArea = std::abs( zone->Outline()->Area() );
1242 double outArea = std::abs( outside.Area() );
1243 double ratio = tdArea > 0 ? outArea / tdArea : 0.0;
1244
1245 BOOST_TEST_MESSAGE( wxString::Format( "Teardrop on layer %d: area %.0f, area outside corridor %.0f (%.1f%%)",
1246 (int) layer,
1247 tdArea,
1248 outArea,
1249 ratio * 100.0 ) );
1250
1251 if( ratio > 0.02 )
1252 {
1253 foundSpike = true;
1254 BOOST_TEST_MESSAGE( wxString::Format( "Teardrop on layer %d sweeps %.1f%% of its area outside the "
1255 "track/pad corridor (spike)",
1256 (int) layer,
1257 ratio * 100.0 ) );
1258 }
1259 }
1260
1261 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1262 BOOST_CHECK_MESSAGE( !foundSpike, "A teardrop vertex spikes outside the track/pad corridor it should follow" );
1263}
1264
1265
1274BOOST_FIXTURE_TEST_CASE( RegressionTeardropCustomPadAnchor, ZONE_FILL_TEST_FIXTURE )
1275{
1276 KI_TEST::LoadBoard( m_settingsManager, "teardrop_custom_pad_anchor", m_board );
1277
1278 TOOL_MANAGER toolMgr;
1279 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1280
1281 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1282 toolMgr.RegisterTool( dummyTool );
1283
1284 BOARD_COMMIT commit( dummyTool );
1285 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1286 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1287
1288 if( !commit.Empty() )
1289 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1290
1291 // A teardrop reaches at most its max length along the track plus the pad's minor width into
1292 // the pad, so their sum bounds every legitimate vertex and still sits far below the 7mm spike
1293 int maxReach = 0;
1294
1295 for( FOOTPRINT* fp : m_board->Footprints() )
1296 {
1297 for( PAD* pad : fp->Pads() )
1298 {
1299 const TEARDROP_PARAMETERS& prms = pad->GetTeardropParams();
1300 VECTOR2I size = pad->GetSize( PADSTACK::ALL_LAYERS );
1301
1302 maxReach = std::max( maxReach, prms.m_TdMaxLen + std::min( size.x, size.y ) );
1303 }
1304 }
1305
1306 BOOST_REQUIRE( maxReach > 0 );
1307
1308 int teardropCount = 0;
1309 bool foundSpike = false;
1310
1311 for( ZONE* zone : m_board->Zones() )
1312 {
1313 if( !zone->IsTeardropArea() )
1314 continue;
1315
1316 teardropCount++;
1317
1318 PCB_LAYER_ID layer = zone->GetFirstLayer();
1319
1320 for( const VECTOR2I& pt : zone->Outline()->Outline( 0 ).CPoints() )
1321 {
1322 SEG::ecoord bestSq = std::numeric_limits<SEG::ecoord>::max();
1323
1324 for( PCB_TRACK* track : m_board->Tracks() )
1325 {
1326 if( !track->IsOnLayer( layer ) )
1327 continue;
1328
1329 bestSq = std::min( bestSq, SEG( track->GetStart(), track->GetEnd() ).SquaredDistance( pt ) );
1330 }
1331
1332 double dist = std::sqrt( (double) bestSq );
1333
1334 if( dist > maxReach )
1335 {
1336 foundSpike = true;
1337 BOOST_TEST_MESSAGE( wxString::Format( "Teardrop vertex (%.4f, %.4f) is %.4f mm from the nearest "
1338 "track, max allowed %.4f mm",
1339 pcbIUScale.IUTomm( pt.x ), pcbIUScale.IUTomm( pt.y ),
1340 pcbIUScale.IUTomm( KiROUND( dist ) ),
1341 pcbIUScale.IUTomm( maxReach ) ) );
1342 }
1343 }
1344 }
1345
1346 // Both pads numbered "2" cover the track end, so without this a dropped custom-pad teardrop
1347 // would leave the circular pad's teardrop passing the spike check alone
1348 BOOST_CHECK_MESSAGE( teardropCount == 2, wxString::Format( "Expected a teardrop on each of the two pads covering "
1349 "the track end, found %d",
1350 teardropCount ) );
1351 BOOST_CHECK_MESSAGE( !foundSpike, "A teardrop reaches far past its anchor track, indicating it was built around "
1352 "the custom pad's anchor position instead of the copper the track enters" );
1353}
1354
1355
1357{
1358 KI_TEST::LoadBoard( m_settingsManager, "teardrop_arc_anchor", m_board );
1359
1360 TOOL_MANAGER toolMgr;
1361 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1362
1363 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1364 toolMgr.RegisterTool( dummyTool );
1365
1366 BOARD_COMMIT commit( dummyTool );
1367 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1368 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1369
1370 if( !commit.Empty() )
1371 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1372
1373 BOOST_REQUIRE( !m_board->Footprints().empty() );
1374 BOOST_REQUIRE( !m_board->Footprints().front()->Pads().empty() );
1375
1376 PAD* pad = m_board->Footprints().front()->Pads().front();
1377
1378 const int minEdgeLen = 2 * m_board->GetDesignSettings().m_MaxError;
1379
1380 int teardropCount = 0;
1381
1382 for( ZONE* zone : m_board->Zones() )
1383 {
1384 if( !zone->IsTeardropArea() )
1385 continue;
1386
1387 teardropCount++;
1388
1389 const SHAPE_LINE_CHAIN& chain = zone->Outline()->COutline( 0 );
1390
1391 SEG throat;
1392 SEG::ecoord bestSq = -1;
1393
1394 for( int ii = 0; ii < chain.SegmentCount(); ii++ )
1395 {
1396 SEG seg = chain.CSegment( ii );
1397
1398 if( seg.Length() < minEdgeLen )
1399 continue;
1400
1401 SEG::ecoord distSq = ( seg.Center() - pad->GetPosition() ).SquaredEuclideanNorm();
1402
1403 if( distSq > bestSq )
1404 {
1405 bestSq = distSq;
1406 throat = seg;
1407 }
1408 }
1409
1410 BOOST_REQUIRE( bestSq >= 0 );
1411
1412 for( const VECTOR2I& pt : { throat.A, throat.B } )
1413 {
1414 bool onTrack = false;
1415
1416 for( PCB_TRACK* track : m_board->Tracks() )
1417 onTrack |= track->HitTest( pt, 0 );
1418
1419 BOOST_CHECK_MESSAGE( onTrack, wxString::Format( "Teardrop anchor (%.6f, %.6f) mm is off the "
1420 "track copper",
1421 pcbIUScale.IUTomm( pt.x ), pcbIUScale.IUTomm( pt.y ) ) );
1422 }
1423 }
1424
1425 BOOST_CHECK_MESSAGE( teardropCount == 1, wxString::Format( "Expected 1 teardrop zone, found %d", teardropCount ) );
1426}
1427
1428
1437{
1438 KI_TEST::LoadBoard( m_settingsManager, "oval_teardrop", m_board );
1439
1440 // Set up tool manager for teardrop generation
1441 TOOL_MANAGER toolMgr;
1442 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1443
1444 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1445 toolMgr.RegisterTool( dummyTool );
1446
1447 // Generate teardrops
1448 BOARD_COMMIT commit( dummyTool );
1449 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1450 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1451
1452 if( !commit.Empty() )
1453 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1454
1455 // Find teardrop zones
1456 int teardropCount = 0;
1457 bool foundBadTeardrop = false;
1458
1459 for( ZONE* zone : m_board->Zones() )
1460 {
1461 if( !zone->IsTeardropArea() )
1462 continue;
1463
1464 teardropCount++;
1465
1466 const SHAPE_POLY_SET* outline = zone->Outline();
1467
1468 if( !outline || outline->OutlineCount() == 0 )
1469 continue;
1470
1471 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1472
1473 // Check for excessive concave vertices that would indicate the teardrop curve
1474 // is not tangent to the oval's semicircular end
1475 int concaveCount = 0;
1476
1477 for( int i = 0; i < chain.PointCount(); i++ )
1478 {
1479 int prev = ( i == 0 ) ? chain.PointCount() - 1 : i - 1;
1480 int next = ( i + 1 ) % chain.PointCount();
1481
1482 VECTOR2I v1 = chain.CPoint( i ) - chain.CPoint( prev );
1483 VECTOR2I v2 = chain.CPoint( next ) - chain.CPoint( i );
1484
1485 int64_t cross = (int64_t) v1.x * v2.y - (int64_t) v1.y * v2.x;
1486
1487 if( cross < -1000 )
1488 concaveCount++;
1489 }
1490
1491 if( concaveCount > 5 )
1492 {
1493 BOOST_TEST_MESSAGE( wxString::Format( "Oval teardrop has %d concave vertices", concaveCount ) );
1494 foundBadTeardrop = true;
1495 }
1496 }
1497
1498 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1499
1500 BOOST_CHECK_MESSAGE( !foundBadTeardrop, "Found teardrop with excessive concave vertices on oval pad, "
1501 "indicating curve is not tangent to semicircular end" );
1502}
1503
1504
1513{
1514 KI_TEST::LoadBoard( m_settingsManager, "large_circle_teardrop", m_board );
1515
1516 // Set up tool manager for teardrop generation
1517 TOOL_MANAGER toolMgr;
1518 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1519
1520 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1521 toolMgr.RegisterTool( dummyTool );
1522
1523 // Generate teardrops
1524 BOARD_COMMIT commit( dummyTool );
1525 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1526 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1527
1528 if( !commit.Empty() )
1529 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1530
1531 // Find the pad and its teardrop
1532 PAD* largePad = nullptr;
1533
1534 for( FOOTPRINT* fp : m_board->Footprints() )
1535 {
1536 for( PAD* pad : fp->Pads() )
1537 {
1538 if( pad->GetShape( F_Cu ) == PAD_SHAPE::CIRCLE )
1539 {
1540 largePad = pad;
1541 break;
1542 }
1543 }
1544 }
1545
1546 BOOST_REQUIRE_MESSAGE( largePad != nullptr, "Expected a circular pad in test board" );
1547
1548 int padRadius = largePad->GetSize( F_Cu ).x / 2;
1549 VECTOR2I padCenter = largePad->GetPosition();
1550
1551 // Find teardrop zones
1552 int teardropCount = 0;
1553 bool foundBadTeardrop = false;
1554
1555 for( ZONE* zone : m_board->Zones() )
1556 {
1557 if( !zone->IsTeardropArea() )
1558 continue;
1559
1560 teardropCount++;
1561
1562 const SHAPE_POLY_SET* outline = zone->Outline();
1563
1564 if( !outline || outline->OutlineCount() == 0 )
1565 continue;
1566
1567 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1568
1569 // Check for excessive concave vertices
1570 int concaveCount = 0;
1571
1572 for( int i = 0; i < chain.PointCount(); i++ )
1573 {
1574 int prev = ( i == 0 ) ? chain.PointCount() - 1 : i - 1;
1575 int next = ( i + 1 ) % chain.PointCount();
1576
1577 VECTOR2I v1 = chain.CPoint( i ) - chain.CPoint( prev );
1578 VECTOR2I v2 = chain.CPoint( next ) - chain.CPoint( i );
1579
1580 int64_t cross = (int64_t) v1.x * v2.y - (int64_t) v1.y * v2.x;
1581
1582 if( cross < -1000 )
1583 concaveCount++;
1584 }
1585
1586 if( concaveCount > 5 )
1587 {
1588 BOOST_TEST_MESSAGE( wxString::Format( "Large circle teardrop has %d concave vertices", concaveCount ) );
1589 foundBadTeardrop = true;
1590 }
1591
1592 // Also verify that the teardrop anchor points near the pad are approximately
1593 // on the circle edge (within tolerance)
1594 int maxError = m_board->GetDesignSettings().m_MaxError;
1595
1596 for( int i = 0; i < chain.PointCount(); i++ )
1597 {
1598 VECTOR2I pt = chain.CPoint( i );
1599 double dist = ( pt - padCenter ).EuclideanNorm();
1600
1601 // Points that are close to the circle should be approximately on it
1602 if( dist > padRadius * 0.5 && dist < padRadius * 1.5 )
1603 {
1604 double deviation = std::abs( dist - padRadius );
1605
1606 // Allow some tolerance for polygon approximation
1607 if( deviation > maxError * 5 && deviation < padRadius * 0.2 )
1608 {
1609 BOOST_TEST_MESSAGE( wxString::Format(
1610 "Teardrop point at distance %.2f from pad center (radius %.2f), "
1611 "deviation %.2f exceeds tolerance",
1612 dist / 1000.0, padRadius / 1000.0, deviation / 1000.0 ) );
1613 }
1614 }
1615 }
1616 }
1617
1618 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1619
1620 BOOST_CHECK_MESSAGE( !foundBadTeardrop,
1621 "Found teardrop with excessive concave vertices on large circle, "
1622 "indicating anchor points may not be on circle edge" );
1623}
1624
1625
1636BOOST_FIXTURE_TEST_CASE( RegressionCoincidentPadClearance, ZONE_FILL_TEST_FIXTURE )
1637{
1638 KI_TEST::LoadBoard( m_settingsManager, "issue23123_minimal", m_board );
1639
1640 KI_TEST::FillZones( m_board.get() );
1641
1642 // After filling, every pad whose net differs from the zone must have clearance.
1643 // Check each zone/pad combination on each shared layer.
1644 int violations = 0;
1645
1646 for( ZONE* zone : m_board->Zones() )
1647 {
1648 if( zone->GetIsRuleArea() )
1649 continue;
1650
1651 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1652 {
1653 if( !zone->HasFilledPolysForLayer( layer ) )
1654 continue;
1655
1656 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
1657
1658 for( PAD* pad : m_board->GetPads() )
1659 {
1660 if( !pad->IsOnLayer( layer ) )
1661 continue;
1662
1663 if( pad->GetNetCode() == zone->GetNetCode() )
1664 continue;
1665
1666 std::shared_ptr<SHAPE> padShape = pad->GetEffectiveShape( layer );
1667 int clearance = padShape->GetClearance( fill.get() );
1668
1669 if( clearance < 1 )
1670 {
1671 BOOST_TEST_MESSAGE( wxString::Format(
1672 "Pad %s (net %s) at (%d, %d) has zero clearance to zone %s "
1673 "on layer %s",
1674 pad->GetNumber(), pad->GetNetname(),
1675 pad->GetPosition().x, pad->GetPosition().y,
1676 zone->GetNetname(), m_board->GetLayerName( layer ) ) );
1677 violations++;
1678 }
1679 }
1680 }
1681 }
1682
1683 BOOST_CHECK_MESSAGE( violations == 0,
1684 wxString::Format( "Found %d pads with missing zone clearance. "
1685 "Coincident pads with different nets must not be "
1686 "deduplicated in zone fill knockout.",
1687 violations ) );
1688}
1689
1690
1701{
1702 KI_TEST::LoadBoard( m_settingsManager, "connect/connect", m_board );
1703
1704 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1705
1706 KI_TEST::FillZones( m_board.get() );
1707
1708 std::vector<DRC_ITEM> violations;
1709
1710 bds.m_DRCEngine->InitEngine( wxFileName() );
1711
1713 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
1714 const std::function<void( PCB_MARKER* )>& aPathGenerator )
1715 {
1716 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
1717 {
1718 BOARD_ITEM* item_a = m_board->ResolveItem( aItem->GetMainItemID() );
1719 BOARD_ITEM* item_b = m_board->ResolveItem( aItem->GetAuxItemID() );
1720
1721 ZONE* zone_a = dynamic_cast<ZONE*>( item_a );
1722 ZONE* zone_b = dynamic_cast<ZONE*>( item_b );
1723
1724 if( zone_a || zone_b )
1725 violations.push_back( *aItem );
1726 }
1727 } );
1728
1729 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
1730
1731 BOOST_CHECK_EQUAL( violations.size(), 0 );
1732}
1733
1734
1746{
1747 KI_TEST::LoadBoard( m_settingsManager, "issue23339_zone_layer_rules", m_board );
1748
1749 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1750
1751 // First verify that EvalRules returns the correct clearance per layer
1752 ZONE* hvZone = nullptr;
1753 ZONE* lvZone = nullptr;
1754
1755 for( ZONE* zone : m_board->Zones() )
1756 {
1757 if( zone->GetNetname() == "HV_NET" )
1758 hvZone = zone;
1759 else if( zone->GetNetname() == "LV_NET" )
1760 lvZone = zone;
1761 }
1762
1763 BOOST_REQUIRE( hvZone );
1764 BOOST_REQUIRE( lvZone );
1765
1766 // Outer layer rule should give 4.6mm clearance on F.Cu
1767 DRC_CONSTRAINT outerConstraint = bds.m_DRCEngine->EvalRules( CLEARANCE_CONSTRAINT, hvZone, lvZone, F_Cu );
1768
1769 BOOST_TEST_MESSAGE( "F.Cu clearance: " << outerConstraint.GetValue().Min()
1770 << " (expected " << pcbIUScale.mmToIU( 4.6 ) << ")" );
1771 BOOST_CHECK_EQUAL( outerConstraint.GetValue().Min(), pcbIUScale.mmToIU( 4.6 ) );
1772
1773 // Inner layer rule should give 2.3mm clearance on In1.Cu
1774 DRC_CONSTRAINT innerConstraint = bds.m_DRCEngine->EvalRules( CLEARANCE_CONSTRAINT, hvZone, lvZone, In1_Cu );
1775
1776 BOOST_TEST_MESSAGE( "In1.Cu clearance: " << innerConstraint.GetValue().Min()
1777 << " (expected " << pcbIUScale.mmToIU( 2.3 ) << ")" );
1778 BOOST_CHECK_EQUAL( innerConstraint.GetValue().Min(), pcbIUScale.mmToIU( 2.3 ) );
1779
1780 // Now fill zones and check that fills actually respect the clearances
1781 KI_TEST::FillZones( m_board.get() );
1782
1783 // Run DRC and verify no clearance violations between zones
1784 std::vector<DRC_ITEM> violations;
1785
1786 bds.m_DRCEngine->InitEngine( wxFileName() );
1787
1789 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
1790 const std::function<void( PCB_MARKER* )>& aPathGenerator )
1791 {
1792 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
1793 {
1794 BOARD_ITEM* item_a = m_board->ResolveItem( aItem->GetMainItemID() );
1795 BOARD_ITEM* item_b = m_board->ResolveItem( aItem->GetAuxItemID() );
1796
1797 ZONE* zone_a = dynamic_cast<ZONE*>( item_a );
1798 ZONE* zone_b = dynamic_cast<ZONE*>( item_b );
1799
1800 if( zone_a && zone_b )
1801 {
1802 BOOST_TEST_MESSAGE( "Zone-to-zone clearance violation on layer "
1803 << aLayer << ": " << aItem->GetErrorMessage( true ) );
1804 violations.push_back( *aItem );
1805 }
1806 }
1807 } );
1808
1809 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
1810
1811 BOOST_CHECK_EQUAL( violations.size(), 0 );
1812}
1813
1814
1815BOOST_FIXTURE_TEST_CASE( RegressionZoneFillMinWidthAfterKnockout, ZONE_FILL_TEST_FIXTURE )
1816{
1817 KI_TEST::LoadBoard( m_settingsManager, "issue23332_min_width/issue23332_min_width", m_board );
1818
1819 KI_TEST::FillZones( m_board.get() );
1820
1821 int epsilon = pcbIUScale.mmToIU( 0.001 );
1822
1823 for( ZONE* zone : m_board->Zones() )
1824 {
1825 int half_min_width = zone->GetMinThickness() / 2;
1826
1827 if( half_min_width - epsilon <= epsilon )
1828 continue;
1829
1830 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1831 {
1832 if( !zone->HasFilledPolysForLayer( layer ) )
1833 continue;
1834
1835 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1836
1837 if( !fill || fill->OutlineCount() == 0 )
1838 continue;
1839
1840 // Check each filled island individually so that a tiny thin sliver
1841 // isn't masked by a large zone's total area
1842 for( int ii = 0; ii < fill->OutlineCount(); ii++ )
1843 {
1844 SHAPE_POLY_SET island;
1845 island.AddOutline( fill->Outline( ii ) );
1846
1847 for( int jj = 0; jj < fill->HoleCount( ii ); jj++ )
1848 island.AddHole( fill->Hole( ii, jj ) );
1849
1850 double originalArea = island.Area();
1851
1852 if( originalArea <= 0 )
1853 continue;
1854
1856
1857 test.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS, ARC_HIGH_DEF );
1858
1859 test.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS, ARC_HIGH_DEF, true );
1860
1861 double prunedArea = test.Area();
1862 double areaLoss = ( originalArea - prunedArea ) / originalArea;
1863
1864 BOOST_TEST_MESSAGE( wxString::Format( "Zone %s layer %d island %d: area=%.0f, loss=%.4f%%",
1865 zone->GetNetname(),
1866 static_cast<int>( layer ),
1867 ii,
1868 originalArea,
1869 areaLoss * 100.0 ) );
1870
1871 BOOST_CHECK_MESSAGE( areaLoss < 0.01,
1872 wxString::Format( "Zone %s layer %d island %d lost %.2f%% area from "
1873 "min-width pruning (min_width=%.3fmm)",
1874 zone->GetNetname(),
1875 static_cast<int>( layer ),
1876 ii,
1877 areaLoss * 100.0,
1878 zone->GetMinThickness() / (double) pcbIUScale.IU_PER_MM ) );
1879 }
1880 }
1881 }
1882}
1883
1884
1885BOOST_FIXTURE_TEST_CASE( RegressionSameNetOverlappingZones, ZONE_FILL_TEST_FIXTURE )
1886{
1887 KI_TEST::LoadBoard( m_settingsManager, "issue23418/testing", m_board );
1888
1889 KI_TEST::FillZones( m_board.get() );
1890
1891 int epsilon = pcbIUScale.mmToIU( 0.001 );
1892
1893 for( ZONE* zone : m_board->Zones() )
1894 {
1895 int half_min_width = zone->GetMinThickness() / 2;
1896
1897 if( half_min_width - epsilon <= epsilon )
1898 continue;
1899
1900 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1901 {
1902 if( !zone->HasFilledPolysForLayer( layer ) )
1903 continue;
1904
1905 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1906
1907 if( !fill || fill->OutlineCount() == 0 )
1908 continue;
1909
1910 for( int ii = 0; ii < fill->OutlineCount(); ii++ )
1911 {
1912 SHAPE_POLY_SET island;
1913 island.AddOutline( fill->Outline( ii ) );
1914
1915 for( int jj = 0; jj < fill->HoleCount( ii ); jj++ )
1916 island.AddHole( fill->Hole( ii, jj ) );
1917
1918 double originalArea = island.Area();
1919
1920 if( originalArea <= 0 )
1921 continue;
1922
1924
1925 test.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS, ARC_HIGH_DEF );
1926
1927 test.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS, ARC_HIGH_DEF, true );
1928
1929 double prunedArea = test.Area();
1930 double areaLoss = ( originalArea - prunedArea ) / originalArea;
1931
1932 BOOST_CHECK_MESSAGE( areaLoss < 0.01,
1933 wxString::Format( "Zone %s (priority %d) layer %d island %d lost %.2f%% area "
1934 "from min-width pruning, suggesting degenerate geometry "
1935 "from overlapping same-net zones",
1936 zone->GetNetname(),
1937 zone->GetAssignedPriority(),
1938 static_cast<int>( layer ),
1939 ii,
1940 areaLoss * 100.0 ) );
1941 }
1942 }
1943 }
1944}
1945
1946
1947BOOST_FIXTURE_TEST_CASE( RegressionDiffNetOverlappingZones, ZONE_FILL_TEST_FIXTURE )
1948{
1949 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
1950 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
1951
1952 struct ScopeGuard
1953 {
1954 bool& ref;
1955 bool orig;
1956 ~ScopeGuard() { ref = orig; }
1957 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
1958
1959 auto runAreaLossCheck =
1960 [this]( bool aIterative )
1961 {
1962 ADVANCED_CFG& innerCfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
1963 innerCfg.m_ZoneFillIterativeRefill = aIterative;
1964
1965 KI_TEST::LoadBoard( m_settingsManager, "issue23418_diffnet/testing", m_board );
1966 KI_TEST::FillZones( m_board.get() );
1967
1968 int epsilon = pcbIUScale.mmToIU( 0.001 );
1969
1970 for( ZONE* zone : m_board->Zones() )
1971 {
1972 int half_min_width = zone->GetMinThickness() / 2;
1973
1974 if( half_min_width - epsilon <= epsilon )
1975 continue;
1976
1977 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1978 {
1979 if( !zone->HasFilledPolysForLayer( layer ) )
1980 continue;
1981
1982 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1983
1984 if( !fill || fill->OutlineCount() == 0 )
1985 continue;
1986
1987 for( int ii = 0; ii < fill->OutlineCount(); ii++ )
1988 {
1989 SHAPE_POLY_SET island;
1990 island.AddOutline( fill->Outline( ii ) );
1991
1992 for( int jj = 0; jj < fill->HoleCount( ii ); jj++ )
1993 island.AddHole( fill->Hole( ii, jj ) );
1994
1995 double originalArea = island.Area();
1996
1997 if( originalArea <= 0 )
1998 continue;
1999
2001
2002 test.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS,
2003 ARC_HIGH_DEF );
2004
2005 test.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS,
2006 ARC_HIGH_DEF, true );
2007
2008 double prunedArea = test.Area();
2009 double areaLoss = ( originalArea - prunedArea ) / originalArea;
2010
2011 BOOST_CHECK_MESSAGE( areaLoss < 0.01,
2012 wxString::Format( "Zone %s (priority %d) layer %d island %d lost "
2013 "%.2f%% area (iterative=%d), suggesting degenerate "
2014 "geometry from different-net zone knockouts",
2015 zone->GetNetname(),
2016 zone->GetAssignedPriority(),
2017 static_cast<int>( layer ),
2018 ii,
2019 areaLoss * 100.0,
2020 aIterative ) );
2021 }
2022 }
2023 }
2024 };
2025
2026 runAreaLossCheck( false );
2027 runAreaLossCheck( true );
2028}
2029
2030
2046BOOST_FIXTURE_TEST_CASE( RegressionThermalReliefsToNowhere, ZONE_FILL_TEST_FIXTURE )
2047{
2048 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
2049 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
2050 cfg.m_ZoneFillIterativeRefill = true;
2051
2052 struct ScopeGuard
2053 {
2054 bool& ref;
2055 bool orig;
2056 ~ScopeGuard() { ref = orig; }
2057 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
2058
2059 KI_TEST::LoadBoard( m_settingsManager, "issue23535_minimal/issue23535_minimal", m_board );
2060
2061 KI_TEST::FillZones( m_board.get() );
2062
2063 ZONE* gndZone = nullptr;
2064
2065 for( ZONE* zone : m_board->Zones() )
2066 {
2067 if( zone->GetNetname() == "GND" )
2068 gndZone = zone;
2069 }
2070
2071 BOOST_REQUIRE( gndZone );
2073
2074 const std::shared_ptr<SHAPE_POLY_SET>& gndFill = gndZone->GetFilledPolysList( F_Cu );
2075
2076 // The pad is at (6.5mm, 5mm) with size 1.5mm and thermal gap 0.5mm.
2077 // The right edge of the pad is at x=7.25mm, thermal gap extends to x=7.75mm.
2078 // After zone knockout, GND fill stops at roughly x=7.5mm.
2079 //
2080 // A thermal-relief-to-nowhere spoke would create copper at a point inside the
2081 // thermal gap but past the zone fill boundary. Check a point at (7.4mm, 5mm)
2082 // which is in the thermal gap (x > 7.25) and near the knockout edge.
2083 VECTOR2I spokeTestPoint( pcbIUScale.mmToIU( 7.4 ), pcbIUScale.mmToIU( 5.0 ) );
2084
2085 bool hasSpokeToNowhere = gndFill->Contains( spokeTestPoint );
2086
2087 BOOST_CHECK_MESSAGE( !hasSpokeToNowhere, "GND zone fill contains copper at the thermal gap test point (7.4, 5.0), "
2088 "indicating a thermal relief spoke to nowhere (issue 23535)." );
2089
2090 // Also verify that the left-pointing spoke still connects properly.
2091 // A point at (5.6mm, 5mm) is in the thermal gap on the left side and should have
2092 // copper from a valid left-pointing spoke.
2093 VECTOR2I validSpokePoint( pcbIUScale.mmToIU( 5.6 ), pcbIUScale.mmToIU( 5.0 ) );
2094
2095 bool hasValidSpoke = gndFill->Contains( validSpokePoint );
2096
2097 BOOST_CHECK_MESSAGE( hasValidSpoke, "GND zone fill does not contain copper at the valid spoke test point "
2098 "(5.6, 5.0). The fix may have incorrectly removed valid spokes." );
2099}
2100
2101
2112{
2113 KI_TEST::LoadBoard( m_settingsManager, "off_center_teardrop", m_board );
2114
2115 TOOL_MANAGER toolMgr;
2116 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2117
2118 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2119 toolMgr.RegisterTool( dummyTool );
2120
2121 BOARD_COMMIT commit( dummyTool );
2122 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2123 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2124
2125 if( !commit.Empty() )
2126 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2127
2128 // The test board has a 3mm circle pad at (100, 100) with a 0.25mm track connecting
2129 // at (100.75, 99) heading to (115, 99). The track enters the pad off-center: 1mm above
2130 // and 0.75mm right of center. The teardrop should be approximately symmetric about the
2131 // track's axis (the line from ~(100.75, 99) toward (115, 99), i.e., horizontal).
2132
2133 int teardropCount = 0;
2134
2135 for( ZONE* zone : m_board->Zones() )
2136 {
2137 if( !zone->IsTeardropArea() )
2138 continue;
2139
2140 teardropCount++;
2141
2142 const SHAPE_POLY_SET* outline = zone->Outline();
2143
2144 if( !outline || outline->OutlineCount() == 0 )
2145 continue;
2146
2147 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2148
2149 // The track axis is approximately at Y=99mm (in board coordinates = 99 * 1e6 nm).
2150 // Measure the maximum extent above and below this axis across all teardrop vertices.
2151 int trackY = pcbIUScale.mmToIU( 99 );
2152 int maxAbove = 0;
2153 int maxBelow = 0;
2154
2155 for( int i = 0; i < chain.PointCount(); i++ )
2156 {
2157 int dy = chain.CPoint( i ).y - trackY;
2158
2159 if( dy < 0 )
2160 maxAbove = std::max( maxAbove, -dy );
2161 else
2162 maxBelow = std::max( maxBelow, dy );
2163 }
2164
2165 // Both sides should have some extent (the teardrop flares out on both sides)
2166 BOOST_CHECK_MESSAGE( maxAbove > 0 && maxBelow > 0,
2167 "Teardrop should extend on both sides of the track axis" );
2168
2169 if( maxAbove > 0 && maxBelow > 0 )
2170 {
2171 // The two sides should be approximately equal. Allow 30% asymmetry tolerance
2172 // to account for polygon approximation of the circular pad and convex hull rounding.
2173 double ratio = static_cast<double>( std::min( maxAbove, maxBelow ) )
2174 / static_cast<double>( std::max( maxAbove, maxBelow ) );
2175
2176 BOOST_CHECK_MESSAGE( ratio > 0.7,
2177 wxString::Format( "Teardrop asymmetry ratio %.2f is too low (above=%d, below=%d). "
2178 "Expected roughly symmetric about the track axis.",
2179 ratio,
2180 maxAbove,
2181 maxBelow ) );
2182 }
2183 }
2184
2185 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone for off-center track" );
2186}
2187
2188
2197BOOST_FIXTURE_TEST_CASE( ElongatedPadTeardropContainment, ZONE_FILL_TEST_FIXTURE )
2198{
2199 KI_TEST::LoadBoard( m_settingsManager, "teardrop_elongated_pad", m_board );
2200
2201 TOOL_MANAGER toolMgr;
2202 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2203
2204 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2205 toolMgr.RegisterTool( dummyTool );
2206
2207 BOARD_COMMIT commit( dummyTool );
2208 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2209 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2210
2211 if( !commit.Empty() )
2212 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2213
2214 // Find the pad to build an expanded outline for containment checking.
2215 // The pad is at board position (136.45, 100.819) with size (3.5, 0.3) rotated 270 deg,
2216 // giving board extents X: [136.3, 136.6], Y: [99.069, 102.569].
2217 PAD* testPad = nullptr;
2218
2219 for( FOOTPRINT* fp : m_board->Footprints() )
2220 {
2221 for( PAD* pad : fp->Pads() )
2222 {
2223 if( pad->GetNumber() == "7" )
2224 {
2225 testPad = pad;
2226 break;
2227 }
2228 }
2229 }
2230
2231 BOOST_REQUIRE_MESSAGE( testPad != nullptr, "Could not find pad 7 in test board" );
2232
2233 // Build the pad outline polygon with a small tolerance for the track half-width
2234 int tolerance = std::max( m_board->GetDesignSettings().m_MaxError, pcbIUScale.mmToIU( 0.001 ) );
2235 SHAPE_POLY_SET padPoly;
2236 testPad->TransformShapeToPolygon( padPoly, B_Cu, tolerance, m_board->GetDesignSettings().m_MaxError,
2237 ERROR_OUTSIDE );
2238
2239 int teardropCount = 0;
2240
2241 for( ZONE* zone : m_board->Zones() )
2242 {
2243 if( !zone->IsTeardropArea() )
2244 continue;
2245
2246 const SHAPE_POLY_SET* outline = zone->Outline();
2247
2248 BOOST_REQUIRE_MESSAGE( outline && outline->OutlineCount() > 0, "Teardrop zone has no outline" );
2249
2250 teardropCount++;
2251
2252 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2253
2254 // Check each vertex of the teardrop. Vertices on the pad side (closer to pad center
2255 // than to the track anchor) must be inside the expanded pad outline.
2256 VECTOR2I padCenter = testPad->GetPosition();
2257
2258 // The track anchor region is near (136.45, 99.16) in mm, i.e., outside the pad.
2259 // We only check vertices that are closer to the pad center than to the track anchor.
2260 VECTOR2I trackAnchor( pcbIUScale.mmToIU( 136.45 ), pcbIUScale.mmToIU( 99.16 ) );
2261
2262 for( int i = 0; i < chain.PointCount(); i++ )
2263 {
2264 VECTOR2I pt = chain.CPoint( i );
2265 double distToPad = ( VECTOR2D( pt ) - VECTOR2D( padCenter ) ).EuclideanNorm();
2266 double distToTrack = ( VECTOR2D( pt ) - VECTOR2D( trackAnchor ) ).EuclideanNorm();
2267
2268 // Only check vertices on the pad side of the teardrop
2269 if( distToPad < distToTrack )
2270 {
2271 BOOST_CHECK_MESSAGE( padPoly.Contains( pt ),
2272 wxString::Format( "Teardrop vertex (%d, %d) is outside the pad outline with "
2273 "%d nm tolerance",
2274 pt.x, pt.y,
2275 tolerance ) );
2276 }
2277 }
2278 }
2279
2280 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone for elongated pad" );
2281}
2282
2283
2292BOOST_FIXTURE_TEST_CASE( TwoSegmentAngledTeardropNoSelfIntersection, ZONE_FILL_TEST_FIXTURE )
2293{
2294 auto runVariant =
2295 [&]( bool aCurvedEdges )
2296 {
2297 KI_TEST::LoadBoard( m_settingsManager, "two_segment_teardrop", m_board );
2298
2299 for( PCB_TRACK* track : m_board->Tracks() )
2300 {
2301 if( track->Type() == PCB_VIA_T )
2302 {
2303 static_cast<PCB_VIA*>( track )->SetTeardropCurved( aCurvedEdges );
2304 break;
2305 }
2306 }
2307
2308 TOOL_MANAGER toolMgr;
2309 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2310
2311 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2312 toolMgr.RegisterTool( dummyTool );
2313
2314 BOARD_COMMIT commit( dummyTool );
2315 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2316 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2317
2318 if( !commit.Empty() )
2319 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2320
2321 int teardropCount = 0;
2322 bool foundSelfIntersection = false;
2323
2324 for( ZONE* zone : m_board->Zones() )
2325 {
2326 if( !zone->IsTeardropArea() )
2327 continue;
2328
2329 teardropCount++;
2330
2331 const SHAPE_POLY_SET* outline = zone->Outline();
2332
2333 if( !outline || outline->OutlineCount() == 0 )
2334 continue;
2335
2336 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2337 int n = chain.PointCount();
2338
2339 for( int i = 0; i < n && !foundSelfIntersection; i++ )
2340 {
2341 SEG segA( chain.CPoint( i ), chain.CPoint( ( i + 1 ) % n ) );
2342
2343 for( int j = i + 2; j < n; j++ )
2344 {
2345 if( i == 0 && j == n - 1 )
2346 continue;
2347
2348 SEG segB( chain.CPoint( j ), chain.CPoint( ( j + 1 ) % n ) );
2349 OPT_VECTOR2I hit = segA.Intersect( segB );
2350
2351 if( hit.has_value() )
2352 {
2353 BOOST_TEST_MESSAGE( wxString::Format( "Self-intersection at (%d, %d) between edges "
2354 "%d and %d (curved=%s)",
2355 hit->x, hit->y,
2356 i,
2357 j,
2358 aCurvedEdges ? "yes" : "no" ) );
2359
2360 for( int k = 0; k < n; k++ )
2361 {
2362 BOOST_TEST_MESSAGE( wxString::Format( " pt[%d] = (%d, %d)", k,
2363 chain.CPoint( k ).x, chain.CPoint( k ).y ) );
2364 }
2365
2366 foundSelfIntersection = true;
2367 break;
2368 }
2369 }
2370 }
2371 }
2372
2373 BOOST_CHECK_MESSAGE( teardropCount > 0,
2374 wxString::Format( "Expected at least one teardrop zone (curved=%s)",
2375 aCurvedEdges ? "yes" : "no" ) );
2376
2377 BOOST_CHECK_MESSAGE( !foundSelfIntersection,
2378 wxString::Format( "Teardrop polygon has self-intersecting edges (curved=%s)",
2379 aCurvedEdges ? "yes" : "no" ) );
2380 };
2381
2382 runVariant( true );
2383 runVariant( false );
2384}
2385
2386
2405BOOST_FIXTURE_TEST_CASE( OffCenterTwoSegmentTeardropNoSpike, ZONE_FILL_TEST_FIXTURE )
2406{
2407 auto runVariant = [&]( bool aCurvedEdges )
2408 {
2409 KI_TEST::LoadBoard( m_settingsManager, "teardrop_offcenter_two_segment", m_board );
2410
2411 VECTOR2I viaPos;
2412 int viaRadius = 0;
2413
2414 for( PCB_TRACK* track : m_board->Tracks() )
2415 {
2416 if( track->Type() == PCB_VIA_T )
2417 {
2418 PCB_VIA* via = static_cast<PCB_VIA*>( track );
2419 via->SetPadstackMode( PADSTACK::MODE::NORMAL );
2420 via->SetTeardropCurved( aCurvedEdges );
2421 viaPos = via->GetPosition();
2422 viaRadius = via->GetWidth( PADSTACK::ALL_LAYERS ) / 2;
2423 break;
2424 }
2425 }
2426
2427 BOOST_REQUIRE( viaRadius > 0 );
2428
2429 TOOL_MANAGER toolMgr;
2430 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2431
2432 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2433 toolMgr.RegisterTool( dummyTool );
2434
2435 BOARD_COMMIT commit( dummyTool );
2436 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2437 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2438
2439 if( !commit.Empty() )
2440 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2441
2442 // The crafted board's first segment emerges from the via by ~10 um on a 100 um
2443 // track width; the emerging-length filter rejects it and no teardrop is built.
2444 const double maxBackSideDist = viaRadius * 1.2;
2445 int teardropCount = 0;
2446 int spikingPoints = 0;
2447 VECTOR2I worstPoint;
2448 double worstDistance = 0.0;
2449
2450 for( ZONE* zone : m_board->Zones() )
2451 {
2452 if( !zone->IsTeardropArea() )
2453 continue;
2454
2455 teardropCount++;
2456
2457 const SHAPE_POLY_SET* outline = zone->Outline();
2458
2459 if( !outline || outline->OutlineCount() == 0 )
2460 continue;
2461
2462 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2463
2464 for( int i = 0; i < chain.PointCount(); i++ )
2465 {
2466 const VECTOR2I& pt = chain.CPoint( i );
2467 VECTOR2I rel = pt - viaPos;
2468
2469 // Only consider points on the back side (opposite the track entry).
2470 if( rel.x >= 0 )
2471 continue;
2472
2473 double dist = rel.EuclideanNorm();
2474
2475 if( dist > maxBackSideDist )
2476 {
2477 spikingPoints++;
2478
2479 if( dist > worstDistance )
2480 {
2481 worstDistance = dist;
2482 worstPoint = pt;
2483 }
2484 }
2485 }
2486 }
2487
2488 BOOST_CHECK_MESSAGE( teardropCount == 0,
2489 wxString::Format( "Expected no teardrop on grazing-entry track (emergence below "
2490 "track width), got %d (curved=%s)",
2491 teardropCount,
2492 aCurvedEdges ? "yes" : "no" ) );
2493
2494 BOOST_CHECK_MESSAGE( spikingPoints == 0,
2495 wxString::Format( "Found %d teardrop polygon vertex/vertices outside the expected "
2496 "envelope (worst at (%d, %d), %f mm from via center; curved=%s)",
2497 spikingPoints,
2498 worstPoint.x, worstPoint.y,
2499 worstDistance / pcbIUScale.IU_PER_MM,
2500 aCurvedEdges ? "yes" : "no" ) );
2501 };
2502
2503 runVariant( true );
2504 runVariant( false );
2505}
2506
2507
2519BOOST_FIXTURE_TEST_CASE( MultiTrackSharedInsideJunctionNoSelfIntersection, ZONE_FILL_TEST_FIXTURE )
2520{
2521 auto runVariant = [&]( bool aCurvedEdges )
2522 {
2523 KI_TEST::LoadBoard( m_settingsManager, "teardrop_multi_inside_via", m_board );
2524
2525 for( PCB_TRACK* track : m_board->Tracks() )
2526 {
2527 if( track->Type() == PCB_VIA_T )
2528 {
2529 static_cast<PCB_VIA*>( track )->SetTeardropCurved( aCurvedEdges );
2530 break;
2531 }
2532 }
2533
2534 TOOL_MANAGER toolMgr;
2535 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2536
2537 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2538 toolMgr.RegisterTool( dummyTool );
2539
2540 BOARD_COMMIT commit( dummyTool );
2541 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2542 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2543
2544 if( !commit.Empty() )
2545 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2546
2547 int teardropCount = 0;
2548 int selfIntersectingCount = 0;
2549 VECTOR2I worstPoint;
2550
2551 for( ZONE* zone : m_board->Zones() )
2552 {
2553 if( !zone->IsTeardropArea() )
2554 continue;
2555
2556 teardropCount++;
2557
2558 const SHAPE_POLY_SET* outline = zone->Outline();
2559
2560 if( !outline || outline->OutlineCount() == 0 )
2561 continue;
2562
2563 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2564 int n = chain.PointCount();
2565 bool intersected = false;
2566
2567 for( int i = 0; i < n && !intersected; i++ )
2568 {
2569 SEG segA( chain.CPoint( i ), chain.CPoint( ( i + 1 ) % n ) );
2570
2571 for( int j = i + 2; j < n; j++ )
2572 {
2573 if( i == 0 && j == n - 1 )
2574 continue;
2575
2576 SEG segB( chain.CPoint( j ), chain.CPoint( ( j + 1 ) % n ) );
2577 OPT_VECTOR2I hit = segA.Intersect( segB );
2578
2579 if( hit.has_value() )
2580 {
2581 BOOST_TEST_MESSAGE( wxString::Format( "Teardrop polygon self-intersection at (%d, %d) "
2582 "between edges %d and %d (curved=%s)",
2583 hit->x, hit->y,
2584 i,
2585 j,
2586 aCurvedEdges ? "yes" : "no" ) );
2587
2588 worstPoint = hit.value();
2589 intersected = true;
2590 break;
2591 }
2592 }
2593 }
2594
2595 if( intersected )
2596 selfIntersectingCount++;
2597 }
2598
2599 BOOST_CHECK_MESSAGE( teardropCount > 0,
2600 wxString::Format( "Expected at least one teardrop zone (curved=%s)",
2601 aCurvedEdges ? "yes" : "no" ) );
2602
2603 BOOST_CHECK_MESSAGE( selfIntersectingCount == 0,
2604 wxString::Format( "%d of %d teardrop polygon(s) self-intersect (worst at (%d, %d); "
2605 "curved=%s)",
2606 selfIntersectingCount,
2607 teardropCount,
2608 worstPoint.x, worstPoint.y,
2609 aCurvedEdges ? "yes" : "no" ) );
2610 };
2611
2612 runVariant( true );
2613 runVariant( false );
2614}
2615
2616
2619BOOST_FIXTURE_TEST_CASE( CloseViaShortRadialTrackTeardrop, ZONE_FILL_TEST_FIXTURE )
2620{
2621 auto runVariant = [&]( const wxString& aFixture, bool aCurvedEdges )
2622 {
2623 KI_TEST::LoadBoard( m_settingsManager, aFixture, m_board );
2624
2625 for( PCB_TRACK* track : m_board->Tracks() )
2626 {
2627 if( track->Type() == PCB_VIA_T )
2628 static_cast<PCB_VIA*>( track )->SetTeardropCurved( aCurvedEdges );
2629 }
2630
2631 for( FOOTPRINT* footprint : m_board->Footprints() )
2632 {
2633 for( PAD* pad : footprint->Pads() )
2634 pad->SetTeardropCurved( aCurvedEdges );
2635 }
2636
2637 TOOL_MANAGER toolMgr;
2638 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2639
2640 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2641 toolMgr.RegisterTool( dummyTool );
2642
2643 BOARD_COMMIT commit( dummyTool );
2644 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2645 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2646
2647 if( !commit.Empty() )
2648 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2649
2650 int teardropCount = 0;
2651 VECTOR2I padPos = ( *m_board->Footprints().begin() )->Pads()[0]->GetPosition();
2652 bool padHasTeardrop = false;
2653
2654 for( ZONE* zone : m_board->Zones() )
2655 {
2656 if( !zone->IsTeardropArea() )
2657 continue;
2658
2659 teardropCount++;
2660
2661 if( zone->Outline()->Contains( padPos ) )
2662 padHasTeardrop = true;
2663 }
2664
2665 BOOST_CHECK_MESSAGE( padHasTeardrop,
2666 wxString::Format( "Expected the pad-anchored teardrop on the short track joining the "
2667 "pad and the close via in %s, got %d teardrop(s) (curved=%s)",
2668 aFixture,
2669 teardropCount,
2670 aCurvedEdges ? "yes" : "no" ) );
2671 };
2672
2673 runVariant( "teardrop_close_via", true );
2674 runVariant( "teardrop_close_via", false );
2675 runVariant( "teardrop_close_via_rotated_pad", true );
2676 runVariant( "teardrop_close_via_rotated_pad", false );
2677}
2678
2679
2687BOOST_FIXTURE_TEST_CASE( RegressionKeepoutBoundaryMissingFill, ZONE_FILL_TEST_FIXTURE )
2688{
2689 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
2690 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
2691
2692 struct ScopeGuard
2693 {
2694 bool& ref;
2695 bool orig;
2696 ~ScopeGuard() { ref = orig; }
2697 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
2698
2699 auto getTotalFilledArea =
2700 [this]() -> double
2701 {
2702 double totalArea = 0;
2703
2704 for( ZONE* zone : m_board->Zones() )
2705 {
2706 if( zone->GetIsRuleArea() )
2707 continue;
2708
2709 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
2710 {
2711 if( !zone->HasFilledPolysForLayer( layer ) )
2712 continue;
2713
2714 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
2715
2716 if( fill )
2717 totalArea += std::abs( fill->Area() );
2718 }
2719 }
2720
2721 return totalArea;
2722 };
2723
2724 auto refillAndMeasure =
2725 [this, &cfg, &getTotalFilledArea]( bool aIterative ) -> double
2726 {
2727 cfg.m_ZoneFillIterativeRefill = aIterative;
2728
2729 KI_TEST::LoadBoard( m_settingsManager, "issue23515/issue23515", m_board );
2730
2731 double storedArea = getTotalFilledArea();
2732
2733 BOOST_REQUIRE_MESSAGE( storedArea > 0, "Stored v9 fill has zero area" );
2734
2735 KI_TEST::FillZones( m_board.get() );
2736 return getTotalFilledArea();
2737 };
2738
2739 KI_TEST::LoadBoard( m_settingsManager, "issue23515/issue23515", m_board );
2740
2741 double storedArea = getTotalFilledArea();
2742
2743 BOOST_REQUIRE_MESSAGE( storedArea > 0, "Stored v9 fill has zero area" );
2744
2745 double nonIterativeArea = refillAndMeasure( false );
2746 double iterativeArea = refillAndMeasure( true );
2747 double nonIterativeAreaRatio = nonIterativeArea / storedArea;
2748 double iterativeAreaRatio = iterativeArea / storedArea;
2749
2750 BOOST_CHECK_MESSAGE( nonIterativeAreaRatio > 0.99999,
2751 wxString::Format( "Non-iterative refill lost %.4f%% versus stored v9 fill "
2752 "(stored=%.2f mm^2, non-iterative=%.2f mm^2). This suggests "
2753 "missing pieces near keepout boundaries (issue 23515).",
2754 ( 1.0 - nonIterativeAreaRatio ) * 100.0,
2755 storedArea / 1e6,
2756 nonIterativeArea / 1e6 ) );
2757
2758 BOOST_CHECK_MESSAGE( iterativeAreaRatio > 0.99999,
2759 wxString::Format( "Iterative refill lost %.4f%% versus stored v9 fill "
2760 "(stored=%.2f mm^2, iterative=%.2f mm^2). "
2761 "This suggests missing pieces near keepout boundaries (issue 23515).",
2762 ( 1.0 - iterativeAreaRatio ) * 100.0,
2763 storedArea / 1e6,
2764 iterativeArea / 1e6 ) );
2765}
2766
2767
2776BOOST_FIXTURE_TEST_CASE( HatchZoneViaConnectionRespectsSetting, ZONE_FILL_TEST_FIXTURE )
2777{
2778 m_board = std::make_unique<BOARD>();
2779
2780 // Two-layer board is sufficient for this test
2781 m_board->SetCopperLayerCount( 2 );
2782
2783 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
2784 bds.SetCopperLayerCount( 2 );
2785
2786 bds.m_MinClearance = pcbIUScale.mmToIU( 0.2 );
2787
2788 // Add a GND net
2789 NETINFO_ITEM* gndNet = new NETINFO_ITEM( m_board.get(), wxT( "GND" ) );
2790 m_board->Add( gndNet );
2791 int gndNetCode = gndNet->GetNetCode();
2792
2793 // Via dimensions: 2.0mm diameter, 1.0mm drill - large enough to span multiple hatch cells
2794 // so the via always touches webbing lines regardless of position within the hatch grid.
2795 int viaDiam = pcbIUScale.mmToIU( 2.0 );
2796 int viaDrill = pcbIUScale.mmToIU( 1.0 );
2797
2798 // Hatch zone parameters: 0.5mm gap, 0.3mm thickness. The via (radius=1.0mm) is wider
2799 // than the gap, so it will always intersect webbing in FULL mode. The thermal gap
2800 // (0.5mm) makes the knockout circle radius = 1.0+0.5 = 1.5mm.
2801 int hatchGap = pcbIUScale.mmToIU( 0.5 );
2802 int hatchThickness = pcbIUScale.mmToIU( 0.3 );
2803
2804 // Via center at 10mm,10mm (middle of the zone)
2805 VECTOR2I viaPos( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) );
2806
2807 auto makeVia =
2808 [&]() -> PCB_VIA*
2809 {
2810 PCB_VIA* via = new PCB_VIA( m_board.get() );
2811 via->SetPadstackMode( PADSTACK::MODE::NORMAL );
2812 via->SetPosition( viaPos );
2813 via->SetLayerPair( F_Cu, B_Cu );
2814 via->SetDrill( viaDrill );
2815 via->SetWidth( PADSTACK::ALL_LAYERS, viaDiam );
2816 via->SetNetCode( gndNetCode );
2817 m_board->Add( via );
2818 return via;
2819 };
2820
2821 auto makeHatchZone =
2822 [&]( ZONE_CONNECTION aConnection ) -> ZONE*
2823 {
2824 ZONE* zone = new ZONE( m_board.get() );
2825 zone->SetLayer( F_Cu );
2826 zone->SetNetCode( gndNetCode );
2828 zone->SetHatchGap( hatchGap );
2829 zone->SetHatchThickness( hatchThickness );
2830 zone->SetPadConnection( aConnection );
2831 zone->SetMinThickness( pcbIUScale.mmToIU( 0.2 ) );
2832 zone->SetThermalReliefGap( pcbIUScale.mmToIU( 0.5 ) );
2833 zone->SetThermalReliefSpokeWidth( pcbIUScale.mmToIU( 0.5 ) );
2834
2835 SHAPE_POLY_SET outline;
2836 outline.NewOutline();
2837 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2838 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 19 ), pcbIUScale.mmToIU( 1 ) ) );
2839 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 19 ), pcbIUScale.mmToIU( 19 ) ) );
2840 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 19 ) ) );
2841 zone->AddPolygon( outline.COutline( 0 ) );
2842
2843 m_board->Add( zone );
2844 return zone;
2845 };
2846
2847 auto initDRC =
2848 [&]()
2849 {
2850 m_board->BuildConnectivity();
2851 auto drcEngine = std::make_shared<DRC_ENGINE>( m_board.get(), &bds );
2852 drcEngine->InitEngine( wxFileName() );
2853 bds.m_DRCEngine = drcEngine;
2854 };
2855
2856 // The thermal relief adds a circular ring around the via that covers hatch holes which
2857 // would otherwise be open. With viaRadius=1.0mm, thermalGap=0.5mm, spokeWidth=0.5mm:
2858 // ring outer radius = 1.75mm, inner radius = 1.25mm
2859 // ring area added inside hatch holes > knockout area removed from webbing
2860 // net result: THERMAL fill area > FULL fill area by ~0.4 sq mm
2861 // FULL connection skips both the knockout and the ring addition, so the THERMAL fill
2862 // should be measurably larger than the FULL fill.
2863
2864 double fullFillArea = 0.0;
2865 double thermalFillArea = 0.0;
2866
2867 // Test 1: FULL connection
2868 {
2869 PCB_VIA* via = makeVia();
2870 ZONE* zone = makeHatchZone( ZONE_CONNECTION::FULL );
2871
2872 initDRC();
2873 KI_TEST::FillZones( m_board.get() );
2874
2875 BOOST_REQUIRE_MESSAGE( zone->HasFilledPolysForLayer( F_Cu ),
2876 "Zone should have fill on F.Cu with FULL connection" );
2877
2878 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
2879
2880 for( int i = 0; i < fill->OutlineCount(); i++ )
2881 fullFillArea += std::abs( fill->Outline( i ).Area() );
2882
2883 m_board->Remove( via );
2884 m_board->Remove( zone );
2885 delete via;
2886 delete zone;
2887 }
2888
2889 // Test 2: THERMAL connection
2890 {
2891 PCB_VIA* via = makeVia();
2892 ZONE* zone = makeHatchZone( ZONE_CONNECTION::THERMAL );
2893
2894 initDRC();
2895 KI_TEST::FillZones( m_board.get() );
2896
2897 BOOST_REQUIRE_MESSAGE( zone->HasFilledPolysForLayer( F_Cu ),
2898 "Zone should have fill on F.Cu with THERMAL connection" );
2899
2900 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
2901
2902 for( int i = 0; i < fill->OutlineCount(); i++ )
2903 thermalFillArea += std::abs( fill->Outline( i ).Area() );
2904
2905 m_board->Remove( via );
2906 m_board->Remove( zone );
2907 delete via;
2908 delete zone;
2909 }
2910
2911 // The THERMAL fill should have more area than the FULL fill because a thermal ring was
2912 // added around the via, filling hatch holes that would otherwise be open.
2913 // Use a 0.2 sq mm threshold to avoid sensitivity to small edge effects.
2914 double iuPerMM = pcbIUScale.IU_PER_MM;
2915 double areaThreshold = 0.2 * iuPerMM * iuPerMM; // 0.2 sq mm in IU^2
2916
2917 double areaIU2toMM2 = 1.0 / ( iuPerMM * iuPerMM );
2918
2919 BOOST_CHECK_MESSAGE( thermalFillArea > fullFillArea + areaThreshold,
2920 wxString::Format( "THERMAL connection fill area (%.2f sq mm) should be larger than "
2921 "FULL fill area (%.2f sq mm) by at least 0.2 sq mm. If they are "
2922 "equal or FULL is larger, thermal ring was not added for THERMAL "
2923 "connection, or thermal ring was incorrectly added for FULL connection "
2924 "(issue 23516 regression).",
2925 thermalFillArea * areaIU2toMM2,
2926 fullFillArea * areaIU2toMM2 ) );
2927}
2928
2929
2936BOOST_FIXTURE_TEST_CASE( HatchZoneFullViaStaysConnected, ZONE_FILL_TEST_FIXTURE )
2937{
2938 m_board = std::make_unique<BOARD>();
2939 m_board->SetCopperLayerCount( 2 );
2940
2941 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
2942 bds.SetCopperLayerCount( 2 );
2943 bds.m_MinClearance = pcbIUScale.mmToIU( 0.2 );
2944
2945 NETINFO_ITEM* gndNet = new NETINFO_ITEM( m_board.get(), wxT( "GND" ) );
2946 m_board->Add( gndNet );
2947 int gndNetCode = gndNet->GetNetCode();
2948
2949 // Via diameter (0.4mm) is much smaller than the hatch gap (2.0mm), so a via centred in a
2950 // hole sits entirely inside that hole with no copper around it unless the hole is dropped.
2951 int viaDiam = pcbIUScale.mmToIU( 0.4 );
2952 int viaDrill = pcbIUScale.mmToIU( 0.2 );
2953
2954 int hatchGap = pcbIUScale.mmToIU( 2.0 );
2955 int hatchThickness = pcbIUScale.mmToIU( 0.3 );
2956
2957 auto makeHatchZone = [&]() -> ZONE*
2958 {
2959 ZONE* zone = new ZONE( m_board.get() );
2960 zone->SetLayer( F_Cu );
2961 zone->SetNetCode( gndNetCode );
2963 zone->SetHatchGap( hatchGap );
2964 zone->SetHatchThickness( hatchThickness );
2966 zone->SetMinThickness( pcbIUScale.mmToIU( 0.2 ) );
2967
2968 SHAPE_POLY_SET outline;
2969 outline.NewOutline();
2970 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ) );
2971 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 20 ), pcbIUScale.mmToIU( 0 ) ) );
2972 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 20 ), pcbIUScale.mmToIU( 20 ) ) );
2973 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 20 ) ) );
2974 zone->AddPolygon( outline.COutline( 0 ) );
2975
2976 m_board->Add( zone );
2977 return zone;
2978 };
2979
2980 // Sweep over one full grid period (gridsize = hatchThickness + hatchGap = 2.3mm) so the
2981 // via is guaranteed to land inside a hole at several positions regardless of grid phase.
2982 const int steps = 8;
2983 const double startMM = 9.0;
2984 const double stepMM = 2.3 / steps;
2985
2986 int isolatedCount = 0;
2987 int testedCount = 0;
2988
2989 for( int ix = 0; ix < steps; ix++ )
2990 {
2991 for( int iy = 0; iy < steps; iy++ )
2992 {
2993 VECTOR2I viaPos( pcbIUScale.mmToIU( startMM + ix * stepMM ), pcbIUScale.mmToIU( startMM + iy * stepMM ) );
2994
2995 PCB_VIA* via = new PCB_VIA( m_board.get() );
2996 via->SetPadstackMode( PADSTACK::MODE::NORMAL );
2997 via->SetPosition( viaPos );
2998 via->SetLayerPair( F_Cu, B_Cu );
2999 via->SetDrill( viaDrill );
3000 via->SetWidth( PADSTACK::ALL_LAYERS, viaDiam );
3001 via->SetNetCode( gndNetCode );
3002 m_board->Add( via );
3003
3004 ZONE* zone = makeHatchZone();
3005
3006 m_board->BuildConnectivity();
3007 auto drcEngine = std::make_shared<DRC_ENGINE>( m_board.get(), &bds );
3008 drcEngine->InitEngine( wxFileName() );
3009 bds.m_DRCEngine = drcEngine;
3010
3011 KI_TEST::FillZones( m_board.get() );
3012
3014
3015 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
3016 std::shared_ptr<SHAPE> viaShape = via->GetEffectiveShape( F_Cu );
3017
3018 // The zone fill must touch the via. If it does not, the via is isolated copper
3019 // inside a hatch hole (the issue 24559 regression).
3020 if( !fill->Collide( viaShape.get(), 0 ) )
3021 isolatedCount++;
3022
3023 testedCount++;
3024
3025 m_board->Remove( via );
3026 m_board->Remove( zone );
3027 delete via;
3028 delete zone;
3029 }
3030 }
3031
3032 BOOST_CHECK_MESSAGE( isolatedCount == 0, wxString::Format( "%d of %d FULL-connection via positions were left "
3033 "isolated from the hatch fill (issue 24559).",
3034 isolatedCount, testedCount ) );
3035}
3036
3037
3055BOOST_FIXTURE_TEST_CASE( RegressionCascadingIslandRefill, ZONE_FILL_TEST_FIXTURE )
3056{
3057 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
3058 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
3059 cfg.m_ZoneFillIterativeRefill = true;
3060
3061 struct ScopeGuard
3062 {
3063 bool& ref;
3064 bool orig;
3065 ~ScopeGuard() { ref = orig; }
3066 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
3067
3068 KI_TEST::LoadBoard( m_settingsManager, "zone_refill_cascading_islands", m_board );
3069 KI_TEST::FillZones( m_board.get() );
3070
3071 const std::vector<std::string> checkedNames = { "hi1", "hi2", "hi3", "hi4", "hi5", "hi6", "hi7",
3072 "lo1", "lo2", "lo3", "lo4", "lo5", "lo6" };
3073 std::map<std::string, ZONE*> zoneByName;
3074
3075 for( ZONE* zone : m_board->Zones() )
3076 zoneByName[zone->GetZoneName().ToStdString()] = zone;
3077
3078 for( const std::string& name : checkedNames )
3079 {
3080 BOOST_REQUIRE_MESSAGE( zoneByName.count( name ), "Zone '" + name + "' not found in test board" );
3081 BOOST_REQUIRE_MESSAGE( zoneByName[name]->HasFilledPolysForLayer( F_Cu ),
3082 "Zone '" + name + "' has no fill on F.Cu" );
3083 }
3084
3085 // hi3, hi5, hi7 each split into two copper islands (one standalone, one merged with lo2/lo4/lo6).
3086 for( const std::string& name : { "hi3", "hi5", "hi7" } )
3087 {
3088 int islands = zoneByName[name]->GetFilledPolysList( F_Cu )->OutlineCount();
3089
3090 BOOST_CHECK_MESSAGE( islands == 2, wxString::Format( "Zone '%s' should have 2 filled islands but has %d. "
3091 "Cascading island removal did not converge correctly.",
3092 name, islands ) );
3093 }
3094
3095 // All lo zones and hi2/hi4/hi6 are single zones.
3096 for( const std::string& name : { "lo1", "lo2", "lo3", "lo4", "lo5", "lo6", "hi2", "hi4", "hi6" } )
3097 {
3098 int islands = zoneByName[name]->GetFilledPolysList( F_Cu )->OutlineCount();
3099
3100 BOOST_CHECK_MESSAGE( islands == 1, wxString::Format( "Zone '%s' should have 1 filled island but has %d. "
3101 "Iterative refill may have incorrectly blocked or "
3102 "expanded this zone.",
3103 name, islands ) );
3104 }
3105}
3106
3107
3114BOOST_FIXTURE_TEST_CASE( CopperThievingZone_HatchSurvivesTrackBisection, ZONE_FILL_TEST_FIXTURE )
3115{
3116 KI_TEST::LoadBoard( m_settingsManager, "zone_thieving_track_bisection", m_board );
3117 KI_TEST::FillZones( m_board.get() );
3118
3119 ZONE* thievingZone = nullptr;
3120
3121 for( ZONE* z : m_board->Zones() )
3122 {
3123 if( z->GetFillMode() == ZONE_FILL_MODE::COPPER_THIEVING )
3124 {
3125 thievingZone = z;
3126 break;
3127 }
3128 }
3129
3130 BOOST_REQUIRE( thievingZone );
3131
3132 const std::shared_ptr<SHAPE_POLY_SET>& fill = thievingZone->GetFilledPolysList( F_Cu );
3133 BOOST_REQUIRE( fill );
3134
3135 // The track splits the fill area in two; before the fix the connectivity
3136 // pass classified the narrow side as an isolated island and deleted it.
3137 // Expect at least two outlines covering both halves of the original zone.
3138 BOOST_CHECK_GE( fill->OutlineCount(), 2 );
3139
3140 // The fill must span the full zone width (left edge through right edge).
3141 BOX2I fillBox = fill->BBox();
3142 BOX2I zoneBox = thievingZone->Outline()->BBox();
3143
3144 BOOST_CHECK_LT( fillBox.GetLeft(), zoneBox.GetLeft() + pcbIUScale.mmToIU( 2.0 ) );
3145 BOOST_CHECK_GT( fillBox.GetRight(), zoneBox.GetRight() - pcbIUScale.mmToIU( 2.0 ) );
3146
3147 // The mesh must have real structure on both sides.
3148 BOOST_CHECK_GT( fill->TotalVertices(), 200 );
3149}
3150
3151
3164BOOST_FIXTURE_TEST_CASE( IterativeRefillConvergenceLimit, ZONE_FILL_TEST_FIXTURE )
3165{
3166 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
3167 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
3168 cfg.m_ZoneFillIterativeRefill = true;
3169
3170 struct ScopeGuard
3171 {
3172 bool& ref;
3173 bool orig;
3174 ~ScopeGuard() { ref = orig; }
3175 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
3176
3177
3178 KI_TEST::SCOPED_COUNTING_WXLOG countingLog( nullptr, wxLOG_Warning );
3179
3180 KI_TEST::LoadBoard( m_settingsManager, "zone_refill_convergence_limit", m_board );
3181 KI_TEST::FillZones( m_board.get() );
3182
3183 BOOST_CHECK_MESSAGE( countingLog.GetCount() > 0, "Expected a wxLogWarning when iterative refill hits the iteration "
3184 "limit, but none was emitted. The convergence-limit board may no "
3185 "longer trigger the cap, or the warning path has changed." );
3186}
3187
3188
3194BOOST_FIXTURE_TEST_CASE( CopperThievingZone_NonCopperLayerStampsNotSolid, ZONE_FILL_TEST_FIXTURE )
3195{
3196 m_board = std::make_unique<BOARD>();
3197
3198 ZONE* zone = new ZONE( m_board.get() );
3199 zone->SetLayer( F_SilkS );
3200 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
3201 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
3202 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
3203 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
3205
3206 THIEVING_SETTINGS thieving;
3208 thieving.element_size = pcbIUScale.mmToIU( 0.5 );
3209 thieving.gap = pcbIUScale.mmToIU( 1.5 );
3210 zone->SetThievingSettings( thieving );
3212 m_board->Add( zone );
3213
3214 KI_TEST::FillZones( m_board.get() );
3215
3216 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_SilkS );
3217 BOOST_REQUIRE( fill );
3218
3219 // A solid fill would have one outline (the zone polygon); a dots grid
3220 // produces dozens. Lower bound is conservative to avoid edge-clipping flakiness.
3221 BOOST_CHECK_GT( fill->OutlineCount(), 5 );
3222}
3223
3224
3233{
3234 m_board = std::make_unique<BOARD>();
3235 m_board->SetCopperLayerCount( 2 );
3236
3237 // 10 mm x 10 mm zone outline
3238 ZONE* zone = new ZONE( m_board.get() );
3239 zone->SetLayer( F_Cu );
3240 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
3241 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
3242 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
3243 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
3244
3246
3247 THIEVING_SETTINGS thieving;
3249 thieving.element_size = pcbIUScale.mmToIU( 0.5 );
3250 thieving.gap = pcbIUScale.mmToIU( 2.0 );
3251 thieving.line_width = pcbIUScale.mmToIU( 0.3 );
3252 thieving.stagger = false;
3253 thieving.orientation = ANGLE_0;
3254 zone->SetThievingSettings( thieving );
3255
3257
3258 m_board->Add( zone );
3259
3260 KI_TEST::FillZones( m_board.get() );
3261
3262 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
3263 BOOST_REQUIRE( fill );
3264 BOOST_REQUIRE_GT( fill->OutlineCount(), 0 );
3265
3266 // 2.5 mm pitch, 0.5 mm dot. The four positions whose disc touches the
3267 // zone edge (x or y at 0 or 10 mm) are dropped, leaving a 3 x 3 grid.
3268 BOOST_CHECK_GE( fill->OutlineCount(), 6 );
3269 BOOST_CHECK_LE( fill->OutlineCount(), 12 );
3270
3271 // 10% slack covers the polygonal circle approximation plus post-fill corner rounding.
3272 const double fullDotArea = M_PI * std::pow( pcbIUScale.mmToIU( 0.25 ), 2 );
3273 CheckAllOutlineAreasAtLeast( fill, 0.9 * fullDotArea, wxT( "Dot" ) );
3274}
3275
3276
3283BOOST_FIXTURE_TEST_CASE( CopperThievingZone_StaggerProducesDifferentLayout, ZONE_FILL_TEST_FIXTURE )
3284{
3285 auto countDots = []( bool stagger ) -> int
3286 {
3287 auto board = std::make_unique<BOARD>();
3288 board->SetCopperLayerCount( 2 );
3289
3290 ZONE* zone = new ZONE( board.get() );
3291 zone->SetLayer( F_Cu );
3292 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
3293 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 20 ), 0 ), -1 );
3294 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 20 ), pcbIUScale.mmToIU( 20 ) ), -1 );
3295 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 20 ) ), -1 );
3297
3298 THIEVING_SETTINGS thieving;
3300 thieving.element_size = pcbIUScale.mmToIU( 0.5 );
3301 thieving.gap = pcbIUScale.mmToIU( 2.0 );
3302 thieving.stagger = stagger;
3303 zone->SetThievingSettings( thieving );
3305 board->Add( zone );
3306
3307 KI_TEST::FillZones( board.get() );
3308 return zone->GetFilledPolysList( F_Cu )->OutlineCount();
3309 };
3310
3311 int plain = countDots( false );
3312 int staggered = countDots( true );
3313
3314 BOOST_TEST_MESSAGE( "plain dots: " << plain << " staggered dots: " << staggered );
3315
3316 // Within a factor of two — catches the offset walking dots off the board
3317 // (returning ~0) without being brittle about edge-clipping rounding.
3318 BOOST_CHECK_NE( plain, staggered );
3319 BOOST_CHECK_GE( staggered, plain / 2 );
3320 BOOST_CHECK_LE( staggered, plain * 2 );
3321}
3322
3323
3329BOOST_FIXTURE_TEST_CASE( CopperThievingZone_SquaresGrid, ZONE_FILL_TEST_FIXTURE )
3330{
3331 m_board = std::make_unique<BOARD>();
3332 m_board->SetCopperLayerCount( 2 );
3333
3334 ZONE* zone = new ZONE( m_board.get() );
3335 zone->SetLayer( F_Cu );
3336 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
3337 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
3338 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
3339 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
3341
3342 THIEVING_SETTINGS thieving;
3344 thieving.element_size = pcbIUScale.mmToIU( 0.6 );
3345 thieving.gap = pcbIUScale.mmToIU( 2.0 );
3346 zone->SetThievingSettings( thieving );
3348 m_board->Add( zone );
3349
3350 KI_TEST::FillZones( m_board.get() );
3351
3352 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
3353 BOOST_REQUIRE( fill );
3354 BOOST_REQUIRE_GT( fill->OutlineCount(), 0 );
3355
3356 // 2.6 mm pitch, 0.6 mm square. Same edge-drop behavior as the dots test:
3357 // strict-containment leaves a 3 x 3 grid of full squares.
3358 BOOST_CHECK_GE( fill->OutlineCount(), 6 );
3359 BOOST_CHECK_LE( fill->OutlineCount(), 12 );
3360
3361 const double fullSquareArea = std::pow( pcbIUScale.mmToIU( 0.6 ), 2 );
3362 CheckAllOutlineAreasAtLeast( fill, 0.9 * fullSquareArea, wxT( "Square" ) );
3363}
3364
3365
3374BOOST_FIXTURE_TEST_CASE( CopperThievingZone_HighDensityPerformance, ZONE_FILL_TEST_FIXTURE )
3375{
3376 m_board = std::make_unique<BOARD>();
3377 m_board->SetCopperLayerCount( 2 );
3378
3379 ZONE* zone = new ZONE( m_board.get() );
3380 zone->SetLayer( F_Cu );
3381 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
3382 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 100 ), 0 ), -1 );
3383 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 100 ), pcbIUScale.mmToIU( 100 ) ), -1 );
3384 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 100 ) ), -1 );
3386
3387 THIEVING_SETTINGS thieving;
3389 thieving.element_size = pcbIUScale.mmToIU( 0.3 );
3390 thieving.gap = pcbIUScale.mmToIU( 1.0 );
3391 zone->SetThievingSettings( thieving );
3393 m_board->Add( zone );
3394
3395 auto start = std::chrono::steady_clock::now();
3396 KI_TEST::FillZones( m_board.get() );
3397 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
3398 std::chrono::steady_clock::now() - start )
3399 .count();
3400
3401 BOOST_TEST_MESSAGE( "5.9k-dot fill elapsed: " << elapsed << " ms" );
3402
3404 BOOST_CHECK_GT( zone->GetFilledPolysList( F_Cu )->OutlineCount(), 4000 );
3405
3406 // Sanitizer instrumentation changes execution cost, not the fill contract
3407#if !defined( KICAD_SANITIZE_THREADS ) && !defined( KICAD_SANITIZE_ADDRESS )
3408 // 30 s upper bound on QABUILD with assertions on; current implementation
3409 // measures in low seconds.
3410 BOOST_CHECK_LT( elapsed, 30000 );
3411#endif
3412}
3413
3414
3421BOOST_FIXTURE_TEST_CASE( CopperThievingZone_HatchPattern, ZONE_FILL_TEST_FIXTURE )
3422{
3423 m_board = std::make_unique<BOARD>();
3424 m_board->SetCopperLayerCount( 2 );
3425
3426 ZONE* zone = new ZONE( m_board.get() );
3427 zone->SetLayer( F_Cu );
3428 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
3429 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
3430 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
3431 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
3433
3434 THIEVING_SETTINGS thieving;
3436 thieving.gap = pcbIUScale.mmToIU( 2.0 );
3437 thieving.line_width = pcbIUScale.mmToIU( 0.3 );
3438 zone->SetThievingSettings( thieving );
3440 m_board->Add( zone );
3441
3442 KI_TEST::FillZones( m_board.get() );
3443
3444 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
3445 BOOST_REQUIRE( fill );
3446 BOOST_REQUIRE_GT( fill->TotalVertices(), 0 );
3447
3448 // Subtractive hatch produces a single connected outline after fracturing
3449 // (perimeter border + interior mesh linked through bridges). A dot grid
3450 // in the same outline would have dozens of disconnected pieces.
3451 BOOST_CHECK_EQUAL( fill->OutlineCount(), 1 );
3452
3453 // The fill bounding box must reach the zone corners — the perimeter
3454 // border is what differentiates hatch from a dot grid. Solid would also
3455 // reach the corners; the high vertex count below catches that case.
3456 BOX2I fillBox = fill->BBox();
3457 BOOST_CHECK_LT( fillBox.GetLeft(), pcbIUScale.mmToIU( 0.5 ) );
3458 BOOST_CHECK_GT( fillBox.GetRight(), pcbIUScale.mmToIU( 9.5 ) );
3459 BOOST_CHECK_LT( fillBox.GetTop(), pcbIUScale.mmToIU( 0.5 ) );
3460 BOOST_CHECK_GT( fillBox.GetBottom(), pcbIUScale.mmToIU( 9.5 ) );
3461
3462 // A solid 10x10 mm rectangle would have ~4 vertices. A hatched mesh has
3463 // many vertices because each void cut adds outline segments.
3464 BOOST_CHECK_GT( fill->TotalVertices(), 30 );
3465}
3466
3467
3475BOOST_FIXTURE_TEST_CASE( RegressionNonCopperZoneKeepoutIslands, ZONE_FILL_TEST_FIXTURE )
3476{
3477 KI_TEST::LoadBoard( m_settingsManager, "issue24089/issue24089", m_board );
3478
3479 auto countIslands =
3480 [this]() -> int
3481 {
3482 int total = 0;
3483
3484 for( ZONE* zone : m_board->Zones() )
3485 {
3486 if( zone->GetIsRuleArea() )
3487 continue;
3488
3489 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
3490 {
3491 if( !zone->HasFilledPolysForLayer( layer ) )
3492 continue;
3493
3494 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
3495
3496 if( fill )
3497 total += fill->OutlineCount();
3498 }
3499 }
3500
3501 return total;
3502 };
3503
3504 int storedIslands = countIslands();
3505
3506 BOOST_REQUIRE_MESSAGE( storedIslands >= 3,
3507 wxString::Format( "Stored v9 fill should have at least 3 silk islands; "
3508 "found %d",
3509 storedIslands ) );
3510
3511 KI_TEST::FillZones( m_board.get() );
3512
3513 int refilledIslands = countIslands();
3514
3515 BOOST_CHECK_MESSAGE( refilledIslands == storedIslands,
3516 wxString::Format( "Refill lost silk islands: stored=%d, refilled=%d. "
3517 "Outline 0 of every non-copper multi-island zone "
3518 "was being incorrectly removed (issue 24089).",
3519 storedIslands, refilledIslands ) );
3520}
3521
3522
3528BOOST_FIXTURE_TEST_CASE( OverlappingPriorityPadFlashing, ZONE_FILL_TEST_FIXTURE )
3529{
3530 m_board = std::make_unique<BOARD>();
3531 m_board->SetCopperLayerCount( 4 );
3532
3533 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3534 bds.SetCopperLayerCount( 4 );
3535 bds.m_MinClearance = pcbIUScale.mmToIU( 0.2 );
3536
3537 NETINFO_ITEM* gndNet = new NETINFO_ITEM( m_board.get(), wxT( "GND" ) );
3538 m_board->Add( gndNet );
3539 int gndNetCode = gndNet->GetNetCode();
3540
3541 NETINFO_ITEM* vccNet = new NETINFO_ITEM( m_board.get(), wxT( "VCC" ) );
3542 m_board->Add( vccNet );
3543 int vccNetCode = vccNet->GetNetCode();
3544
3545 ZONE* gndZone = new ZONE( m_board.get() );
3546 gndZone->SetLayer( In1_Cu );
3547 gndZone->SetNetCode( gndNetCode );
3548 gndZone->SetAssignedPriority( 0 );
3549 gndZone->SetMinThickness( pcbIUScale.mmToIU( 0.2 ) );
3550 gndZone->SetThermalReliefGap( pcbIUScale.mmToIU( 0.5 ) );
3551 gndZone->SetThermalReliefSpokeWidth( pcbIUScale.mmToIU( 0.5 ) );
3553 {
3554 SHAPE_POLY_SET outline;
3555 outline.NewOutline();
3556 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ) );
3557 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 0 ) ) );
3558 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 20 ) ) );
3559 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 20 ) ) );
3560 gndZone->AddPolygon( outline.COutline( 0 ) );
3561 }
3562 m_board->Add( gndZone );
3563
3564 ZONE* vccZone = new ZONE( m_board.get() );
3565 vccZone->SetLayer( In1_Cu );
3566 vccZone->SetNetCode( vccNetCode );
3567 vccZone->SetAssignedPriority( 5 );
3568 vccZone->SetMinThickness( pcbIUScale.mmToIU( 4.0 ) );
3569 vccZone->SetThermalReliefGap( pcbIUScale.mmToIU( 0.5 ) );
3570 vccZone->SetThermalReliefSpokeWidth( pcbIUScale.mmToIU( 0.5 ) );
3572 {
3573 // A "barbell": a bulky right lobe joined to a thin left neck by a 0.5mm-tall corridor.
3574 // VCC's 4mm min-thickness prunes the neck and corridor in the deflate/inflate pass, so
3575 // VCC fills only the right lobe while its outline still encloses the pad at (15, 10).
3576 SHAPE_POLY_SET outline;
3577 outline.NewOutline();
3578 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 13.5 ), pcbIUScale.mmToIU( 9.75 ) ) );
3579 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 18 ), pcbIUScale.mmToIU( 9.75 ) ) );
3580 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 18 ), pcbIUScale.mmToIU( 0 ) ) );
3581 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 0 ) ) );
3582 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 20 ) ) );
3583 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 18 ), pcbIUScale.mmToIU( 20 ) ) );
3584 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 18 ), pcbIUScale.mmToIU( 10.25 ) ) );
3585 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 13.5 ), pcbIUScale.mmToIU( 10.25 ) ) );
3586 vccZone->AddPolygon( outline.COutline( 0 ) );
3587 }
3588 m_board->Add( vccZone );
3589
3590 // REMOVE_EXCEPT_START_AND_END makes inner-layer flashing conditional on a same-net
3591 // connection, which is what issue 24175 gets wrong.
3592 auto footprint = std::make_unique<FOOTPRINT>( m_board.get() );
3593
3594 PAD* pad = new PAD( footprint.get() );
3595 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
3596 pad->SetAttribute( PAD_ATTRIB::PTH );
3597 pad->SetLayerSet( LSET::AllCuMask() );
3598 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.5 ), pcbIUScale.mmToIU( 1.5 ) ) );
3599 pad->SetDrillSize( VECTOR2I( pcbIUScale.mmToIU( 0.8 ), pcbIUScale.mmToIU( 0.8 ) ) );
3600 pad->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 10 ) ) );
3601 pad->SetUnconnectedLayerMode( UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END );
3602 pad->SetNetCode( gndNetCode );
3603
3604 footprint->Add( pad );
3605 footprint->SetPosition( VECTOR2I( 0, 0 ) );
3606 m_board->Add( footprint.release() );
3607
3608 m_board->BuildConnectivity();
3609 auto drcEngine = std::make_shared<DRC_ENGINE>( m_board.get(), &bds );
3610 drcEngine->InitEngine( wxFileName() );
3611 bds.m_DRCEngine = drcEngine;
3612
3613 KI_TEST::FillZones( m_board.get() );
3614
3615 // Guard the preconditions so a future fill change cannot make this pass for the wrong
3616 // reason: VCC's outline must enclose the pad while its fill must not reach it.
3617 BOOST_REQUIRE_MESSAGE( vccZone->Outline()->Contains( pad->GetPosition() ),
3618 "VCC outline must contain the pad position to reproduce issue 24175." );
3619 BOOST_REQUIRE_MESSAGE( vccZone->HasFilledPolysForLayer( In1_Cu ),
3620 "VCC zone should still have fill in its right lobe." );
3621
3622 {
3623 const std::shared_ptr<SHAPE_POLY_SET>& vccFill = vccZone->GetFilledPolysList( In1_Cu );
3624
3625 BOOST_REQUIRE_MESSAGE( !vccFill->Contains( pad->GetPosition() ),
3626 "VCC fill should NOT contain the pad position (corridor must be "
3627 "pruned by min-thickness for the test to exercise issue 24175)." );
3628 }
3629
3630 // Before the fix the higher-priority VCC outline forced ZLO_FORCE_NO_ZONE_CONNECTION on
3631 // the pad; now the same-net GND zone wins the flashing decision.
3632 BOOST_CHECK_MESSAGE( pad->FlashLayer( In1_Cu ),
3633 "PTH pad inside higher-priority different-net zone must still flash "
3634 "when a same-net lower-priority zone covers it (issue 24175)." );
3635
3636 BOOST_REQUIRE_MESSAGE( gndZone->HasFilledPolysForLayer( In1_Cu ),
3637 "GND zone should have fill on In1.Cu" );
3638
3639 const std::shared_ptr<SHAPE_POLY_SET>& gndFill = gndZone->GetFilledPolysList( In1_Cu );
3640
3641 // Sampling a ring just outside the pad proves the GND fill actually surrounds it.
3642 int samples = 16;
3643 int sampleR = pcbIUScale.mmToIU( 1.6 ); // just outside the pad (radius 0.75) + clearance
3644 bool foundCopperAround = false;
3645
3646 for( int i = 0; i < samples; i++ )
3647 {
3648 double angle = ( 2.0 * M_PI * i ) / samples;
3649 VECTOR2I p( pad->GetPosition().x + KiROUND( sampleR * std::cos( angle ) ),
3650 pad->GetPosition().y + KiROUND( sampleR * std::sin( angle ) ) );
3651
3652 if( gndFill->Contains( p ) )
3653 {
3654 foundCopperAround = true;
3655 break;
3656 }
3657 }
3658
3659 BOOST_CHECK_MESSAGE( foundCopperAround,
3660 "Lower-priority GND zone should have copper around GND pad even when "
3661 "a higher-priority different-net zone outline contains the pad "
3662 "(issue 24175)." );
3663}
3664
3665
3666// Reproduces the scripting/API zone-fill path used by KiKit panelization (issue 24643).
3667//
3668// The interactive GUI and the board loader always create and initialize the board's DRC engine
3669// before filling. The Python/API ZONE_FILLER path can reach Fill() with no engine, so the
3670// worker-thread EvalRules() calls dereferenced a null engine and crashed the process. This test
3671// drops the engine after loading to drive that path, then verifies Fill() completes and leaves a
3672// usable engine behind.
3673BOOST_FIXTURE_TEST_CASE( RegressionApiSubsetFillPanelized, ZONE_FILL_TEST_FIXTURE )
3674{
3675 KI_TEST::LoadBoard( m_settingsManager, "issue24643/issue24643", m_board );
3676
3677 // The test harness loads boards with an initialized engine; the headless API path does not.
3678 // Drop it so Fill() must reconstruct one, which is the condition that crashed.
3679 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3680 bds.m_DRCEngine.reset();
3681 BOOST_REQUIRE( !bds.m_DRCEngine );
3682
3683 // Mirror the script: select non-rule-area zones on B.Cu that are not already filled.
3684 PCB_LAYER_ID targetLayer = m_board->GetLayerID( wxT( "B.Cu" ) );
3685 std::vector<ZONE*> toFill;
3686
3687 for( ZONE* zone : m_board->Zones() )
3688 {
3689 if( zone->GetIsRuleArea() )
3690 continue;
3691
3692 if( !zone->IsOnLayer( targetLayer ) )
3693 continue;
3694
3695 if( zone->IsFilled() )
3696 continue;
3697
3698 toFill.push_back( zone );
3699 }
3700
3701 BOOST_REQUIRE_MESSAGE( !toFill.empty(),
3702 "Expected at least one unfilled B.Cu zone to exercise the API path." );
3703
3704 // The API path builds the filler with a null commit (see new_ZONE_FILLER in the SWIG
3705 // wrapper) and fills only the selected subset. This must complete without crashing
3706 // (issue 24643).
3707 ZONE_FILLER filler( m_board.get(), nullptr );
3708
3709 BOOST_CHECK_NO_THROW( filler.Fill( toFill ) );
3710
3711 // Fill() must have created and initialized a usable engine in place of the one we dropped.
3713 BOOST_CHECK( bds.m_DRCEngine->RulesValid() );
3714}
3715
3716
3717// Issue 23790: overlapping same-net zones must merge across a notch a higher-priority
3718// different-net zone carved into the higher-priority same-net zone.
3719BOOST_FIXTURE_TEST_CASE( RegressionSameNetMergeAroundHigherPriorityZone, ZONE_FILL_TEST_FIXTURE )
3720{
3721 // The reconciliation only runs inside the iterative refill.
3722 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
3723
3724 struct ScopeGuard
3725 {
3726 bool& ref;
3727 bool orig;
3728 ~ScopeGuard() { ref = orig; }
3730
3731 cfg.m_ZoneFillIterativeRefill = true;
3732
3733 KI_TEST::LoadBoard( m_settingsManager, "issue23790/issue23790", m_board );
3734 KI_TEST::FillZones( m_board.get() );
3735
3736 const PCB_LAYER_ID layer = F_Cu;
3737 const int margin = pcbIUScale.mmToIU( 0.05 );
3738
3739 std::map<int, SHAPE_POLY_SET> mergedByNet;
3740
3741 for( ZONE* zone : m_board->Zones() )
3742 {
3743 if( zone->GetIsRuleArea() || !zone->HasFilledPolysForLayer( layer ) )
3744 continue;
3745
3746 mergedByNet[zone->GetNetCode()].BooleanAdd( *zone->GetFilledPolysList( layer ) );
3747 }
3748
3749 // Areas legitimately free of this net's copper: keepouts and higher-priority
3750 // different-net fills (grown by a clearance allowance).
3751 auto buildLegitVoids =
3752 [&]( const ZONE* aLower, const ZONE* aHigher ) -> SHAPE_POLY_SET
3753 {
3754 SHAPE_POLY_SET voids;
3755 int allowance = pcbIUScale.mmToIU( 0.6 );
3756
3757 for( ZONE* other : m_board->Zones() )
3758 {
3759 if( !other->GetLayerSet().Contains( layer ) )
3760 continue;
3761
3762 if( other->GetIsRuleArea() )
3763 {
3764 if( other->GetDoNotAllowZoneFills() )
3765 voids.BooleanAdd( *other->Outline() );
3766
3767 continue;
3768 }
3769
3770 if( other->GetNetCode() == aLower->GetNetCode()
3771 || other->GetAssignedPriority() <= aLower->GetAssignedPriority()
3772 || other->GetAssignedPriority() <= aHigher->GetAssignedPriority()
3773 || !other->HasFilledPolysForLayer( layer ) )
3774 {
3775 continue;
3776 }
3777
3778 SHAPE_POLY_SET fill = *other->GetFilledPolysList( layer );
3780 voids.BooleanAdd( fill );
3781 }
3782
3783 return voids;
3784 };
3785
3786 std::vector<ZONE*> zones;
3787
3788 for( ZONE* zone : m_board->Zones() )
3789 {
3790 if( !zone->GetIsRuleArea() && zone->GetNetCode() > 0 && zone->GetLayerSet().Contains( layer ) )
3791 zones.push_back( zone );
3792 }
3793
3794 int checkedPairs = 0;
3795
3796 for( size_t i = 0; i < zones.size(); ++i )
3797 {
3798 for( size_t j = i + 1; j < zones.size(); ++j )
3799 {
3800 ZONE* a = zones[i];
3801 ZONE* b = zones[j];
3802
3803 if( a->GetNetCode() != b->GetNetCode() )
3804 continue;
3805
3806 SHAPE_POLY_SET overlap = *a->Outline();
3807 overlap.BooleanIntersection( *b->Outline() );
3808
3809 if( overlap.OutlineCount() == 0 )
3810 continue;
3811
3812 const ZONE* lower = a->GetAssignedPriority() <= b->GetAssignedPriority() ? a : b;
3813 const ZONE* higher = ( lower == a ) ? b : a;
3814
3815 overlap.BooleanSubtract( buildLegitVoids( lower, higher ) );
3816
3817 // Stay clear of outer-boundary min-width rounding.
3819
3820 if( overlap.OutlineCount() == 0 )
3821 continue;
3822
3823 SHAPE_POLY_SET uncovered = overlap;
3824 uncovered.BooleanSubtract( mergedByNet[a->GetNetCode()] );
3825
3826 double uncoveredArea =
3827 uncovered.Area() / ( pcbIUScale.IU_PER_MM * (double) pcbIUScale.IU_PER_MM );
3828
3829 BOOST_CHECK_MESSAGE( uncoveredArea < 0.01,
3830 wxString::Format( "Same-net zones (priorities %d and %d) left %.4f mm^2 of "
3831 "their overlap unfilled; overlapping same-net zones must "
3832 "merge (issue 23790).",
3834 uncoveredArea ) );
3835 checkedPairs++;
3836 }
3837 }
3838
3839 BOOST_CHECK_MESSAGE( checkedPairs >= 2,
3840 wxString::Format( "Expected at least two overlapping same-net zone pairs "
3841 "to exercise the merge, found %d.", checkedPairs ) );
3842}
3843
3844
3845// Issue 24935: a lower-priority same-net zone must not pour through the hatch windows of a
3846// higher-priority hatched zone during the iterative refill.
3847BOOST_FIXTURE_TEST_CASE( RegressionHatchedZonePriorityRefill, ZONE_FILL_TEST_FIXTURE )
3848{
3849 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
3850
3851 struct ScopeGuard
3852 {
3853 bool& ref;
3854 bool orig;
3855 ~ScopeGuard() { ref = orig; }
3857
3858 for( bool iterative : { true, false } )
3859 {
3860 cfg.m_ZoneFillIterativeRefill = iterative;
3861
3862 KI_TEST::LoadBoard( m_settingsManager, "issue24935/issue24935", m_board );
3863 KI_TEST::FillZones( m_board.get() );
3864
3865 const PCB_LAYER_ID layer = F_Cu;
3866 ZONE* hatched = nullptr;
3867 ZONE* solid = nullptr;
3868
3869 for( ZONE* zone : m_board->Zones() )
3870 {
3871 if( zone->GetFillMode() == ZONE_FILL_MODE::HATCH_PATTERN )
3872 hatched = zone;
3873 else
3874 solid = zone;
3875 }
3876
3877 BOOST_REQUIRE( hatched && solid );
3878 BOOST_REQUIRE( hatched->GetAssignedPriority() > solid->GetAssignedPriority() );
3879 BOOST_REQUIRE( hatched->SameNet( solid ) );
3880 BOOST_REQUIRE( hatched->HasFilledPolysForLayer( layer ) );
3881 BOOST_REQUIRE( solid->HasFilledPolysForLayer( layer ) );
3882
3883 const double mm2 = pcbIUScale.IU_PER_MM * (double) pcbIUScale.IU_PER_MM;
3884
3885 // Guard against a degenerate fixture: the hatch must leave most of its outline open,
3886 // otherwise the containment check below proves nothing.
3887 SHAPE_POLY_SET hatchedFill = hatched->GetFilledPolysList( layer )->CloneDropTriangulation();
3888 SHAPE_POLY_SET hatchedOutline = hatched->Outline()->CloneDropTriangulation();
3889 BOOST_REQUIRE( hatchedFill.Area() < 0.8 * hatchedOutline.Area() );
3890
3891 // The lower-priority zone owns nothing inside the hatched zone's outline.
3892 SHAPE_POLY_SET window = hatchedOutline;
3894
3895 SHAPE_POLY_SET leaked = solid->GetFilledPolysList( layer )->CloneDropTriangulation();
3896 leaked.BooleanIntersection( window );
3897
3898 double leakedArea = leaked.Area() / mm2;
3899
3900 BOOST_CHECK_MESSAGE( leakedArea < 0.01,
3901 wxString::Format( "%s: lower-priority zone poured %.3f mm^2 inside the "
3902 "higher-priority hatched zone (issue 24935).",
3903 iterative ? wxS( "iterative refill" ) : wxS( "single pass" ),
3904 leakedArea ) );
3905 }
3906}
3907
3908
3909// Issue 24758: this board is densely tiled with same-net zones, so the zone-fill dependency-DAG
3910// scheduler builds a large successor graph. The scheduler returned once its logical work counter
3911// reached zero while detached worker tasks -- which captured the successor/in-degree vectors by
3912// reference -- were still in flight, dereferencing the freed locals inside ZONE_FILLER::Fill.
3913// Filling repeatedly drives that window; under AddressSanitizer the use-after-free is reported
3914// deterministically without the fix.
3915BOOST_FIXTURE_TEST_CASE( RegressionSameNetZoneFillScheduler, ZONE_FILL_TEST_FIXTURE )
3916{
3917 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
3918
3919 struct ScopeGuard
3920 {
3921 bool& ref;
3922 bool orig;
3923 ~ScopeGuard() { ref = orig; }
3925
3926 cfg.m_ZoneFillIterativeRefill = true;
3927
3928 KI_TEST::LoadBoard( m_settingsManager, "issue24758/issue24758", m_board );
3929
3930 const PCB_LAYER_ID layer = F_Cu;
3931
3932 for( int pass = 0; pass < 64; ++pass )
3933 {
3934 BOOST_REQUIRE_NO_THROW( KI_TEST::FillZones( m_board.get() ) );
3935
3936 // Every same-net pour must come back with copper; a torn-down scheduler also corrupts
3937 // or drops fills, so assert the result is usable on every pass.
3938 SHAPE_POLY_SET merged;
3939
3940 for( ZONE* zone : m_board->Zones() )
3941 {
3942 if( zone->GetIsRuleArea() || !zone->HasFilledPolysForLayer( layer ) )
3943 continue;
3944
3945 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
3946
3947 BOOST_REQUIRE( fill != nullptr );
3948 merged.BooleanAdd( *fill );
3949 }
3950
3951 BOOST_CHECK_MESSAGE( merged.Area() > 0.0,
3952 wxString::Format( "Fill pass %d produced no copper.", pass ) );
3953 }
3954}
3955
3956
3957// Issue 24758: a hatch zone must keep its solid border where a higher-priority zone carves into
3958// it, not run the mesh into the carved edge. Board issue24758 carves net-B zones into net-A hatch.
3959BOOST_FIXTURE_TEST_CASE( RegressionHatchBorderAroundOverlap, ZONE_FILL_TEST_FIXTURE )
3960{
3961 KI_TEST::LoadBoard( m_settingsManager, "issue24758/issue24758", m_board );
3962 KI_TEST::FillZones( m_board.get() );
3963
3964 const PCB_LAYER_ID layer = F_Cu;
3965
3966 auto outlineNoArcs =
3967 []( ZONE* z )
3968 {
3969 SHAPE_POLY_SET o = *z->Outline();
3970 o.ClearArcs();
3971 return o;
3972 };
3973
3974 int checkedPairs = 0;
3975
3976 for( ZONE* hatch : m_board->Zones() )
3977 {
3978 if( hatch->GetIsRuleArea() || hatch->GetFillMode() != ZONE_FILL_MODE::HATCH_PATTERN
3979 || !hatch->HasFilledPolysForLayer( layer ) )
3980 {
3981 continue;
3982 }
3983
3984 SHAPE_POLY_SET hatchOutline = outlineNoArcs( hatch );
3985 SHAPE_POLY_SET fill = *hatch->GetFilledPolysList( layer );
3986
3987 for( ZONE* other : m_board->Zones() )
3988 {
3989 if( other == hatch || other->GetIsRuleArea() || !other->GetLayerSet().Contains( layer )
3990 || other->GetAssignedPriority() <= hatch->GetAssignedPriority() )
3991 {
3992 continue;
3993 }
3994
3995 SHAPE_POLY_SET carved = outlineNoArcs( other );
3996 carved.BooleanIntersection( hatchOutline );
3997
3998 // Need real claimed area to have a border to test.
3999 if( carved.Area() < pcbIUScale.mmToIU( 0.1 ) * (double) pcbIUScale.mmToIU( 0.1 ) )
4000 continue;
4001
4002 // Ring past the clearance void: solid with the border, ~half hatch holes without it.
4003 SHAPE_POLY_SET outer = carved;
4005
4006 SHAPE_POLY_SET inner = carved;
4008
4009 SHAPE_POLY_SET band = outer;
4010 band.BooleanSubtract( inner );
4011 band.BooleanIntersection( hatchOutline );
4012
4013 if( band.Area() <= 0 )
4014 continue;
4015
4016 SHAPE_POLY_SET covered = band;
4017 covered.BooleanIntersection( fill );
4018
4019 double coverage = covered.Area() / band.Area();
4020 checkedPairs++;
4021
4022 BOOST_CHECK_MESSAGE( coverage >= 0.85,
4023 wxString::Format( "Hatch zone %s carved by %s: border ring only %.0f%% filled; "
4024 "the hatch border was not re-established around the carved "
4025 "area (issue 24758).",
4026 hatch->GetZoneName(), other->GetZoneName(),
4027 coverage * 100.0 ) );
4028 }
4029 }
4030
4031 BOOST_CHECK_MESSAGE( checkedPairs >= 3,
4032 wxString::Format( "Expected at least three carved hatch borders to check, "
4033 "found %d.", checkedPairs ) );
4034}
4035
4036
4037// A pair separated by more than the clearance but less than the knockout reach used to fill
4038// unordered, so the knocked-out copper depended on which task finished first.
4039BOOST_FIXTURE_TEST_CASE( ZoneFillDependencyKnockoutMargin, ZONE_FILL_TEST_FIXTURE )
4040{
4041 m_board = std::make_unique<BOARD>();
4042
4043 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
4044 const int clearance = pcbIUScale.mmToIU( 2 );
4045
4046 // Also the board's worst clearance, so the separation below lands between the worst
4047 // clearance and the knockout reach.
4049
4050 NETINFO_ITEM* netA = new NETINFO_ITEM( m_board.get(), wxT( "NET_A" ), 1 );
4051 NETINFO_ITEM* netB = new NETINFO_ITEM( m_board.get(), wxT( "NET_B" ), 2 );
4052 m_board->Add( netA );
4053 m_board->Add( netB );
4054
4055 // Separation lands inside the max-error part of the reach, so the gate's error term and
4056 // the fill ordering are both exercised.
4057 const int extraMargin = pcbIUScale.mmToIU( ADVANCED_CFG::GetCfg().m_ExtraClearance );
4058 const int maxError = bds.m_MaxError;
4059 const int sep = clearance + extraMargin + maxError / 2;
4060 const int ax = pcbIUScale.mmToIU( 100 ) + sep;
4061
4062 ZONE* zoneA = new ZONE( m_board.get() );
4063 zoneA->SetLayer( F_Cu );
4064 zoneA->SetNet( netA );
4065 zoneA->SetAssignedPriority( 0 );
4066 zoneA->AppendCorner( VECTOR2I( ax, 0 ), -1 );
4067 zoneA->AppendCorner( VECTOR2I( ax + pcbIUScale.mmToIU( 2 ), 0 ), -1 );
4068 zoneA->AppendCorner( VECTOR2I( ax + pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 10 ) ), -1 );
4069 zoneA->AppendCorner( VECTOR2I( ax, pcbIUScale.mmToIU( 10 ) ), -1 );
4070 m_board->Add( zoneA );
4071
4072 // The sawtooth keeps this fill busy long enough that, without a dependency edge, the
4073 // small zone (seeded first) reliably fills before this fill publishes.
4074 ZONE* zoneB = new ZONE( m_board.get() );
4075 zoneB->SetLayer( F_Cu );
4076 zoneB->SetNet( netB );
4077 zoneB->SetAssignedPriority( 1 );
4078 zoneB->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 1 ), 0 ), -1 );
4079 zoneB->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 100 ), 0 ), -1 );
4080 zoneB->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 100 ), pcbIUScale.mmToIU( 100 ) ), -1 );
4081 zoneB->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 100 ) ), -1 );
4082
4083 for( int ii = 0; ii < 1000; ++ii )
4084 {
4085 int yTop = pcbIUScale.mmToIU( 100 ) - ii * pcbIUScale.mmToIU( 0.1 );
4086
4087 zoneB->AppendCorner( VECTOR2I( 0, yTop - pcbIUScale.mmToIU( 0.05 ) ), -1 );
4088 zoneB->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 1 ), yTop - pcbIUScale.mmToIU( 0.1 ) ), -1 );
4089 }
4090
4091 m_board->Add( zoneB );
4092
4093 KI_TEST::FillZones( m_board.get() );
4094
4095 std::shared_ptr<SHAPE_POLY_SET> fillA = zoneA->GetFilledPolysList( F_Cu );
4096 std::shared_ptr<SHAPE_POLY_SET> fillB = zoneB->GetFilledPolysList( F_Cu );
4097
4098 BOOST_REQUIRE( fillA && fillA->OutlineCount() > 0 );
4099 BOOST_REQUIRE( fillB && fillB->OutlineCount() > 0 );
4100
4101 // Threshold sits between the knocked-out gap and the un-knocked outline separation, so the
4102 // check distinguishes an ordered fill from a raced one.
4103 BOOST_CHECK_MESSAGE( !fillA->Collide( fillB.get(), clearance + extraMargin + maxError * 3 / 4 ),
4104 "Lower-priority zone filled before the higher-priority knockout was "
4105 "published; the fill depends on thread scheduling." );
4106}
4107
4108
4118BOOST_FIXTURE_TEST_CASE( RegressionZoneFillNarrowBridge, ZONE_FILL_TEST_FIXTURE )
4119{
4120 KI_TEST::LoadBoard( m_settingsManager, "issue24312/issue24312", m_board );
4121
4122 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
4123
4124 // Force connection-width severity so the regression assertion does not silently
4125 // weaken if the reproduction project is updated to ignore this code.
4127
4128 KI_TEST::FillZones( m_board.get() );
4129
4130 std::vector<DRC_ITEM> violations;
4131
4133 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
4134 const std::function<void( PCB_MARKER* )>& aPathGenerator )
4135 {
4136 if( aItem->GetErrorCode() == DRCE_CONNECTION_WIDTH )
4137 violations.push_back( *aItem );
4138 } );
4139
4140 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
4141
4142 if( !violations.empty() )
4143 {
4144 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::MM );
4145
4146 std::map<KIID, EDA_ITEM*> itemMap;
4147 m_board->FillItemMap( itemMap );
4148
4149 for( const DRC_ITEM& item : violations )
4150 BOOST_TEST_MESSAGE( item.ShowReport( &unitsProvider, RPT_SEVERITY_ERROR, itemMap ) );
4151 }
4152
4153 BOOST_CHECK_MESSAGE( violations.empty(),
4154 wxString::Format( "Zone fill produced %zu connection_width violations; "
4155 "expected 0 (issue 24312).",
4156 violations.size() ) );
4157}
4158
4159
4160BOOST_FIXTURE_TEST_CASE( RegressionIterativeRefillFullWidthBridge, ZONE_FILL_TEST_FIXTURE )
4161{
4162 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
4163 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
4164
4165 struct ScopeGuard
4166 {
4167 bool& ref;
4168 bool orig;
4169 ~ScopeGuard() { ref = orig; }
4170 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
4171
4172 cfg.m_ZoneFillIterativeRefill = true;
4173 KI_TEST::LoadBoard( m_settingsManager, "issue24835/issue24835-min", m_board );
4174
4175 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
4176 bds.m_MinConn = pcbIUScale.mmToIU( 0.1016 );
4178 bds.m_DRCEngine->InitEngine( wxFileName() );
4179
4180 KI_TEST::FillZones( m_board.get() );
4181
4182 const VECTOR2I bridgeCenter( pcbIUScale.mmToIU( 104.220616 ),
4183 pcbIUScale.mmToIU( 103.646866 ) );
4184
4185 for( PCB_LAYER_ID layer : { In1_Cu, In4_Cu } )
4186 {
4187 double localCopperArea = 0.0;
4188
4189 for( ZONE* zone : m_board->Zones() )
4190 {
4191 if( !zone->IsOnLayer( layer ) )
4192 continue;
4193
4194 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
4195 SHAPE_POLY_SET local;
4196 int radius = pcbIUScale.mmToIU( 0.2 );
4197
4198 local.NewOutline();
4199 local.Append( bridgeCenter + VECTOR2I( -radius, -radius ) );
4200 local.Append( bridgeCenter + VECTOR2I( radius, -radius ) );
4201 local.Append( bridgeCenter + VECTOR2I( radius, radius ) );
4202 local.Append( bridgeCenter + VECTOR2I( -radius, radius ) );
4203
4204 if( fill )
4205 local.BooleanIntersection( *fill );
4206
4207 localCopperArea = std::max( localCopperArea, std::abs( local.Area() ) );
4208 }
4209
4210 double minimumLocalCopperArea = 0.05 * pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM;
4211
4212 BOOST_CHECK_MESSAGE( localCopperArea > minimumLocalCopperArea,
4213 wxString::Format( "Expected copper around the bridge on %s; the "
4214 "bridge must be widened, not removed.",
4215 LSET::Name( layer ) ) );
4216 }
4217
4218 std::vector<DRC_ITEM> violations;
4219
4221 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
4222 const std::function<void( PCB_MARKER* )>& )
4223 {
4224 if( aItem->GetErrorCode() == DRCE_CONNECTION_WIDTH )
4225 violations.push_back( *aItem );
4226 } );
4227
4228 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
4229
4230 BOOST_CHECK_MESSAGE( violations.empty(),
4231 wxString::Format( "Iterative refill produced %zu connection_width "
4232 "violations; expected full-width bridges (issue 24835).",
4233 violations.size() ) );
4234}
4235
4236
4237// Issue 23790: the iterative refill's min-width cycle runs after the fill has been trimmed to the
4238// zone outline, so without the same-net apron a border shared with an abutting same-net zone looks
4239// like a free convex corner and gets rounded off. Both fill modes must agree along such a border.
4240BOOST_FIXTURE_TEST_CASE( SameNetBorderIdenticalAcrossFillModes, ZONE_FILL_TEST_FIXTURE )
4241{
4242 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
4243
4244 struct ScopeGuard
4245 {
4246 bool& ref;
4247 bool orig;
4248 ~ScopeGuard() { ref = orig; }
4250
4251 const PCB_LAYER_ID layer = F_Cu;
4252
4253 auto fill =
4254 [&]( bool aIterativeRefill, std::unique_ptr<BOARD>& aBoard )
4255 {
4256 cfg.m_ZoneFillIterativeRefill = aIterativeRefill;
4257 KI_TEST::LoadBoard( m_settingsManager, "issue23790/issue23790", aBoard );
4258 KI_TEST::FillZones( aBoard.get() );
4259 };
4260
4261 std::unique_ptr<BOARD> incremental;
4262 std::unique_ptr<BOARD> oneShot;
4263
4264 fill( true, incremental );
4265 fill( false, oneShot );
4266
4267 std::map<KIID, SHAPE_POLY_SET> incrementalFills;
4268
4269 for( ZONE* zone : incremental->Zones() )
4270 {
4271 if( !zone->GetIsRuleArea() && zone->HasFilledPolysForLayer( layer ) )
4272 incrementalFills[zone->m_Uuid] = *zone->GetFilledPolysList( layer );
4273 }
4274
4275 // The band in which a zone can be reached by an abutting same-net zone's copper. Confining the
4276 // comparison to it excludes free outer corners, where the two modes legitimately round a little
4277 // differently.
4278 auto sameNetBorderBand =
4279 [&]( ZONE* aZone ) -> SHAPE_POLY_SET
4280 {
4281 SHAPE_POLY_SET self = aZone->Outline()->CloneDropTriangulation();
4282 self.ClearArcs();
4283 self.Inflate( aZone->GetMinThickness(), CORNER_STRATEGY::ROUND_ALL_CORNERS,
4284 ARC_HIGH_DEF );
4285
4286 SHAPE_POLY_SET band;
4287
4288 for( ZONE* other : oneShot->Zones() )
4289 {
4290 if( other == aZone || other->GetIsRuleArea()
4291 || other->GetNetCode() != aZone->GetNetCode()
4292 || !other->GetLayerSet().Contains( layer ) )
4293 {
4294 continue;
4295 }
4296
4297 SHAPE_POLY_SET reach = other->Outline()->CloneDropTriangulation();
4298 reach.ClearArcs();
4299 reach.Inflate( other->GetMinThickness(), CORNER_STRATEGY::ROUND_ALL_CORNERS,
4300 ARC_HIGH_DEF );
4301 reach.BooleanIntersection( self );
4302 band.BooleanAdd( reach );
4303 }
4304
4305 return band;
4306 };
4307
4308 const double sqMM = pcbIUScale.IU_PER_MM * (double) pcbIUScale.IU_PER_MM;
4309 int checkedZones = 0;
4310
4311 for( ZONE* zone : oneShot->Zones() )
4312 {
4313 if( zone->GetIsRuleArea() || zone->GetNetCode() <= 0
4314 || !zone->HasFilledPolysForLayer( layer ) )
4315 {
4316 continue;
4317 }
4318
4319 SHAPE_POLY_SET band = sameNetBorderBand( zone );
4320
4321 if( band.OutlineCount() == 0 )
4322 continue;
4323
4324 auto incrementalIt = incrementalFills.find( zone->m_Uuid );
4325
4326 BOOST_REQUIRE_MESSAGE( incrementalIt != incrementalFills.end(),
4327 wxString::Format( "Zone %s is missing from the incremental fill.",
4328 zone->m_Uuid.AsString() ) );
4329
4330 SHAPE_POLY_SET difference = *zone->GetFilledPolysList( layer );
4331 difference.BooleanXor( incrementalIt->second );
4332 difference.BooleanIntersection( band );
4333
4334 const double differenceArea = difference.Area() / sqMM;
4335
4336 BOOST_CHECK_MESSAGE( differenceArea < 0.01,
4337 wxString::Format( "Zone %s (priority %d) fills %.4f mm^2 differently along a "
4338 "shared same-net border depending on the fill mode; the two "
4339 "must agree there (issue 23790).",
4340 zone->m_Uuid.AsString(), zone->GetAssignedPriority(),
4341 differenceArea ) );
4342 checkedZones++;
4343 }
4344
4345 BOOST_REQUIRE_MESSAGE( checkedZones >= 3,
4346 wxString::Format( "Expected at least three zones sharing a same-net "
4347 "border, found %d.", checkedZones ) );
4348}
4349
4350
4355BOOST_FIXTURE_TEST_CASE( ThermalOverrideOnSolidZoneKeepsSpokes, ZONE_FILL_TEST_FIXTURE )
4356{
4357 KI_TEST::LoadBoard( m_settingsManager, "issue24865/issue24865", m_board );
4358
4359 // Move the zone default away from thermal, so only the override asks for a relief.
4360 for( ZONE* zone : m_board->Zones() )
4361 {
4362 if( !zone->GetIsRuleArea() )
4363 zone->SetPadConnection( ZONE_CONNECTION::FULL );
4364 }
4365
4366 PAD* overriddenPad = nullptr;
4367 ZONE* hostZone = nullptr;
4368 PCB_LAYER_ID hostLayer = UNDEFINED_LAYER;
4369
4370 for( FOOTPRINT* footprint : m_board->Footprints() )
4371 {
4372 for( PAD* pad : footprint->Pads() )
4373 {
4374 for( ZONE* zone : m_board->Zones() )
4375 {
4376 if( zone->GetIsRuleArea() || zone->GetNetCode() != pad->GetNetCode() )
4377 continue;
4378
4379 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
4380 {
4381 if( !pad->IsOnLayer( layer ) || !pad->FlashLayer( layer ) )
4382 continue;
4383
4384 if( !zone->Outline()->Contains( pad->GetPosition() ) )
4385 continue;
4386
4387 overriddenPad = pad;
4388 hostZone = zone;
4389 hostLayer = layer;
4390 break;
4391 }
4392
4393 if( overriddenPad )
4394 break;
4395 }
4396
4397 if( overriddenPad )
4398 break;
4399 }
4400
4401 if( overriddenPad )
4402 break;
4403 }
4404
4405 BOOST_REQUIRE_MESSAGE( overriddenPad, "No same-net pad sits inside a zone on this board." );
4406
4408
4409 KI_TEST::FillZones( m_board.get() );
4410
4411 std::shared_ptr<SHAPE_POLY_SET> fill = hostZone->GetFilledPolysList( hostLayer );
4412
4413 BOOST_REQUIRE_MESSAGE( fill && !fill->IsEmpty(),
4414 wxString::Format( "Zone on %s did not fill.",
4415 m_board->GetLayerName( hostLayer ) ) );
4416
4417 // A relief with spokes still overlaps the pad. A relief without them leaves a clear gap.
4418 std::shared_ptr<SHAPE> padShape = overriddenPad->GetEffectiveShape( hostLayer );
4419
4420 BOOST_CHECK_MESSAGE( fill->Collide( padShape.get(), 0 ),
4421 wxString::Format( "Pad %s on net %s overrides the zone to a thermal "
4422 "connection, but the fill no longer reaches it on %s.",
4423 overriddenPad->GetNumber(),
4424 overriddenPad->GetNetname(),
4425 m_board->GetLayerName( hostLayer ) ) );
4426}
const char * name
@ ERROR_OUTSIDE
constexpr int ARC_HIGH_DEF
Definition base_units.h:137
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
BOX2< VECTOR2I > BOX2I
Definition box2.h:927
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:995
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< NET_SETTINGS > m_NetSettings
std::map< int, SEVERITY > m_DRCSeverities
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:225
constexpr coord_type GetRight() const
Definition box2.h:214
constexpr coord_type GetTop() const
Definition box2.h:226
constexpr coord_type GetBottom() const
Definition box2.h:219
bool Empty() const
Definition commit.h:142
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:164
bool RulesValid()
Definition drc_engine.h:282
DRC_CONSTRAINT EvalRules(DRC_CONSTRAINT_T aConstraintType, const BOARD_ITEM *a, const BOARD_ITEM *b, PCB_LAYER_ID aLayer, REPORTER *aReporter=nullptr)
void ClearViolationHandler()
Definition drc_engine.h:169
void InitEngine(const wxFileName &aRulePath)
Initialize the DRC engine.
const KIID m_Uuid
Definition eda_item.h:597
Definition kiid.h:46
A scoped application of a wxLog target that counts error-level messages.
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:595
static wxString Name(PCB_LAYER_ID aLayerId)
Return the fixed name association with aLayerId.
Definition lset.cpp:184
T Min() const
Definition minoptmax.h:29
void SetClearance(int aClearance)
Definition netclass.h:133
Handle the data for a net.
Definition netinfo.h:50
int GetNetCode() const
Definition netinfo.h:104
std::shared_ptr< NETCLASS > GetDefaultNetclass() const
Gets the default netclass for the project.
@ NORMAL
Shape is the same on all layers.
Definition padstack.h:170
static constexpr PCB_LAYER_ID ALL_LAYERS
! The layer identifier to use for the single defintion on normal padstacks
Definition padstack.h:179
Definition pad.h:61
const wxString & GetNumber() const
Definition pad.h:143
VECTOR2I GetPosition() const override
Definition pad.cpp:246
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:3010
void SetLocalZoneConnection(ZONE_CONNECTION aType)
Definition pad.h:608
VECTOR2I GetSize(PCB_LAYER_ID aLayer) const
Definition pad.cpp:288
std::shared_ptr< SHAPE > GetEffectiveShape(PCB_LAYER_ID aLayer=UNDEFINED_LAYER, FLASHING aFlash=FLASHING::DEFAULT, DRC_CONSTRAINT_T aUsage=NULL_CONSTRAINT) const override
Some pad shapes can be complex (rounded/chamfered rectangle), even without considering custom shapes.
Definition pad.cpp:1241
Definition seg.h:38
VECTOR2I A
Definition seg.h:45
ecoord SquaredDistance(const SEG &aSeg) const
Definition seg.cpp:76
VECTOR2I::extended_type ecoord
Definition seg.h:40
VECTOR2I B
Definition seg.h:46
int Length() const
Return the length (this).
Definition seg.h:339
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:442
VECTOR2I Center() const
Definition seg.h:375
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 BooleanXor(const SHAPE_POLY_SET &b)
Perform boolean polyset exclusive or.
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 Deflate(int aAmount, CORNER_STRATEGY aCornerStrategy, int aMaxError)
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:86
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:422
TEARDROP_PARAMETARS is a helper class to handle parameters needed to build teardrops for a board thes...
int m_TdMaxLen
max allowed length for teardrops in IU. <= 0 to disable
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:279
bool Fill(const std::vector< ZONE * > &aZones, bool aCheck=false, wxWindow *aParent=nullptr)
Fills the given list of zones.
Handle a list of polygons defining a copper zone.
Definition zone.h:70
void SetHatchThickness(int aThickness)
Definition zone.h:326
bool AppendCorner(const 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:1441
void AddPolygon(std::vector< VECTOR2I > &aPolygon)
Add a polygon to the zone outline.
Definition zone.cpp:1424
std::shared_ptr< SHAPE_POLY_SET > GetFilledPolysList(PCB_LAYER_ID aLayer) const
Definition zone.h:692
void SetMinThickness(int aMinThickness)
Definition zone.h:316
double GetFilledArea()
This area is cached from the most recent call to CalculateFilledArea().
Definition zone.h:279
void SetThermalReliefSpokeWidth(int aThermalReliefSpokeWidth)
Definition zone.h:251
virtual void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
Definition zone.cpp:641
SHAPE_POLY_SET * Outline()
Definition zone.h:418
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:623
void SetFillMode(ZONE_FILL_MODE aFillMode)
Definition zone.cpp:647
bool HasFilledPolysForLayer(PCB_LAYER_ID aLayer) const
Definition zone.h:683
void SetThievingSettings(const THIEVING_SETTINGS &aSettings)
Definition zone.h:352
void SetNet(NETINFO_ITEM *aNetInfo) override
Override that drops aNetInfo when this zone is in copper-thieving fill mode.
Definition zone.cpp:632
void SetThermalReliefGap(int aThermalReliefGap)
Definition zone.h:240
double CalculateFilledArea()
Compute the area currently occupied by the zone fill.
Definition zone.cpp:1893
void SetAssignedPriority(unsigned aPriority)
Definition zone.h:117
void SetPadConnection(ZONE_CONNECTION aPadConnection)
Definition zone.h:313
void SetIslandRemovalMode(ISLAND_REMOVAL_MODE aRemove)
Definition zone.h:830
void SetHatchGap(int aStep)
Definition zone.h:329
unsigned GetAssignedPriority() const
Definition zone.h:122
bool SameNet(const ZONE *aOther) const
Definition zone.cpp:523
@ CHAMFER_ALL_CORNERS
All angles are chamfered.
@ ROUND_ALL_CORNERS
All angles are rounded.
@ DRCE_CLEARANCE
Definition drc_item.h:41
@ DRCE_COPPER_SLIVER
Definition drc_item.h:96
@ DRCE_SHORTING_ITEMS
Definition drc_item.h:38
@ DRCE_CONNECTION_WIDTH
Definition drc_item.h:57
@ CLEARANCE_CONSTRAINT
Definition drc_rule.h:51
#define _(s)
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:422
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:56
@ B_Cu
Definition layer_ids.h:61
@ F_SilkS
Definition layer_ids.h:96
@ In4_Cu
Definition layer_ids.h:65
@ UNDEFINED_LAYER
Definition layer_ids.h:57
@ In1_Cu
Definition layer_ids.h:62
@ F_Cu
Definition layer_ids.h:60
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:411
@ PTH
Plated through hole pad.
Definition padstack.h:97
CITER next(CITER it)
Definition ptree.cpp:120
@ RPT_SEVERITY_ERROR
const double epsilon
#define SKIP_SET_DIRTY
Definition sch_commit.h:40
#define SKIP_UNDO
Definition sch_commit.h:38
std::optional< VECTOR2I > OPT_VECTOR2I
Definition seg.h:35
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)
const SHAPE_LINE_CHAIN chain
int radius
int clearance
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
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:89
@ PCB_ARC_T
class PCB_ARC, an arc track segment on a copper layer
Definition typeinfo.h:90
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
VECTOR2< double > VECTOR2D
Definition vector2d.h:682
ZONE_CONNECTION
How pads are covered by copper in zone.
Definition zones.h:43
@ THERMAL
Use thermal relief for pads.
Definition zones.h:46
@ FULL
pads are covered by copper
Definition zones.h:47