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>
42#include <cmath>
43
44
47static void CheckAllOutlineAreasAtLeast( const std::shared_ptr<SHAPE_POLY_SET>& aFill,
48 double aMinArea, const wxString& aLabel )
49{
50 for( int ii = 0; ii < aFill->OutlineCount(); ++ii )
51 {
52 const double area = std::abs( aFill->Outline( ii ).Area() );
53
54 BOOST_CHECK_MESSAGE( area >= aMinArea,
55 wxString::Format( "%s %d area %.0f IU^2 below %.0f IU^2; partial "
56 "stamps should not survive.",
57 aLabel, ii, area, aMinArea ) );
58 }
59}
60
61
70
71
72int delta = KiROUND( 0.006 * pcbIUScale.IU_PER_MM );
73
74
76{
77 KI_TEST::LoadBoard( m_settingsManager, "zone_filler", m_board );
78
79 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
80
81 KI_TEST::FillZones( m_board.get() );
82
83 // Now that the zones are filled we're going to increase the size of -some- pads and
84 // tracks so that they generate DRC errors. The test then makes sure that those errors
85 // are generated, and that the other pads and tracks do -not- generate errors.
86
87 for( PAD* pad : m_board->Footprints()[0]->Pads() )
88 {
89 if( pad->GetNumber() == "2" || pad->GetNumber() == "4" || pad->GetNumber() == "6" )
90 {
91 pad->SetSize( PADSTACK::ALL_LAYERS,
92 pad->GetSize( PADSTACK::ALL_LAYERS ) + VECTOR2I( delta, delta ) );
93 }
94 }
95
96 int ii = 0;
97 KIID arc8;
98 KIID arc12;
99
100 for( PCB_TRACK* track : m_board->Tracks() )
101 {
102 if( track->Type() == PCB_ARC_T )
103 {
104 ii++;
105
106 if( ii == 8 )
107 {
108 arc8 = track->m_Uuid;
109 track->SetWidth( track->GetWidth() + delta + delta );
110 }
111 else if( ii == 12 )
112 {
113 arc12 = track->m_Uuid;
114 track->Move( VECTOR2I( -delta, -delta ) );
115 }
116 }
117 }
118
119 bool foundPad2Error = false;
120 bool foundPad4Error = false;
121 bool foundPad6Error = false;
122 bool foundArc8Error = false;
123 bool foundArc12Error = false;
124 bool foundOtherError = false;
125
126 bds.m_DRCEngine->InitEngine( wxFileName() ); // Just to be sure to be sure
127
129 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
130 const std::function<void( PCB_MARKER* )>& aPathGenerator )
131 {
132 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
133 {
134 BOARD_ITEM* item_a = m_board->ResolveItem( aItem->GetMainItemID() );
135 PAD* pad_a = dynamic_cast<PAD*>( item_a );
136 PCB_TRACK* trk_a = dynamic_cast<PCB_TRACK*>( item_a );
137
138 BOARD_ITEM* item_b = m_board->ResolveItem( aItem->GetAuxItemID() );
139 PAD* pad_b = dynamic_cast<PAD*>( item_b );
140 PCB_TRACK* trk_b = dynamic_cast<PCB_TRACK*>( item_b );
141
142 if( pad_a && pad_a->GetNumber() == "2" ) foundPad2Error = true;
143 else if( pad_a && pad_a->GetNumber() == "4" ) foundPad4Error = true;
144 else if( pad_a && pad_a->GetNumber() == "6" ) foundPad6Error = true;
145 else if( pad_b && pad_b->GetNumber() == "2" ) foundPad2Error = true;
146 else if( pad_b && pad_b->GetNumber() == "4" ) foundPad4Error = true;
147 else if( pad_b && pad_b->GetNumber() == "6" ) foundPad6Error = true;
148 else if( trk_a && trk_a->m_Uuid == arc8 ) foundArc8Error = true;
149 else if( trk_a && trk_a->m_Uuid == arc12 ) foundArc12Error = true;
150 else if( trk_b && trk_b->m_Uuid == arc8 ) foundArc8Error = true;
151 else if( trk_b && trk_b->m_Uuid == arc12 ) foundArc12Error = true;
152 else foundOtherError = true;
153
154 }
155 } );
156
157 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
158
159 BOOST_CHECK_EQUAL( foundPad2Error, true );
160 BOOST_CHECK_EQUAL( foundPad4Error, true );
161 BOOST_CHECK_EQUAL( foundPad6Error, true );
162 BOOST_CHECK_EQUAL( foundArc8Error, true );
163 BOOST_CHECK_EQUAL( foundArc12Error, true );
164 BOOST_CHECK_EQUAL( foundOtherError, false );
165}
166
167
169{
170 KI_TEST::LoadBoard( m_settingsManager, "notched_zones", m_board );
171
172 // Older algorithms had trouble where the filleted zones intersected and left notches.
173 // See:
174 // https://gitlab.com/kicad/code/kicad/-/issues/2737
175 // https://gitlab.com/kicad/code/kicad/-/issues/2752
176 SHAPE_POLY_SET frontCopper;
177
178 KI_TEST::FillZones( m_board.get() );
179
180 frontCopper = SHAPE_POLY_SET();
181
182 for( ZONE* zone : m_board->Zones() )
183 {
184 if( zone->GetLayerSet().Contains( F_Cu ) )
185 {
186 frontCopper.BooleanAdd( *zone->GetFilledPolysList( F_Cu ) );
187 }
188 }
189
190 BOOST_CHECK_EQUAL( frontCopper.OutlineCount(), 2 );
191}
192
193
194static const std::vector<wxString> RegressionZoneFillTests_tests = {
195 "issue18",
196 "issue2568",
197 "issue3812",
198 "issue5102",
199 "issue5313",
200 "issue5320",
201 "issue5567",
202 "issue5830",
203 "issue6039",
204 "issue6260",
205 "issue6284",
206 "issue7086",
207 "issue14294", // Bad Clipper2 fill
208 "fill_bad" // Missing zone clearance expansion
209};
210
211
213 boost::unit_test::data::make( RegressionZoneFillTests_tests ), relPath )
214{
215 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
216
217 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
218
219 KI_TEST::FillZones( m_board.get() );
220
221 std::vector<DRC_ITEM> violations;
222
224 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
225 const std::function<void( PCB_MARKER* )>& aPathGenerator )
226 {
227 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
228 violations.push_back( *aItem );
229 } );
230
231 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
232
233 if( violations.empty() )
234 {
235 BOOST_CHECK_EQUAL( 1, 1 ); // quiet "did not check any assertions" warning
236 BOOST_TEST_MESSAGE( wxString::Format( "Zone fill regression: %s passed", relPath ) );
237 }
238 else
239 {
240 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::INCH );
241
242 std::map<KIID, EDA_ITEM*> itemMap;
243 m_board->FillItemMap( itemMap );
244
245 for( const DRC_ITEM& item : violations )
246 BOOST_TEST_MESSAGE( item.ShowReport( &unitsProvider, RPT_SEVERITY_ERROR, itemMap ) );
247
248 BOOST_ERROR( wxString::Format( "Zone fill regression: %s failed", relPath ) );
249 }
250}
251
252
262BOOST_FIXTURE_TEST_CASE( RegressionZoneClearanceWithIterativeRefill, ZONE_FILL_TEST_FIXTURE )
263{
264 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
265 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
266
267 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
268 guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
269
270 auto runDrcClearanceCheck =
271 [this]( bool aIterative ) -> int
272 {
273 ADVANCED_CFG& innerCfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
274 innerCfg.m_ZoneFillIterativeRefill = aIterative;
275
276 KI_TEST::LoadBoard( m_settingsManager, "issue23053/issue23053", m_board );
277
278 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
279
280 KI_TEST::FillZones( m_board.get() );
281
282 std::vector<DRC_ITEM> violations;
283
284 std::map<KIID, EDA_ITEM*> itemMap;
285 m_board->FillItemMap( itemMap );
286 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::MM );
287
289 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos,
290 int aLayer,
291 const std::function<void( PCB_MARKER* )>& aPathGenerator )
292 {
293 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
294 {
295 BOARD_ITEM* itemA = m_board->ResolveItem( aItem->GetMainItemID() );
296 BOARD_ITEM* itemB = m_board->ResolveItem( aItem->GetAuxItemID() );
297
298 if( dynamic_cast<ZONE*>( itemA ) && dynamic_cast<ZONE*>( itemB ) )
299 {
300 violations.push_back( *aItem );
301
303 aItem->ShowReport( &unitsProvider,
304 RPT_SEVERITY_ERROR, itemMap ) );
305 }
306 }
307 } );
308
309 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
310
311 return static_cast<int>( violations.size() );
312 };
313
314 int iterativeViolations = runDrcClearanceCheck( true );
315
316 BOOST_CHECK_MESSAGE( iterativeViolations == 0,
317 wxString::Format( "Iterative refill produced %d zone-to-zone clearance "
318 "violations (expected 0)", iterativeViolations ) );
319
320 int nonIterativeViolations = runDrcClearanceCheck( false );
321
322 BOOST_CHECK_MESSAGE( nonIterativeViolations == 0,
323 wxString::Format( "Non-iterative refill produced %d zone-to-zone clearance "
324 "violations (expected 0)", nonIterativeViolations ) );
325}
326
327
328static const std::vector<wxString> RegressionSliverZoneFillTests_tests = {
329 "issue16182" // Slivers
330};
331
332
333BOOST_DATA_TEST_CASE_F( ZONE_FILL_TEST_FIXTURE, RegressionSliverZoneFillTests,
334 boost::unit_test::data::make( RegressionSliverZoneFillTests_tests ),
335 relPath )
336{
337 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
338
339 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
340
341 KI_TEST::FillZones( m_board.get() );
342
343 std::vector<DRC_ITEM> violations;
344
346 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
347 const std::function<void( PCB_MARKER* )>& aPathGenerator )
348 {
349 if( aItem->GetErrorCode() == DRCE_COPPER_SLIVER )
350 violations.push_back( *aItem );
351 } );
352
353 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
354
355 if( violations.empty() )
356 {
357 BOOST_CHECK_EQUAL( 1, 1 ); // quiet "did not check any assertions" warning
358 BOOST_TEST_MESSAGE( wxString::Format( "Zone fill copper sliver regression: %s passed", relPath ) );
359 }
360 else
361 {
362 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::INCH );
363
364 std::map<KIID, EDA_ITEM*> itemMap;
365 m_board->FillItemMap( itemMap );
366
367 for( const DRC_ITEM& item : violations )
368 BOOST_TEST_MESSAGE( item.ShowReport( &unitsProvider, RPT_SEVERITY_ERROR, itemMap ) );
369
370 BOOST_ERROR( wxString::Format( "Zone fill copper sliver regression: %s failed", relPath ) );
371 }
372}
373
374
375static const std::vector<std::pair<wxString,int>> RegressionTeardropFill_tests = {
376 { "teardrop_issue_JPC2", 5 }, // Arcs with teardrops connecting to pads
377};
378
379
381 boost::unit_test::data::make( RegressionTeardropFill_tests ), test )
382{
383 const wxString& relPath = test.first;
384 const int count = test.second;
385
386 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
387
388 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
389
390 KI_TEST::FillZones( m_board.get() );
391
392 int zoneCount = 0;
393
394 for( ZONE* zone : m_board->Zones() )
395 {
396 if( zone->IsTeardropArea() )
397 zoneCount++;
398 }
399
400 BOOST_CHECK_MESSAGE( zoneCount == count, "Expected " << count << " teardrop zones in "
401 << relPath << ", found "
402 << zoneCount );
403}
404
405
407{
408
409 std::vector<wxString> tests = { { "issue19956/issue19956" } // Arcs with teardrops connecting to pads
410 };
411
412 for( const wxString& relPath : tests )
413 {
414 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
415 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
416 KI_TEST::FillZones( m_board.get() );
417
418 for( ZONE* zone : m_board->Zones() )
419 {
420 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
421 {
422 std::shared_ptr<SHAPE> a_shape( zone->GetEffectiveShape( layer ) );
423
424 for( PAD* pad : m_board->GetPads() )
425 {
426 std::shared_ptr<SHAPE> pad_shape( pad->GetEffectiveShape( layer ) );
427 int clearance = pad_shape->GetClearance( a_shape.get() );
428 BOOST_CHECK_MESSAGE( pad->GetNetCode() == zone->GetNetCode() || clearance != 0,
429 wxString::Format( "Pad %s from Footprint %s has net code %s and "
430 "is connected to zone with net code %s",
431 pad->GetNumber(),
432 pad->GetParentFootprint()->GetReferenceAsString(),
433 pad->GetNetname(),
434 zone->GetNetname() ) );
435 }
436 }
437 }
438 }
439}
440
441
456BOOST_FIXTURE_TEST_CASE( RegressionZonePriorityIsolatedIslands, ZONE_FILL_TEST_FIXTURE )
457{
458 // Enable iterative refill to fix issue 21746
459 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
460 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
461 cfg.m_ZoneFillIterativeRefill = true;
462
463 // Restore config at end of scope to avoid polluting other tests
464 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
465
466 KI_TEST::LoadBoard( m_settingsManager, "issue21746/issue21746", m_board );
467
468 KI_TEST::FillZones( m_board.get() );
469
470 // Find the GND zone
471 ZONE* gndZone = nullptr;
472
473 for( ZONE* zone : m_board->Zones() )
474 {
475 if( zone->GetNetname() == "GND" )
476 {
477 gndZone = zone;
478 break;
479 }
480 }
481
482 BOOST_REQUIRE_MESSAGE( gndZone != nullptr, "GND zone not found in test board" );
483
484 // Calculate board outline area
485 SHAPE_POLY_SET boardOutline;
486 bool hasOutline = m_board->GetBoardPolygonOutlines( boardOutline, true );
487 BOOST_REQUIRE_MESSAGE( hasOutline, "Board outline not found" );
488
489 double boardArea = 0.0;
490
491 for( int i = 0; i < boardOutline.OutlineCount(); i++ )
492 boardArea += boardOutline.Outline( i ).Area();
493
494 // Get GND zone filled area
495 gndZone->CalculateFilledArea();
496 double gndFilledArea = gndZone->GetFilledArea();
497
498 // The GND zone should fill at least 25% of the board area
499 // With the bug, it fills almost nothing because VDD knocks it out
500 double fillRatio = gndFilledArea / boardArea;
501
502 BOOST_TEST_MESSAGE( wxString::Format( "Board area: %.2f sq mm, GND filled area: %.2f sq mm, "
503 "Fill ratio: %.1f%%",
504 boardArea / 1e6, gndFilledArea / 1e6,
505 fillRatio * 100.0 ) );
506
507 BOOST_CHECK_MESSAGE( fillRatio >= 0.25,
508 wxString::Format( "GND zone fill ratio %.1f%% is less than expected 25%%. "
509 "This indicates issue 21746 - lower priority zones not "
510 "filling areas where higher priority isolated islands "
511 "were removed.",
512 fillRatio * 100.0 ) );
513}
514
515
529BOOST_FIXTURE_TEST_CASE( RegressionViaFlashingUnreachableZone, ZONE_FILL_TEST_FIXTURE )
530{
531 KI_TEST::LoadBoard( m_settingsManager, "issue22010/issue22010", m_board );
532
533 KI_TEST::FillZones( m_board.get() );
534
535 // Find vias with zone_layer_connections set for In1.Cu or In2.Cu
536 // After filling, vias that the zone doesn't actually reach should NOT be flashed
537 int viasWithUnreachableFlashing = 0;
538 int totalConditionalVias = 0;
539
540 PCB_LAYER_ID in1Cu = m_board->GetLayerID( wxT( "In1.Cu" ) );
541 PCB_LAYER_ID in2Cu = m_board->GetLayerID( wxT( "In2.Cu" ) );
542
543 for( PCB_TRACK* track : m_board->Tracks() )
544 {
545 if( track->Type() != PCB_VIA_T )
546 continue;
547
548 PCB_VIA* via = static_cast<PCB_VIA*>( track );
549
550 if( !via->GetRemoveUnconnected() )
551 continue;
552
553 totalConditionalVias++;
554
555 // Check if via is flashed on In1.Cu or In2.Cu
556 bool flashedOnIn1 = via->FlashLayer( in1Cu );
557 bool flashedOnIn2 = via->FlashLayer( in2Cu );
558
559 if( !flashedOnIn1 && !flashedOnIn2 )
560 continue;
561
562 VECTOR2I viaCenter = via->GetPosition();
563 int holeRadius = via->GetDrillValue() / 2;
564
565 // Check if any zone fill actually reaches this via
566 bool zoneReachesVia = false;
567
568 for( ZONE* zone : m_board->Zones() )
569 {
570 if( zone->GetIsRuleArea() )
571 continue;
572
573 if( zone->GetNetCode() != via->GetNetCode() )
574 continue;
575
576 for( PCB_LAYER_ID layer : { in1Cu, in2Cu } )
577 {
578 if( !zone->IsOnLayer( layer ) )
579 continue;
580
581 if( !zone->HasFilledPolysForLayer( layer ) )
582 continue;
583
584 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
585
586 if( fill->Contains( viaCenter, -1, holeRadius ) )
587 {
588 zoneReachesVia = true;
589 break;
590 }
591 }
592
593 if( zoneReachesVia )
594 break;
595 }
596
597 // If via is flashed but zone doesn't reach it, that's the bug
598 if( !zoneReachesVia && ( flashedOnIn1 || flashedOnIn2 ) )
599 viasWithUnreachableFlashing++;
600 }
601
602 BOOST_TEST_MESSAGE( wxString::Format( "Total conditional vias: %d, Vias with unreachable "
603 "flashing: %d", totalConditionalVias,
604 viasWithUnreachableFlashing ) );
605
606 BOOST_CHECK_MESSAGE( viasWithUnreachableFlashing == 0,
607 wxString::Format( "Found %d vias flashed on zone layers where the zone "
608 "fill doesn't actually reach them. This indicates "
609 "issue 22010 is not fixed.",
610 viasWithUnreachableFlashing ) );
611}
612
613
627{
628 KI_TEST::LoadBoard( m_settingsManager, "issue12964/issue12964", m_board );
629
630 KI_TEST::FillZones( m_board.get() );
631
632 int viasShortingZones = 0;
633 int totalConditionalVias = 0;
634
635 for( PCB_TRACK* track : m_board->Tracks() )
636 {
637 if( track->Type() != PCB_VIA_T )
638 continue;
639
640 PCB_VIA* via = static_cast<PCB_VIA*>( track );
641
642 if( !via->GetRemoveUnconnected() )
643 continue;
644
645 totalConditionalVias++;
646
647 VECTOR2I viaCenter = via->GetPosition();
648
649 for( ZONE* zone : m_board->Zones() )
650 {
651 if( zone->GetIsRuleArea() )
652 continue;
653
654 if( zone->GetNetCode() == via->GetNetCode() )
655 continue;
656
657 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
658 {
659 if( !via->FlashLayer( layer ) )
660 continue;
661
662 if( !zone->HasFilledPolysForLayer( layer ) )
663 continue;
664
665 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
666 int viaRadius = via->GetWidth( layer ) / 2;
667
668 if( fill->Contains( viaCenter, -1, viaRadius ) )
669 {
670 BOOST_TEST_MESSAGE( wxString::Format(
671 "Via at (%d, %d) on net %s is flashing on layer %s where zone "
672 "net %s is filled - this creates a short!",
673 viaCenter.x, viaCenter.y, via->GetNetname(),
674 m_board->GetLayerName( layer ), zone->GetNetname() ) );
675 viasShortingZones++;
676 }
677 }
678 }
679 }
680
681 BOOST_TEST_MESSAGE( wxString::Format( "Total conditional vias: %d, Vias shorting zones: %d",
682 totalConditionalVias, viasShortingZones ) );
683
684 BOOST_CHECK_MESSAGE( viasShortingZones == 0,
685 wxString::Format( "Found %d vias flashed on layers where they short to "
686 "zones with different nets. This indicates issue 12964 "
687 "is not fixed.",
688 viasShortingZones ) );
689}
690
691
704BOOST_FIXTURE_TEST_CASE( HatchZoneThermalConnectivity, ZONE_FILL_TEST_FIXTURE )
705{
706 KI_TEST::LoadBoard( m_settingsManager, "hatch_thermal_connectivity/hatch_thermal_connectivity",
707 m_board );
708
709 KI_TEST::FillZones( m_board.get() );
710
711 m_board->BuildConnectivity();
712
713 int unconnectedCount = m_board->GetConnectivity()->GetUnconnectedCount( false );
714
715 BOOST_CHECK_MESSAGE( unconnectedCount == 0,
716 wxString::Format( "Found %d unconnected items after zone fill. "
717 "Hatch zone thermal reliefs should maintain connectivity "
718 "even with large hatch gaps.",
719 unconnectedCount ) );
720}
721
722
739BOOST_FIXTURE_TEST_CASE( RegressionShallowArcZoneFill, ZONE_FILL_TEST_FIXTURE )
740{
741 KI_TEST::LoadBoard( m_settingsManager, "issue22475/issue22475", m_board );
742
743 PCB_LAYER_ID in1Cu = m_board->GetLayerID( wxT( "In1.Cu" ) );
744
745 ZONE* gndZone = nullptr;
746
747 for( ZONE* zone : m_board->Zones() )
748 {
749 if( zone->GetNetname() == "GND" && zone->IsOnLayer( in1Cu ) )
750 {
751 gndZone = zone;
752 break;
753 }
754 }
755
756 BOOST_REQUIRE_MESSAGE( gndZone != nullptr, "GND zone on In1.Cu not found in test board" );
757
758 if( !gndZone )
759 return;
760
761 KI_TEST::FillZones( m_board.get() );
762
763 BOOST_REQUIRE_MESSAGE( gndZone->HasFilledPolysForLayer( in1Cu ),
764 "GND zone has no fill on In1.Cu" );
765
766 const std::shared_ptr<SHAPE_POLY_SET>& fill = gndZone->GetFilledPolysList( in1Cu );
767
768 // The zone fill should produce a single contiguous outline. Multiple outlines
769 // indicate disconnected fill areas caused by malformed clearance holes.
770 BOOST_CHECK_EQUAL( fill->OutlineCount(), 1 );
771
772 double zoneOutlineArea = gndZone->Outline()->Area();
773
774 BOOST_REQUIRE_MESSAGE( zoneOutlineArea > 0.0, "Zone outline area must be positive" );
775
776 double fillArea = 0.0;
777
778 for( int i = 0; i < fill->OutlineCount(); i++ )
779 fillArea += std::abs( fill->Outline( i ).Area() );
780
781 double fillRatio = fillArea / zoneOutlineArea;
782
783 // The zone should be mostly filled. A low fill ratio indicates excessive voids
784 // from malformed clearance holes around shallow arcs.
785 BOOST_CHECK_GE( fillRatio, 0.90 );
786}
787
788
801BOOST_FIXTURE_TEST_CASE( RegressionIterativeRefillRespectsKeepouts, ZONE_FILL_TEST_FIXTURE )
802{
803 // Enable iterative refill
804 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
805 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
806 cfg.m_ZoneFillIterativeRefill = true;
807
808 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
809 guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
810
811 KI_TEST::LoadBoard( m_settingsManager, "issue22809/issue22809", m_board );
812
813 KI_TEST::FillZones( m_board.get() );
814
815 // Find all zone keepouts
816 std::vector<ZONE*> keepouts;
817
818 for( ZONE* zone : m_board->Zones() )
819 {
820 if( zone->GetIsRuleArea() && zone->GetDoNotAllowZoneFills() )
821 keepouts.push_back( zone );
822 }
823
824 BOOST_REQUIRE_MESSAGE( !keepouts.empty(), "No zone keepouts found in test board" );
825
826 // For each keepout, check that no zone fill exists inside it
827 int violationCount = 0;
828
829 for( ZONE* keepout : keepouts )
830 {
831 for( PCB_LAYER_ID layer : keepout->GetLayerSet().Seq() )
832 {
833 SHAPE_POLY_SET keepoutOutline( *keepout->Outline() );
834 keepoutOutline.ClearArcs();
835
836 for( ZONE* zone : m_board->Zones() )
837 {
838 if( zone->GetIsRuleArea() )
839 continue;
840
841 if( !zone->IsOnLayer( layer ) )
842 continue;
843
844 if( !zone->HasFilledPolysForLayer( layer ) )
845 continue;
846
847 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
848
849 // Check if any fill intersects the keepout
850 SHAPE_POLY_SET intersection = *fill;
851 intersection.BooleanIntersection( keepoutOutline );
852
853 if( intersection.OutlineCount() > 0 )
854 {
855 double intersectionArea = 0;
856
857 for( int i = 0; i < intersection.OutlineCount(); i++ )
858 intersectionArea += std::abs( intersection.Outline( i ).Area() );
859
860 // Allow for small numerical errors (less than 1 square mm)
861 if( intersectionArea > 1e6 )
862 {
863 BOOST_TEST_MESSAGE( wxString::Format(
864 "Zone %s fill on layer %s overlaps keepout by %.2f sq mm",
865 zone->GetNetname(),
866 m_board->GetLayerName( layer ),
867 intersectionArea / 1e6 ) );
868 violationCount++;
869 }
870 }
871 }
872 }
873 }
874
875 BOOST_CHECK_MESSAGE( violationCount == 0,
876 wxString::Format( "Found %d zone fills overlapping keepout areas. "
877 "This indicates issue 22809 - iterative refiller "
878 "ignores zone keepouts.", violationCount ) );
879}
880
881
895BOOST_FIXTURE_TEST_CASE( RegressionTHPadInnerLayerFlashing, ZONE_FILL_TEST_FIXTURE )
896{
897 KI_TEST::LoadBoard( m_settingsManager, "issue22826/issue22826", m_board );
898
899 KI_TEST::FillZones( m_board.get() );
900
901 PCB_LAYER_ID in2Cu = m_board->GetLayerID( wxT( "In2.Cu" ) );
902 int padsWithMissingFlashing = 0;
903 int totalConditionalPads = 0;
904
905 for( FOOTPRINT* footprint : m_board->Footprints() )
906 {
907 for( PAD* pad : footprint->Pads() )
908 {
909 if( !pad->GetRemoveUnconnected() )
910 continue;
911
912 if( !pad->HasHole() )
913 continue;
914
915 if( pad->GetNetname() != "VBUS_DUT" && pad->GetNetname() != "VBUS_DBG" )
916 continue;
917
918 totalConditionalPads++;
919
920 // Check if the pad should flash on In2.Cu
921 bool shouldFlash = false;
922
923 for( ZONE* zone : m_board->Zones() )
924 {
925 if( zone->GetIsRuleArea() )
926 continue;
927
928 if( zone->GetNetCode() != pad->GetNetCode() )
929 continue;
930
931 if( !zone->IsOnLayer( in2Cu ) )
932 continue;
933
934 if( zone->Outline()->Contains( pad->GetPosition() ) )
935 {
936 shouldFlash = true;
937 break;
938 }
939 }
940
941 if( shouldFlash && !pad->FlashLayer( in2Cu ) )
942 {
943 BOOST_TEST_MESSAGE( wxString::Format(
944 "Pad %s at (%d, %d) on net %s is inside zone but not flashing on In2.Cu",
945 pad->GetNumber(), pad->GetPosition().x, pad->GetPosition().y,
946 pad->GetNetname() ) );
947 padsWithMissingFlashing++;
948 }
949 }
950 }
951
952 BOOST_TEST_MESSAGE( wxString::Format( "Total conditional pads: %d, Pads with missing "
953 "flashing: %d", totalConditionalPads,
954 padsWithMissingFlashing ) );
955
956 BOOST_CHECK_MESSAGE( padsWithMissingFlashing == 0,
957 wxString::Format( "Found %d TH pads that should flash on inner layers "
958 "but don't. This indicates issue 22826 is not fixed.",
959 padsWithMissingFlashing ) );
960}
961
962
971BOOST_FIXTURE_TEST_CASE( RegressionRoundRectTeardropGeometry, ZONE_FILL_TEST_FIXTURE )
972{
973 KI_TEST::LoadBoard( m_settingsManager, "issue19405_roundrect_teardrop", m_board );
974
975 // Set up tool manager for teardrop generation
976 TOOL_MANAGER toolMgr;
977 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
978
979 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
980 toolMgr.RegisterTool( dummyTool );
981
982 // Generate teardrops
983 BOARD_COMMIT commit( dummyTool );
984 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
985 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
986
987 if( !commit.Empty() )
988 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
989
990 // Find teardrop zones
991 int teardropCount = 0;
992 bool foundBadTeardrop = false;
993
994 for( ZONE* zone : m_board->Zones() )
995 {
996 if( !zone->IsTeardropArea() )
997 continue;
998
999 teardropCount++;
1000
1001 // Get the teardrop outline
1002 const SHAPE_POLY_SET* outline = zone->Outline();
1003
1004 if( !outline || outline->OutlineCount() == 0 )
1005 continue;
1006
1007 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1008
1009 // Check that the teardrop polygon is convex or at least doesn't have
1010 // any sharp concave angles that would indicate intersection with the pad corner.
1011 // A well-formed teardrop should have all turns in the same direction
1012 // (or very close to it) except at the pad anchor points.
1013 int concaveCount = 0;
1014
1015 for( int i = 0; i < chain.PointCount(); i++ )
1016 {
1017 int prev = ( i == 0 ) ? chain.PointCount() - 1 : i - 1;
1018 int next = ( i + 1 ) % chain.PointCount();
1019
1020 VECTOR2I v1 = chain.CPoint( i ) - chain.CPoint( prev );
1021 VECTOR2I v2 = chain.CPoint( next ) - chain.CPoint( i );
1022
1023 // Cross product gives handedness of turn
1024 int64_t cross = (int64_t) v1.x * v2.y - (int64_t) v1.y * v2.x;
1025
1026 // Count significant concave turns (negative cross product for CCW polygons)
1027 // Small values are numerical noise
1028 if( cross < -1000 )
1029 concaveCount++;
1030 }
1031
1032 // A teardrop should have at most 2-3 concave points (at the pad anchor points)
1033 // Many concave points indicate the curve is intersecting the pad corner
1034 if( concaveCount > 5 )
1035 {
1036 BOOST_TEST_MESSAGE( wxString::Format( "Teardrop has %d concave vertices, "
1037 "indicating possible corner intersection",
1038 concaveCount ) );
1039 foundBadTeardrop = true;
1040 }
1041 }
1042
1043 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1044
1045 BOOST_CHECK_MESSAGE( !foundBadTeardrop,
1046 "Found teardrop with excessive concave vertices, indicating "
1047 "issue 19405 - teardrop curve intersecting rounded rectangle corner" );
1048}
1049
1050
1057{
1058 KI_TEST::LoadBoard( m_settingsManager, "teardrop_spike", m_board );
1059
1060 TOOL_MANAGER toolMgr;
1061 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1062
1063 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1064 toolMgr.RegisterTool( dummyTool );
1065
1066 BOARD_COMMIT commit( dummyTool );
1067 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1068 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1069
1070 if( !commit.Empty() )
1071 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1072
1073 int teardropCount = 0;
1074 bool foundSpike = false;
1075
1076 const int maxError = m_board->GetDesignSettings().m_MaxError;
1077
1078 for( ZONE* zone : m_board->Zones() )
1079 {
1080 if( !zone->IsTeardropArea() )
1081 continue;
1082
1083 teardropCount++;
1084
1085 PCB_LAYER_ID layer = zone->GetFirstLayer();
1086 int netcode = zone->GetNetCode();
1087
1088 // A well-formed teardrop only ever covers the copper it bridges: the pads/vias it
1089 // anchors on and the track(s) it follows. Build that corridor from all copper on the
1090 // teardrop's net and layer (generously inflated) and require the teardrop to lie
1091 // inside it. A spike sweeps area outside the corridor.
1092 SHAPE_POLY_SET corridor;
1093
1094 for( FOOTPRINT* fp : m_board->Footprints() )
1095 {
1096 for( PAD* pad : fp->Pads() )
1097 {
1098 if( pad->GetNetCode() == netcode && pad->IsOnLayer( layer ) )
1099 pad->TransformShapeToPolygon( corridor, layer, 0, maxError, ERROR_OUTSIDE );
1100 }
1101 }
1102
1103 for( PCB_TRACK* track : m_board->Tracks() )
1104 {
1105 if( track->GetNetCode() == netcode && track->IsOnLayer( layer ) )
1106 track->TransformShapeToPolygon( corridor, layer, 0, maxError, ERROR_OUTSIDE );
1107 }
1108
1109 // Inflate by a full track width so the teardrop's flare toward the pad, which is
1110 // legitimately wider than the bare track, is comfortably inside the corridor.
1111 corridor.Inflate( pcbIUScale.mmToIU( 0.127 ), CORNER_STRATEGY::ROUND_ALL_CORNERS,
1112 maxError );
1113 corridor.Simplify();
1114
1115 SHAPE_POLY_SET outside = *zone->Outline();
1116 outside.BooleanSubtract( corridor );
1117
1118 double tdArea = std::abs( zone->Outline()->Area() );
1119 double outArea = std::abs( outside.Area() );
1120 double ratio = tdArea > 0 ? outArea / tdArea : 0.0;
1121
1122 BOOST_TEST_MESSAGE( wxString::Format(
1123 "Teardrop on layer %d: area %.0f, area outside corridor %.0f (%.1f%%)",
1124 (int) layer, tdArea, outArea, ratio * 100.0 ) );
1125
1126 if( ratio > 0.02 )
1127 {
1128 foundSpike = true;
1129 BOOST_TEST_MESSAGE( wxString::Format(
1130 "Teardrop on layer %d sweeps %.1f%% of its area outside the track/pad "
1131 "corridor (spike)",
1132 (int) layer, ratio * 100.0 ) );
1133 }
1134 }
1135
1136 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1137 BOOST_CHECK_MESSAGE( !foundSpike,
1138 "A teardrop vertex spikes outside the track/pad corridor it should "
1139 "follow" );
1140}
1141
1142
1151{
1152 KI_TEST::LoadBoard( m_settingsManager, "oval_teardrop", m_board );
1153
1154 // Set up tool manager for teardrop generation
1155 TOOL_MANAGER toolMgr;
1156 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1157
1158 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1159 toolMgr.RegisterTool( dummyTool );
1160
1161 // Generate teardrops
1162 BOARD_COMMIT commit( dummyTool );
1163 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1164 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1165
1166 if( !commit.Empty() )
1167 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1168
1169 // Find teardrop zones
1170 int teardropCount = 0;
1171 bool foundBadTeardrop = false;
1172
1173 for( ZONE* zone : m_board->Zones() )
1174 {
1175 if( !zone->IsTeardropArea() )
1176 continue;
1177
1178 teardropCount++;
1179
1180 const SHAPE_POLY_SET* outline = zone->Outline();
1181
1182 if( !outline || outline->OutlineCount() == 0 )
1183 continue;
1184
1185 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1186
1187 // Check for excessive concave vertices that would indicate the teardrop curve
1188 // is not tangent to the oval's semicircular end
1189 int concaveCount = 0;
1190
1191 for( int i = 0; i < chain.PointCount(); i++ )
1192 {
1193 int prev = ( i == 0 ) ? chain.PointCount() - 1 : i - 1;
1194 int next = ( i + 1 ) % chain.PointCount();
1195
1196 VECTOR2I v1 = chain.CPoint( i ) - chain.CPoint( prev );
1197 VECTOR2I v2 = chain.CPoint( next ) - chain.CPoint( i );
1198
1199 int64_t cross = (int64_t) v1.x * v2.y - (int64_t) v1.y * v2.x;
1200
1201 if( cross < -1000 )
1202 concaveCount++;
1203 }
1204
1205 if( concaveCount > 5 )
1206 {
1207 BOOST_TEST_MESSAGE( wxString::Format( "Oval teardrop has %d concave vertices",
1208 concaveCount ) );
1209 foundBadTeardrop = true;
1210 }
1211 }
1212
1213 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1214
1215 BOOST_CHECK_MESSAGE( !foundBadTeardrop,
1216 "Found teardrop with excessive concave vertices on oval pad, "
1217 "indicating curve is not tangent to semicircular end" );
1218}
1219
1220
1229{
1230 KI_TEST::LoadBoard( m_settingsManager, "large_circle_teardrop", m_board );
1231
1232 // Set up tool manager for teardrop generation
1233 TOOL_MANAGER toolMgr;
1234 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1235
1236 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1237 toolMgr.RegisterTool( dummyTool );
1238
1239 // Generate teardrops
1240 BOARD_COMMIT commit( dummyTool );
1241 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1242 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1243
1244 if( !commit.Empty() )
1245 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1246
1247 // Find the pad and its teardrop
1248 PAD* largePad = nullptr;
1249
1250 for( FOOTPRINT* fp : m_board->Footprints() )
1251 {
1252 for( PAD* pad : fp->Pads() )
1253 {
1254 if( pad->GetShape( F_Cu ) == PAD_SHAPE::CIRCLE )
1255 {
1256 largePad = pad;
1257 break;
1258 }
1259 }
1260 }
1261
1262 BOOST_REQUIRE_MESSAGE( largePad != nullptr, "Expected a circular pad in test board" );
1263
1264 int padRadius = largePad->GetSize( F_Cu ).x / 2;
1265 VECTOR2I padCenter = largePad->GetPosition();
1266
1267 // Find teardrop zones
1268 int teardropCount = 0;
1269 bool foundBadTeardrop = false;
1270
1271 for( ZONE* zone : m_board->Zones() )
1272 {
1273 if( !zone->IsTeardropArea() )
1274 continue;
1275
1276 teardropCount++;
1277
1278 const SHAPE_POLY_SET* outline = zone->Outline();
1279
1280 if( !outline || outline->OutlineCount() == 0 )
1281 continue;
1282
1283 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1284
1285 // Check for excessive concave vertices
1286 int concaveCount = 0;
1287
1288 for( int i = 0; i < chain.PointCount(); i++ )
1289 {
1290 int prev = ( i == 0 ) ? chain.PointCount() - 1 : i - 1;
1291 int next = ( i + 1 ) % chain.PointCount();
1292
1293 VECTOR2I v1 = chain.CPoint( i ) - chain.CPoint( prev );
1294 VECTOR2I v2 = chain.CPoint( next ) - chain.CPoint( i );
1295
1296 int64_t cross = (int64_t) v1.x * v2.y - (int64_t) v1.y * v2.x;
1297
1298 if( cross < -1000 )
1299 concaveCount++;
1300 }
1301
1302 if( concaveCount > 5 )
1303 {
1304 BOOST_TEST_MESSAGE( wxString::Format( "Large circle teardrop has %d concave vertices",
1305 concaveCount ) );
1306 foundBadTeardrop = true;
1307 }
1308
1309 // Also verify that the teardrop anchor points near the pad are approximately
1310 // on the circle edge (within tolerance)
1311 int maxError = m_board->GetDesignSettings().m_MaxError;
1312
1313 for( int i = 0; i < chain.PointCount(); i++ )
1314 {
1315 VECTOR2I pt = chain.CPoint( i );
1316 double dist = ( pt - padCenter ).EuclideanNorm();
1317
1318 // Points that are close to the circle should be approximately on it
1319 if( dist > padRadius * 0.5 && dist < padRadius * 1.5 )
1320 {
1321 double deviation = std::abs( dist - padRadius );
1322
1323 // Allow some tolerance for polygon approximation
1324 if( deviation > maxError * 5 && deviation < padRadius * 0.2 )
1325 {
1326 BOOST_TEST_MESSAGE( wxString::Format(
1327 "Teardrop point at distance %.2f from pad center (radius %.2f), "
1328 "deviation %.2f exceeds tolerance",
1329 dist / 1000.0, padRadius / 1000.0, deviation / 1000.0 ) );
1330 }
1331 }
1332 }
1333 }
1334
1335 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone" );
1336
1337 BOOST_CHECK_MESSAGE( !foundBadTeardrop,
1338 "Found teardrop with excessive concave vertices on large circle, "
1339 "indicating anchor points may not be on circle edge" );
1340}
1341
1342
1353BOOST_FIXTURE_TEST_CASE( RegressionCoincidentPadClearance, ZONE_FILL_TEST_FIXTURE )
1354{
1355 KI_TEST::LoadBoard( m_settingsManager, "issue23123_minimal", m_board );
1356
1357 KI_TEST::FillZones( m_board.get() );
1358
1359 // After filling, every pad whose net differs from the zone must have clearance.
1360 // Check each zone/pad combination on each shared layer.
1361 int violations = 0;
1362
1363 for( ZONE* zone : m_board->Zones() )
1364 {
1365 if( zone->GetIsRuleArea() )
1366 continue;
1367
1368 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1369 {
1370 if( !zone->HasFilledPolysForLayer( layer ) )
1371 continue;
1372
1373 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
1374
1375 for( PAD* pad : m_board->GetPads() )
1376 {
1377 if( !pad->IsOnLayer( layer ) )
1378 continue;
1379
1380 if( pad->GetNetCode() == zone->GetNetCode() )
1381 continue;
1382
1383 std::shared_ptr<SHAPE> padShape = pad->GetEffectiveShape( layer );
1384 int clearance = padShape->GetClearance( fill.get() );
1385
1386 if( clearance < 1 )
1387 {
1388 BOOST_TEST_MESSAGE( wxString::Format(
1389 "Pad %s (net %s) at (%d, %d) has zero clearance to zone %s "
1390 "on layer %s",
1391 pad->GetNumber(), pad->GetNetname(),
1392 pad->GetPosition().x, pad->GetPosition().y,
1393 zone->GetNetname(), m_board->GetLayerName( layer ) ) );
1394 violations++;
1395 }
1396 }
1397 }
1398 }
1399
1400 BOOST_CHECK_MESSAGE( violations == 0,
1401 wxString::Format( "Found %d pads with missing zone clearance. "
1402 "Coincident pads with different nets must not be "
1403 "deduplicated in zone fill knockout.",
1404 violations ) );
1405}
1406
1407
1418{
1419 KI_TEST::LoadBoard( m_settingsManager, "connect/connect", m_board );
1420
1421 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1422
1423 KI_TEST::FillZones( m_board.get() );
1424
1425 std::vector<DRC_ITEM> violations;
1426
1427 bds.m_DRCEngine->InitEngine( wxFileName() );
1428
1430 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
1431 const std::function<void( PCB_MARKER* )>& aPathGenerator )
1432 {
1433 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
1434 {
1435 BOARD_ITEM* item_a = m_board->ResolveItem( aItem->GetMainItemID() );
1436 BOARD_ITEM* item_b = m_board->ResolveItem( aItem->GetAuxItemID() );
1437
1438 ZONE* zone_a = dynamic_cast<ZONE*>( item_a );
1439 ZONE* zone_b = dynamic_cast<ZONE*>( item_b );
1440
1441 if( zone_a || zone_b )
1442 violations.push_back( *aItem );
1443 }
1444 } );
1445
1446 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
1447
1448 BOOST_CHECK_EQUAL( violations.size(), 0 );
1449}
1450
1451
1463{
1464 KI_TEST::LoadBoard( m_settingsManager, "issue23339_zone_layer_rules", m_board );
1465
1466 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1467
1468 // First verify that EvalRules returns the correct clearance per layer
1469 ZONE* hvZone = nullptr;
1470 ZONE* lvZone = nullptr;
1471
1472 for( ZONE* zone : m_board->Zones() )
1473 {
1474 if( zone->GetNetname() == "HV_NET" )
1475 hvZone = zone;
1476 else if( zone->GetNetname() == "LV_NET" )
1477 lvZone = zone;
1478 }
1479
1480 BOOST_REQUIRE( hvZone );
1481 BOOST_REQUIRE( lvZone );
1482
1483 // Outer layer rule should give 4.6mm clearance on F.Cu
1485 hvZone, lvZone, F_Cu );
1486
1487 BOOST_TEST_MESSAGE( "F.Cu clearance: " << outerConstraint.GetValue().Min()
1488 << " (expected " << pcbIUScale.mmToIU( 4.6 ) << ")" );
1489 BOOST_CHECK_EQUAL( outerConstraint.GetValue().Min(), pcbIUScale.mmToIU( 4.6 ) );
1490
1491 // Inner layer rule should give 2.3mm clearance on In1.Cu
1493 hvZone, lvZone, In1_Cu );
1494
1495 BOOST_TEST_MESSAGE( "In1.Cu clearance: " << innerConstraint.GetValue().Min()
1496 << " (expected " << pcbIUScale.mmToIU( 2.3 ) << ")" );
1497 BOOST_CHECK_EQUAL( innerConstraint.GetValue().Min(), pcbIUScale.mmToIU( 2.3 ) );
1498
1499 // Now fill zones and check that fills actually respect the clearances
1500 KI_TEST::FillZones( m_board.get() );
1501
1502 // Run DRC and verify no clearance violations between zones
1503 std::vector<DRC_ITEM> violations;
1504
1505 bds.m_DRCEngine->InitEngine( wxFileName() );
1506
1508 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
1509 const std::function<void( PCB_MARKER* )>& aPathGenerator )
1510 {
1511 if( aItem->GetErrorCode() == DRCE_CLEARANCE )
1512 {
1513 BOARD_ITEM* item_a = m_board->ResolveItem( aItem->GetMainItemID() );
1514 BOARD_ITEM* item_b = m_board->ResolveItem( aItem->GetAuxItemID() );
1515
1516 ZONE* zone_a = dynamic_cast<ZONE*>( item_a );
1517 ZONE* zone_b = dynamic_cast<ZONE*>( item_b );
1518
1519 if( zone_a && zone_b )
1520 {
1521 BOOST_TEST_MESSAGE( "Zone-to-zone clearance violation on layer "
1522 << aLayer << ": " << aItem->GetErrorMessage( true ) );
1523 violations.push_back( *aItem );
1524 }
1525 }
1526 } );
1527
1528 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
1529
1530 BOOST_CHECK_EQUAL( violations.size(), 0 );
1531}
1532
1533
1534BOOST_FIXTURE_TEST_CASE( RegressionZoneFillMinWidthAfterKnockout, ZONE_FILL_TEST_FIXTURE )
1535{
1536 KI_TEST::LoadBoard( m_settingsManager, "issue23332_min_width/issue23332_min_width", m_board );
1537
1538 KI_TEST::FillZones( m_board.get() );
1539
1540 int epsilon = pcbIUScale.mmToIU( 0.001 );
1541
1542 for( ZONE* zone : m_board->Zones() )
1543 {
1544 int half_min_width = zone->GetMinThickness() / 2;
1545
1546 if( half_min_width - epsilon <= epsilon )
1547 continue;
1548
1549 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1550 {
1551 if( !zone->HasFilledPolysForLayer( layer ) )
1552 continue;
1553
1554 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1555
1556 if( !fill || fill->OutlineCount() == 0 )
1557 continue;
1558
1559 // Check each filled island individually so that a tiny thin sliver
1560 // isn't masked by a large zone's total area
1561 for( int ii = 0; ii < fill->OutlineCount(); ii++ )
1562 {
1563 SHAPE_POLY_SET island;
1564 island.AddOutline( fill->Outline( ii ) );
1565
1566 for( int jj = 0; jj < fill->HoleCount( ii ); jj++ )
1567 island.AddHole( fill->Hole( ii, jj ) );
1568
1569 double originalArea = island.Area();
1570
1571 if( originalArea <= 0 )
1572 continue;
1573
1575
1576 test.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS,
1577 ARC_HIGH_DEF );
1578
1579 test.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS,
1580 ARC_HIGH_DEF, true );
1581
1582 double prunedArea = test.Area();
1583 double areaLoss = ( originalArea - prunedArea ) / originalArea;
1584
1585 BOOST_TEST_MESSAGE( wxString::Format(
1586 "Zone %s layer %d island %d: area=%.0f, loss=%.4f%%",
1587 zone->GetNetname(), static_cast<int>( layer ), ii,
1588 originalArea, areaLoss * 100.0 ) );
1589
1590 BOOST_CHECK_MESSAGE( areaLoss < 0.01,
1591 wxString::Format(
1592 "Zone %s layer %d island %d lost %.2f%% area from "
1593 "min-width pruning (min_width=%.3fmm)",
1594 zone->GetNetname(), static_cast<int>( layer ), ii,
1595 areaLoss * 100.0,
1596 zone->GetMinThickness()
1597 / static_cast<double>( pcbIUScale.IU_PER_MM ) ) );
1598 }
1599 }
1600 }
1601}
1602
1603
1604BOOST_FIXTURE_TEST_CASE( RegressionSameNetOverlappingZones, ZONE_FILL_TEST_FIXTURE )
1605{
1606 KI_TEST::LoadBoard( m_settingsManager, "issue23418/testing", m_board );
1607
1608 KI_TEST::FillZones( m_board.get() );
1609
1610 int epsilon = pcbIUScale.mmToIU( 0.001 );
1611
1612 for( ZONE* zone : m_board->Zones() )
1613 {
1614 int half_min_width = zone->GetMinThickness() / 2;
1615
1616 if( half_min_width - epsilon <= epsilon )
1617 continue;
1618
1619 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1620 {
1621 if( !zone->HasFilledPolysForLayer( layer ) )
1622 continue;
1623
1624 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1625
1626 if( !fill || fill->OutlineCount() == 0 )
1627 continue;
1628
1629 for( int ii = 0; ii < fill->OutlineCount(); ii++ )
1630 {
1631 SHAPE_POLY_SET island;
1632 island.AddOutline( fill->Outline( ii ) );
1633
1634 for( int jj = 0; jj < fill->HoleCount( ii ); jj++ )
1635 island.AddHole( fill->Hole( ii, jj ) );
1636
1637 double originalArea = island.Area();
1638
1639 if( originalArea <= 0 )
1640 continue;
1641
1643
1644 test.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS,
1645 ARC_HIGH_DEF );
1646
1647 test.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS,
1648 ARC_HIGH_DEF, true );
1649
1650 double prunedArea = test.Area();
1651 double areaLoss = ( originalArea - prunedArea ) / originalArea;
1652
1653 BOOST_CHECK_MESSAGE( areaLoss < 0.01,
1654 wxString::Format(
1655 "Zone %s (priority %d) layer %d island %d lost "
1656 "%.2f%% area from min-width pruning, suggesting "
1657 "degenerate geometry from overlapping same-net zones",
1658 zone->GetNetname(),
1659 zone->GetAssignedPriority(),
1660 static_cast<int>( layer ), ii,
1661 areaLoss * 100.0 ) );
1662 }
1663 }
1664 }
1665}
1666
1667
1668BOOST_FIXTURE_TEST_CASE( RegressionDiffNetOverlappingZones, ZONE_FILL_TEST_FIXTURE )
1669{
1670 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
1671 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
1672
1673 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
1674 guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
1675
1676 auto runAreaLossCheck =
1677 [this]( bool aIterative )
1678 {
1679 ADVANCED_CFG& innerCfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
1680 innerCfg.m_ZoneFillIterativeRefill = aIterative;
1681
1682 KI_TEST::LoadBoard( m_settingsManager, "issue23418_diffnet/testing", m_board );
1683 KI_TEST::FillZones( m_board.get() );
1684
1685 int epsilon = pcbIUScale.mmToIU( 0.001 );
1686
1687 for( ZONE* zone : m_board->Zones() )
1688 {
1689 int half_min_width = zone->GetMinThickness() / 2;
1690
1691 if( half_min_width - epsilon <= epsilon )
1692 continue;
1693
1694 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
1695 {
1696 if( !zone->HasFilledPolysForLayer( layer ) )
1697 continue;
1698
1699 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
1700
1701 if( !fill || fill->OutlineCount() == 0 )
1702 continue;
1703
1704 for( int ii = 0; ii < fill->OutlineCount(); ii++ )
1705 {
1706 SHAPE_POLY_SET island;
1707 island.AddOutline( fill->Outline( ii ) );
1708
1709 for( int jj = 0; jj < fill->HoleCount( ii ); jj++ )
1710 island.AddHole( fill->Hole( ii, jj ) );
1711
1712 double originalArea = island.Area();
1713
1714 if( originalArea <= 0 )
1715 continue;
1716
1718
1719 test.Deflate( half_min_width - epsilon,
1721
1722 test.Inflate( half_min_width - epsilon,
1724
1725 double prunedArea = test.Area();
1726 double areaLoss = ( originalArea - prunedArea ) / originalArea;
1727
1729 areaLoss < 0.01,
1730 wxString::Format(
1731 "Zone %s (priority %d) layer %d island %d lost "
1732 "%.2f%% area (iterative=%d), suggesting degenerate "
1733 "geometry from different-net zone knockouts",
1734 zone->GetNetname(), zone->GetAssignedPriority(),
1735 static_cast<int>( layer ), ii, areaLoss * 100.0,
1736 aIterative ) );
1737 }
1738 }
1739 }
1740 };
1741
1742 runAreaLossCheck( false );
1743 runAreaLossCheck( true );
1744}
1745
1746
1762BOOST_FIXTURE_TEST_CASE( RegressionThermalReliefsToNowhere, ZONE_FILL_TEST_FIXTURE )
1763{
1764 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
1765 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
1766 cfg.m_ZoneFillIterativeRefill = true;
1767
1768 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
1769 guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
1770
1771 KI_TEST::LoadBoard( m_settingsManager, "issue23535_minimal/issue23535_minimal", m_board );
1772
1773 KI_TEST::FillZones( m_board.get() );
1774
1775 ZONE* gndZone = nullptr;
1776
1777 for( ZONE* zone : m_board->Zones() )
1778 {
1779 if( zone->GetNetname() == "GND" )
1780 gndZone = zone;
1781 }
1782
1783 BOOST_REQUIRE( gndZone );
1785
1786 const std::shared_ptr<SHAPE_POLY_SET>& gndFill = gndZone->GetFilledPolysList( F_Cu );
1787
1788 // The pad is at (6.5mm, 5mm) with size 1.5mm and thermal gap 0.5mm.
1789 // The right edge of the pad is at x=7.25mm, thermal gap extends to x=7.75mm.
1790 // After zone knockout, GND fill stops at roughly x=7.5mm.
1791 //
1792 // A thermal-relief-to-nowhere spoke would create copper at a point inside the
1793 // thermal gap but past the zone fill boundary. Check a point at (7.4mm, 5mm)
1794 // which is in the thermal gap (x > 7.25) and near the knockout edge.
1795 VECTOR2I spokeTestPoint( pcbIUScale.mmToIU( 7.4 ), pcbIUScale.mmToIU( 5.0 ) );
1796
1797 bool hasSpokeToNowhere = gndFill->Contains( spokeTestPoint );
1798
1799 BOOST_CHECK_MESSAGE( !hasSpokeToNowhere,
1800 "GND zone fill contains copper at the thermal gap test point (7.4, 5.0), "
1801 "indicating a thermal relief spoke to nowhere (issue 23535)." );
1802
1803 // Also verify that the left-pointing spoke still connects properly.
1804 // A point at (5.6mm, 5mm) is in the thermal gap on the left side and should have
1805 // copper from a valid left-pointing spoke.
1806 VECTOR2I validSpokePoint( pcbIUScale.mmToIU( 5.6 ), pcbIUScale.mmToIU( 5.0 ) );
1807
1808 bool hasValidSpoke = gndFill->Contains( validSpokePoint );
1809
1810 BOOST_CHECK_MESSAGE( hasValidSpoke,
1811 "GND zone fill does not contain copper at the valid spoke test point "
1812 "(5.6, 5.0). The fix may have incorrectly removed valid spokes." );
1813}
1814
1815
1826{
1827 KI_TEST::LoadBoard( m_settingsManager, "off_center_teardrop", m_board );
1828
1829 TOOL_MANAGER toolMgr;
1830 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1831
1832 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1833 toolMgr.RegisterTool( dummyTool );
1834
1835 BOARD_COMMIT commit( dummyTool );
1836 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1837 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1838
1839 if( !commit.Empty() )
1840 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1841
1842 // The test board has a 3mm circle pad at (100, 100) with a 0.25mm track connecting
1843 // at (100.75, 99) heading to (115, 99). The track enters the pad off-center: 1mm above
1844 // and 0.75mm right of center. The teardrop should be approximately symmetric about the
1845 // track's axis (the line from ~(100.75, 99) toward (115, 99), i.e., horizontal).
1846
1847 int teardropCount = 0;
1848
1849 for( ZONE* zone : m_board->Zones() )
1850 {
1851 if( !zone->IsTeardropArea() )
1852 continue;
1853
1854 teardropCount++;
1855
1856 const SHAPE_POLY_SET* outline = zone->Outline();
1857
1858 if( !outline || outline->OutlineCount() == 0 )
1859 continue;
1860
1861 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1862
1863 // The track axis is approximately at Y=99mm (in board coordinates = 99 * 1e6 nm).
1864 // Measure the maximum extent above and below this axis across all teardrop vertices.
1865 int trackY = pcbIUScale.mmToIU( 99 );
1866 int maxAbove = 0;
1867 int maxBelow = 0;
1868
1869 for( int i = 0; i < chain.PointCount(); i++ )
1870 {
1871 int dy = chain.CPoint( i ).y - trackY;
1872
1873 if( dy < 0 )
1874 maxAbove = std::max( maxAbove, -dy );
1875 else
1876 maxBelow = std::max( maxBelow, dy );
1877 }
1878
1879 // Both sides should have some extent (the teardrop flares out on both sides)
1880 BOOST_CHECK_MESSAGE( maxAbove > 0 && maxBelow > 0,
1881 "Teardrop should extend on both sides of the track axis" );
1882
1883 if( maxAbove > 0 && maxBelow > 0 )
1884 {
1885 // The two sides should be approximately equal. Allow 30% asymmetry tolerance
1886 // to account for polygon approximation of the circular pad and convex hull rounding.
1887 double ratio = static_cast<double>( std::min( maxAbove, maxBelow ) )
1888 / static_cast<double>( std::max( maxAbove, maxBelow ) );
1889
1890 BOOST_CHECK_MESSAGE( ratio > 0.7,
1891 wxString::Format( "Teardrop asymmetry ratio %.2f is too low "
1892 "(above=%d, below=%d). Expected roughly "
1893 "symmetric about the track axis.",
1894 ratio, maxAbove, maxBelow ) );
1895 }
1896 }
1897
1898 BOOST_CHECK_MESSAGE( teardropCount > 0, "Expected at least one teardrop zone for off-center track" );
1899}
1900
1901
1910BOOST_FIXTURE_TEST_CASE( ElongatedPadTeardropContainment, ZONE_FILL_TEST_FIXTURE )
1911{
1912 KI_TEST::LoadBoard( m_settingsManager, "teardrop_elongated_pad", m_board );
1913
1914 TOOL_MANAGER toolMgr;
1915 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
1916
1917 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
1918 toolMgr.RegisterTool( dummyTool );
1919
1920 BOARD_COMMIT commit( dummyTool );
1921 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
1922 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
1923
1924 if( !commit.Empty() )
1925 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
1926
1927 // Find the pad to build an expanded outline for containment checking.
1928 // The pad is at board position (136.45, 100.819) with size (3.5, 0.3) rotated 270 deg,
1929 // giving board extents X: [136.3, 136.6], Y: [99.069, 102.569].
1930 PAD* testPad = nullptr;
1931
1932 for( FOOTPRINT* fp : m_board->Footprints() )
1933 {
1934 for( PAD* pad : fp->Pads() )
1935 {
1936 if( pad->GetNumber() == "7" )
1937 {
1938 testPad = pad;
1939 break;
1940 }
1941 }
1942 }
1943
1944 BOOST_REQUIRE_MESSAGE( testPad != nullptr, "Could not find pad 7 in test board" );
1945
1946 // Build the pad outline polygon with a small tolerance for the track half-width
1947 int tolerance = std::max( m_board->GetDesignSettings().m_MaxError,
1948 pcbIUScale.mmToIU( 0.001 ) );
1949 SHAPE_POLY_SET padPoly;
1950 testPad->TransformShapeToPolygon( padPoly, B_Cu, tolerance,
1951 m_board->GetDesignSettings().m_MaxError, ERROR_OUTSIDE );
1952
1953 int teardropCount = 0;
1954
1955 for( ZONE* zone : m_board->Zones() )
1956 {
1957 if( !zone->IsTeardropArea() )
1958 continue;
1959
1960 const SHAPE_POLY_SET* outline = zone->Outline();
1961
1962 BOOST_REQUIRE_MESSAGE( outline && outline->OutlineCount() > 0,
1963 "Teardrop zone has no outline" );
1964
1965 teardropCount++;
1966
1967 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
1968
1969 // Check each vertex of the teardrop. Vertices on the pad side (closer to pad center
1970 // than to the track anchor) must be inside the expanded pad outline.
1971 VECTOR2I padCenter = testPad->GetPosition();
1972
1973 // The track anchor region is near (136.45, 99.16) in mm, i.e., outside the pad.
1974 // We only check vertices that are closer to the pad center than to the track anchor.
1975 VECTOR2I trackAnchor( pcbIUScale.mmToIU( 136.45 ), pcbIUScale.mmToIU( 99.16 ) );
1976
1977 for( int i = 0; i < chain.PointCount(); i++ )
1978 {
1979 VECTOR2I pt = chain.CPoint( i );
1980 double distToPad = ( VECTOR2D( pt ) - VECTOR2D( padCenter ) ).EuclideanNorm();
1981 double distToTrack = ( VECTOR2D( pt ) - VECTOR2D( trackAnchor ) ).EuclideanNorm();
1982
1983 // Only check vertices on the pad side of the teardrop
1984 if( distToPad < distToTrack )
1985 {
1987 padPoly.Contains( pt ),
1988 wxString::Format( "Teardrop vertex (%d, %d) is outside the pad "
1989 "outline with %d nm tolerance",
1990 pt.x, pt.y, tolerance ) );
1991 }
1992 }
1993 }
1994
1995 BOOST_CHECK_MESSAGE( teardropCount > 0,
1996 "Expected at least one teardrop zone for elongated pad" );
1997}
1998
1999
2008BOOST_FIXTURE_TEST_CASE( TwoSegmentAngledTeardropNoSelfIntersection, ZONE_FILL_TEST_FIXTURE )
2009{
2010 auto runVariant = [&]( bool aCurvedEdges )
2011 {
2012 KI_TEST::LoadBoard( m_settingsManager, "two_segment_teardrop", m_board );
2013
2014 for( PCB_TRACK* track : m_board->Tracks() )
2015 {
2016 if( track->Type() == PCB_VIA_T )
2017 {
2018 static_cast<PCB_VIA*>( track )->SetTeardropCurved( aCurvedEdges );
2019 break;
2020 }
2021 }
2022
2023 TOOL_MANAGER toolMgr;
2024 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2025
2026 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2027 toolMgr.RegisterTool( dummyTool );
2028
2029 BOARD_COMMIT commit( dummyTool );
2030 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2031 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2032
2033 if( !commit.Empty() )
2034 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2035
2036 int teardropCount = 0;
2037 bool foundSelfIntersection = false;
2038
2039 for( ZONE* zone : m_board->Zones() )
2040 {
2041 if( !zone->IsTeardropArea() )
2042 continue;
2043
2044 teardropCount++;
2045
2046 const SHAPE_POLY_SET* outline = zone->Outline();
2047
2048 if( !outline || outline->OutlineCount() == 0 )
2049 continue;
2050
2051 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2052 int n = chain.PointCount();
2053
2054 for( int i = 0; i < n && !foundSelfIntersection; i++ )
2055 {
2056 SEG segA( chain.CPoint( i ), chain.CPoint( ( i + 1 ) % n ) );
2057
2058 for( int j = i + 2; j < n; j++ )
2059 {
2060 if( i == 0 && j == n - 1 )
2061 continue;
2062
2063 SEG segB( chain.CPoint( j ), chain.CPoint( ( j + 1 ) % n ) );
2064 OPT_VECTOR2I hit = segA.Intersect( segB );
2065
2066 if( hit.has_value() )
2067 {
2068 BOOST_TEST_MESSAGE( wxString::Format(
2069 "Self-intersection at (%d, %d) between edges %d and %d "
2070 "(curved=%s)",
2071 hit->x, hit->y, i, j,
2072 aCurvedEdges ? "yes" : "no" ) );
2073
2074 for( int k = 0; k < n; k++ )
2075 {
2076 BOOST_TEST_MESSAGE( wxString::Format(
2077 " pt[%d] = (%d, %d)", k,
2078 chain.CPoint( k ).x, chain.CPoint( k ).y ) );
2079 }
2080
2081 foundSelfIntersection = true;
2082 break;
2083 }
2084 }
2085 }
2086 }
2087
2088 BOOST_CHECK_MESSAGE( teardropCount > 0,
2089 wxString::Format( "Expected at least one teardrop zone "
2090 "(curved=%s)",
2091 aCurvedEdges ? "yes" : "no" ) );
2092
2093 BOOST_CHECK_MESSAGE( !foundSelfIntersection,
2094 wxString::Format( "Teardrop polygon has self-intersecting "
2095 "edges (curved=%s)",
2096 aCurvedEdges ? "yes" : "no" ) );
2097 };
2098
2099 runVariant( true );
2100 runVariant( false );
2101}
2102
2103
2122BOOST_FIXTURE_TEST_CASE( OffCenterTwoSegmentTeardropNoSpike, ZONE_FILL_TEST_FIXTURE )
2123{
2124 auto runVariant = [&]( bool aCurvedEdges )
2125 {
2126 KI_TEST::LoadBoard( m_settingsManager, "teardrop_offcenter_two_segment", m_board );
2127
2128 VECTOR2I viaPos;
2129 int viaRadius = 0;
2130
2131 for( PCB_TRACK* track : m_board->Tracks() )
2132 {
2133 if( track->Type() == PCB_VIA_T )
2134 {
2135 PCB_VIA* via = static_cast<PCB_VIA*>( track );
2136 via->SetTeardropCurved( aCurvedEdges );
2137 viaPos = via->GetPosition();
2138 viaRadius = via->GetWidth( PADSTACK::ALL_LAYERS ) / 2;
2139 break;
2140 }
2141 }
2142
2143 BOOST_REQUIRE( viaRadius > 0 );
2144
2145 TOOL_MANAGER toolMgr;
2146 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2147
2148 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2149 toolMgr.RegisterTool( dummyTool );
2150
2151 BOARD_COMMIT commit( dummyTool );
2152 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2153 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2154
2155 if( !commit.Empty() )
2156 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2157
2158 // The crafted board's first segment emerges from the via by ~10 um on a 100 um
2159 // track width; the emerging-length filter rejects it and no teardrop is built.
2160 const double maxBackSideDist = viaRadius * 1.2;
2161 int teardropCount = 0;
2162 int spikingPoints = 0;
2163 VECTOR2I worstPoint;
2164 double worstDistance = 0.0;
2165
2166 for( ZONE* zone : m_board->Zones() )
2167 {
2168 if( !zone->IsTeardropArea() )
2169 continue;
2170
2171 teardropCount++;
2172
2173 const SHAPE_POLY_SET* outline = zone->Outline();
2174
2175 if( !outline || outline->OutlineCount() == 0 )
2176 continue;
2177
2178 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2179
2180 for( int i = 0; i < chain.PointCount(); i++ )
2181 {
2182 const VECTOR2I& pt = chain.CPoint( i );
2183 VECTOR2I rel = pt - viaPos;
2184
2185 // Only consider points on the back side (opposite the track entry).
2186 if( rel.x >= 0 )
2187 continue;
2188
2189 double dist = rel.EuclideanNorm();
2190
2191 if( dist > maxBackSideDist )
2192 {
2193 spikingPoints++;
2194
2195 if( dist > worstDistance )
2196 {
2197 worstDistance = dist;
2198 worstPoint = pt;
2199 }
2200 }
2201 }
2202 }
2203
2204 BOOST_CHECK_MESSAGE( teardropCount == 0,
2205 wxString::Format( "Expected no teardrop on grazing-entry "
2206 "track (emergence below track width), got "
2207 "%d (curved=%s)",
2208 teardropCount,
2209 aCurvedEdges ? "yes" : "no" ) );
2210
2211 BOOST_CHECK_MESSAGE( spikingPoints == 0,
2212 wxString::Format( "Found %d teardrop polygon vertex/vertices "
2213 "outside the expected envelope (worst at "
2214 "(%d, %d), %f mm from via center; curved=%s)",
2215 spikingPoints,
2216 worstPoint.x, worstPoint.y,
2217 worstDistance / pcbIUScale.IU_PER_MM,
2218 aCurvedEdges ? "yes" : "no" ) );
2219 };
2220
2221 runVariant( true );
2222 runVariant( false );
2223}
2224
2225
2237BOOST_FIXTURE_TEST_CASE( MultiTrackSharedInsideJunctionNoSelfIntersection,
2239{
2240 auto runVariant = [&]( bool aCurvedEdges )
2241 {
2242 KI_TEST::LoadBoard( m_settingsManager, "teardrop_multi_inside_via", m_board );
2243
2244 for( PCB_TRACK* track : m_board->Tracks() )
2245 {
2246 if( track->Type() == PCB_VIA_T )
2247 {
2248 static_cast<PCB_VIA*>( track )->SetTeardropCurved( aCurvedEdges );
2249 break;
2250 }
2251 }
2252
2253 TOOL_MANAGER toolMgr;
2254 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, nullptr );
2255
2256 KI_TEST::DUMMY_TOOL* dummyTool = new KI_TEST::DUMMY_TOOL();
2257 toolMgr.RegisterTool( dummyTool );
2258
2259 BOARD_COMMIT commit( dummyTool );
2260 TEARDROP_MANAGER teardropMgr( m_board.get(), &toolMgr );
2261 teardropMgr.UpdateTeardrops( commit, nullptr, nullptr, true );
2262
2263 if( !commit.Empty() )
2264 commit.Push( _( "Add teardrops" ), SKIP_UNDO | SKIP_SET_DIRTY );
2265
2266 int teardropCount = 0;
2267 int selfIntersectingCount = 0;
2268 VECTOR2I worstPoint;
2269
2270 for( ZONE* zone : m_board->Zones() )
2271 {
2272 if( !zone->IsTeardropArea() )
2273 continue;
2274
2275 teardropCount++;
2276
2277 const SHAPE_POLY_SET* outline = zone->Outline();
2278
2279 if( !outline || outline->OutlineCount() == 0 )
2280 continue;
2281
2282 const SHAPE_LINE_CHAIN& chain = outline->Outline( 0 );
2283 int n = chain.PointCount();
2284 bool intersected = false;
2285
2286 for( int i = 0; i < n && !intersected; i++ )
2287 {
2288 SEG segA( chain.CPoint( i ), chain.CPoint( ( i + 1 ) % n ) );
2289
2290 for( int j = i + 2; j < n; j++ )
2291 {
2292 if( i == 0 && j == n - 1 )
2293 continue;
2294
2295 SEG segB( chain.CPoint( j ), chain.CPoint( ( j + 1 ) % n ) );
2296 OPT_VECTOR2I hit = segA.Intersect( segB );
2297
2298 if( hit.has_value() )
2299 {
2300 BOOST_TEST_MESSAGE( wxString::Format(
2301 "Teardrop polygon self-intersection at (%d, %d) "
2302 "between edges %d and %d (curved=%s)",
2303 hit->x, hit->y, i, j, aCurvedEdges ? "yes" : "no" ) );
2304
2305 worstPoint = hit.value();
2306 intersected = true;
2307 break;
2308 }
2309 }
2310 }
2311
2312 if( intersected )
2313 selfIntersectingCount++;
2314 }
2315
2316 BOOST_CHECK_MESSAGE( teardropCount > 0,
2317 wxString::Format( "Expected at least one teardrop zone "
2318 "(curved=%s)",
2319 aCurvedEdges ? "yes" : "no" ) );
2320
2321 BOOST_CHECK_MESSAGE( selfIntersectingCount == 0,
2322 wxString::Format( "%d of %d teardrop polygon(s) self-intersect "
2323 "(worst at (%d, %d); curved=%s)",
2324 selfIntersectingCount, teardropCount,
2325 worstPoint.x, worstPoint.y,
2326 aCurvedEdges ? "yes" : "no" ) );
2327 };
2328
2329 runVariant( true );
2330 runVariant( false );
2331}
2332
2333
2341BOOST_FIXTURE_TEST_CASE( RegressionKeepoutBoundaryMissingFill, ZONE_FILL_TEST_FIXTURE )
2342{
2343 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
2344 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
2345
2346 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
2347 guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
2348
2349 auto getTotalFilledArea =
2350 [this]() -> double
2351 {
2352 double totalArea = 0;
2353
2354 for( ZONE* zone : m_board->Zones() )
2355 {
2356 if( zone->GetIsRuleArea() )
2357 continue;
2358
2359 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
2360 {
2361 if( !zone->HasFilledPolysForLayer( layer ) )
2362 continue;
2363
2364 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
2365
2366 if( fill )
2367 totalArea += std::abs( fill->Area() );
2368 }
2369 }
2370
2371 return totalArea;
2372 };
2373
2374 auto refillAndMeasure =
2375 [this, &cfg, &getTotalFilledArea]( bool aIterative ) -> double
2376 {
2377 cfg.m_ZoneFillIterativeRefill = aIterative;
2378
2379 KI_TEST::LoadBoard( m_settingsManager, "issue23515/issue23515", m_board );
2380
2381 double storedArea = getTotalFilledArea();
2382
2383 BOOST_REQUIRE_MESSAGE( storedArea > 0, "Stored v9 fill has zero area" );
2384
2385 KI_TEST::FillZones( m_board.get() );
2386 return getTotalFilledArea();
2387 };
2388
2389 KI_TEST::LoadBoard( m_settingsManager, "issue23515/issue23515", m_board );
2390
2391 double storedArea = getTotalFilledArea();
2392
2393 BOOST_REQUIRE_MESSAGE( storedArea > 0, "Stored v9 fill has zero area" );
2394
2395 double nonIterativeArea = refillAndMeasure( false );
2396 double iterativeArea = refillAndMeasure( true );
2397 double nonIterativeAreaRatio = nonIterativeArea / storedArea;
2398 double iterativeAreaRatio = iterativeArea / storedArea;
2399
2401 nonIterativeAreaRatio > 0.99999,
2402 wxString::Format(
2403 "Non-iterative refill lost %.4f%% versus stored v9 fill "
2404 "(stored=%.2f mm^2, non-iterative=%.2f mm^2). "
2405 "This suggests missing pieces near keepout boundaries (issue 23515).",
2406 ( 1.0 - nonIterativeAreaRatio ) * 100.0,
2407 storedArea / 1e6, nonIterativeArea / 1e6 ) );
2408
2410 iterativeAreaRatio > 0.99999,
2411 wxString::Format(
2412 "Iterative refill lost %.4f%% versus stored v9 fill "
2413 "(stored=%.2f mm^2, iterative=%.2f mm^2). "
2414 "This suggests missing pieces near keepout boundaries (issue 23515).",
2415 ( 1.0 - iterativeAreaRatio ) * 100.0,
2416 storedArea / 1e6, iterativeArea / 1e6 ) );
2417}
2418
2419
2428BOOST_FIXTURE_TEST_CASE( HatchZoneViaConnectionRespectsSetting, ZONE_FILL_TEST_FIXTURE )
2429{
2430 m_board = std::make_unique<BOARD>();
2431
2432 // Two-layer board is sufficient for this test
2433 m_board->SetCopperLayerCount( 2 );
2434
2435 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
2436 bds.SetCopperLayerCount( 2 );
2437
2438 bds.m_MinClearance = pcbIUScale.mmToIU( 0.2 );
2439
2440 // Add a GND net
2441 NETINFO_ITEM* gndNet = new NETINFO_ITEM( m_board.get(), wxT( "GND" ) );
2442 m_board->Add( gndNet );
2443 int gndNetCode = gndNet->GetNetCode();
2444
2445 // Via dimensions: 2.0mm diameter, 1.0mm drill - large enough to span multiple hatch cells
2446 // so the via always touches webbing lines regardless of position within the hatch grid.
2447 int viaDiam = pcbIUScale.mmToIU( 2.0 );
2448 int viaDrill = pcbIUScale.mmToIU( 1.0 );
2449
2450 // Hatch zone parameters: 0.5mm gap, 0.3mm thickness. The via (radius=1.0mm) is wider
2451 // than the gap, so it will always intersect webbing in FULL mode. The thermal gap
2452 // (0.5mm) makes the knockout circle radius = 1.0+0.5 = 1.5mm.
2453 int hatchGap = pcbIUScale.mmToIU( 0.5 );
2454 int hatchThickness = pcbIUScale.mmToIU( 0.3 );
2455
2456 // Via center at 10mm,10mm (middle of the zone)
2457 VECTOR2I viaPos( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) );
2458
2459 auto makeVia =
2460 [&]() -> PCB_VIA*
2461 {
2462 PCB_VIA* via = new PCB_VIA( m_board.get() );
2463 via->SetPosition( viaPos );
2464 via->SetLayerPair( F_Cu, B_Cu );
2465 via->SetDrill( viaDrill );
2466 via->SetWidth( PADSTACK::ALL_LAYERS, viaDiam );
2467 via->SetNetCode( gndNetCode );
2468 m_board->Add( via );
2469 return via;
2470 };
2471
2472 auto makeHatchZone =
2473 [&]( ZONE_CONNECTION aConnection ) -> ZONE*
2474 {
2475 ZONE* zone = new ZONE( m_board.get() );
2476 zone->SetLayer( F_Cu );
2477 zone->SetNetCode( gndNetCode );
2479 zone->SetHatchGap( hatchGap );
2480 zone->SetHatchThickness( hatchThickness );
2481 zone->SetPadConnection( aConnection );
2482 zone->SetMinThickness( pcbIUScale.mmToIU( 0.2 ) );
2483 zone->SetThermalReliefGap( pcbIUScale.mmToIU( 0.5 ) );
2484 zone->SetThermalReliefSpokeWidth( pcbIUScale.mmToIU( 0.5 ) );
2485
2486 SHAPE_POLY_SET outline;
2487 outline.NewOutline();
2488 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2489 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 19 ), pcbIUScale.mmToIU( 1 ) ) );
2490 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 19 ), pcbIUScale.mmToIU( 19 ) ) );
2491 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 19 ) ) );
2492 zone->AddPolygon( outline.COutline( 0 ) );
2493
2494 m_board->Add( zone );
2495 return zone;
2496 };
2497
2498 auto initDRC =
2499 [&]()
2500 {
2501 m_board->BuildConnectivity();
2502 auto drcEngine = std::make_shared<DRC_ENGINE>( m_board.get(), &bds );
2503 drcEngine->InitEngine( wxFileName() );
2504 bds.m_DRCEngine = drcEngine;
2505 };
2506
2507 // The thermal relief adds a circular ring around the via that covers hatch holes which
2508 // would otherwise be open. With viaRadius=1.0mm, thermalGap=0.5mm, spokeWidth=0.5mm:
2509 // ring outer radius = 1.75mm, inner radius = 1.25mm
2510 // ring area added inside hatch holes > knockout area removed from webbing
2511 // net result: THERMAL fill area > FULL fill area by ~0.4 sq mm
2512 // FULL connection skips both the knockout and the ring addition, so the THERMAL fill
2513 // should be measurably larger than the FULL fill.
2514
2515 double fullFillArea = 0.0;
2516 double thermalFillArea = 0.0;
2517
2518 // Test 1: FULL connection
2519 {
2520 PCB_VIA* via = makeVia();
2521 ZONE* zone = makeHatchZone( ZONE_CONNECTION::FULL );
2522
2523 initDRC();
2524 KI_TEST::FillZones( m_board.get() );
2525
2526 BOOST_REQUIRE_MESSAGE( zone->HasFilledPolysForLayer( F_Cu ),
2527 "Zone should have fill on F.Cu with FULL connection" );
2528
2529 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
2530
2531 for( int i = 0; i < fill->OutlineCount(); i++ )
2532 fullFillArea += std::abs( fill->Outline( i ).Area() );
2533
2534 m_board->Remove( via );
2535 m_board->Remove( zone );
2536 delete via;
2537 delete zone;
2538 }
2539
2540 // Test 2: THERMAL connection
2541 {
2542 PCB_VIA* via = makeVia();
2543 ZONE* zone = makeHatchZone( ZONE_CONNECTION::THERMAL );
2544
2545 initDRC();
2546 KI_TEST::FillZones( m_board.get() );
2547
2548 BOOST_REQUIRE_MESSAGE( zone->HasFilledPolysForLayer( F_Cu ),
2549 "Zone should have fill on F.Cu with THERMAL connection" );
2550
2551 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
2552
2553 for( int i = 0; i < fill->OutlineCount(); i++ )
2554 thermalFillArea += std::abs( fill->Outline( i ).Area() );
2555
2556 m_board->Remove( via );
2557 m_board->Remove( zone );
2558 delete via;
2559 delete zone;
2560 }
2561
2562 // The THERMAL fill should have more area than the FULL fill because a thermal ring was
2563 // added around the via, filling hatch holes that would otherwise be open.
2564 // Use a 0.2 sq mm threshold to avoid sensitivity to small edge effects.
2565 double iuPerMM = pcbIUScale.IU_PER_MM;
2566 double areaThreshold = 0.2 * iuPerMM * iuPerMM; // 0.2 sq mm in IU^2
2567
2568 double areaIU2toMM2 = 1.0 / ( iuPerMM * iuPerMM );
2569
2570 BOOST_CHECK_MESSAGE( thermalFillArea > fullFillArea + areaThreshold,
2571 wxString::Format(
2572 "THERMAL connection fill area (%.2f sq mm) should be larger "
2573 "than FULL fill area (%.2f sq mm) by at least 0.2 sq mm. "
2574 "If they are equal or FULL is larger, thermal ring was not "
2575 "added for THERMAL connection, or thermal ring was incorrectly "
2576 "added for FULL connection (issue 23516 regression).",
2577 thermalFillArea * areaIU2toMM2, fullFillArea * areaIU2toMM2 ) );
2578}
2579
2580
2587BOOST_FIXTURE_TEST_CASE( HatchZoneFullViaStaysConnected, ZONE_FILL_TEST_FIXTURE )
2588{
2589 m_board = std::make_unique<BOARD>();
2590 m_board->SetCopperLayerCount( 2 );
2591
2592 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
2593 bds.SetCopperLayerCount( 2 );
2594 bds.m_MinClearance = pcbIUScale.mmToIU( 0.2 );
2595
2596 NETINFO_ITEM* gndNet = new NETINFO_ITEM( m_board.get(), wxT( "GND" ) );
2597 m_board->Add( gndNet );
2598 int gndNetCode = gndNet->GetNetCode();
2599
2600 // Via diameter (0.4mm) is much smaller than the hatch gap (2.0mm), so a via centred in a
2601 // hole sits entirely inside that hole with no copper around it unless the hole is dropped.
2602 int viaDiam = pcbIUScale.mmToIU( 0.4 );
2603 int viaDrill = pcbIUScale.mmToIU( 0.2 );
2604
2605 int hatchGap = pcbIUScale.mmToIU( 2.0 );
2606 int hatchThickness = pcbIUScale.mmToIU( 0.3 );
2607
2608 auto makeHatchZone = [&]() -> ZONE*
2609 {
2610 ZONE* zone = new ZONE( m_board.get() );
2611 zone->SetLayer( F_Cu );
2612 zone->SetNetCode( gndNetCode );
2614 zone->SetHatchGap( hatchGap );
2615 zone->SetHatchThickness( hatchThickness );
2617 zone->SetMinThickness( pcbIUScale.mmToIU( 0.2 ) );
2618
2619 SHAPE_POLY_SET outline;
2620 outline.NewOutline();
2621 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ) );
2622 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 20 ), pcbIUScale.mmToIU( 0 ) ) );
2623 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 20 ), pcbIUScale.mmToIU( 20 ) ) );
2624 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 20 ) ) );
2625 zone->AddPolygon( outline.COutline( 0 ) );
2626
2627 m_board->Add( zone );
2628 return zone;
2629 };
2630
2631 // Sweep over one full grid period (gridsize = hatchThickness + hatchGap = 2.3mm) so the
2632 // via is guaranteed to land inside a hole at several positions regardless of grid phase.
2633 const int steps = 8;
2634 const double startMM = 9.0;
2635 const double stepMM = 2.3 / steps;
2636
2637 int isolatedCount = 0;
2638 int testedCount = 0;
2639
2640 for( int ix = 0; ix < steps; ix++ )
2641 {
2642 for( int iy = 0; iy < steps; iy++ )
2643 {
2644 VECTOR2I viaPos( pcbIUScale.mmToIU( startMM + ix * stepMM ), pcbIUScale.mmToIU( startMM + iy * stepMM ) );
2645
2646 PCB_VIA* via = new PCB_VIA( m_board.get() );
2647 via->SetPosition( viaPos );
2648 via->SetLayerPair( F_Cu, B_Cu );
2649 via->SetDrill( viaDrill );
2650 via->SetWidth( PADSTACK::ALL_LAYERS, viaDiam );
2651 via->SetNetCode( gndNetCode );
2652 m_board->Add( via );
2653
2654 ZONE* zone = makeHatchZone();
2655
2656 m_board->BuildConnectivity();
2657 auto drcEngine = std::make_shared<DRC_ENGINE>( m_board.get(), &bds );
2658 drcEngine->InitEngine( wxFileName() );
2659 bds.m_DRCEngine = drcEngine;
2660
2661 KI_TEST::FillZones( m_board.get() );
2662
2664
2665 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
2666 std::shared_ptr<SHAPE> viaShape = via->GetEffectiveShape( F_Cu );
2667
2668 // The zone fill must touch the via. If it does not, the via is isolated copper
2669 // inside a hatch hole (the issue 24559 regression).
2670 if( !fill->Collide( viaShape.get(), 0 ) )
2671 isolatedCount++;
2672
2673 testedCount++;
2674
2675 m_board->Remove( via );
2676 m_board->Remove( zone );
2677 delete via;
2678 delete zone;
2679 }
2680 }
2681
2682 BOOST_CHECK_MESSAGE( isolatedCount == 0, wxString::Format( "%d of %d FULL-connection via positions were left "
2683 "isolated from the hatch fill (issue 24559).",
2684 isolatedCount, testedCount ) );
2685}
2686
2687
2705BOOST_FIXTURE_TEST_CASE( RegressionCascadingIslandRefill, ZONE_FILL_TEST_FIXTURE )
2706{
2707 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
2708 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
2709 cfg.m_ZoneFillIterativeRefill = true;
2710
2711 struct ScopeGuard
2712 {
2713 bool& ref;
2714 bool orig;
2715 ~ScopeGuard() { ref = orig; }
2716 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
2717
2718 KI_TEST::LoadBoard( m_settingsManager, "zone_refill_cascading_islands", m_board );
2719 KI_TEST::FillZones( m_board.get() );
2720
2721 const std::vector<std::string> checkedNames = { "hi1", "hi2", "hi3", "hi4", "hi5", "hi6", "hi7",
2722 "lo1", "lo2", "lo3", "lo4", "lo5", "lo6" };
2723 std::map<std::string, ZONE*> zoneByName;
2724
2725 for( ZONE* zone : m_board->Zones() )
2726 zoneByName[zone->GetZoneName().ToStdString()] = zone;
2727
2728 for( const std::string& name : checkedNames )
2729 {
2730 BOOST_REQUIRE_MESSAGE( zoneByName.count( name ), "Zone '" + name + "' not found in test board" );
2731 BOOST_REQUIRE_MESSAGE( zoneByName[name]->HasFilledPolysForLayer( F_Cu ),
2732 "Zone '" + name + "' has no fill on F.Cu" );
2733 }
2734
2735 // hi3, hi5, hi7 each split into two copper islands (one standalone, one merged with lo2/lo4/lo6).
2736 for( const std::string& name : { "hi3", "hi5", "hi7" } )
2737 {
2738 int islands = zoneByName[name]->GetFilledPolysList( F_Cu )->OutlineCount();
2739
2740 BOOST_CHECK_MESSAGE( islands == 2, wxString::Format( "Zone '%s' should have 2 filled islands but has %d. "
2741 "Cascading island removal did not converge correctly.",
2742 name, islands ) );
2743 }
2744
2745 // All lo zones and hi2/hi4/hi6 are single zones.
2746 for( const std::string& name : { "lo1", "lo2", "lo3", "lo4", "lo5", "lo6", "hi2", "hi4", "hi6" } )
2747 {
2748 int islands = zoneByName[name]->GetFilledPolysList( F_Cu )->OutlineCount();
2749
2750 BOOST_CHECK_MESSAGE( islands == 1, wxString::Format( "Zone '%s' should have 1 filled island but has %d. "
2751 "Iterative refill may have incorrectly blocked or "
2752 "expanded this zone.",
2753 name, islands ) );
2754 }
2755}
2756
2757
2764BOOST_FIXTURE_TEST_CASE( CopperThievingZone_HatchSurvivesTrackBisection, ZONE_FILL_TEST_FIXTURE )
2765{
2766 KI_TEST::LoadBoard( m_settingsManager, "zone_thieving_track_bisection", m_board );
2767 KI_TEST::FillZones( m_board.get() );
2768
2769 ZONE* thievingZone = nullptr;
2770
2771 for( ZONE* z : m_board->Zones() )
2772 {
2773 if( z->GetFillMode() == ZONE_FILL_MODE::COPPER_THIEVING )
2774 {
2775 thievingZone = z;
2776 break;
2777 }
2778 }
2779
2780 BOOST_REQUIRE( thievingZone );
2781
2782 const std::shared_ptr<SHAPE_POLY_SET>& fill = thievingZone->GetFilledPolysList( F_Cu );
2783 BOOST_REQUIRE( fill );
2784
2785 // The track splits the fill area in two; before the fix the connectivity
2786 // pass classified the narrow side as an isolated island and deleted it.
2787 // Expect at least two outlines covering both halves of the original zone.
2788 BOOST_CHECK_GE( fill->OutlineCount(), 2 );
2789
2790 // The fill must span the full zone width (left edge through right edge).
2791 BOX2I fillBox = fill->BBox();
2792 BOX2I zoneBox = thievingZone->Outline()->BBox();
2793
2794 BOOST_CHECK_LT( fillBox.GetLeft(), zoneBox.GetLeft() + pcbIUScale.mmToIU( 2.0 ) );
2795 BOOST_CHECK_GT( fillBox.GetRight(), zoneBox.GetRight() - pcbIUScale.mmToIU( 2.0 ) );
2796
2797 // The mesh must have real structure on both sides.
2798 BOOST_CHECK_GT( fill->TotalVertices(), 200 );
2799}
2800
2801
2814BOOST_FIXTURE_TEST_CASE( IterativeRefillConvergenceLimit, ZONE_FILL_TEST_FIXTURE )
2815{
2816 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
2817 bool originalIterativeRefill = cfg.m_ZoneFillIterativeRefill;
2818 cfg.m_ZoneFillIterativeRefill = true;
2819
2820 struct ScopeGuard
2821 {
2822 bool& ref;
2823 bool orig;
2824 ~ScopeGuard() { ref = orig; }
2825 } guard{ cfg.m_ZoneFillIterativeRefill, originalIterativeRefill };
2826
2827 // Capture wxLogWarning calls so we can assert that the iteration cap fires.
2828 class WarningCapture : public wxLog
2829 {
2830 public:
2831 bool m_hadWarning = false;
2832
2833 protected:
2834 void DoLogRecord( wxLogLevel aLevel, const wxString&, const wxLogRecordInfo& ) override
2835 {
2836 if( aLevel == wxLOG_Warning )
2837 m_hadWarning = true;
2838 }
2839 };
2840
2841 auto* capture = new WarningCapture();
2842 wxLog* oldLog = wxLog::SetActiveTarget( capture );
2843
2844 struct LogGuard
2845 {
2846 wxLog* old;
2847 ~LogGuard() { wxLog::SetActiveTarget( old ); }
2848 } logGuard{ oldLog };
2849
2850 KI_TEST::LoadBoard( m_settingsManager, "zone_refill_convergence_limit", m_board );
2851 KI_TEST::FillZones( m_board.get() );
2852
2853 BOOST_CHECK_MESSAGE( capture->m_hadWarning, "Expected a wxLogWarning when iterative refill hits the iteration "
2854 "limit, but none was emitted. The convergence-limit board may no "
2855 "longer trigger the cap, or the warning path has changed." );
2856}
2857
2858
2864BOOST_FIXTURE_TEST_CASE( CopperThievingZone_NonCopperLayerStampsNotSolid, ZONE_FILL_TEST_FIXTURE )
2865{
2866 m_board = std::make_unique<BOARD>();
2867
2868 ZONE* zone = new ZONE( m_board.get() );
2869 zone->SetLayer( F_SilkS );
2870 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
2871 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
2872 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
2873 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
2875
2876 THIEVING_SETTINGS thieving;
2878 thieving.element_size = pcbIUScale.mmToIU( 0.5 );
2879 thieving.gap = pcbIUScale.mmToIU( 1.5 );
2880 zone->SetThievingSettings( thieving );
2882 m_board->Add( zone );
2883
2884 KI_TEST::FillZones( m_board.get() );
2885
2886 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_SilkS );
2887 BOOST_REQUIRE( fill );
2888
2889 // A solid fill would have one outline (the zone polygon); a dots grid
2890 // produces dozens. Lower bound is conservative to avoid edge-clipping flakiness.
2891 BOOST_CHECK_GT( fill->OutlineCount(), 5 );
2892}
2893
2894
2903{
2904 m_board = std::make_unique<BOARD>();
2905 m_board->SetCopperLayerCount( 2 );
2906
2907 // 10 mm x 10 mm zone outline
2908 ZONE* zone = new ZONE( m_board.get() );
2909 zone->SetLayer( F_Cu );
2910 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
2911 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
2912 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
2913 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
2914
2916
2917 THIEVING_SETTINGS thieving;
2919 thieving.element_size = pcbIUScale.mmToIU( 0.5 );
2920 thieving.gap = pcbIUScale.mmToIU( 2.0 );
2921 thieving.line_width = pcbIUScale.mmToIU( 0.3 );
2922 thieving.stagger = false;
2923 thieving.orientation = ANGLE_0;
2924 zone->SetThievingSettings( thieving );
2925
2927
2928 m_board->Add( zone );
2929
2930 KI_TEST::FillZones( m_board.get() );
2931
2932 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
2933 BOOST_REQUIRE( fill );
2934 BOOST_REQUIRE_GT( fill->OutlineCount(), 0 );
2935
2936 // 2.5 mm pitch, 0.5 mm dot. The four positions whose disc touches the
2937 // zone edge (x or y at 0 or 10 mm) are dropped, leaving a 3 x 3 grid.
2938 BOOST_CHECK_GE( fill->OutlineCount(), 6 );
2939 BOOST_CHECK_LE( fill->OutlineCount(), 12 );
2940
2941 // 10% slack covers the polygonal circle approximation plus post-fill corner rounding.
2942 const double fullDotArea = M_PI * std::pow( pcbIUScale.mmToIU( 0.25 ), 2 );
2943 CheckAllOutlineAreasAtLeast( fill, 0.9 * fullDotArea, wxT( "Dot" ) );
2944}
2945
2946
2953BOOST_FIXTURE_TEST_CASE( CopperThievingZone_StaggerProducesDifferentLayout, ZONE_FILL_TEST_FIXTURE )
2954{
2955 auto countDots = []( bool stagger ) -> int
2956 {
2957 auto board = std::make_unique<BOARD>();
2958 board->SetCopperLayerCount( 2 );
2959
2960 ZONE* zone = new ZONE( board.get() );
2961 zone->SetLayer( F_Cu );
2962 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
2963 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 20 ), 0 ), -1 );
2964 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 20 ), pcbIUScale.mmToIU( 20 ) ), -1 );
2965 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 20 ) ), -1 );
2967
2968 THIEVING_SETTINGS thieving;
2970 thieving.element_size = pcbIUScale.mmToIU( 0.5 );
2971 thieving.gap = pcbIUScale.mmToIU( 2.0 );
2972 thieving.stagger = stagger;
2973 zone->SetThievingSettings( thieving );
2975 board->Add( zone );
2976
2977 KI_TEST::FillZones( board.get() );
2978 return zone->GetFilledPolysList( F_Cu )->OutlineCount();
2979 };
2980
2981 int plain = countDots( false );
2982 int staggered = countDots( true );
2983
2984 BOOST_TEST_MESSAGE( "plain dots: " << plain << " staggered dots: " << staggered );
2985
2986 // Within a factor of two — catches the offset walking dots off the board
2987 // (returning ~0) without being brittle about edge-clipping rounding.
2988 BOOST_CHECK_NE( plain, staggered );
2989 BOOST_CHECK_GE( staggered, plain / 2 );
2990 BOOST_CHECK_LE( staggered, plain * 2 );
2991}
2992
2993
2999BOOST_FIXTURE_TEST_CASE( CopperThievingZone_SquaresGrid, ZONE_FILL_TEST_FIXTURE )
3000{
3001 m_board = std::make_unique<BOARD>();
3002 m_board->SetCopperLayerCount( 2 );
3003
3004 ZONE* zone = new ZONE( m_board.get() );
3005 zone->SetLayer( F_Cu );
3006 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
3007 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
3008 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
3009 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
3011
3012 THIEVING_SETTINGS thieving;
3014 thieving.element_size = pcbIUScale.mmToIU( 0.6 );
3015 thieving.gap = pcbIUScale.mmToIU( 2.0 );
3016 zone->SetThievingSettings( thieving );
3018 m_board->Add( zone );
3019
3020 KI_TEST::FillZones( m_board.get() );
3021
3022 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
3023 BOOST_REQUIRE( fill );
3024 BOOST_REQUIRE_GT( fill->OutlineCount(), 0 );
3025
3026 // 2.6 mm pitch, 0.6 mm square. Same edge-drop behavior as the dots test:
3027 // strict-containment leaves a 3 x 3 grid of full squares.
3028 BOOST_CHECK_GE( fill->OutlineCount(), 6 );
3029 BOOST_CHECK_LE( fill->OutlineCount(), 12 );
3030
3031 const double fullSquareArea = std::pow( pcbIUScale.mmToIU( 0.6 ), 2 );
3032 CheckAllOutlineAreasAtLeast( fill, 0.9 * fullSquareArea, wxT( "Square" ) );
3033}
3034
3035
3044BOOST_FIXTURE_TEST_CASE( CopperThievingZone_HighDensityPerformance, ZONE_FILL_TEST_FIXTURE )
3045{
3046 m_board = std::make_unique<BOARD>();
3047 m_board->SetCopperLayerCount( 2 );
3048
3049 ZONE* zone = new ZONE( m_board.get() );
3050 zone->SetLayer( F_Cu );
3051 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
3052 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 100 ), 0 ), -1 );
3053 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 100 ), pcbIUScale.mmToIU( 100 ) ), -1 );
3054 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 100 ) ), -1 );
3056
3057 THIEVING_SETTINGS thieving;
3059 thieving.element_size = pcbIUScale.mmToIU( 0.3 );
3060 thieving.gap = pcbIUScale.mmToIU( 1.0 );
3061 zone->SetThievingSettings( thieving );
3063 m_board->Add( zone );
3064
3065 auto start = std::chrono::steady_clock::now();
3066 KI_TEST::FillZones( m_board.get() );
3067 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
3068 std::chrono::steady_clock::now() - start )
3069 .count();
3070
3071 BOOST_TEST_MESSAGE( "5.9k-dot fill elapsed: " << elapsed << " ms" );
3072
3074 BOOST_CHECK_GT( zone->GetFilledPolysList( F_Cu )->OutlineCount(), 4000 );
3075
3076 // 30 s upper bound on QABUILD with assertions on; current implementation
3077 // measures in low seconds.
3078 BOOST_CHECK_LT( elapsed, 30000 );
3079}
3080
3081
3088BOOST_FIXTURE_TEST_CASE( CopperThievingZone_HatchPattern, ZONE_FILL_TEST_FIXTURE )
3089{
3090 m_board = std::make_unique<BOARD>();
3091 m_board->SetCopperLayerCount( 2 );
3092
3093 ZONE* zone = new ZONE( m_board.get() );
3094 zone->SetLayer( F_Cu );
3095 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
3096 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
3097 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
3098 zone->AppendCorner( VECTOR2I( 0, pcbIUScale.mmToIU( 10 ) ), -1 );
3100
3101 THIEVING_SETTINGS thieving;
3103 thieving.gap = pcbIUScale.mmToIU( 2.0 );
3104 thieving.line_width = pcbIUScale.mmToIU( 0.3 );
3105 zone->SetThievingSettings( thieving );
3107 m_board->Add( zone );
3108
3109 KI_TEST::FillZones( m_board.get() );
3110
3111 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( F_Cu );
3112 BOOST_REQUIRE( fill );
3113 BOOST_REQUIRE_GT( fill->TotalVertices(), 0 );
3114
3115 // Subtractive hatch produces a single connected outline after fracturing
3116 // (perimeter border + interior mesh linked through bridges). A dot grid
3117 // in the same outline would have dozens of disconnected pieces.
3118 BOOST_CHECK_EQUAL( fill->OutlineCount(), 1 );
3119
3120 // The fill bounding box must reach the zone corners — the perimeter
3121 // border is what differentiates hatch from a dot grid. Solid would also
3122 // reach the corners; the high vertex count below catches that case.
3123 BOX2I fillBox = fill->BBox();
3124 BOOST_CHECK_LT( fillBox.GetLeft(), pcbIUScale.mmToIU( 0.5 ) );
3125 BOOST_CHECK_GT( fillBox.GetRight(), pcbIUScale.mmToIU( 9.5 ) );
3126 BOOST_CHECK_LT( fillBox.GetTop(), pcbIUScale.mmToIU( 0.5 ) );
3127 BOOST_CHECK_GT( fillBox.GetBottom(), pcbIUScale.mmToIU( 9.5 ) );
3128
3129 // A solid 10x10 mm rectangle would have ~4 vertices. A hatched mesh has
3130 // many vertices because each void cut adds outline segments.
3131 BOOST_CHECK_GT( fill->TotalVertices(), 30 );
3132}
3133
3134
3142BOOST_FIXTURE_TEST_CASE( RegressionNonCopperZoneKeepoutIslands, ZONE_FILL_TEST_FIXTURE )
3143{
3144 KI_TEST::LoadBoard( m_settingsManager, "issue24089/issue24089", m_board );
3145
3146 auto countIslands =
3147 [this]() -> int
3148 {
3149 int total = 0;
3150
3151 for( ZONE* zone : m_board->Zones() )
3152 {
3153 if( zone->GetIsRuleArea() )
3154 continue;
3155
3156 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
3157 {
3158 if( !zone->HasFilledPolysForLayer( layer ) )
3159 continue;
3160
3161 std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
3162
3163 if( fill )
3164 total += fill->OutlineCount();
3165 }
3166 }
3167
3168 return total;
3169 };
3170
3171 int storedIslands = countIslands();
3172
3173 BOOST_REQUIRE_MESSAGE( storedIslands >= 3,
3174 wxString::Format( "Stored v9 fill should have at least 3 silk islands; "
3175 "found %d",
3176 storedIslands ) );
3177
3178 KI_TEST::FillZones( m_board.get() );
3179
3180 int refilledIslands = countIslands();
3181
3182 BOOST_CHECK_MESSAGE( refilledIslands == storedIslands,
3183 wxString::Format( "Refill lost silk islands: stored=%d, refilled=%d. "
3184 "Outline 0 of every non-copper multi-island zone "
3185 "was being incorrectly removed (issue 24089).",
3186 storedIslands, refilledIslands ) );
3187}
3188
3189
3195BOOST_FIXTURE_TEST_CASE( OverlappingPriorityPadFlashing, ZONE_FILL_TEST_FIXTURE )
3196{
3197 m_board = std::make_unique<BOARD>();
3198 m_board->SetCopperLayerCount( 4 );
3199
3200 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3201 bds.SetCopperLayerCount( 4 );
3202 bds.m_MinClearance = pcbIUScale.mmToIU( 0.2 );
3203
3204 NETINFO_ITEM* gndNet = new NETINFO_ITEM( m_board.get(), wxT( "GND" ) );
3205 m_board->Add( gndNet );
3206 int gndNetCode = gndNet->GetNetCode();
3207
3208 NETINFO_ITEM* vccNet = new NETINFO_ITEM( m_board.get(), wxT( "VCC" ) );
3209 m_board->Add( vccNet );
3210 int vccNetCode = vccNet->GetNetCode();
3211
3212 ZONE* gndZone = new ZONE( m_board.get() );
3213 gndZone->SetLayer( In1_Cu );
3214 gndZone->SetNetCode( gndNetCode );
3215 gndZone->SetAssignedPriority( 0 );
3216 gndZone->SetMinThickness( pcbIUScale.mmToIU( 0.2 ) );
3217 gndZone->SetThermalReliefGap( pcbIUScale.mmToIU( 0.5 ) );
3218 gndZone->SetThermalReliefSpokeWidth( pcbIUScale.mmToIU( 0.5 ) );
3220 {
3221 SHAPE_POLY_SET outline;
3222 outline.NewOutline();
3223 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ) );
3224 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 0 ) ) );
3225 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 20 ) ) );
3226 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 20 ) ) );
3227 gndZone->AddPolygon( outline.COutline( 0 ) );
3228 }
3229 m_board->Add( gndZone );
3230
3231 ZONE* vccZone = new ZONE( m_board.get() );
3232 vccZone->SetLayer( In1_Cu );
3233 vccZone->SetNetCode( vccNetCode );
3234 vccZone->SetAssignedPriority( 5 );
3235 vccZone->SetMinThickness( pcbIUScale.mmToIU( 4.0 ) );
3236 vccZone->SetThermalReliefGap( pcbIUScale.mmToIU( 0.5 ) );
3237 vccZone->SetThermalReliefSpokeWidth( pcbIUScale.mmToIU( 0.5 ) );
3239 {
3240 // A "barbell": a bulky right lobe joined to a thin left neck by a 0.5mm-tall corridor.
3241 // VCC's 4mm min-thickness prunes the neck and corridor in the deflate/inflate pass, so
3242 // VCC fills only the right lobe while its outline still encloses the pad at (15, 10).
3243 SHAPE_POLY_SET outline;
3244 outline.NewOutline();
3245 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 13.5 ), pcbIUScale.mmToIU( 9.75 ) ) );
3246 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 18 ), pcbIUScale.mmToIU( 9.75 ) ) );
3247 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 18 ), pcbIUScale.mmToIU( 0 ) ) );
3248 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 0 ) ) );
3249 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 20 ) ) );
3250 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 18 ), pcbIUScale.mmToIU( 20 ) ) );
3251 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 18 ), pcbIUScale.mmToIU( 10.25 ) ) );
3252 outline.Append( VECTOR2I( pcbIUScale.mmToIU( 13.5 ), pcbIUScale.mmToIU( 10.25 ) ) );
3253 vccZone->AddPolygon( outline.COutline( 0 ) );
3254 }
3255 m_board->Add( vccZone );
3256
3257 // REMOVE_EXCEPT_START_AND_END makes inner-layer flashing conditional on a same-net
3258 // connection, which is what issue 24175 gets wrong.
3259 auto footprint = std::make_unique<FOOTPRINT>( m_board.get() );
3260
3261 PAD* pad = new PAD( footprint.get() );
3262 pad->SetAttribute( PAD_ATTRIB::PTH );
3263 pad->SetLayerSet( LSET::AllCuMask() );
3264 pad->SetSize( PADSTACK::ALL_LAYERS,
3265 VECTOR2I( pcbIUScale.mmToIU( 1.5 ), pcbIUScale.mmToIU( 1.5 ) ) );
3266 pad->SetDrillSize( VECTOR2I( pcbIUScale.mmToIU( 0.8 ), pcbIUScale.mmToIU( 0.8 ) ) );
3267 pad->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 10 ) ) );
3268 pad->SetUnconnectedLayerMode( UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END );
3269 pad->SetNetCode( gndNetCode );
3270
3271 footprint->Add( pad );
3272 footprint->SetPosition( VECTOR2I( 0, 0 ) );
3273 m_board->Add( footprint.release() );
3274
3275 m_board->BuildConnectivity();
3276 auto drcEngine = std::make_shared<DRC_ENGINE>( m_board.get(), &bds );
3277 drcEngine->InitEngine( wxFileName() );
3278 bds.m_DRCEngine = drcEngine;
3279
3280 KI_TEST::FillZones( m_board.get() );
3281
3282 // Guard the preconditions so a future fill change cannot make this pass for the wrong
3283 // reason: VCC's outline must enclose the pad while its fill must not reach it.
3284 BOOST_REQUIRE_MESSAGE( vccZone->Outline()->Contains( pad->GetPosition() ),
3285 "VCC outline must contain the pad position to reproduce issue 24175." );
3286 BOOST_REQUIRE_MESSAGE( vccZone->HasFilledPolysForLayer( In1_Cu ),
3287 "VCC zone should still have fill in its right lobe." );
3288
3289 {
3290 const std::shared_ptr<SHAPE_POLY_SET>& vccFill = vccZone->GetFilledPolysList( In1_Cu );
3291
3292 BOOST_REQUIRE_MESSAGE( !vccFill->Contains( pad->GetPosition() ),
3293 "VCC fill should NOT contain the pad position (corridor must be "
3294 "pruned by min-thickness for the test to exercise issue 24175)." );
3295 }
3296
3297 // Before the fix the higher-priority VCC outline forced ZLO_FORCE_NO_ZONE_CONNECTION on
3298 // the pad; now the same-net GND zone wins the flashing decision.
3299 BOOST_CHECK_MESSAGE( pad->FlashLayer( In1_Cu ),
3300 "PTH pad inside higher-priority different-net zone must still flash "
3301 "when a same-net lower-priority zone covers it (issue 24175)." );
3302
3303 BOOST_REQUIRE_MESSAGE( gndZone->HasFilledPolysForLayer( In1_Cu ),
3304 "GND zone should have fill on In1.Cu" );
3305
3306 const std::shared_ptr<SHAPE_POLY_SET>& gndFill = gndZone->GetFilledPolysList( In1_Cu );
3307
3308 // Sampling a ring just outside the pad proves the GND fill actually surrounds it.
3309 int samples = 16;
3310 int sampleR = pcbIUScale.mmToIU( 1.6 ); // just outside the pad (radius 0.75) + clearance
3311 bool foundCopperAround = false;
3312
3313 for( int i = 0; i < samples; i++ )
3314 {
3315 double angle = ( 2.0 * M_PI * i ) / samples;
3316 VECTOR2I p( pad->GetPosition().x + KiROUND( sampleR * std::cos( angle ) ),
3317 pad->GetPosition().y + KiROUND( sampleR * std::sin( angle ) ) );
3318
3319 if( gndFill->Contains( p ) )
3320 {
3321 foundCopperAround = true;
3322 break;
3323 }
3324 }
3325
3326 BOOST_CHECK_MESSAGE( foundCopperAround,
3327 "Lower-priority GND zone should have copper around GND pad even when "
3328 "a higher-priority different-net zone outline contains the pad "
3329 "(issue 24175)." );
3330}
3331
3332
3333// Reproduces the scripting/API zone-fill path used by KiKit panelization (issue 24643).
3334//
3335// The interactive GUI and the board loader always create and initialize the board's DRC engine
3336// before filling. The Python/API ZONE_FILLER path can reach Fill() with no engine, so the
3337// worker-thread EvalRules() calls dereferenced a null engine and crashed the process. This test
3338// drops the engine after loading to drive that path, then verifies Fill() completes and leaves a
3339// usable engine behind.
3340BOOST_FIXTURE_TEST_CASE( RegressionApiSubsetFillPanelized, ZONE_FILL_TEST_FIXTURE )
3341{
3342 KI_TEST::LoadBoard( m_settingsManager, "issue24643/issue24643", m_board );
3343
3344 // The test harness loads boards with an initialized engine; the headless API path does not.
3345 // Drop it so Fill() must reconstruct one, which is the condition that crashed.
3346 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3347 bds.m_DRCEngine.reset();
3348 BOOST_REQUIRE( !bds.m_DRCEngine );
3349
3350 // Mirror the script: select non-rule-area zones on B.Cu that are not already filled.
3351 PCB_LAYER_ID targetLayer = m_board->GetLayerID( wxT( "B.Cu" ) );
3352 std::vector<ZONE*> toFill;
3353
3354 for( ZONE* zone : m_board->Zones() )
3355 {
3356 if( zone->GetIsRuleArea() )
3357 continue;
3358
3359 if( !zone->IsOnLayer( targetLayer ) )
3360 continue;
3361
3362 if( zone->IsFilled() )
3363 continue;
3364
3365 toFill.push_back( zone );
3366 }
3367
3368 BOOST_REQUIRE_MESSAGE( !toFill.empty(),
3369 "Expected at least one unfilled B.Cu zone to exercise the API path." );
3370
3371 // The API path builds the filler with a null commit (see new_ZONE_FILLER in the SWIG
3372 // wrapper) and fills only the selected subset. This must complete without crashing
3373 // (issue 24643).
3374 ZONE_FILLER filler( m_board.get(), nullptr );
3375
3376 BOOST_CHECK_NO_THROW( filler.Fill( toFill ) );
3377
3378 // Fill() must have created and initialized a usable engine in place of the one we dropped.
3380 BOOST_CHECK( bds.m_DRCEngine->RulesValid() );
3381}
3382
3383
3384// Issue 23790: overlapping same-net zones must merge across a notch a higher-priority
3385// different-net zone carved into the higher-priority same-net zone.
3386BOOST_FIXTURE_TEST_CASE( RegressionSameNetMergeAroundHigherPriorityZone, ZONE_FILL_TEST_FIXTURE )
3387{
3388 // The reconciliation only runs inside the iterative refill.
3389 ADVANCED_CFG& cfg = const_cast<ADVANCED_CFG&>( ADVANCED_CFG::GetCfg() );
3390 struct ScopeGuard { bool& ref; bool orig; ~ScopeGuard() { ref = orig; } }
3392 cfg.m_ZoneFillIterativeRefill = true;
3393
3394 KI_TEST::LoadBoard( m_settingsManager, "issue23790/issue23790", m_board );
3395 KI_TEST::FillZones( m_board.get() );
3396
3397 const PCB_LAYER_ID layer = F_Cu;
3398 const int margin = pcbIUScale.mmToIU( 0.05 );
3399
3400 std::map<int, SHAPE_POLY_SET> mergedByNet;
3401
3402 for( ZONE* zone : m_board->Zones() )
3403 {
3404 if( zone->GetIsRuleArea() || !zone->HasFilledPolysForLayer( layer ) )
3405 continue;
3406
3407 mergedByNet[zone->GetNetCode()].BooleanAdd( *zone->GetFilledPolysList( layer ) );
3408 }
3409
3410 // Areas legitimately free of this net's copper: keepouts and higher-priority
3411 // different-net fills (grown by a clearance allowance).
3412 auto buildLegitVoids =
3413 [&]( const ZONE* aLower, const ZONE* aHigher ) -> SHAPE_POLY_SET
3414 {
3415 SHAPE_POLY_SET voids;
3416 int allowance = pcbIUScale.mmToIU( 0.6 );
3417
3418 for( ZONE* other : m_board->Zones() )
3419 {
3420 if( !other->GetLayerSet().Contains( layer ) )
3421 continue;
3422
3423 if( other->GetIsRuleArea() )
3424 {
3425 if( other->GetDoNotAllowZoneFills() )
3426 voids.BooleanAdd( *other->Outline() );
3427
3428 continue;
3429 }
3430
3431 if( other->GetNetCode() == aLower->GetNetCode()
3432 || other->GetAssignedPriority() <= aLower->GetAssignedPriority()
3433 || other->GetAssignedPriority() <= aHigher->GetAssignedPriority()
3434 || !other->HasFilledPolysForLayer( layer ) )
3435 {
3436 continue;
3437 }
3438
3439 SHAPE_POLY_SET fill = *other->GetFilledPolysList( layer );
3441 voids.BooleanAdd( fill );
3442 }
3443
3444 return voids;
3445 };
3446
3447 std::vector<ZONE*> zones;
3448
3449 for( ZONE* zone : m_board->Zones() )
3450 {
3451 if( !zone->GetIsRuleArea() && zone->GetNetCode() > 0 && zone->GetLayerSet().Contains( layer ) )
3452 zones.push_back( zone );
3453 }
3454
3455 int checkedPairs = 0;
3456
3457 for( size_t i = 0; i < zones.size(); ++i )
3458 {
3459 for( size_t j = i + 1; j < zones.size(); ++j )
3460 {
3461 ZONE* a = zones[i];
3462 ZONE* b = zones[j];
3463
3464 if( a->GetNetCode() != b->GetNetCode() )
3465 continue;
3466
3467 SHAPE_POLY_SET overlap = *a->Outline();
3468 overlap.BooleanIntersection( *b->Outline() );
3469
3470 if( overlap.OutlineCount() == 0 )
3471 continue;
3472
3473 const ZONE* lower = a->GetAssignedPriority() <= b->GetAssignedPriority() ? a : b;
3474 const ZONE* higher = ( lower == a ) ? b : a;
3475
3476 overlap.BooleanSubtract( buildLegitVoids( lower, higher ) );
3477
3478 // Stay clear of outer-boundary min-width rounding.
3480
3481 if( overlap.OutlineCount() == 0 )
3482 continue;
3483
3484 SHAPE_POLY_SET uncovered = overlap;
3485 uncovered.BooleanSubtract( mergedByNet[a->GetNetCode()] );
3486
3487 double uncoveredArea =
3488 uncovered.Area() / ( pcbIUScale.IU_PER_MM * (double) pcbIUScale.IU_PER_MM );
3489
3490 BOOST_CHECK_MESSAGE( uncoveredArea < 0.01,
3491 wxString::Format( "Same-net zones (priorities %d and %d) left %.4f mm^2 of "
3492 "their overlap unfilled; overlapping same-net zones must "
3493 "merge (issue 23790).",
3495 uncoveredArea ) );
3496 checkedPairs++;
3497 }
3498 }
3499
3500 BOOST_CHECK_MESSAGE( checkedPairs >= 2,
3501 wxString::Format( "Expected at least two overlapping same-net zone pairs "
3502 "to exercise the merge, found %d.", checkedPairs ) );
3503}
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< 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:81
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:44
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:595
T Min() const
Definition minoptmax.h:29
Handle the data for a net.
Definition netinfo.h:46
int GetNetCode() const
Definition netinfo.h:94
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
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:90
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
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:1350
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:599
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:581
void SetFillMode(ZONE_FILL_MODE aFillMode)
Definition zone.cpp:605
bool HasFilledPolysForLayer(PCB_LAYER_ID aLayer) const
Definition zone.h:688
void SetThievingSettings(const THIEVING_SETTINGS &aSettings)
Definition zone.h:352
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:1367
double CalculateFilledArea()
Compute the area currently occupied by the zone fill.
Definition zone.cpp:1798
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:834
void SetHatchGap(int aStep)
Definition zone.h:329
unsigned GetAssignedPriority() const
Definition zone.h:122
@ CHAMFER_ALL_CORNERS
All angles are chamfered.
@ ROUND_ALL_CORNERS
All angles are rounded.
@ DRCE_CLEARANCE
Definition drc_item.h:40
@ DRCE_COPPER_SLIVER
Definition drc_item.h:90
@ 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
@ 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)
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
const SHAPE_LINE_CHAIN chain
int clearance
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2I v2(1, 0)
static const std::vector< wxString > RegressionZoneFillTests_tests
int delta
static const std::vector< std::pair< wxString, int > > RegressionTeardropFill_tests
BOOST_DATA_TEST_CASE_F(ZONE_FILL_TEST_FIXTURE, RegressionZoneFillTests, boost::unit_test::data::make(RegressionZoneFillTests_tests), relPath)
static void CheckAllOutlineAreasAtLeast(const std::shared_ptr< SHAPE_POLY_SET > &aFill, double aMinArea, const wxString &aLabel)
Assert every outline in aFill has at least aMinArea — used to verify thieving stamps survived the fil...
static const std::vector< wxString > RegressionSliverZoneFillTests_tests
BOOST_FIXTURE_TEST_CASE(BasicZoneFills, ZONE_FILL_TEST_FIXTURE)
#define M_PI
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h: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