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