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