KiCad PCB EDA Suite
Loading...
Searching...
No Matches
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 (C) 2014-2017 CERN
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 * @author Tomasz Włostowski <[email protected]>
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License as published by the
10 * Free Software Foundation, either version 3 of the License, or (at your
11 * option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, you may find one here:
20 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
21 * or you may search the http://www.gnu.org website for the version 2 license,
22 * or you may write to the Free Software Foundation, Inc.,
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 */
25
26#include <atomic>
27#include <future>
28#include <hash.h>
29#include <set>
30#include <unordered_set>
31#include <core/kicad_algo.h>
32#include <advanced_config.h>
33#include <board.h>
34#include <core/profile.h>
35
41static const wxChar traceZoneFiller[] = wxT( "KICAD_ZONE_FILLER" );
43#include <drc/drc_engine.h>
44#include <zone.h>
45#include <footprint.h>
46#include <pad.h>
47#include <pcb_target.h>
48#include <pcb_track.h>
49#include <pcb_text.h>
50#include <pcb_textbox.h>
51#include <pcb_tablecell.h>
52#include <pcb_table.h>
53#include <pcb_dimension.h>
56#include <board_commit.h>
57#include <progress_reporter.h>
61#include <geometry/vertex_set.h>
62#include <kidialog.h>
63#include <thread_pool.h>
64#include <math/util.h> // for KiROUND
65#include "zone_filler.h"
66#include "project.h"
68#include "pcb_barcode.h"
69
70// Helper classes for connect_nearby_polys
72{
73public:
74 RESULTS( int aOutline1, int aOutline2, int aVertex1, int aVertex2 ) :
75 m_outline1( aOutline1 ), m_outline2( aOutline2 ),
76 m_vertex1( aVertex1 ), m_vertex2( aVertex2 )
77 {
78 }
79
80 bool operator<( const RESULTS& aOther ) const
81 {
82 if( m_outline1 != aOther.m_outline1 )
83 return m_outline1 < aOther.m_outline1;
84 if( m_outline2 != aOther.m_outline2 )
85 return m_outline2 < aOther.m_outline2;
86 if( m_vertex1 != aOther.m_vertex1 )
87 return m_vertex1 < aOther.m_vertex1;
88 return m_vertex2 < aOther.m_vertex2;
89 }
90
95};
96
98{
99public:
100 VERTEX_CONNECTOR( const BOX2I& aBBox, const SHAPE_POLY_SET& aPolys, int aDist ) : VERTEX_SET( 0 )
101 {
102 SetBoundingBox( aBBox );
103 VERTEX* tail = nullptr;
104
105 for( int i = 0; i < aPolys.OutlineCount(); i++ )
106 tail = createList( aPolys.Outline( i ), tail, (void*)( intptr_t )( i ) );
107
108 if( tail )
109 tail->updateList();
110 m_dist = aDist;
111 }
112
113 VERTEX* getPoint( VERTEX* aPt ) const
114 {
115 // z-order range for the current point ± limit bounding box
116 const uint32_t maxZ = zOrder( aPt->x + m_dist, aPt->y + m_dist );
117 const uint32_t minZ = zOrder( aPt->x - m_dist, aPt->y - m_dist );
118 const SEG::ecoord limit2 = SEG::Square( m_dist );
119
120 // first look for points in increasing z-order
121 SEG::ecoord min_dist = std::numeric_limits<SEG::ecoord>::max();
122 VERTEX* retval = nullptr;
123
124 auto check_pt = [&]( VERTEX* p )
125 {
126 VECTOR2D diff( p->x - aPt->x, p->y - aPt->y );
127 SEG::ecoord dist2 = diff.SquaredEuclideanNorm();
128
129 if( dist2 > 0 && dist2 < limit2 && dist2 < min_dist && p->isEar( true ) )
130 {
131 min_dist = dist2;
132 retval = p;
133 }
134 };
135
136 VERTEX* p = aPt->nextZ;
137
138 while( p && p->z <= maxZ )
139 {
140 check_pt( p );
141 p = p->nextZ;
142 }
143
144 p = aPt->prevZ;
145
146 while( p && p->z >= minZ )
147 {
148 check_pt( p );
149 p = p->prevZ;
150 }
151
152 return retval;
153 }
154
156 {
157 if( m_vertices.empty() )
158 return;
159
160 VERTEX* p = m_vertices.front().next;
161 std::set<VERTEX*> visited;
162
163 while( p != &m_vertices.front() )
164 {
165 // Skip points that are concave
166 if( !p->isEar() )
167 {
168 p = p->next;
169 continue;
170 }
171
172 VERTEX* q = nullptr;
173
174 if( ( visited.empty() || !visited.contains( p ) ) && ( q = getPoint( p ) ) )
175 {
176 visited.insert( p );
177
178 if( !visited.contains( q ) &&
179 m_results.emplace( (intptr_t) p->GetUserData(), (intptr_t) q->GetUserData(),
180 p->i, q->i ).second )
181 {
182 // We don't want to connect multiple points in the same vicinity, so skip
183 // 2 points before and after each point and match.
184 visited.insert( p->prev );
185 visited.insert( p->prev->prev );
186 visited.insert( p->next );
187 visited.insert( p->next->next );
188
189 visited.insert( q->prev );
190 visited.insert( q->prev->prev );
191 visited.insert( q->next );
192 visited.insert( q->next->next );
193
194 visited.insert( q );
195 }
196 }
197
198 p = p->next;
199 }
200 }
201
202 std::set<RESULTS> GetResults() const
203 {
204 return m_results;
205 }
206
207private:
208 std::set<RESULTS> m_results;
210};
211
212
218namespace
219{
220
224struct PAD_KNOCKOUT_KEY
225{
226 VECTOR2I position;
227 VECTOR2I effectiveSize; // For circular: max of drill and pad; otherwise pad size
228 int shape; // PAD_SHAPE enum value
229 EDA_ANGLE orientation;
230
231 bool operator==( const PAD_KNOCKOUT_KEY& other ) const
232 {
233 return position == other.position && effectiveSize == other.effectiveSize
234 && shape == other.shape && orientation == other.orientation;
235 }
236};
237
238struct PAD_KNOCKOUT_KEY_HASH
239{
240 size_t operator()( const PAD_KNOCKOUT_KEY& key ) const
241 {
242 return hash_val( key.position.x, key.position.y, key.effectiveSize.x, key.effectiveSize.y,
243 key.shape, key.orientation.AsDegrees() );
244 }
245};
246
248struct VIA_KNOCKOUT_KEY
249{
250 VECTOR2I position;
251 int effectiveSize; // max of drill and via width
252
253 bool operator==( const VIA_KNOCKOUT_KEY& other ) const
254 {
255 return position == other.position && effectiveSize == other.effectiveSize;
256 }
257};
258
259struct VIA_KNOCKOUT_KEY_HASH
260{
261 size_t operator()( const VIA_KNOCKOUT_KEY& key ) const
262 {
263 return hash_val( key.position.x, key.position.y, key.effectiveSize );
264 }
265};
266
269struct TRACK_KNOCKOUT_KEY
270{
271 VECTOR2I start;
272 VECTOR2I end;
273 int width;
274
275 TRACK_KNOCKOUT_KEY( const VECTOR2I& aStart, const VECTOR2I& aEnd, int aWidth ) :
276 width( aWidth )
277 {
278 // Canonicalize endpoint order for consistent hashing
279 if( aStart.x < aEnd.x || ( aStart.x == aEnd.x && aStart.y <= aEnd.y ) )
280 {
281 start = aStart;
282 end = aEnd;
283 }
284 else
285 {
286 start = aEnd;
287 end = aStart;
288 }
289 }
290
291 bool operator==( const TRACK_KNOCKOUT_KEY& other ) const
292 {
293 return start == other.start && end == other.end && width == other.width;
294 }
295};
296
297struct TRACK_KNOCKOUT_KEY_HASH
298{
299 size_t operator()( const TRACK_KNOCKOUT_KEY& key ) const
300 {
301 return hash_val( key.start.x, key.start.y, key.end.x, key.end.y, key.width );
302 }
303};
304
305} // anonymous namespace
306
307
309 m_board( aBoard ),
310 m_brdOutlinesValid( false ),
311 m_commit( aCommit ),
312 m_progressReporter( nullptr ),
314{
315 m_maxError = aBoard->GetDesignSettings().m_MaxError;
316
317 // To enable add "DebugZoneFiller=1" to kicad_advanced settings file.
319}
320
321
325
326
328{
329 m_progressReporter = aReporter;
330}
331
332
343bool ZONE_FILLER::Fill( const std::vector<ZONE*>& aZones, bool aCheck, wxWindow* aParent )
344{
345 std::lock_guard<KISPINLOCK> lock( m_board->GetConnectivity()->GetLock() );
346
347 std::vector<std::pair<ZONE*, PCB_LAYER_ID>> toFill;
348 std::map<std::pair<ZONE*, PCB_LAYER_ID>, HASH_128> oldFillHashes;
349 std::map<ZONE*, std::map<PCB_LAYER_ID, ISOLATED_ISLANDS>> isolatedIslandsMap;
350
351 std::shared_ptr<CONNECTIVITY_DATA> connectivity = m_board->GetConnectivity();
352
353 // Ensure that multiple threads don't attempt to initialize the advanced cfg global at the same
354 // time.
356
357 // Rebuild (from scratch, ignoring dirty flags) just in case. This really needs to be reliable.
358 connectivity->ClearRatsnest();
359 connectivity->Build( m_board, m_progressReporter );
360
361 m_worstClearance = m_board->GetMaxClearanceValue();
362
364 {
365 m_progressReporter->Report( aCheck ? _( "Checking zone fills..." )
366 : _( "Building zone fills..." ) );
367 m_progressReporter->SetMaxProgress( aZones.size() );
368 m_progressReporter->KeepRefreshing();
369 }
370
371 // The board outlines is used to clip solid areas inside the board (when outlines are valid)
372 m_boardOutline.RemoveAllContours();
373 m_brdOutlinesValid = m_board->GetBoardPolygonOutlines( m_boardOutline, true );
374
375 // Update and cache zone bounding boxes and pad effective shapes so that we don't have to
376 // make them thread-safe.
377 //
378 for( ZONE* zone : m_board->Zones() )
379 zone->CacheBoundingBox();
380
381 for( FOOTPRINT* footprint : m_board->Footprints() )
382 {
383 for( PAD* pad : footprint->Pads() )
384 {
385 if( pad->IsDirty() )
386 {
387 pad->BuildEffectiveShapes();
388 pad->BuildEffectivePolygon( ERROR_OUTSIDE );
389 }
390 }
391
392 for( ZONE* zone : footprint->Zones() )
393 zone->CacheBoundingBox();
394
395 // Rules may depend on insideCourtyard() or other expressions
396 footprint->BuildCourtyardCaches();
397 footprint->BuildNetTieCache();
398 }
399
400 LSET boardCuMask = LSET::AllCuMask( m_board->GetCopperLayerCount() );
401
402 auto findHighestPriorityZone =
403 [&]( const BOX2I& bbox, PCB_LAYER_ID itemLayer, int netcode,
404 const std::function<bool( const ZONE* )>& testFn ) -> ZONE*
405 {
406 unsigned highestPriority = 0;
407 ZONE* highestPriorityZone = nullptr;
408
409 for( ZONE* zone : m_board->Zones() )
410 {
411 // Rule areas are not filled
412 if( zone->GetIsRuleArea() )
413 continue;
414
415 if( zone->GetAssignedPriority() < highestPriority )
416 continue;
417
418 if( !zone->IsOnLayer( itemLayer ) )
419 continue;
420
421 // Degenerate zones will cause trouble; skip them
422 if( zone->GetNumCorners() <= 2 )
423 continue;
424
425 if( !zone->GetBoundingBox().Intersects( bbox ) )
426 continue;
427
428 if( !testFn( zone ) )
429 continue;
430
431 // Prefer highest priority and matching netcode
432 if( zone->GetAssignedPriority() > highestPriority
433 || zone->GetNetCode() == netcode )
434 {
435 highestPriority = zone->GetAssignedPriority();
436 highestPriorityZone = zone;
437 }
438 }
439
440 return highestPriorityZone;
441 };
442
443 auto isInPourKeepoutArea =
444 [&]( const BOX2I& bbox, PCB_LAYER_ID itemLayer, const VECTOR2I& testPoint ) -> bool
445 {
446 for( ZONE* zone : m_board->Zones() )
447 {
448 if( !zone->GetIsRuleArea() )
449 continue;
450
451 if( !zone->HasKeepoutParametersSet() )
452 continue;
453
454 if( !zone->GetDoNotAllowZoneFills() )
455 continue;
456
457 if( !zone->IsOnLayer( itemLayer ) )
458 continue;
459
460 // Degenerate zones will cause trouble; skip them
461 if( zone->GetNumCorners() <= 2 )
462 continue;
463
464 if( !zone->GetBoundingBox().Intersects( bbox ) )
465 continue;
466
467 if( zone->Outline()->Contains( testPoint ) )
468 return true;
469 }
470
471 return false;
472 };
473
474 // Determine state of conditional via flashing
475 // This is now done completely deterministically prior to filling due to the pathological
476 // case presented in https://gitlab.com/kicad/code/kicad/-/issues/12964.
477 for( PCB_TRACK* track : m_board->Tracks() )
478 {
479 if( track->Type() == PCB_VIA_T )
480 {
481 PCB_VIA* via = static_cast<PCB_VIA*>( track );
482 PADSTACK& padstack = via->Padstack();
483
484 via->ClearZoneLayerOverrides();
485
486 if( !via->GetRemoveUnconnected() )
487 continue;
488
489 BOX2I bbox = via->GetBoundingBox();
490 VECTOR2I center = via->GetPosition();
491 int holeRadius = via->GetDrillValue() / 2 + 1;
492 int netcode = via->GetNetCode();
493 LSET layers = via->GetLayerSet() & boardCuMask;
494
495 // Checking if the via hole touches the zone outline
496 auto viaTestFn =
497 [&]( const ZONE* aZone ) -> bool
498 {
499 return aZone->Outline()->Contains( center, -1, holeRadius );
500 };
501
502 for( PCB_LAYER_ID layer : layers )
503 {
504 if( !via->ConditionallyFlashed( layer ) )
505 continue;
506
507 if( isInPourKeepoutArea( bbox, layer, center ) )
508 {
509 via->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
510 }
511 else
512 {
513 ZONE* zone = findHighestPriorityZone( bbox, layer, netcode, viaTestFn );
514
515 if( zone && zone->GetNetCode() == via->GetNetCode()
517 || layer == padstack.Drill().start
518 || layer == padstack.Drill().end ) )
519 {
520 via->SetZoneLayerOverride( layer, ZLO_FORCE_FLASHED );
521 }
522 else
523 {
524 via->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
525 }
526 }
527 }
528 }
529 }
530
531 // Determine state of conditional pad flashing
532 for( FOOTPRINT* footprint : m_board->Footprints() )
533 {
534 for( PAD* pad : footprint->Pads() )
535 {
536 pad->ClearZoneLayerOverrides();
537
538 if( !pad->GetRemoveUnconnected() )
539 continue;
540
541 BOX2I bbox = pad->GetBoundingBox();
542 VECTOR2I center = pad->GetPosition();
543 int netcode = pad->GetNetCode();
544 LSET layers = pad->GetLayerSet() & boardCuMask;
545
546 auto padTestFn =
547 [&]( const ZONE* aZone ) -> bool
548 {
549 return aZone->Outline()->Contains( center );
550 };
551
552 for( PCB_LAYER_ID layer : layers )
553 {
554 if( !pad->ConditionallyFlashed( layer ) )
555 continue;
556
557 if( isInPourKeepoutArea( bbox, layer, center ) )
558 {
559 pad->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
560 }
561 else
562 {
563 ZONE* zone = findHighestPriorityZone( bbox, layer, netcode, padTestFn );
564
565 if( zone && zone->GetNetCode() == pad->GetNetCode() )
566 pad->SetZoneLayerOverride( layer, ZLO_FORCE_FLASHED );
567 else
568 pad->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
569 }
570 }
571 }
572 }
573
574 for( ZONE* zone : aZones )
575 {
576 // Rule areas are not filled
577 if( zone->GetIsRuleArea() )
578 continue;
579
580 // Degenerate zones will cause trouble; skip them
581 if( zone->GetNumCorners() <= 2 )
582 continue;
583
584 if( m_commit )
585 m_commit->Modify( zone );
586
587 // calculate the hash value for filled areas. it will be used later to know if the
588 // current filled areas are up to date
589 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
590 {
591 zone->BuildHashValue( layer );
592 oldFillHashes[ { zone, layer } ] = zone->GetHashValue( layer );
593
594 // Add the zone to the list of zones to test or refill
595 toFill.emplace_back( std::make_pair( zone, layer ) );
596
597 isolatedIslandsMap[zone][layer] = ISOLATED_ISLANDS();
598 }
599
600 // Remove existing fill first to prevent drawing invalid polygons on some platforms
601 zone->UnFill();
602 }
603
604 auto check_fill_dependency =
605 [&]( ZONE* aZone, PCB_LAYER_ID aLayer, ZONE* aOtherZone ) -> bool
606 {
607 // Check to see if we have to knock-out the filled areas of a higher-priority
608 // zone. If so we have to wait until said zone is filled before we can fill.
609
610 // If the other zone is already filled on the requested layer then we're
611 // good-to-go
612 if( aOtherZone->GetFillFlag( aLayer ) )
613 return false;
614
615 // Even if keepouts exclude copper pours, the exclusion is by outline rather than
616 // filled area, so we're good-to-go here too
617 if( aOtherZone->GetIsRuleArea() )
618 return false;
619
620 // If the other zone is never going to be filled then don't wait for it
621 if( aOtherZone->GetNumCorners() <= 2 )
622 return false;
623
624 // If the zones share no common layers
625 if( !aOtherZone->GetLayerSet().test( aLayer ) )
626 return false;
627
628 if( aZone->HigherPriority( aOtherZone ) )
629 return false;
630
631 // Same-net zones always use outlines to produce determinate results
632 if( aOtherZone->SameNet( aZone ) )
633 return false;
634
635 // A higher priority zone is found: if we intersect and it's not filled yet
636 // then we have to wait.
637 BOX2I inflatedBBox = aZone->GetBoundingBox();
638 inflatedBBox.Inflate( m_worstClearance );
639
640 if( !inflatedBBox.Intersects( aOtherZone->GetBoundingBox() ) )
641 return false;
642
643 return aZone->Outline()->Collide( aOtherZone->Outline(), m_worstClearance );
644 };
645
646 auto fill_lambda =
647 [&]( std::pair<ZONE*, PCB_LAYER_ID> aFillItem ) -> int
648 {
649 PCB_LAYER_ID layer = aFillItem.second;
650 ZONE* zone = aFillItem.first;
651 bool canFill = true;
652
653 // Check for any fill dependencies. If our zone needs to be clipped by
654 // another zone then we can't fill until that zone is filled.
655 for( ZONE* otherZone : aZones )
656 {
657 if( otherZone == zone )
658 continue;
659
660 if( check_fill_dependency( zone, layer, otherZone ) )
661 {
662 canFill = false;
663 break;
664 }
665 }
666
667 if( m_progressReporter && m_progressReporter->IsCancelled() )
668 return 0;
669
670 if( !canFill )
671 return 0;
672
673 // Now we're ready to fill.
674 {
675 std::unique_lock<std::mutex> zoneLock( zone->GetLock(), std::try_to_lock );
676
677 if( !zoneLock.owns_lock() )
678 return 0;
679
680 SHAPE_POLY_SET fillPolys;
681
682 if( !fillSingleZone( zone, layer, fillPolys ) )
683 return 0;
684
685 zone->SetFilledPolysList( layer, fillPolys );
686 }
687
689 m_progressReporter->AdvanceProgress();
690
691 return 1;
692 };
693
694 auto tesselate_lambda =
695 [&]( std::pair<ZONE*, PCB_LAYER_ID> aFillItem ) -> int
696 {
697 if( m_progressReporter && m_progressReporter->IsCancelled() )
698 return 0;
699
700 PCB_LAYER_ID layer = aFillItem.second;
701 ZONE* zone = aFillItem.first;
702
703 {
704 std::unique_lock<std::mutex> zoneLock( zone->GetLock(), std::try_to_lock );
705
706 if( !zoneLock.owns_lock() )
707 return 0;
708
709 zone->CacheTriangulation( layer );
710 zone->SetFillFlag( layer, true );
711 }
712
713 return 1;
714 };
715
716 // Calculate the copper fills (NB: this is multi-threaded)
717 //
718 std::vector<std::pair<std::future<int>, int>> returns;
719 returns.reserve( toFill.size() );
720 size_t finished = 0;
721 std::atomic<bool> cancelled( false );
722
724
725 for( const std::pair<ZONE*, PCB_LAYER_ID>& fillItem : toFill )
726 {
727 returns.emplace_back( std::make_pair( tp.submit_task(
728 [&, fillItem]()
729 {
730 return fill_lambda( fillItem );
731 } ), 0 ) );
732 }
733
734 while( !cancelled && finished != 2 * toFill.size() )
735 {
736 for( size_t ii = 0; ii < returns.size(); ++ii )
737 {
738 auto& ret = returns[ii];
739
740 if( ret.second > 1 )
741 continue;
742
743 std::future_status status = ret.first.wait_for( std::chrono::seconds( 0 ) );
744
745 if( status == std::future_status::ready )
746 {
747 if( ret.first.get() ) // lambda completed
748 {
749 ++finished;
750 ret.second++; // go to next step
751 }
752
753 if( !cancelled )
754 {
755 // Queue the next step (will re-queue the existing step if it didn't complete)
756 if( ret.second == 0 )
757 {
758 returns[ii].first = tp.submit_task(
759 [&, idx = ii]()
760 {
761 return fill_lambda( toFill[idx] );
762 } );
763 }
764 else if( ret.second == 1 )
765 {
766 returns[ii].first = tp.submit_task(
767 [&, idx = ii]()
768 {
769 return tesselate_lambda( toFill[idx] );
770 } );
771 }
772 }
773 }
774 }
775
776 std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
777
778
780 {
781 m_progressReporter->KeepRefreshing();
782
783 if( m_progressReporter->IsCancelled() )
784 cancelled = true;
785 }
786 }
787
788 // Make sure that all futures have finished.
789 // This can happen when the user cancels the above operation
790 for( auto& ret : returns )
791 {
792 if( ret.first.valid() )
793 {
794 std::future_status status = ret.first.wait_for( std::chrono::seconds( 0 ) );
795
796 while( status != std::future_status::ready )
797 {
799 m_progressReporter->KeepRefreshing();
800
801 status = ret.first.wait_for( std::chrono::milliseconds( 100 ) );
802 }
803 }
804 }
805
806 // Now update the connectivity to check for isolated copper islands
807 // (NB: FindIsolatedCopperIslands() is multi-threaded)
808 //
810 {
811 if( m_progressReporter->IsCancelled() )
812 return false;
813
814 m_progressReporter->AdvancePhase();
815 m_progressReporter->Report( _( "Removing isolated copper islands..." ) );
816 m_progressReporter->KeepRefreshing();
817 }
818
819 connectivity->SetProgressReporter( m_progressReporter );
820 connectivity->FillIsolatedIslandsMap( isolatedIslandsMap );
821 connectivity->SetProgressReporter( nullptr );
822
823 if( m_progressReporter && m_progressReporter->IsCancelled() )
824 return false;
825
826 for( ZONE* zone : aZones )
827 {
828 // Keepout zones are not filled
829 if( zone->GetIsRuleArea() )
830 continue;
831
832 zone->SetIsFilled( true );
833 }
834
835 // Now remove isolated copper islands according to the isolated islands strategy assigned
836 // by the user (always, never, below-certain-size).
837 //
838 // Track zones that had islands removed for potential iterative refill
839 std::set<ZONE*> zonesWithRemovedIslands;
840
841 for( const auto& [ zone, zoneIslands ] : isolatedIslandsMap )
842 {
843 // If *all* the polygons are islands, do not remove any of them
844 bool allIslands = true;
845
846 for( const auto& [ layer, layerIslands ] : zoneIslands )
847 {
848 if( layerIslands.m_IsolatedOutlines.size()
849 != static_cast<size_t>( zone->GetFilledPolysList( layer )->OutlineCount() ) )
850 {
851 allIslands = false;
852 break;
853 }
854 }
855
856 if( allIslands )
857 continue;
858
859 for( const auto& [ layer, layerIslands ] : zoneIslands )
860 {
861 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( layer ) )
862 continue;
863
864 if( layerIslands.m_IsolatedOutlines.empty() )
865 continue;
866
867 std::vector<int> islands = layerIslands.m_IsolatedOutlines;
868
869 // The list of polygons to delete must be explored from last to first in list,
870 // to allow deleting a polygon from list without breaking the remaining of the list
871 std::sort( islands.begin(), islands.end(), std::greater<int>() );
872
873 std::shared_ptr<SHAPE_POLY_SET> poly = zone->GetFilledPolysList( layer );
874 long long int minArea = zone->GetMinIslandArea();
875 ISLAND_REMOVAL_MODE mode = zone->GetIslandRemovalMode();
876
877 wxLogTrace( traceZoneFiller, wxT( "Zone %s layer %d: %zu islands to process, mode=%d, poly has %d outlines, area %.0f" ),
878 zone->GetNetname(), static_cast<int>( layer ), islands.size(),
879 static_cast<int>( mode ), poly->OutlineCount(), poly->Area() );
880
881 for( int idx : islands )
882 {
883 SHAPE_LINE_CHAIN& outline = poly->Outline( idx );
884
885 if( mode == ISLAND_REMOVAL_MODE::ALWAYS )
886 {
887 wxLogTrace( traceZoneFiller, wxT( "Removing island %d from zone %s (ALWAYS mode)" ),
888 idx, zone->GetNetname() );
889 poly->DeletePolygonAndTriangulationData( idx, false );
890 zonesWithRemovedIslands.insert( zone );
891 }
892 else if ( mode == ISLAND_REMOVAL_MODE::AREA && outline.Area( true ) < minArea )
893 {
894 wxLogTrace( traceZoneFiller, wxT( "Removing island %d from zone %s (AREA mode, area=%.0f < min=%.0f)" ),
895 idx, zone->GetNetname(), outline.Area( true ),
896 static_cast<double>( minArea ) );
897 poly->DeletePolygonAndTriangulationData( idx, false );
898 zonesWithRemovedIslands.insert( zone );
899 }
900 else
901 {
902 zone->SetIsIsland( layer, idx );
903 }
904 }
905
906 poly->UpdateTriangulationDataHash();
907 zone->CalculateFilledArea();
908
909 BOX2I bbox = poly->BBox();
910 wxLogTrace( traceZoneFiller, wxT( "After island removal, zone %s: %d outlines, area %.0f, bbox (%d,%d)-(%d,%d)" ),
911 zone->GetNetname(), poly->OutlineCount(), poly->Area(),
912 bbox.GetX(), bbox.GetY(), bbox.GetRight(), bbox.GetBottom() );
913
914 if( m_progressReporter && m_progressReporter->IsCancelled() )
915 return false;
916 }
917 }
918
919 // Iterative refill: If islands were removed from higher-priority zones, lower-priority zones
920 // may need to be refilled to occupy the now-available space (issue 21746).
921 //
922 const bool iterativeRefill = ADVANCED_CFG::GetCfg().m_ZoneFillIterativeRefill;
923
924 if( iterativeRefill && !zonesWithRemovedIslands.empty() )
925 {
926 PROF_TIMER timer;
927
928 wxLogTrace( traceZoneFiller, wxT( "Iterative refill: %zu zones had islands removed, cache size: %zu" ),
929 zonesWithRemovedIslands.size(), m_preKnockoutFillCache.size() );
930
931 // Find lower-priority zones that may need refilling.
932 // A zone needs refilling if it overlaps with a zone that had islands removed
933 // and has lower priority than that zone.
934 std::vector<std::pair<ZONE*, PCB_LAYER_ID>> zonesToRefill;
935
936 for( ZONE* zoneWithIsland : zonesWithRemovedIslands )
937 {
938 BOX2I islandZoneBBox = zoneWithIsland->GetBoundingBox();
939 islandZoneBBox.Inflate( m_worstClearance );
940
941 for( ZONE* zone : aZones )
942 {
943 // Skip the zone that had islands removed
944 if( zone == zoneWithIsland )
945 continue;
946
947 // Skip keepout zones
948 if( zone->GetIsRuleArea() )
949 continue;
950
951 // Only refill zones with lower priority than the zone that had islands removed
952 if( !zoneWithIsland->HigherPriority( zone ) )
953 continue;
954
955 // Check for layer overlap
956 LSET commonLayers = zone->GetLayerSet() & zoneWithIsland->GetLayerSet();
957
958 if( commonLayers.none() )
959 continue;
960
961 // Check for bounding box overlap
962 if( !zone->GetBoundingBox().Intersects( islandZoneBBox ) )
963 continue;
964
965 // Add zone/layer pairs for refilling
966 for( PCB_LAYER_ID layer : commonLayers )
967 {
968 auto fillItem = std::make_pair( zone, layer );
969
970 if( std::find( zonesToRefill.begin(), zonesToRefill.end(), fillItem ) == zonesToRefill.end() )
971 {
972 zonesToRefill.push_back( fillItem );
973 }
974 }
975 }
976 }
977
978 if( !zonesToRefill.empty() )
979 {
980 wxLogTrace( traceZoneFiller, wxT( "Iterative refill: refilling %zu zone/layer pairs" ),
981 zonesToRefill.size() );
982
984 {
985 m_progressReporter->AdvancePhase();
986 m_progressReporter->Report( _( "Refilling zones after island removal..." ) );
987 m_progressReporter->KeepRefreshing();
988 }
989
990 // Refill using cached pre-knockout fills - much faster than full refill
991 // since we only need to re-apply the higher-priority zone knockout
992 auto cached_refill_lambda =
993 [&]( const std::pair<ZONE*, PCB_LAYER_ID>& aFillItem ) -> int
994 {
995 ZONE* zone = aFillItem.first;
996 PCB_LAYER_ID layer = aFillItem.second;
997 SHAPE_POLY_SET fillPolys;
998
999 if( !refillZoneFromCache( zone, layer, fillPolys ) )
1000 return 0;
1001
1002 wxLogTrace( traceZoneFiller, wxT( "Cached refill for zone %s: %d outlines, area %.0f" ),
1003 zone->GetNetname(), fillPolys.OutlineCount(), fillPolys.Area() );
1004
1005 zone->SetFilledPolysList( layer, fillPolys );
1006 zone->SetFillFlag( layer, true );
1007 return 1;
1008 };
1009
1010 std::vector<std::pair<std::future<int>, int>> refillReturns;
1011 refillReturns.reserve( zonesToRefill.size() );
1012 size_t refillFinished = 0;
1013
1014 for( const auto& fillItem : zonesToRefill )
1015 {
1016 refillReturns.emplace_back( std::make_pair( tp.submit_task(
1017 [&, fillItem]()
1018 {
1019 return cached_refill_lambda( fillItem );
1020 } ), 0 ) );
1021 }
1022
1023 while( !cancelled && refillFinished != 2 * zonesToRefill.size() )
1024 {
1025 for( size_t ii = 0; ii < refillReturns.size(); ++ii )
1026 {
1027 auto& ret = refillReturns[ii];
1028
1029 if( ret.second > 1 )
1030 continue;
1031
1032 std::future_status status = ret.first.wait_for( std::chrono::seconds( 0 ) );
1033
1034 if( status == std::future_status::ready )
1035 {
1036 if( ret.first.get() )
1037 {
1038 ++refillFinished;
1039 ret.second++;
1040 }
1041 else if( ret.second == 0 )
1042 {
1043 // Cache miss is a permanent failure. Skip both phases to avoid
1044 // an infinite retry loop.
1045 refillFinished += 2;
1046 ret.second = 2;
1047 continue;
1048 }
1049
1050 if( !cancelled )
1051 {
1052 if( ret.second == 1 )
1053 {
1054 refillReturns[ii].first = tp.submit_task(
1055 [&, idx = ii]()
1056 {
1057 return tesselate_lambda( zonesToRefill[idx] );
1058 } );
1059 }
1060 }
1061 }
1062 }
1063
1064 std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
1065
1066 if( m_progressReporter )
1067 {
1068 m_progressReporter->KeepRefreshing();
1069
1070 if( m_progressReporter->IsCancelled() )
1071 cancelled = true;
1072 }
1073 }
1074
1075 // Wait for all refill tasks to complete
1076 for( auto& ret : refillReturns )
1077 {
1078 if( ret.first.valid() )
1079 {
1080 std::future_status status = ret.first.wait_for( std::chrono::seconds( 0 ) );
1081
1082 while( status != std::future_status::ready )
1083 {
1084 if( m_progressReporter )
1085 m_progressReporter->KeepRefreshing();
1086
1087 status = ret.first.wait_for( std::chrono::milliseconds( 100 ) );
1088 }
1089 }
1090 }
1091
1092 // Re-run island detection for refilled zones
1093 std::map<ZONE*, std::map<PCB_LAYER_ID, ISOLATED_ISLANDS>> refillIslandsMap;
1094 std::set<ZONE*> refillZones;
1095
1096 for( const auto& [zone, layer] : zonesToRefill )
1097 refillZones.insert( zone );
1098
1099 for( ZONE* zone : refillZones )
1100 {
1101 refillIslandsMap[zone] = std::map<PCB_LAYER_ID, ISOLATED_ISLANDS>();
1102
1103 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
1104 refillIslandsMap[zone][layer] = ISOLATED_ISLANDS();
1105 }
1106
1107 connectivity->FillIsolatedIslandsMap( refillIslandsMap );
1108
1109 // Remove islands from refilled zones
1110 for( const auto& [ zone, zoneIslands ] : refillIslandsMap )
1111 {
1112 bool allIslands = true;
1113
1114 for( const auto& [ layer, layerIslands ] : zoneIslands )
1115 {
1116 if( layerIslands.m_IsolatedOutlines.size()
1117 != static_cast<size_t>( zone->GetFilledPolysList( layer )->OutlineCount() ) )
1118 {
1119 allIslands = false;
1120 break;
1121 }
1122 }
1123
1124 if( allIslands )
1125 continue;
1126
1127 for( const auto& [ layer, layerIslands ] : zoneIslands )
1128 {
1129 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( layer ) )
1130 continue;
1131
1132 if( layerIslands.m_IsolatedOutlines.empty() )
1133 continue;
1134
1135 std::vector<int> islands = layerIslands.m_IsolatedOutlines;
1136 std::sort( islands.begin(), islands.end(), std::greater<int>() );
1137
1138 std::shared_ptr<SHAPE_POLY_SET> poly = zone->GetFilledPolysList( layer );
1139 long long int minArea = zone->GetMinIslandArea();
1140 ISLAND_REMOVAL_MODE mode = zone->GetIslandRemovalMode();
1141
1142 for( int idx : islands )
1143 {
1144 SHAPE_LINE_CHAIN& outline = poly->Outline( idx );
1145
1146 if( mode == ISLAND_REMOVAL_MODE::ALWAYS )
1147 poly->DeletePolygonAndTriangulationData( idx, false );
1148 else if( mode == ISLAND_REMOVAL_MODE::AREA && outline.Area( true ) < minArea )
1149 poly->DeletePolygonAndTriangulationData( idx, false );
1150 else
1151 zone->SetIsIsland( layer, idx );
1152 }
1153
1154 poly->UpdateTriangulationDataHash();
1155 zone->CalculateFilledArea();
1156 }
1157 }
1158
1159 wxLogTrace( traceZoneFiller, wxT( "Iterative refill completed in %0.3f ms" ), timer.msecs() );
1160 }
1161 }
1162
1163 // Now remove islands which are either outside the board edge or fail to meet the minimum
1164 // area requirements
1165 using island_check_return = std::vector<std::pair<std::shared_ptr<SHAPE_POLY_SET>, int>>;
1166
1167 std::vector<std::pair<std::shared_ptr<SHAPE_POLY_SET>, double>> polys_to_check;
1168
1169 // rough estimate to save re-allocation time
1170 polys_to_check.reserve( m_board->GetCopperLayerCount() * aZones.size() );
1171
1172 for( ZONE* zone : aZones )
1173 {
1174 // Don't check for connections on layers that only exist in the zone but
1175 // were disabled in the board
1176 BOARD* board = zone->GetBoard();
1177 LSET zoneCopperLayers = zone->GetLayerSet() & LSET::AllCuMask( board->GetCopperLayerCount() );
1178
1179 // Min-thickness is the web thickness. On the other hand, a blob min-thickness by
1180 // min-thickness is not useful. Since there's no obvious definition of web vs. blob, we
1181 // arbitrarily choose "at least 3X the area".
1182 double minArea = (double) zone->GetMinThickness() * zone->GetMinThickness() * 3;
1183
1184 for( PCB_LAYER_ID layer : zoneCopperLayers )
1185 {
1186 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( layer ) )
1187 continue;
1188
1189 polys_to_check.emplace_back( zone->GetFilledPolysList( layer ), minArea );
1190 }
1191 }
1192
1193 auto island_lambda =
1194 [&]( int aStart, int aEnd ) -> island_check_return
1195 {
1196 island_check_return retval;
1197
1198 for( int ii = aStart; ii < aEnd && !cancelled; ++ii )
1199 {
1200 auto [poly, minArea] = polys_to_check[ii];
1201
1202 for( int jj = poly->OutlineCount() - 1; jj >= 0; jj-- )
1203 {
1204 SHAPE_POLY_SET island;
1205 SHAPE_POLY_SET intersection;
1206 const SHAPE_LINE_CHAIN& test_poly = poly->Polygon( jj ).front();
1207 double island_area = test_poly.Area();
1208
1209 if( island_area < minArea )
1210 continue;
1211
1212
1213 island.AddOutline( test_poly );
1214 intersection.BooleanIntersection( m_boardOutline, island );
1215
1216 // Nominally, all of these areas should be either inside or outside the
1217 // board outline. So this test should be able to just compare areas (if
1218 // they are equal, you are inside). But in practice, we sometimes have
1219 // slight overlap at the edges, so testing against half-size area acts as
1220 // a fail-safe.
1221 if( intersection.Area() < island_area / 2.0 )
1222 retval.emplace_back( poly, jj );
1223 }
1224 }
1225
1226 return retval;
1227 };
1228
1229 auto island_returns = tp.submit_blocks( 0, polys_to_check.size(), island_lambda );
1230 cancelled = false;
1231
1232 // Allow island removal threads to finish
1233 for( size_t ii = 0; ii < island_returns.size(); ++ii )
1234 {
1235 std::future<island_check_return>& ret = island_returns[ii];
1236
1237 if( ret.valid() )
1238 {
1239 std::future_status status = ret.wait_for( std::chrono::seconds( 0 ) );
1240
1241 while( status != std::future_status::ready )
1242 {
1243 if( m_progressReporter )
1244 {
1245 m_progressReporter->KeepRefreshing();
1246
1247 if( m_progressReporter->IsCancelled() )
1248 cancelled = true;
1249 }
1250
1251 status = ret.wait_for( std::chrono::milliseconds( 100 ) );
1252 }
1253 }
1254 }
1255
1256 if( cancelled )
1257 return false;
1258
1259 for( size_t ii = 0; ii < island_returns.size(); ++ii )
1260 {
1261 std::future<island_check_return>& ret = island_returns[ii];
1262
1263 if( ret.valid() )
1264 {
1265 for( auto& action_item : ret.get() )
1266 action_item.first->DeletePolygonAndTriangulationData( action_item.second, true );
1267 }
1268 }
1269
1270 for( ZONE* zone : aZones )
1271 zone->CalculateFilledArea();
1272
1273 // Second pass: Re-evaluate via flashing based on actual filled polygons.
1274 // The first pass (before filling) marks vias as ZLO_FORCE_FLASHED if they're within the
1275 // zone outline. However, if the fill doesn't actually reach the via (due to obstacles like
1276 // tracks), we should not flash the via. See https://gitlab.com/kicad/code/kicad/-/issues/22010
1277 for( PCB_TRACK* track : m_board->Tracks() )
1278 {
1279 if( track->Type() != PCB_VIA_T )
1280 continue;
1281
1282 PCB_VIA* via = static_cast<PCB_VIA*>( track );
1283 VECTOR2I center = via->GetPosition();
1284 int holeRadius = via->GetDrillValue() / 2;
1285 int netcode = via->GetNetCode();
1286 LSET layers = via->GetLayerSet() & boardCuMask;
1287
1288 for( PCB_LAYER_ID layer : layers )
1289 {
1290 if( via->GetZoneLayerOverride( layer ) != ZLO_FORCE_FLASHED )
1291 continue;
1292
1293 bool zoneReachesVia = false;
1294
1295 for( ZONE* zone : m_board->Zones() )
1296 {
1297 if( zone->GetIsRuleArea() )
1298 continue;
1299
1300 if( zone->GetNetCode() != netcode )
1301 continue;
1302
1303 if( !zone->IsOnLayer( layer ) )
1304 continue;
1305
1306 if( !zone->HasFilledPolysForLayer( layer ) )
1307 continue;
1308
1309 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
1310
1311 if( fill->IsEmpty() )
1312 continue;
1313
1314 // Check if the filled zone reaches the via hole. Use holeRadius as reach distance
1315 // to match the pre-fill check logic.
1316 if( fill->Contains( center, -1, holeRadius ) )
1317 {
1318 zoneReachesVia = true;
1319 break;
1320 }
1321 }
1322
1323 if( !zoneReachesVia )
1324 via->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
1325 }
1326 }
1327
1328 // Same logic for pads
1329 for( FOOTPRINT* footprint : m_board->Footprints() )
1330 {
1331 for( PAD* pad : footprint->Pads() )
1332 {
1333 VECTOR2I center = pad->GetPosition();
1334 int netcode = pad->GetNetCode();
1335 LSET layers = pad->GetLayerSet() & boardCuMask;
1336
1337 // For TH pads, use the hole radius as tolerance since the filled zone creates a
1338 // thermal relief around the pad hole, similar to vias.
1339 int holeRadius = 0;
1340
1341 if( pad->HasHole() )
1342 holeRadius = std::min( pad->GetDrillSizeX(), pad->GetDrillSizeY() ) / 2;
1343
1344 for( PCB_LAYER_ID layer : layers )
1345 {
1346 if( pad->GetZoneLayerOverride( layer ) != ZLO_FORCE_FLASHED )
1347 continue;
1348
1349 bool zoneReachesPad = false;
1350
1351 for( ZONE* zone : m_board->Zones() )
1352 {
1353 if( zone->GetIsRuleArea() )
1354 continue;
1355
1356 if( zone->GetNetCode() != netcode )
1357 continue;
1358
1359 if( !zone->IsOnLayer( layer ) )
1360 continue;
1361
1362 if( !zone->HasFilledPolysForLayer( layer ) )
1363 continue;
1364
1365 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
1366
1367 if( fill->IsEmpty() )
1368 continue;
1369
1370 if( fill->Contains( center, -1, holeRadius ) )
1371 {
1372 zoneReachesPad = true;
1373 break;
1374 }
1375 }
1376
1377 if( !zoneReachesPad )
1378 pad->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
1379 }
1380 }
1381 }
1382
1383 if( aCheck )
1384 {
1385 bool outOfDate = false;
1386
1387 for( ZONE* zone : aZones )
1388 {
1389 // Keepout zones are not filled
1390 if( zone->GetIsRuleArea() )
1391 continue;
1392
1393 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
1394 {
1395 zone->BuildHashValue( layer );
1396
1397 if( oldFillHashes[ { zone, layer } ] != zone->GetHashValue( layer ) )
1398 outOfDate = true;
1399 }
1400 }
1401
1402 if( ( m_board->GetProject()
1403 && m_board->GetProject()->GetLocalSettings().m_PrototypeZoneFill ) )
1404 {
1405 KIDIALOG dlg( aParent, _( "Prototype zone fill enabled. Disable setting and refill?" ), _( "Confirmation" ),
1406 wxOK | wxCANCEL | wxICON_WARNING );
1407 dlg.SetOKCancelLabels( _( "Disable and refill" ), _( "Continue without Refill" ) );
1408 dlg.DoNotShowCheckbox( __FILE__, __LINE__ );
1409
1410 if( dlg.ShowModal() == wxID_OK )
1411 {
1412 m_board->GetProject()->GetLocalSettings().m_PrototypeZoneFill = false;
1413 }
1414 else if( !outOfDate )
1415 {
1416 return false;
1417 }
1418 }
1419
1420 if( outOfDate )
1421 {
1422 KIDIALOG dlg( aParent, _( "Zone fills are out-of-date. Refill?" ), _( "Confirmation" ),
1423 wxOK | wxCANCEL | wxICON_WARNING );
1424 dlg.SetOKCancelLabels( _( "Refill" ), _( "Continue without Refill" ) );
1425 dlg.DoNotShowCheckbox( __FILE__, __LINE__ );
1426
1427 if( dlg.ShowModal() == wxID_CANCEL )
1428 return false;
1429 }
1430 else
1431 {
1432 // No need to commit something that hasn't changed (and committing will set
1433 // the modified flag).
1434 return false;
1435 }
1436 }
1437
1438 if( m_progressReporter )
1439 {
1440 if( m_progressReporter->IsCancelled() )
1441 return false;
1442
1443 m_progressReporter->AdvancePhase();
1444 m_progressReporter->KeepRefreshing();
1445 }
1446
1447 return true;
1448}
1449
1450
1455void ZONE_FILLER::addKnockout( BOARD_ITEM* aItem, PCB_LAYER_ID aLayer, int aGap, SHAPE_POLY_SET& aHoles )
1456{
1457 if( aItem->Type() == PCB_PAD_T && static_cast<PAD*>( aItem )->GetShape( aLayer ) == PAD_SHAPE::CUSTOM )
1458 {
1459 PAD* pad = static_cast<PAD*>( aItem );
1460 SHAPE_POLY_SET poly;
1461 pad->TransformShapeToPolygon( poly, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1462
1463 // the pad shape in zone can be its convex hull or the shape itself
1464 if( pad->GetCustomShapeInZoneOpt() == CUSTOM_SHAPE_ZONE_MODE::CONVEXHULL )
1465 {
1466 std::vector<VECTOR2I> convex_hull;
1467 BuildConvexHull( convex_hull, poly );
1468
1469 aHoles.NewOutline();
1470
1471 for( const VECTOR2I& pt : convex_hull )
1472 aHoles.Append( pt );
1473 }
1474 else
1475 {
1476 aHoles.Append( poly );
1477 }
1478 }
1479 else
1480 {
1481 aItem->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1482 }
1483}
1484
1485
1489void ZONE_FILLER::addHoleKnockout( PAD* aPad, int aGap, SHAPE_POLY_SET& aHoles )
1490{
1491 aPad->TransformHoleToPolygon( aHoles, aGap, m_maxError, ERROR_OUTSIDE );
1492}
1493
1494
1496{
1497 int minorAxis = 0;
1498
1499 if( aItem->Type() == PCB_PAD_T )
1500 {
1501 PAD* pad = static_cast<PAD*>( aItem );
1502 VECTOR2I padSize = pad->GetSize( aLayer );
1503
1504 minorAxis = std::min( padSize.x, padSize.y );
1505 }
1506 else if( aItem->Type() == PCB_VIA_T )
1507 {
1508 PCB_VIA* via = static_cast<PCB_VIA*>( aItem );
1509
1510 minorAxis = via->GetWidth( aLayer );
1511 }
1512
1513 return ( aZone->GetHatchGap() - aZone->GetHatchThickness() - minorAxis ) / 2;
1514}
1515
1516
1521void ZONE_FILLER::addKnockout( BOARD_ITEM* aItem, PCB_LAYER_ID aLayer, int aGap,
1522 bool aIgnoreLineWidth, SHAPE_POLY_SET& aHoles )
1523{
1524 switch( aItem->Type() )
1525 {
1526 case PCB_FIELD_T:
1527 case PCB_TEXT_T:
1528 {
1529 PCB_TEXT* text = static_cast<PCB_TEXT*>( aItem );
1530
1531 if( text->IsVisible() )
1532 {
1533 if( text->IsKnockout() )
1534 {
1535 // Knockout text should only leave holes where the text is, not where the copper fill
1536 // around it would be.
1537 PCB_TEXT textCopy = *text;
1538 textCopy.SetIsKnockout( false );
1539 textCopy.TransformTextToPolySet( aHoles, 0, m_maxError, ERROR_INSIDE );
1540 }
1541 else
1542 {
1543 text->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1544 }
1545 }
1546
1547 break;
1548 }
1549
1550 case PCB_TEXTBOX_T:
1551 case PCB_TABLE_T:
1552 case PCB_SHAPE_T:
1553 case PCB_TARGET_T:
1554 aItem->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE, aIgnoreLineWidth );
1555 break;
1556
1557 case PCB_BARCODE_T:
1558 {
1559 PCB_BARCODE* barcode = static_cast<PCB_BARCODE*>( aItem );
1560 barcode->GetBoundingHull( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1561 break;
1562 }
1563
1564 case PCB_DIM_ALIGNED_T:
1565 case PCB_DIM_LEADER_T:
1566 case PCB_DIM_CENTER_T:
1567 case PCB_DIM_RADIAL_T:
1569 {
1570 PCB_DIMENSION_BASE* dim = static_cast<PCB_DIMENSION_BASE*>( aItem );
1571
1572 dim->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE, false );
1573 dim->PCB_TEXT::TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1574 break;
1575 }
1576
1577 default:
1578 break;
1579 }
1580}
1581
1582
1588 SHAPE_POLY_SET& aFill,
1589 std::vector<BOARD_ITEM*>& aThermalConnectionPads,
1590 std::vector<PAD*>& aNoConnectionPads )
1591{
1592 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1593 ZONE_CONNECTION connection;
1594 DRC_CONSTRAINT constraint;
1595 int padClearance;
1596 std::shared_ptr<SHAPE> padShape;
1597 int holeClearance;
1598 SHAPE_POLY_SET holes;
1599
1600 // Deduplication sets for coincident pads and vias
1601 std::unordered_set<PAD_KNOCKOUT_KEY, PAD_KNOCKOUT_KEY_HASH> processedPads;
1602 std::unordered_set<VIA_KNOCKOUT_KEY, VIA_KNOCKOUT_KEY_HASH> processedVias;
1603
1604 for( FOOTPRINT* footprint : m_board->Footprints() )
1605 {
1606 for( PAD* pad : footprint->Pads() )
1607 {
1608 if( !pad->IsOnLayer( aLayer ) )
1609 continue;
1610
1611 BOX2I padBBox = pad->GetBoundingBox();
1612 padBBox.Inflate( m_worstClearance );
1613
1614 if( !padBBox.Intersects( aZone->GetBoundingBox() ) )
1615 continue;
1616
1617 // Deduplicate coincident pads (skip custom pads - they have complex shapes)
1618 PAD_SHAPE padShapeType = pad->GetShape( aLayer );
1619
1620 if( padShapeType != PAD_SHAPE::CUSTOM )
1621 {
1622 // For circular pads: use max of drill and pad size; otherwise just pad size
1623 VECTOR2I padSize = pad->GetSize( aLayer );
1624 VECTOR2I effectiveSize;
1625
1626 if( padShapeType == PAD_SHAPE::CIRCLE )
1627 {
1628 int drill = std::max( pad->GetDrillSize().x, pad->GetDrillSize().y );
1629 int maxDim = std::max( { padSize.x, padSize.y, drill } );
1630 effectiveSize = VECTOR2I( maxDim, maxDim );
1631 }
1632 else
1633 {
1634 effectiveSize = padSize;
1635 }
1636
1637 PAD_KNOCKOUT_KEY padKey{ pad->GetPosition(), effectiveSize,
1638 static_cast<int>( padShapeType ), pad->GetOrientation() };
1639
1640 if( !processedPads.insert( padKey ).second )
1641 continue;
1642 }
1643
1644 bool noConnection = pad->GetNetCode() != aZone->GetNetCode();
1645
1646 if( !aZone->IsTeardropArea() )
1647 {
1648 if( aZone->GetNetCode() == 0
1649 || pad->GetZoneLayerOverride( aLayer ) == ZLO_FORCE_NO_ZONE_CONNECTION )
1650 {
1651 noConnection = true;
1652 }
1653 }
1654
1655 // Check if the pad is backdrilled or post-machined on this layer
1656 if( pad->IsBackdrilledOrPostMachined( aLayer ) )
1657 noConnection = true;
1658
1659 if( noConnection )
1660 {
1661 // collect these for knockout in buildCopperItemClearances()
1662 aNoConnectionPads.push_back( pad );
1663 continue;
1664 }
1665
1666 // For hatch zones, respect the zone connection type just like solid zones
1667 // Pads with THERMAL connection get thermal rings; FULL connections get no knockout;
1668 // NONE connections get handled later in buildCopperItemClearances.
1670 {
1671 constraint = bds.m_DRCEngine->EvalZoneConnection( pad, aZone, aLayer );
1672 connection = constraint.m_ZoneConnection;
1673
1674 if( connection == ZONE_CONNECTION::THERMAL && !pad->CanFlashLayer( aLayer ) )
1675 connection = ZONE_CONNECTION::NONE;
1676
1677 switch( connection )
1678 {
1680 {
1681 padShape = pad->GetEffectiveShape( aLayer, FLASHING::ALWAYS_FLASHED );
1682
1683 if( aFill.Collide( padShape.get(), 0 ) )
1684 {
1685 // Get the thermal relief gap
1687 aZone, aLayer );
1688 int thermalGap = constraint.GetValue().Min();
1689
1690 // Knock out the thermal gap only - the thermal ring will be added separately
1691 aThermalConnectionPads.push_back( pad );
1692 addKnockout( pad, aLayer, thermalGap, holes );
1693 }
1694
1695 break;
1696 }
1697
1699 // Will be handled by buildCopperItemClearances
1700 aNoConnectionPads.push_back( pad );
1701 break;
1702
1704 default:
1705 // No knockout - pad connects directly to the hatch
1706 break;
1707 }
1708
1709 continue;
1710 }
1711
1712 if( aZone->IsTeardropArea() )
1713 {
1714 connection = ZONE_CONNECTION::FULL;
1715 }
1716 else
1717 {
1718 constraint = bds.m_DRCEngine->EvalZoneConnection( pad, aZone, aLayer );
1719 connection = constraint.m_ZoneConnection;
1720 }
1721
1722 if( connection == ZONE_CONNECTION::THERMAL && !pad->CanFlashLayer( aLayer ) )
1723 connection = ZONE_CONNECTION::NONE;
1724
1725 switch( connection )
1726 {
1728 padShape = pad->GetEffectiveShape( aLayer, FLASHING::ALWAYS_FLASHED );
1729
1730 if( aFill.Collide( padShape.get(), 0 ) )
1731 {
1732 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, pad, aZone, aLayer );
1733 padClearance = constraint.GetValue().Min();
1734
1735 aThermalConnectionPads.push_back( pad );
1736 addKnockout( pad, aLayer, padClearance, holes );
1737 }
1738
1739 break;
1740
1742 constraint = bds.m_DRCEngine->EvalRules( PHYSICAL_CLEARANCE_CONSTRAINT, pad, aZone, aLayer );
1743
1744 if( constraint.GetValue().Min() > aZone->GetLocalClearance().value() )
1745 padClearance = constraint.GetValue().Min();
1746 else
1747 padClearance = aZone->GetLocalClearance().value();
1748
1749 if( pad->FlashLayer( aLayer ) )
1750 {
1751 addKnockout( pad, aLayer, padClearance, holes );
1752 }
1753 else if( pad->GetDrillSize().x > 0 )
1754 {
1755 constraint = bds.m_DRCEngine->EvalRules( PHYSICAL_HOLE_CLEARANCE_CONSTRAINT, pad, aZone, aLayer );
1756
1757 if( constraint.GetValue().Min() > padClearance )
1758 holeClearance = constraint.GetValue().Min();
1759 else
1760 holeClearance = padClearance;
1761
1762 pad->TransformHoleToPolygon( holes, holeClearance, m_maxError, ERROR_OUTSIDE );
1763 }
1764
1765 break;
1766
1767 default:
1768 // No knockout
1769 continue;
1770 }
1771 }
1772 }
1773
1774 // For hatch zones, vias also get proper thermal treatment. They always use thermal connection
1775 // since vias don't have zone connection settings like pads do.
1777 {
1778 for( PCB_TRACK* track : m_board->Tracks() )
1779 {
1780 if( track->Type() == PCB_VIA_T )
1781 {
1782 PCB_VIA* via = static_cast<PCB_VIA*>( track );
1783
1784 if( !via->IsOnLayer( aLayer ) )
1785 continue;
1786
1787 BOX2I viaBBox = via->GetBoundingBox();
1788 viaBBox.Inflate( m_worstClearance );
1789
1790 if( !viaBBox.Intersects( aZone->GetBoundingBox() ) )
1791 continue;
1792
1793 // Deduplicate coincident vias (circular, so use max of drill and width)
1794 int viaEffectiveSize = std::max( via->GetDrillValue(), via->GetWidth( aLayer ) );
1795 VIA_KNOCKOUT_KEY viaKey{ via->GetPosition(), viaEffectiveSize };
1796
1797 if( !processedVias.insert( viaKey ).second )
1798 continue;
1799
1800 bool noConnection = via->GetNetCode() != aZone->GetNetCode()
1801 || ( via->Padstack().UnconnectedLayerMode() == UNCONNECTED_LAYER_MODE::START_END_ONLY
1802 && aLayer != via->Padstack().Drill().start
1803 && aLayer != via->Padstack().Drill().end );
1804
1805 // Check if this layer is affected by backdrill or post-machining
1806 if( via->IsBackdrilledOrPostMachined( aLayer ) )
1807 {
1808 noConnection = true;
1809
1810 // Add knockout for backdrill/post-machining hole
1811 int pmSize = 0;
1812 int bdSize = 0;
1813
1814 const PADSTACK::POST_MACHINING_PROPS& frontPM = via->Padstack().FrontPostMachining();
1815 const PADSTACK::POST_MACHINING_PROPS& backPM = via->Padstack().BackPostMachining();
1816
1819 {
1820 pmSize = std::max( pmSize, frontPM.size );
1821 }
1822
1825 {
1826 pmSize = std::max( pmSize, backPM.size );
1827 }
1828
1829 const PADSTACK::DRILL_PROPS& secDrill = via->Padstack().SecondaryDrill();
1830
1831 if( secDrill.start != UNDEFINED_LAYER && secDrill.end != UNDEFINED_LAYER )
1832 bdSize = secDrill.size.x;
1833
1834 int knockoutSize = std::max( pmSize, bdSize );
1835
1836 if( knockoutSize > 0 )
1837 {
1838 int clearance = aZone->GetLocalClearance().value_or( 0 );
1839
1840 TransformCircleToPolygon( holes, via->GetPosition(), knockoutSize / 2 + clearance,
1842 }
1843 }
1844
1845 if( noConnection )
1846 continue;
1847
1848 // Use proper thermal gap from DRC constraints
1849 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, via, aZone, aLayer );
1850 int thermalGap = constraint.GetValue().Min();
1851
1852 aThermalConnectionPads.push_back( via );
1853 addKnockout( via, aLayer, thermalGap, holes );
1854 }
1855 }
1856 }
1857
1858 aFill.BooleanSubtract( holes );
1859}
1860
1861
1867 const std::vector<PAD*>& aNoConnectionPads,
1868 SHAPE_POLY_SET& aHoles,
1869 bool aIncludeZoneClearances )
1870{
1871 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1872 long ticker = 0;
1873
1874 // Deduplication sets for coincident items
1875 std::unordered_set<PAD_KNOCKOUT_KEY, PAD_KNOCKOUT_KEY_HASH> processedPads;
1876 std::unordered_set<VIA_KNOCKOUT_KEY, VIA_KNOCKOUT_KEY_HASH> processedVias;
1877 std::unordered_set<TRACK_KNOCKOUT_KEY, TRACK_KNOCKOUT_KEY_HASH> processedTracks;
1878
1879 auto checkForCancel =
1880 [&ticker]( PROGRESS_REPORTER* aReporter ) -> bool
1881 {
1882 return aReporter && ( ticker++ % 50 ) == 0 && aReporter->IsCancelled();
1883 };
1884
1885 // A small extra clearance to be sure actual track clearances are not smaller than
1886 // requested clearance due to many approximations in calculations, like arc to segment
1887 // approx, rounding issues, etc.
1888 BOX2I zone_boundingbox = aZone->GetBoundingBox();
1889 int extra_margin = pcbIUScale.mmToIU( ADVANCED_CFG::GetCfg().m_ExtraClearance );
1890
1891 // Items outside the zone bounding box are skipped, so it needs to be inflated by the
1892 // largest clearance value found in the netclasses and rules
1893 zone_boundingbox.Inflate( m_worstClearance + extra_margin );
1894
1895 auto evalRulesForItems =
1896 [&bds]( DRC_CONSTRAINT_T aConstraint, const BOARD_ITEM* a, const BOARD_ITEM* b,
1897 PCB_LAYER_ID aEvalLayer ) -> int
1898 {
1899 DRC_CONSTRAINT c = bds.m_DRCEngine->EvalRules( aConstraint, a, b, aEvalLayer );
1900
1901 if( c.IsNull() )
1902 return -1;
1903 else
1904 return c.GetValue().Min();
1905 };
1906
1907 // Add non-connected pad clearances
1908 //
1909 auto knockoutPadClearance =
1910 [&]( PAD* aPad )
1911 {
1912 int init_gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aPad, aLayer );
1913 int gap = init_gap;
1914 bool hasHole = aPad->GetDrillSize().x > 0;
1915 bool flashLayer = aPad->FlashLayer( aLayer );
1916 bool platedHole = hasHole && aPad->GetAttribute() == PAD_ATTRIB::PTH;
1917
1918 if( flashLayer || platedHole )
1919 {
1920 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aPad, aLayer ) );
1921 }
1922
1923 if( flashLayer && gap >= 0 )
1924 addKnockout( aPad, aLayer, gap + extra_margin, aHoles );
1925
1926 if( hasHole )
1927 {
1928 // NPTH do not need copper clearance gaps to their holes
1929 if( aPad->GetAttribute() == PAD_ATTRIB::NPTH )
1930 gap = init_gap;
1931
1932 gap = std::max( gap, evalRulesForItems( PHYSICAL_HOLE_CLEARANCE_CONSTRAINT, aZone, aPad, aLayer ) );
1933
1934 gap = std::max( gap, evalRulesForItems( HOLE_CLEARANCE_CONSTRAINT, aZone, aPad, aLayer ) );
1935
1936 if( gap >= 0 )
1937 addHoleKnockout( aPad, gap + extra_margin, aHoles );
1938 }
1939
1940 // Handle backdrill and post-machining knockouts
1941 if( aPad->IsBackdrilledOrPostMachined( aLayer ) )
1942 {
1943 int pmSize = 0;
1944 int bdSize = 0;
1945
1946 const PADSTACK::POST_MACHINING_PROPS& frontPM = aPad->Padstack().FrontPostMachining();
1947 const PADSTACK::POST_MACHINING_PROPS& backPM = aPad->Padstack().BackPostMachining();
1948
1951 {
1952 pmSize = std::max( pmSize, frontPM.size );
1953 }
1954
1957 {
1958 pmSize = std::max( pmSize, backPM.size );
1959 }
1960
1961 const PADSTACK::DRILL_PROPS& secDrill = aPad->Padstack().SecondaryDrill();
1962
1963 if( secDrill.start != UNDEFINED_LAYER && secDrill.end != UNDEFINED_LAYER )
1964 bdSize = secDrill.size.x;
1965
1966 int knockoutSize = std::max( pmSize, bdSize );
1967
1968 if( knockoutSize > 0 )
1969 {
1970 int clearance = std::max( gap, 0 ) + extra_margin;
1971
1972 TransformCircleToPolygon( aHoles, aPad->GetPosition(), knockoutSize / 2 + clearance,
1974 }
1975 }
1976 };
1977
1978 for( PAD* pad : aNoConnectionPads )
1979 {
1980 if( checkForCancel( m_progressReporter ) )
1981 return;
1982
1983 // Deduplicate coincident pads (skip custom pads - they have complex shapes)
1984 PAD_SHAPE padShape = pad->GetShape( aLayer );
1985
1986 if( padShape != PAD_SHAPE::CUSTOM )
1987 {
1988 // For circular pads: use max of drill and pad size; otherwise just pad size
1989 VECTOR2I padSize = pad->GetSize( aLayer );
1990 VECTOR2I effectiveSize;
1991
1992 if( padShape == PAD_SHAPE::CIRCLE )
1993 {
1994 int drill = std::max( pad->GetDrillSize().x, pad->GetDrillSize().y );
1995 int maxDim = std::max( { padSize.x, padSize.y, drill } );
1996 effectiveSize = VECTOR2I( maxDim, maxDim );
1997 }
1998 else
1999 {
2000 effectiveSize = padSize;
2001 }
2002
2003 PAD_KNOCKOUT_KEY padKey{ pad->GetPosition(), effectiveSize,
2004 static_cast<int>( padShape ), pad->GetOrientation() };
2005
2006 if( !processedPads.insert( padKey ).second )
2007 continue;
2008 }
2009
2010 knockoutPadClearance( pad );
2011 }
2012
2013 // Add non-connected track clearances
2014 //
2015 auto knockoutTrackClearance =
2016 [&]( PCB_TRACK* aTrack )
2017 {
2018 if( aTrack->GetBoundingBox().Intersects( zone_boundingbox ) )
2019 {
2020 bool sameNet = aTrack->GetNetCode() == aZone->GetNetCode();
2021
2022 if( !aZone->IsTeardropArea() && aZone->GetNetCode() == 0 )
2023 sameNet = false;
2024
2025 int gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aTrack, aLayer );
2026
2027 if( aTrack->Type() == PCB_VIA_T )
2028 {
2029 PCB_VIA* via = static_cast<PCB_VIA*>( aTrack );
2030
2031 if( via->GetZoneLayerOverride( aLayer ) == ZLO_FORCE_NO_ZONE_CONNECTION )
2032 sameNet = false;
2033 }
2034
2035 if( !sameNet )
2036 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aTrack, aLayer ) );
2037
2038 if( aTrack->Type() == PCB_VIA_T )
2039 {
2040 PCB_VIA* via = static_cast<PCB_VIA*>( aTrack );
2041
2042 if( via->FlashLayer( aLayer ) && gap > 0 )
2043 {
2044 via->TransformShapeToPolygon( aHoles, aLayer, gap + extra_margin, m_maxError,
2045 ERROR_OUTSIDE );
2046 }
2047
2048 gap = std::max( gap, evalRulesForItems( PHYSICAL_HOLE_CLEARANCE_CONSTRAINT, aZone, via,
2049 aLayer ) );
2050
2051 if( !sameNet )
2052 gap = std::max( gap, evalRulesForItems( HOLE_CLEARANCE_CONSTRAINT, aZone, via, aLayer ) );
2053
2054 if( gap >= 0 )
2055 {
2056 int radius = via->GetDrillValue() / 2;
2057
2058 TransformCircleToPolygon( aHoles, via->GetPosition(), radius + gap + extra_margin,
2060 }
2061
2062 // Handle backdrill and post-machining knockouts
2063 if( via->IsBackdrilledOrPostMachined( aLayer ) )
2064 {
2065 int pmSize = 0;
2066 int bdSize = 0;
2067
2068 const PADSTACK::POST_MACHINING_PROPS& frontPM = via->Padstack().FrontPostMachining();
2069 const PADSTACK::POST_MACHINING_PROPS& backPM = via->Padstack().BackPostMachining();
2070
2073 {
2074 pmSize = std::max( pmSize, frontPM.size );
2075 }
2076
2079 {
2080 pmSize = std::max( pmSize, backPM.size );
2081 }
2082
2083 const PADSTACK::DRILL_PROPS& secDrill = via->Padstack().SecondaryDrill();
2084
2085 if( secDrill.start != UNDEFINED_LAYER && secDrill.end != UNDEFINED_LAYER )
2086 bdSize = secDrill.size.x;
2087
2088 int knockoutSize = std::max( pmSize, bdSize );
2089
2090 if( knockoutSize > 0 )
2091 {
2092 int clearance = std::max( gap, 0 ) + extra_margin;
2093
2094 TransformCircleToPolygon( aHoles, via->GetPosition(), knockoutSize / 2 + clearance,
2096 }
2097 }
2098 }
2099 else
2100 {
2101 if( gap >= 0 )
2102 {
2103 aTrack->TransformShapeToPolygon( aHoles, aLayer, gap + extra_margin, m_maxError,
2104 ERROR_OUTSIDE );
2105 }
2106 }
2107 }
2108 };
2109
2110 for( PCB_TRACK* track : m_board->Tracks() )
2111 {
2112 if( !track->IsOnLayer( aLayer ) )
2113 continue;
2114
2115 if( checkForCancel( m_progressReporter ) )
2116 return;
2117
2118 // Deduplicate coincident tracks and vias
2119 if( track->Type() == PCB_VIA_T )
2120 {
2121 PCB_VIA* via = static_cast<PCB_VIA*>( track );
2122 int viaEffectiveSize = std::max( via->GetDrillValue(), via->GetWidth( aLayer ) );
2123 VIA_KNOCKOUT_KEY viaKey{ via->GetPosition(), viaEffectiveSize };
2124
2125 if( !processedVias.insert( viaKey ).second )
2126 continue;
2127 }
2128 else
2129 {
2130 TRACK_KNOCKOUT_KEY trackKey( track->GetStart(), track->GetEnd(), track->GetWidth() );
2131
2132 if( !processedTracks.insert( trackKey ).second )
2133 continue;
2134 }
2135
2136 knockoutTrackClearance( track );
2137 }
2138
2139 // Add graphic item clearances.
2140 //
2141 auto knockoutGraphicClearance =
2142 [&]( BOARD_ITEM* aItem )
2143 {
2144 int shapeNet = -1;
2145
2146 if( aItem->Type() == PCB_SHAPE_T )
2147 shapeNet = static_cast<PCB_SHAPE*>( aItem )->GetNetCode();
2148
2149 bool sameNet = shapeNet == aZone->GetNetCode();
2150
2151 if( !aZone->IsTeardropArea() && aZone->GetNetCode() == 0 )
2152 sameNet = false;
2153
2154 // A item on the Edge_Cuts or Margin is always seen as on any layer:
2155 if( aItem->IsOnLayer( aLayer )
2156 || aItem->IsOnLayer( Edge_Cuts )
2157 || aItem->IsOnLayer( Margin ) )
2158 {
2159 if( aItem->GetBoundingBox().Intersects( zone_boundingbox ) )
2160 {
2161 bool ignoreLineWidths = false;
2162 int gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aItem, aLayer );
2163
2164 if( aItem->IsOnLayer( aLayer ) && !sameNet )
2165 {
2166 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aItem, aLayer ) );
2167 }
2168 else if( aItem->IsOnLayer( Edge_Cuts ) )
2169 {
2170 gap = std::max( gap, evalRulesForItems( EDGE_CLEARANCE_CONSTRAINT, aZone, aItem, aLayer ) );
2171 ignoreLineWidths = true;
2172 }
2173 else if( aItem->IsOnLayer( Margin ) )
2174 {
2175 gap = std::max( gap, evalRulesForItems( EDGE_CLEARANCE_CONSTRAINT, aZone, aItem, aLayer ) );
2176 }
2177
2178 if( gap >= 0 )
2179 {
2180 gap += extra_margin;
2181 addKnockout( aItem, aLayer, gap, ignoreLineWidths, aHoles );
2182 }
2183 }
2184 }
2185 };
2186
2187 auto knockoutCourtyardClearance =
2188 [&]( FOOTPRINT* aFootprint )
2189 {
2190 if( aFootprint->GetBoundingBox().Intersects( zone_boundingbox ) )
2191 {
2192 int gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aFootprint, aLayer );
2193
2194 if( gap == 0 )
2195 {
2196 aHoles.Append( aFootprint->GetCourtyard( aLayer ) );
2197 }
2198 else if( gap > 0 )
2199 {
2200 SHAPE_POLY_SET hole = aFootprint->GetCourtyard( aLayer );
2202 aHoles.Append( hole );
2203 }
2204 }
2205 };
2206
2207 for( FOOTPRINT* footprint : m_board->Footprints() )
2208 {
2209 knockoutCourtyardClearance( footprint );
2210 knockoutGraphicClearance( &footprint->Reference() );
2211 knockoutGraphicClearance( &footprint->Value() );
2212
2213 std::set<PAD*> allowedNetTiePads;
2214
2215 // Don't knock out holes for graphic items which implement a net-tie to the zone's net
2216 // on the layer being filled.
2217 if( footprint->IsNetTie() )
2218 {
2219 for( PAD* pad : footprint->Pads() )
2220 {
2221 bool sameNet = pad->GetNetCode() == aZone->GetNetCode();
2222
2223 if( !aZone->IsTeardropArea() && aZone->GetNetCode() == 0 )
2224 sameNet = false;
2225
2226 if( sameNet )
2227 {
2228 if( pad->IsOnLayer( aLayer ) )
2229 allowedNetTiePads.insert( pad );
2230
2231 for( PAD* other : footprint->GetNetTiePads( pad ) )
2232 {
2233 if( other->IsOnLayer( aLayer ) )
2234 allowedNetTiePads.insert( other );
2235 }
2236 }
2237 }
2238 }
2239
2240 for( BOARD_ITEM* item : footprint->GraphicalItems() )
2241 {
2242 if( checkForCancel( m_progressReporter ) )
2243 return;
2244
2245 BOX2I itemBBox = item->GetBoundingBox();
2246
2247 if( !zone_boundingbox.Intersects( itemBBox ) )
2248 continue;
2249
2250 bool skipItem = false;
2251
2252 if( item->IsOnLayer( aLayer ) )
2253 {
2254 std::shared_ptr<SHAPE> itemShape = item->GetEffectiveShape();
2255
2256 for( PAD* pad : allowedNetTiePads )
2257 {
2258 if( pad->GetBoundingBox().Intersects( itemBBox )
2259 && pad->GetEffectiveShape( aLayer )->Collide( itemShape.get() ) )
2260 {
2261 skipItem = true;
2262 break;
2263 }
2264 }
2265 }
2266
2267 if( !skipItem )
2268 knockoutGraphicClearance( item );
2269 }
2270 }
2271
2272 for( BOARD_ITEM* item : m_board->Drawings() )
2273 {
2274 if( checkForCancel( m_progressReporter ) )
2275 return;
2276
2277 knockoutGraphicClearance( item );
2278 }
2279
2280 // Add non-connected zone clearances
2281 //
2282 auto knockoutZoneClearance =
2283 [&]( ZONE* aKnockout )
2284 {
2285 // If the zones share no common layers
2286 if( !aKnockout->GetLayerSet().test( aLayer ) )
2287 return;
2288
2289 if( aKnockout->GetBoundingBox().Intersects( zone_boundingbox ) )
2290 {
2291 if( aKnockout->GetIsRuleArea() )
2292 {
2293 if( aKnockout->GetDoNotAllowZoneFills() && !aZone->IsTeardropArea() )
2294 {
2295 // Keepouts use outline with no clearance
2296 aKnockout->TransformSmoothedOutlineToPolygon( aHoles, 0, m_maxError, ERROR_OUTSIDE,
2297 nullptr );
2298 }
2299 }
2300 else
2301 {
2302 if( aKnockout->HigherPriority( aZone ) && !aKnockout->SameNet( aZone ) )
2303 {
2304 int gap = std::max( 0, evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aKnockout,
2305 aLayer ) );
2306
2307 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aKnockout, aLayer ) );
2308
2309 // Negative clearance permits zones to short
2310 if( gap < 0 )
2311 return;
2312
2313 SHAPE_POLY_SET poly;
2314 aKnockout->TransformShapeToPolygon( poly, aLayer, gap + extra_margin, m_maxError,
2315 ERROR_OUTSIDE );
2316 aHoles.Append( poly );
2317 }
2318 }
2319 }
2320 };
2321
2322 if( aIncludeZoneClearances )
2323 {
2324 for( ZONE* otherZone : m_board->Zones() )
2325 {
2326 if( checkForCancel( m_progressReporter ) )
2327 return;
2328
2329 knockoutZoneClearance( otherZone );
2330 }
2331
2332 for( FOOTPRINT* footprint : m_board->Footprints() )
2333 {
2334 for( ZONE* otherZone : footprint->Zones() )
2335 {
2336 if( checkForCancel( m_progressReporter ) )
2337 return;
2338
2339 knockoutZoneClearance( otherZone );
2340 }
2341 }
2342 }
2343
2344 aHoles.Simplify();
2345}
2346
2347
2353{
2354 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
2355 int extra_margin = pcbIUScale.mmToIU( ADVANCED_CFG::GetCfg().m_ExtraClearance );
2356
2357 BOX2I zone_boundingbox = aZone->GetBoundingBox();
2358 zone_boundingbox.Inflate( m_worstClearance + extra_margin );
2359
2360 auto evalRulesForItems =
2361 [&bds]( DRC_CONSTRAINT_T aConstraint, const BOARD_ITEM* a, const BOARD_ITEM* b,
2362 PCB_LAYER_ID aEvalLayer ) -> int
2363 {
2364 DRC_CONSTRAINT c = bds.m_DRCEngine->EvalRules( aConstraint, a, b, aEvalLayer );
2365
2366 if( c.IsNull() )
2367 return -1;
2368 else
2369 return c.GetValue().Min();
2370 };
2371
2372 auto knockoutZoneClearance =
2373 [&]( ZONE* aKnockout )
2374 {
2375 if( !aKnockout->GetLayerSet().test( aLayer ) )
2376 return;
2377
2378 if( aKnockout->GetBoundingBox().Intersects( zone_boundingbox ) )
2379 {
2380 if( aKnockout->GetIsRuleArea() )
2381 {
2382 if( aKnockout->GetDoNotAllowZoneFills() && !aZone->IsTeardropArea() )
2383 {
2384 aKnockout->TransformSmoothedOutlineToPolygon( aHoles, 0, m_maxError, ERROR_OUTSIDE,
2385 nullptr );
2386 }
2387 }
2388 else
2389 {
2390 if( aKnockout->HigherPriority( aZone ) && !aKnockout->SameNet( aZone ) )
2391 {
2392 int gap = std::max( 0, evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aKnockout,
2393 aLayer ) );
2394
2395 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aKnockout, aLayer ) );
2396
2397 // Negative clearance permits zones to short
2398 if( gap < 0 )
2399 return;
2400
2401 SHAPE_POLY_SET poly;
2402 aKnockout->TransformShapeToPolygon( poly, aLayer, gap + extra_margin, m_maxError,
2403 ERROR_OUTSIDE );
2404 aHoles.Append( poly );
2405 }
2406 }
2407 }
2408 };
2409
2410 for( ZONE* otherZone : m_board->Zones() )
2411 knockoutZoneClearance( otherZone );
2412
2413 for( FOOTPRINT* footprint : m_board->Footprints() )
2414 {
2415 for( ZONE* otherZone : footprint->Zones() )
2416 knockoutZoneClearance( otherZone );
2417 }
2418
2419 aHoles.Simplify();
2420}
2421
2422
2428{
2429 BOX2I zoneBBox = aZone->GetBoundingBox();
2430
2431 auto knockoutZoneOutline =
2432 [&]( ZONE* aKnockout )
2433 {
2434 // If the zones share no common layers
2435 if( !aKnockout->GetLayerSet().test( aLayer ) )
2436 return;
2437
2438 if( aKnockout->GetBoundingBox().Intersects( zoneBBox ) )
2439 {
2440 // Processing of arc shapes in zones is not yet supported because Clipper
2441 // can't do boolean operations on them. The poly outline must be converted to
2442 // segments first.
2443 SHAPE_POLY_SET outline = aKnockout->Outline()->CloneDropTriangulation();
2444 outline.ClearArcs();
2445
2446 aRawFill.BooleanSubtract( outline );
2447 }
2448 };
2449
2450 for( ZONE* otherZone : m_board->Zones() )
2451 {
2452 // Don't use the `HigherPriority()` check here because we _only_ want to knock out zones
2453 // with explicitly higher priorities, not those with equal priorities
2454 if( otherZone->SameNet( aZone )
2455 && otherZone->GetAssignedPriority() > aZone->GetAssignedPriority() )
2456 {
2457 // Do not remove teardrop area: it is not useful and not good
2458 if( !otherZone->IsTeardropArea() )
2459 knockoutZoneOutline( otherZone );
2460 }
2461 }
2462
2463 for( FOOTPRINT* footprint : m_board->Footprints() )
2464 {
2465 for( ZONE* otherZone : footprint->Zones() )
2466 {
2467 if( otherZone->SameNet( aZone ) && otherZone->HigherPriority( aZone ) )
2468 {
2469 // Do not remove teardrop area: it is not useful and not good
2470 if( !otherZone->IsTeardropArea() )
2471 knockoutZoneOutline( otherZone );
2472 }
2473 }
2474 }
2475}
2476
2477
2478void ZONE_FILLER::connect_nearby_polys( SHAPE_POLY_SET& aPolys, double aDistance )
2479{
2480 if( aPolys.OutlineCount() < 1 )
2481 return;
2482
2483 VERTEX_CONNECTOR vs( aPolys.BBoxFromCaches(), aPolys, aDistance );
2484
2485 vs.FindResults();
2486
2487 // This cannot be a reference because we need to do the comparison below while
2488 // changing the values
2489 std::map<int, std::vector<std::pair<int, VECTOR2I>>> insertion_points;
2490
2491 for( const RESULTS& result : vs.GetResults() )
2492 {
2493 SHAPE_LINE_CHAIN& line1 = aPolys.Outline( result.m_outline1 );
2494 SHAPE_LINE_CHAIN& line2 = aPolys.Outline( result.m_outline2 );
2495
2496 VECTOR2I pt1 = line1.CPoint( result.m_vertex1 );
2497 VECTOR2I pt2 = line2.CPoint( result.m_vertex2 );
2498
2499 // We want to insert the existing point first so that we can place the new point
2500 // between the two points at the same location.
2501 insertion_points[result.m_outline1].push_back( { result.m_vertex1, pt1 } );
2502 insertion_points[result.m_outline1].push_back( { result.m_vertex1, pt2 } );
2503 }
2504
2505 for( auto& [outline, vertices] : insertion_points )
2506 {
2507 SHAPE_LINE_CHAIN& line = aPolys.Outline( outline );
2508
2509 if( vertices.empty() )
2510 continue;
2511
2512 // Stable sort here because we want to make sure that we are inserting pt1 first and
2513 // pt2 second but still sorting the rest of the indices
2514 std::stable_sort( vertices.begin(), vertices.end(),
2515 []( const std::pair<int, VECTOR2I>& a, const std::pair<int, VECTOR2I>& b )
2516 {
2517 return a.first < b.first;
2518 } );
2519
2520 std::vector<VECTOR2I> new_points;
2521 new_points.reserve( line.PointCount() + vertices.size() );
2522
2523 size_t vertex_idx = 0;
2524
2525 for( int i = 0; i < line.PointCount(); ++i )
2526 {
2527 new_points.push_back( line.CPoint( i ) );
2528
2529 // Insert all points that should come after position i
2530 while( vertex_idx < vertices.size() && vertices[vertex_idx].first == i )
2531 {
2532 new_points.push_back( vertices[vertex_idx].second );
2533 vertex_idx++;
2534 }
2535 }
2536
2537 line.Clear();
2538
2539 for( const auto& pt : new_points )
2540 line.Append( pt );
2541 }
2542}
2543
2544
2545#define DUMP_POLYS_TO_COPPER_LAYER( a, b, c ) \
2546 { if( m_debugZoneFiller && aDebugLayer == b ) \
2547 { \
2548 m_board->SetLayerName( b, c ); \
2549 SHAPE_POLY_SET d = a; \
2550 d.Fracture(); \
2551 aFillPolys = d; \
2552 return false; \
2553 } \
2554 }
2555
2556
2557/*
2558 * Note that aSmoothedOutline is larger than the zone where it intersects with other, same-net
2559 * zones. This is to prevent the re-inflation post min-width trimming from createing divots
2560 * between adjacent zones. The final aMaxExtents trimming will remove these areas from the final
2561 * fill.
2562 */
2563bool ZONE_FILLER::fillCopperZone( const ZONE* aZone, PCB_LAYER_ID aLayer, PCB_LAYER_ID aDebugLayer,
2564 const SHAPE_POLY_SET& aSmoothedOutline,
2565 const SHAPE_POLY_SET& aMaxExtents, SHAPE_POLY_SET& aFillPolys )
2566{
2567 // m_maxError is initialized in the constructor. Don't reassign here to avoid data races
2568 // when multiple threads call this function concurrently.
2569
2570 // Features which are min_width should survive pruning; features that are *less* than
2571 // min_width should not. Therefore we subtract epsilon from the min_width when
2572 // deflating/inflating.
2573 int half_min_width = aZone->GetMinThickness() / 2;
2574 int epsilon = pcbIUScale.mmToIU( 0.001 );
2575
2576 // Solid polygons are deflated and inflated during calculations. Deflating doesn't cause
2577 // issues, but inflate is tricky as it can create excessively long and narrow spikes for
2578 // acute angles.
2579 // ALLOW_ACUTE_CORNERS cannot be used due to the spike problem.
2580 // CHAMFER_ACUTE_CORNERS is tempting, but can still produce spikes in some unusual
2581 // circumstances (https://gitlab.com/kicad/code/kicad/-/issues/5581).
2582 // It's unclear if ROUND_ACUTE_CORNERS would have the same issues, but is currently avoided
2583 // as a "less-safe" option.
2584 // ROUND_ALL_CORNERS produces the uniformly nicest shapes, but also a lot of segments.
2585 // CHAMFER_ALL_CORNERS improves the segment count.
2588
2589 std::vector<BOARD_ITEM*> thermalConnectionPads;
2590 std::vector<PAD*> noConnectionPads;
2591 std::deque<SHAPE_LINE_CHAIN> thermalSpokes;
2592 SHAPE_POLY_SET clearanceHoles;
2593
2594 aFillPolys = aSmoothedOutline;
2595 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In1_Cu, wxT( "smoothed-outline" ) );
2596
2597 if( m_progressReporter && m_progressReporter->IsCancelled() )
2598 return false;
2599
2600 /* -------------------------------------------------------------------------------------
2601 * Knockout thermal reliefs.
2602 */
2603
2604 knockoutThermalReliefs( aZone, aLayer, aFillPolys, thermalConnectionPads, noConnectionPads );
2605 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In2_Cu, wxT( "minus-thermal-reliefs" ) );
2606
2607 if( m_progressReporter && m_progressReporter->IsCancelled() )
2608 return false;
2609
2610 /* -------------------------------------------------------------------------------------
2611 * For hatch zones, add thermal rings around pads with thermal relief.
2612 * The rings are clipped to the zone boundary and provide the connection point
2613 * for the hatch webbing instead of connecting directly to the pad.
2614 */
2615
2616 SHAPE_POLY_SET thermalRings;
2617
2619 {
2620 buildHatchZoneThermalRings( aZone, aLayer, aSmoothedOutline, thermalConnectionPads,
2621 aFillPolys, thermalRings );
2622 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In2_Cu, wxT( "plus-thermal-rings" ) );
2623 }
2624
2625 if( m_progressReporter && m_progressReporter->IsCancelled() )
2626 return false;
2627
2628 /* -------------------------------------------------------------------------------------
2629 * Knockout electrical clearances.
2630 */
2631
2632 // When iterative refill is enabled, we build zone clearances separately so we can cache
2633 // the fill before zone knockouts are applied (issue 21746).
2634 const bool iterativeRefill = ADVANCED_CFG::GetCfg().m_ZoneFillIterativeRefill;
2635
2636 buildCopperItemClearances( aZone, aLayer, noConnectionPads, clearanceHoles,
2637 !iterativeRefill /* include zone clearances only if not iterative */ );
2638 DUMP_POLYS_TO_COPPER_LAYER( clearanceHoles, In3_Cu, wxT( "clearance-holes" ) );
2639
2640 if( m_progressReporter && m_progressReporter->IsCancelled() )
2641 return false;
2642
2643 /* -------------------------------------------------------------------------------------
2644 * Add thermal relief spokes.
2645 */
2646
2647 buildThermalSpokes( aZone, aLayer, thermalConnectionPads, thermalSpokes );
2648
2649 if( m_progressReporter && m_progressReporter->IsCancelled() )
2650 return false;
2651
2652 // Create a temporary zone that we can hit-test spoke-ends against. It's only temporary
2653 // because the "real" subtract-clearance-holes has to be done after the spokes are added.
2654 static const bool USE_BBOX_CACHES = true;
2655 SHAPE_POLY_SET testAreas = aFillPolys.CloneDropTriangulation();
2656 testAreas.BooleanSubtract( clearanceHoles );
2657 DUMP_POLYS_TO_COPPER_LAYER( testAreas, In4_Cu, wxT( "minus-clearance-holes" ) );
2658
2659 // Prune features that don't meet minimum-width criteria
2660 if( half_min_width - epsilon > epsilon )
2661 {
2662 testAreas.Deflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2663 DUMP_POLYS_TO_COPPER_LAYER( testAreas, In5_Cu, wxT( "spoke-test-deflated" ) );
2664
2665 testAreas.Inflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2666 DUMP_POLYS_TO_COPPER_LAYER( testAreas, In6_Cu, wxT( "spoke-test-reinflated" ) );
2667 }
2668
2669 if( m_progressReporter && m_progressReporter->IsCancelled() )
2670 return false;
2671
2672 // Spoke-end-testing is hugely expensive so we generate cached bounding-boxes to speed
2673 // things up a bit.
2674 testAreas.BuildBBoxCaches();
2675 int interval = 0;
2676
2677 SHAPE_POLY_SET debugSpokes;
2678
2679 for( const SHAPE_LINE_CHAIN& spoke : thermalSpokes )
2680 {
2681 const VECTOR2I& testPt = spoke.CPoint( 3 );
2682
2683 // Hit-test against zone body
2684 if( testAreas.Contains( testPt, -1, 1, USE_BBOX_CACHES ) )
2685 {
2686 if( m_debugZoneFiller )
2687 debugSpokes.AddOutline( spoke );
2688
2689 aFillPolys.AddOutline( spoke );
2690 continue;
2691 }
2692
2693 if( interval++ > 400 )
2694 {
2695 if( m_progressReporter && m_progressReporter->IsCancelled() )
2696 return false;
2697
2698 interval = 0;
2699 }
2700
2701 // Hit-test against other spokes
2702 for( const SHAPE_LINE_CHAIN& other : thermalSpokes )
2703 {
2704 // Hit test in both directions to avoid interactions with round-off errors.
2705 // (See https://gitlab.com/kicad/code/kicad/-/issues/13316.)
2706 if( &other != &spoke
2707 && other.PointInside( testPt, 1, USE_BBOX_CACHES )
2708 && spoke.PointInside( other.CPoint( 3 ), 1, USE_BBOX_CACHES ) )
2709 {
2710 if( m_debugZoneFiller )
2711 debugSpokes.AddOutline( spoke );
2712
2713 aFillPolys.AddOutline( spoke );
2714 break;
2715 }
2716 }
2717 }
2718
2719 DUMP_POLYS_TO_COPPER_LAYER( debugSpokes, In7_Cu, wxT( "spokes" ) );
2720
2721 if( m_progressReporter && m_progressReporter->IsCancelled() )
2722 return false;
2723
2724 aFillPolys.BooleanSubtract( clearanceHoles );
2725 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In8_Cu, wxT( "after-spoke-trimming" ) );
2726
2727 /* -------------------------------------------------------------------------------------
2728 * Prune features that don't meet minimum-width criteria
2729 */
2730
2731 if( half_min_width - epsilon > epsilon )
2732 {
2733 aFillPolys.Deflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2734
2735 // Also deflate thermal rings to match, for correct hatch hole notching
2736 if( thermalRings.OutlineCount() > 0 )
2737 thermalRings.Deflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2738 }
2739
2740 // Min-thickness is the web thickness. On the other hand, a blob min-thickness by
2741 // min-thickness is not useful. Since there's no obvious definition of web vs. blob, we
2742 // arbitrarily choose "at least 2X min-thickness on one axis". (Since we're doing this
2743 // during the deflated state, that means we test for "at least min-thickness".)
2744 for( int ii = aFillPolys.OutlineCount() - 1; ii >= 0; ii-- )
2745 {
2746 std::vector<SHAPE_LINE_CHAIN>& island = aFillPolys.Polygon( ii );
2747 BOX2I islandExtents;
2748
2749 for( const VECTOR2I& pt : island.front().CPoints() )
2750 {
2751 islandExtents.Merge( pt );
2752
2753 if( islandExtents.GetSizeMax() > aZone->GetMinThickness() )
2754 break;
2755 }
2756
2757 if( islandExtents.GetSizeMax() < aZone->GetMinThickness() )
2758 aFillPolys.DeletePolygon( ii );
2759 }
2760
2761 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In9_Cu, wxT( "deflated" ) );
2762
2763 if( m_progressReporter && m_progressReporter->IsCancelled() )
2764 return false;
2765
2766 /* -------------------------------------------------------------------------------------
2767 * Process the hatch pattern (note that we do this while deflated)
2768 */
2769
2771 && ( !m_board->GetProject()
2772 || !m_board->GetProject()->GetLocalSettings().m_PrototypeZoneFill ) )
2773 {
2774 // Combine thermal rings with clearance holes (non-connected pad clearances) so that
2775 // the hatch hole-dropping logic considers both types of rings
2776 SHAPE_POLY_SET ringsToProtect = thermalRings;
2777 ringsToProtect.BooleanAdd( clearanceHoles );
2778
2779 if( !addHatchFillTypeOnZone( aZone, aLayer, aDebugLayer, aFillPolys, ringsToProtect ) )
2780 return false;
2781 }
2782 else
2783 {
2784 /* ---------------------------------------------------------------------------------
2785 * Connect nearby polygons with zero-width lines in order to ensure correct
2786 * re-inflation.
2787 */
2788 aFillPolys.Fracture();
2789 connect_nearby_polys( aFillPolys, aZone->GetMinThickness() );
2790
2791 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In10_Cu, wxT( "connected-nearby-polys" ) );
2792 }
2793
2794 if( m_progressReporter && m_progressReporter->IsCancelled() )
2795 return false;
2796
2797 /* -------------------------------------------------------------------------------------
2798 * Finish minimum-width pruning by re-inflating
2799 */
2800
2801 if( half_min_width - epsilon > epsilon )
2802 aFillPolys.Inflate( half_min_width - epsilon, cornerStrategy, m_maxError, true );
2803
2804 // The deflation/inflation process can leave notches in the outline. Remove these by
2805 // doing a union with the original ring
2806 aFillPolys.BooleanAdd( thermalRings );
2807
2808 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In15_Cu, wxT( "after-reinflating" ) );
2809
2810 /* -------------------------------------------------------------------------------------
2811 * Ensure additive changes (thermal stubs and inflating acute corners) do not add copper
2812 * outside the zone boundary, inside the clearance holes, or between otherwise isolated
2813 * islands
2814 */
2815
2816 for( BOARD_ITEM* item : thermalConnectionPads )
2817 {
2818 if( item->Type() == PCB_PAD_T )
2819 addHoleKnockout( static_cast<PAD*>( item ), 0, clearanceHoles );
2820 }
2821
2822 aFillPolys.BooleanIntersection( aMaxExtents );
2823 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In16_Cu, wxT( "after-trim-to-outline" ) );
2824 aFillPolys.BooleanSubtract( clearanceHoles );
2825 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In17_Cu, wxT( "after-trim-to-clearance-holes" ) );
2826
2827 // Cache the pre-knockout fill for iterative refill optimization (issue 21746).
2828 // The cache stores the fill BEFORE zone-to-zone knockouts so the iterative refill can
2829 // reclaim space when higher-priority zones have islands removed.
2830 if( iterativeRefill )
2831 {
2832 {
2833 std::lock_guard<std::mutex> lock( m_cacheMutex );
2834 m_preKnockoutFillCache[{ aZone, aLayer }] = aFillPolys;
2835 }
2836
2837 BOX2I cacheBBox = aFillPolys.BBox();
2838
2839 wxLogTrace( traceZoneFiller,
2840 wxT( "Cached pre-knockout fill for zone %s layer %d: %d outlines, area %.0f, "
2841 "bbox (%d,%d)-(%d,%d)" ),
2842 aZone->GetNetname(), static_cast<int>( aLayer ),
2843 aFillPolys.OutlineCount(), aFillPolys.Area(),
2844 cacheBBox.GetX(), cacheBBox.GetY(), cacheBBox.GetRight(), cacheBBox.GetBottom() );
2845
2846 // Now apply zone-to-zone knockouts for different-net zones
2847 SHAPE_POLY_SET zoneClearances;
2848 buildDifferentNetZoneClearances( aZone, aLayer, zoneClearances );
2849
2850 if( zoneClearances.OutlineCount() > 0 )
2851 aFillPolys.BooleanSubtract( zoneClearances );
2852 }
2853
2854 /* -------------------------------------------------------------------------------------
2855 * Lastly give any same-net but higher-priority zones control over their own area.
2856 */
2857
2858 subtractHigherPriorityZones( aZone, aLayer, aFillPolys );
2859 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In18_Cu, wxT( "minus-higher-priority-zones" ) );
2860
2861 aFillPolys.Fracture();
2862 return true;
2863}
2864
2865
2867 const SHAPE_POLY_SET& aSmoothedOutline,
2868 SHAPE_POLY_SET& aFillPolys )
2869{
2870 BOX2I zone_boundingbox = aZone->GetBoundingBox();
2871 SHAPE_POLY_SET clearanceHoles;
2872 long ticker = 0;
2873
2874 auto checkForCancel =
2875 [&ticker]( PROGRESS_REPORTER* aReporter ) -> bool
2876 {
2877 return aReporter && ( ticker++ % 50 ) == 0 && aReporter->IsCancelled();
2878 };
2879
2880 auto knockoutGraphicItem =
2881 [&]( BOARD_ITEM* aItem )
2882 {
2883 if( aItem->IsKnockout() && aItem->IsOnLayer( aLayer )
2884 && aItem->GetBoundingBox().Intersects( zone_boundingbox ) )
2885 {
2886 addKnockout( aItem, aLayer, 0, true, clearanceHoles );
2887 }
2888 };
2889
2890 for( FOOTPRINT* footprint : m_board->Footprints() )
2891 {
2892 if( checkForCancel( m_progressReporter ) )
2893 return false;
2894
2895 knockoutGraphicItem( &footprint->Reference() );
2896 knockoutGraphicItem( &footprint->Value() );
2897
2898 for( BOARD_ITEM* item : footprint->GraphicalItems() )
2899 knockoutGraphicItem( item );
2900 }
2901
2902 for( BOARD_ITEM* item : m_board->Drawings() )
2903 {
2904 if( checkForCancel( m_progressReporter ) )
2905 return false;
2906
2907 knockoutGraphicItem( item );
2908 }
2909
2910 aFillPolys = aSmoothedOutline;
2911 aFillPolys.BooleanSubtract( clearanceHoles );
2912
2913 auto subtractKeepout =
2914 [&]( ZONE* candidate )
2915 {
2916 if( !candidate->GetIsRuleArea() )
2917 return;
2918
2919 if( !candidate->HasKeepoutParametersSet() )
2920 return;
2921
2922 if( candidate->GetDoNotAllowZoneFills() && candidate->IsOnLayer( aLayer ) )
2923 {
2924 if( candidate->GetBoundingBox().Intersects( zone_boundingbox ) )
2925 {
2926 if( candidate->Outline()->ArcCount() == 0 )
2927 {
2928 aFillPolys.BooleanSubtract( *candidate->Outline() );
2929 }
2930 else
2931 {
2932 SHAPE_POLY_SET keepoutOutline( *candidate->Outline() );
2933 keepoutOutline.ClearArcs();
2934 aFillPolys.BooleanSubtract( keepoutOutline );
2935 }
2936 }
2937 }
2938 };
2939
2940 for( ZONE* keepout : m_board->Zones() )
2941 {
2942 if( checkForCancel( m_progressReporter ) )
2943 return false;
2944
2945 subtractKeepout( keepout );
2946 }
2947
2948 for( FOOTPRINT* footprint : m_board->Footprints() )
2949 {
2950 if( checkForCancel( m_progressReporter ) )
2951 return false;
2952
2953 for( ZONE* keepout : footprint->Zones() )
2954 subtractKeepout( keepout );
2955 }
2956
2957 // Features which are min_width should survive pruning; features that are *less* than
2958 // min_width should not. Therefore we subtract epsilon from the min_width when
2959 // deflating/inflating.
2960 int half_min_width = aZone->GetMinThickness() / 2;
2961 int epsilon = pcbIUScale.mmToIU( 0.001 );
2962
2963 aFillPolys.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS, m_maxError );
2964
2965 // Remove the non filled areas due to the hatch pattern
2967 {
2968 SHAPE_POLY_SET noThermalRings; // Non-copper zones have no thermal reliefs
2969
2970 if( !addHatchFillTypeOnZone( aZone, aLayer, aLayer, aFillPolys, noThermalRings ) )
2971 return false;
2972 }
2973
2974 // Re-inflate after pruning of areas that don't meet minimum-width criteria
2975 if( half_min_width - epsilon > epsilon )
2976 aFillPolys.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS, m_maxError );
2977
2978 aFillPolys.Fracture();
2979 return true;
2980}
2981
2982
2983/*
2984 * Build the filled solid areas data from real outlines (stored in m_Poly)
2985 * The solid areas can be more than one on copper layers, and do not have holes
2986 * ( holes are linked by overlapping segments to the main outline)
2987 */
2989{
2990 SHAPE_POLY_SET* boardOutline = m_brdOutlinesValid ? &m_boardOutline : nullptr;
2991 SHAPE_POLY_SET maxExtents;
2992 SHAPE_POLY_SET smoothedPoly;
2993 PCB_LAYER_ID debugLayer = UNDEFINED_LAYER;
2994
2995 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( aLayer ) )
2996 {
2997 debugLayer = aLayer;
2998 aLayer = F_Cu;
2999 }
3000
3001 if( !aZone->BuildSmoothedPoly( maxExtents, aLayer, boardOutline, &smoothedPoly ) )
3002 return false;
3003
3004 if( m_progressReporter && m_progressReporter->IsCancelled() )
3005 return false;
3006
3007 if( aZone->IsOnCopperLayer() )
3008 {
3009 if( fillCopperZone( aZone, aLayer, debugLayer, smoothedPoly, maxExtents, aFillPolys ) )
3010 aZone->SetNeedRefill( false );
3011 }
3012 else
3013 {
3014 if( fillNonCopperZone( aZone, aLayer, smoothedPoly, aFillPolys ) )
3015 aZone->SetNeedRefill( false );
3016 }
3017
3018 return true;
3019}
3020
3021
3026 const std::vector<BOARD_ITEM*>& aSpokedPadsList,
3027 std::deque<SHAPE_LINE_CHAIN>& aSpokesList )
3028{
3029 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3030 BOX2I zoneBB = aZone->GetBoundingBox();
3031 DRC_CONSTRAINT constraint;
3032 int zone_half_width = aZone->GetMinThickness() / 2;
3033
3035 zone_half_width = aZone->GetHatchThickness() / 2;
3036
3037 zoneBB.Inflate( std::max( bds.GetBiggestClearanceValue(), aZone->GetLocalClearance().value() ) );
3038
3039 // Is a point on the boundary of the polygon inside or outside?
3040 // The boundary may be off by MaxError
3041 int epsilon = bds.m_MaxError;
3042
3043 for( BOARD_ITEM* item : aSpokedPadsList )
3044 {
3045 // We currently only connect to pads, not pad holes
3046 if( !item->IsOnLayer( aLayer ) )
3047 continue;
3048
3049 int thermalReliefGap = 0;
3050 int spoke_w = 0;
3051 PAD* pad = nullptr;
3052 PCB_VIA* via = nullptr;
3053 bool circular = false;
3054
3055 if( item->Type() == PCB_PAD_T )
3056 {
3057 pad = static_cast<PAD*>( item );
3058 VECTOR2I padSize = pad->GetSize( aLayer );
3059
3060 if( pad->GetShape( aLayer) == PAD_SHAPE::CIRCLE
3061 || ( pad->GetShape( aLayer ) == PAD_SHAPE::OVAL && padSize.x == padSize.y ) )
3062 {
3063 circular = true;
3064 }
3065 }
3066 else if( item->Type() == PCB_VIA_T )
3067 {
3068 via = static_cast<PCB_VIA*>( item );
3069 circular = true;
3070 }
3071
3072 // For hatch zones, use proper DRC constraints for thermal gap and spoke width,
3073 // just like solid zones. This ensures consistent thermal relief appearance and
3074 // respects pad-specific thermal spoke settings.
3076 {
3077 if( pad )
3078 {
3080 aZone, aLayer );
3081 thermalReliefGap = constraint.GetValue().Min();
3082
3084 aZone, aLayer );
3085 spoke_w = constraint.GetValue().Opt();
3086
3087 int spoke_max_allowed_w = std::min( pad->GetSize( aLayer ).x, pad->GetSize( aLayer ).y );
3088 spoke_w = std::clamp( spoke_w, constraint.Value().Min(), constraint.Value().Max() );
3089 spoke_w = std::min( spoke_w, spoke_max_allowed_w );
3090
3091 if( spoke_w < aZone->GetMinThickness() )
3092 continue;
3093 }
3094 else if( via )
3095 {
3097 aZone, aLayer );
3098 thermalReliefGap = constraint.GetValue().Min();
3099
3101 aZone, aLayer );
3102 spoke_w = constraint.GetValue().Opt();
3103
3104 spoke_w = std::min( spoke_w, via->GetWidth( aLayer ) );
3105
3106 if( spoke_w < aZone->GetMinThickness() )
3107 continue;
3108 }
3109 else
3110 {
3111 continue;
3112 }
3113 }
3114 else if( pad )
3115 {
3116 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, pad, aZone, aLayer );
3117 thermalReliefGap = constraint.GetValue().Min();
3118
3119 constraint = bds.m_DRCEngine->EvalRules( THERMAL_SPOKE_WIDTH_CONSTRAINT, pad, aZone, aLayer );
3120 spoke_w = constraint.GetValue().Opt();
3121
3122 // Spoke width should ideally be smaller than the pad minor axis.
3123 // Otherwise the thermal shape is not really a thermal relief,
3124 // and the algo to count the actual number of spokes can fail
3125 int spoke_max_allowed_w = std::min( pad->GetSize( aLayer ).x, pad->GetSize( aLayer ).y );
3126
3127 spoke_w = std::clamp( spoke_w, constraint.Value().Min(), constraint.Value().Max() );
3128
3129 // ensure the spoke width is smaller than the pad minor size
3130 spoke_w = std::min( spoke_w, spoke_max_allowed_w );
3131
3132 // Cannot create stubs having a width < zone min thickness
3133 if( spoke_w < aZone->GetMinThickness() )
3134 continue;
3135 }
3136 else
3137 {
3138 // We don't currently support via thermal connections *except* in a hatched zone.
3139 continue;
3140 }
3141
3142 int spoke_half_w = spoke_w / 2;
3143
3144 // Quick test here to possibly save us some work
3145 BOX2I itemBB = item->GetBoundingBox();
3146 itemBB.Inflate( thermalReliefGap + epsilon );
3147
3148 if( !( itemBB.Intersects( zoneBB ) ) )
3149 continue;
3150
3151 bool customSpokes = false;
3152
3153 if( pad && pad->GetShape( aLayer ) == PAD_SHAPE::CUSTOM )
3154 {
3155 for( const std::shared_ptr<PCB_SHAPE>& primitive : pad->GetPrimitives( aLayer ) )
3156 {
3157 if( primitive->IsProxyItem() && primitive->GetShape() == SHAPE_T::SEGMENT )
3158 {
3159 customSpokes = true;
3160 break;
3161 }
3162 }
3163 }
3164
3165 // Thermal spokes consist of square-ended segments from the pad center to points just
3166 // outside the thermal relief. The outside end has an extra center point (which must be
3167 // at idx 3) which is used for testing whether or not the spoke connects to copper in the
3168 // parent zone.
3169
3170 auto buildSpokesFromOrigin =
3171 [&]( const BOX2I& box, EDA_ANGLE angle )
3172 {
3173 VECTOR2I center = box.GetCenter();
3174 VECTOR2I half_size = KiROUND( box.GetWidth() / 2.0, box.GetHeight() / 2.0 );
3175
3176 // Function to find intersection of line with box edge
3177 auto intersectBBox =
3178 [&]( const EDA_ANGLE& spokeAngle, VECTOR2I* spoke_side ) -> VECTOR2I
3179 {
3180 double dx = spokeAngle.Cos();
3181 double dy = spokeAngle.Sin();
3182
3183 // Short-circuit the axis cases because they will be degenerate in the
3184 // intersection test
3185 if( dx == 0 )
3186 {
3187 *spoke_side = VECTOR2I( spoke_half_w, 0 );
3188 return KiROUND( 0.0, dy * half_size.y );
3189 }
3190 else if( dy == 0 )
3191 {
3192 *spoke_side = VECTOR2I( 0, spoke_half_w );
3193 return KiROUND( dx * half_size.x, 0.0 );
3194 }
3195
3196 // We are going to intersect with one side or the other. Whichever
3197 // we hit first is the fraction of the spoke length we keep
3198 double dist_x = half_size.x / std::abs( dx );
3199 double dist_y = half_size.y / std::abs( dy );
3200
3201 if( dist_x < dist_y )
3202 {
3203 *spoke_side = KiROUND( 0.0, spoke_half_w / ( ANGLE_90 - spokeAngle ).Sin() );
3204 return KiROUND( dx * dist_x, dy * dist_x );
3205 }
3206 else
3207 {
3208 *spoke_side = KiROUND( spoke_half_w / spokeAngle.Sin(), 0.0 );
3209 return KiROUND( dx * dist_y, dy * dist_y );
3210 }
3211 };
3212
3213 // Precalculate angles for four cardinal directions
3214 const EDA_ANGLE angles[4] = {
3215 EDA_ANGLE( 0.0, DEGREES_T ) + angle, // Right
3216 EDA_ANGLE( 90.0, DEGREES_T ) + angle, // Up
3217 EDA_ANGLE( 180.0, DEGREES_T ) + angle, // Left
3218 EDA_ANGLE( 270.0, DEGREES_T ) + angle // Down
3219 };
3220
3221 // Generate four spokes in cardinal directions
3222 for( const EDA_ANGLE& spokeAngle : angles )
3223 {
3224 VECTOR2I spoke_side;
3225 VECTOR2I intersection = intersectBBox( spokeAngle, &spoke_side );
3226
3227 SHAPE_LINE_CHAIN spoke;
3228 spoke.Append( center + spoke_side );
3229 spoke.Append( center - spoke_side );
3230 spoke.Append( center + intersection - spoke_side );
3231 spoke.Append( center + intersection ); // test pt
3232 spoke.Append( center + intersection + spoke_side );
3233 spoke.SetClosed( true );
3234 aSpokesList.push_back( std::move( spoke ) );
3235 }
3236 };
3237
3238 if( customSpokes )
3239 {
3240 SHAPE_POLY_SET thermalPoly;
3241 SHAPE_LINE_CHAIN thermalOutline;
3242
3243 pad->TransformShapeToPolygon( thermalPoly, aLayer, thermalReliefGap + epsilon, m_maxError, ERROR_OUTSIDE );
3244
3245 if( thermalPoly.OutlineCount() )
3246 thermalOutline = thermalPoly.Outline( 0 );
3247
3248 SHAPE_LINE_CHAIN padOutline = pad->GetEffectivePolygon( aLayer, ERROR_OUTSIDE )->Outline( 0 );
3249
3250 auto trimToOutline = [&]( SEG& aSegment )
3251 {
3252 SHAPE_LINE_CHAIN::INTERSECTIONS intersections;
3253
3254 if( padOutline.Intersect( aSegment, intersections ) )
3255 {
3256 intersections.clear();
3257
3258 // Trim the segment to the thermal outline
3259 if( thermalOutline.Intersect( aSegment, intersections ) )
3260 {
3261 aSegment.B = intersections.front().p;
3262 return true;
3263 }
3264 }
3265 return false;
3266 };
3267
3268 for( const std::shared_ptr<PCB_SHAPE>& primitive : pad->GetPrimitives( aLayer ) )
3269 {
3270 if( primitive->IsProxyItem() && primitive->GetShape() == SHAPE_T::SEGMENT )
3271 {
3272 SEG seg( primitive->GetStart(), primitive->GetEnd() );
3273 SHAPE_LINE_CHAIN::INTERSECTIONS intersections;
3274
3275 RotatePoint( seg.A, pad->GetOrientation() );
3276 RotatePoint( seg.B, pad->GetOrientation() );
3277 seg.A += pad->ShapePos( aLayer );
3278 seg.B += pad->ShapePos( aLayer );
3279
3280 // Make sure seg.A is the origin
3281 if( !pad->GetEffectivePolygon( aLayer, ERROR_OUTSIDE )->Contains( seg.A ) )
3282 {
3283 // Do not create this spoke if neither point is in the pad.
3284 if( !pad->GetEffectivePolygon( aLayer, ERROR_OUTSIDE )->Contains( seg.B ) )
3285 continue;
3286
3287 seg.Reverse();
3288 }
3289
3290 // Trim segment to pad and thermal outline polygon.
3291 // If there is no intersection with the pad, don't create the spoke.
3292 if( trimToOutline( seg ) )
3293 {
3294 VECTOR2I direction = ( seg.B - seg.A ).Resize( spoke_half_w );
3295 VECTOR2I offset = direction.Perpendicular().Resize( spoke_half_w );
3296 // Extend the spoke edges by half the spoke width to capture convex pad shapes
3297 // with a maximum of 45 degrees.
3298 SEG segL( seg.A - direction - offset, seg.B + direction - offset );
3299 SEG segR( seg.A - direction + offset, seg.B + direction + offset );
3300
3301 // Only create this spoke if both edges intersect the pad and thermal outline
3302 if( trimToOutline( segL ) && trimToOutline( segR ) )
3303 {
3304 // Extend the spoke by the minimum thickness for the zone to ensure full
3305 // connection width
3306 direction = direction.Resize( aZone->GetMinThickness() );
3307
3308 SHAPE_LINE_CHAIN spoke;
3309
3310 spoke.Append( seg.A + offset );
3311 spoke.Append( seg.A - offset );
3312
3313 spoke.Append( segL.B + direction );
3314 spoke.Append( seg.B + direction ); // test pt at index 3.
3315 spoke.Append( segR.B + direction );
3316
3317 spoke.SetClosed( true );
3318 aSpokesList.push_back( std::move( spoke ) );
3319 }
3320 }
3321 }
3322 }
3323 }
3324 else
3325 {
3326 EDA_ANGLE thermalSpokeAngle;
3327
3328 // Use pad's thermal spoke angle for both solid and hatch zones.
3329 // This ensures custom thermal spoke templates are respected.
3330 if( pad )
3331 thermalSpokeAngle = pad->GetThermalSpokeAngle();
3332
3333 BOX2I spokesBox;
3334 VECTOR2I position;
3335 EDA_ANGLE orientation;
3336
3337 // Since the bounding-box needs to be correclty rotated we use a dummy pad to keep
3338 // from dirtying the real pad's cached shapes.
3339 if( pad )
3340 {
3341 PAD dummy_pad( *pad );
3342 dummy_pad.SetOrientation( ANGLE_0 );
3343
3344 // Spokes are from center of pad shape, not from hole. So the dummy pad has no shape
3345 // offset and is at position 0,0
3346 dummy_pad.SetPosition( VECTOR2I( 0, 0 ) );
3347 dummy_pad.SetOffset( aLayer, VECTOR2I( 0, 0 ) );
3348
3349 spokesBox = dummy_pad.GetBoundingBox( aLayer );
3350 position = pad->ShapePos( aLayer );
3351 orientation = pad->GetOrientation();
3352 }
3353 else if( via )
3354 {
3355 PCB_VIA dummy_via( *via );
3356 dummy_via.SetPosition( VECTOR2I( 0, 0 ) );
3357
3358 spokesBox = dummy_via.GetBoundingBox( aLayer );
3359 position = via->GetPosition();
3360 }
3361
3362 // Add half the zone mininum width to the inflate amount to account for the fact that
3363 // the deflation procedure will shrink the results by half the half the zone min width.
3364 spokesBox.Inflate( thermalReliefGap + epsilon + zone_half_width );
3365
3366 // Yet another wrinkle: the bounding box for circles will overshoot the mark considerably
3367 // when the spokes are near a 45 degree increment. So we build the spokes at 0 degrees
3368 // and then rotate them to the correct position.
3369 if( circular )
3370 {
3371 buildSpokesFromOrigin( spokesBox, ANGLE_0 );
3372
3373 if( thermalSpokeAngle != ANGLE_0 )
3374 {
3375 // Rotate the last four elements of aspokeslist
3376 for( auto it = aSpokesList.rbegin(); it != aSpokesList.rbegin() + 4; ++it )
3377 it->Rotate( thermalSpokeAngle );
3378 }
3379 }
3380 else
3381 {
3382 buildSpokesFromOrigin( spokesBox, thermalSpokeAngle );
3383 }
3384
3385 auto spokeIter = aSpokesList.rbegin();
3386
3387 for( int ii = 0; ii < 4; ++ii, ++spokeIter )
3388 {
3389 spokeIter->Rotate( orientation );
3390 spokeIter->Move( position );
3391 }
3392 }
3393 }
3394
3395 for( size_t ii = 0; ii < aSpokesList.size(); ++ii )
3396 aSpokesList[ii].GenerateBBoxCache();
3397}
3398
3399
3401 const SHAPE_POLY_SET& aSmoothedOutline,
3402 const std::vector<BOARD_ITEM*>& aThermalConnectionPads,
3403 SHAPE_POLY_SET& aFillPolys,
3404 SHAPE_POLY_SET& aThermalRings )
3405{
3406 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3407 DRC_CONSTRAINT constraint;
3408
3409 for( BOARD_ITEM* item : aThermalConnectionPads )
3410 {
3411 if( !item->IsOnLayer( aLayer ) )
3412 continue;
3413
3414 PAD* pad = nullptr;
3415 PCB_VIA* via = nullptr;
3416 bool isCircular = false;
3417 int thermalGap = 0;
3418 int spokeWidth = 0;
3419 VECTOR2I position;
3420 int padRadius = 0;
3421
3422 if( item->Type() == PCB_PAD_T )
3423 {
3424 pad = static_cast<PAD*>( item );
3425 VECTOR2I padSize = pad->GetSize( aLayer );
3426 position = pad->ShapePos( aLayer );
3427
3428 isCircular = ( pad->GetShape( aLayer ) == PAD_SHAPE::CIRCLE
3429 || ( pad->GetShape( aLayer ) == PAD_SHAPE::OVAL && padSize.x == padSize.y ) );
3430
3431 if( isCircular )
3432 padRadius = std::max( padSize.x, padSize.y ) / 2;
3433
3434 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, pad, aZone, aLayer );
3435 thermalGap = constraint.GetValue().Min();
3436
3437 constraint = bds.m_DRCEngine->EvalRules( THERMAL_SPOKE_WIDTH_CONSTRAINT, pad, aZone, aLayer );
3438 spokeWidth = constraint.GetValue().Opt();
3439
3440 // Clamp spoke width to pad size
3441 int spokeMaxWidth = std::min( padSize.x, padSize.y );
3442 spokeWidth = std::min( spokeWidth, spokeMaxWidth );
3443 }
3444 else if( item->Type() == PCB_VIA_T )
3445 {
3446 via = static_cast<PCB_VIA*>( item );
3447 position = via->GetPosition();
3448 isCircular = true;
3449 padRadius = via->GetWidth( aLayer ) / 2;
3450
3451 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, via, aZone, aLayer );
3452 thermalGap = constraint.GetValue().Min();
3453
3454 constraint = bds.m_DRCEngine->EvalRules( THERMAL_SPOKE_WIDTH_CONSTRAINT, via, aZone, aLayer );
3455 spokeWidth = constraint.GetValue().Opt();
3456
3457 // Clamp spoke width to via diameter
3458 spokeWidth = std::min( spokeWidth, padRadius * 2 );
3459 }
3460 else
3461 {
3462 continue;
3463 }
3464
3465 // Don't create a ring if spoke width is too small
3466 if( spokeWidth < aZone->GetMinThickness() )
3467 continue;
3468
3469 SHAPE_POLY_SET thermalRing;
3470
3471 if( isCircular )
3472 {
3473 // For circular pads/vias: create an arc ring
3474 // Ring inner radius = pad radius + thermal gap
3475 // Ring width = spoke width
3476 int ringInnerRadius = padRadius + thermalGap;
3477 int ringWidth = spokeWidth;
3478
3479 TransformRingToPolygon( thermalRing, position, ringInnerRadius + ringWidth / 2,
3480 ringWidth, m_maxError, ERROR_OUTSIDE );
3481 }
3482 else
3483 {
3484 // For non-circular pads: create ring by inflating pad to outer radius,
3485 // then subtracting pad inflated to inner radius
3486 SHAPE_POLY_SET outerShape;
3487 SHAPE_POLY_SET innerShape;
3488
3489 // Outer ring edge = pad + thermal gap + spoke width
3490 pad->TransformShapeToPolygon( outerShape, aLayer, thermalGap + spokeWidth,
3492
3493 // Inner ring edge = pad + thermal gap (this is already knocked out)
3494 pad->TransformShapeToPolygon( innerShape, aLayer, thermalGap,
3496
3497 thermalRing = outerShape;
3498 thermalRing.BooleanSubtract( innerShape );
3499 }
3500
3501 // Clip the thermal ring to the zone boundary so it doesn't overflow
3502 thermalRing.BooleanIntersection( aSmoothedOutline );
3503
3504 // Add the thermal ring to the fill
3505 aFillPolys.BooleanAdd( thermalRing );
3506
3507 // Also collect thermal rings for hatch hole notching to ensure connectivity
3508 aThermalRings.BooleanAdd( thermalRing );
3509 }
3510}
3511
3512
3514 PCB_LAYER_ID aDebugLayer, SHAPE_POLY_SET& aFillPolys,
3515 const SHAPE_POLY_SET& aThermalRings )
3516{
3517 // Build grid:
3518
3519 // obviously line thickness must be > zone min thickness.
3520 // It can happens if a board file was edited by hand by a python script
3521 // Use 1 micron margin to be *sure* there is no issue in Gerber files
3522 // (Gbr file unit = 1 or 10 nm) due to some truncation in coordinates or calculations
3523 // This margin also avoid problems due to rounding coordinates in next calculations
3524 // that can create incorrect polygons
3525 int thickness = std::max( aZone->GetHatchThickness(),
3526 aZone->GetMinThickness() + pcbIUScale.mmToIU( 0.001 ) );
3527
3528 int gridsize = thickness + aZone->GetHatchGap();
3529 int maxError = m_board->GetDesignSettings().m_MaxError;
3530
3531 SHAPE_POLY_SET filledPolys = aFillPolys.CloneDropTriangulation();
3532 // Use a area that contains the rotated bbox by orientation, and after rotate the result
3533 // by -orientation.
3534 if( !aZone->GetHatchOrientation().IsZero() )
3535 filledPolys.Rotate( - aZone->GetHatchOrientation() );
3536
3537 BOX2I bbox = filledPolys.BBox( 0 );
3538
3539 // Build hole shape
3540 // the hole size is aZone->GetHatchGap(), but because the outline thickness
3541 // is aZone->GetMinThickness(), the hole shape size must be larger
3542 SHAPE_LINE_CHAIN hole_base;
3543 int hole_size = aZone->GetHatchGap() + aZone->GetMinThickness();
3544 VECTOR2I corner( 0, 0 );;
3545 hole_base.Append( corner );
3546 corner.x += hole_size;
3547 hole_base.Append( corner );
3548 corner.y += hole_size;
3549 hole_base.Append( corner );
3550 corner.x = 0;
3551 hole_base.Append( corner );
3552 hole_base.SetClosed( true );
3553
3554 // Calculate minimal area of a grid hole.
3555 // All holes smaller than a threshold will be removed
3556 double minimal_hole_area = hole_base.Area() * aZone->GetHatchHoleMinArea();
3557
3558 // Now convert this hole to a smoothed shape:
3559 if( aZone->GetHatchSmoothingLevel() > 0 )
3560 {
3561 // the actual size of chamfer, or rounded corner radius is the half size
3562 // of the HatchFillTypeGap scaled by aZone->GetHatchSmoothingValue()
3563 // aZone->GetHatchSmoothingValue() = 1.0 is the max value for the chamfer or the
3564 // radius of corner (radius = half size of the hole)
3565 int smooth_value = KiROUND( aZone->GetHatchGap()
3566 * aZone->GetHatchSmoothingValue() / 2 );
3567
3568 // Minimal optimization:
3569 // make smoothing only for reasonable smooth values, to avoid a lot of useless segments
3570 // and if the smooth value is small, use chamfer even if fillet is requested
3571 #define SMOOTH_MIN_VAL_MM 0.02
3572 #define SMOOTH_SMALL_VAL_MM 0.04
3573
3574 if( smooth_value > pcbIUScale.mmToIU( SMOOTH_MIN_VAL_MM ) )
3575 {
3576 SHAPE_POLY_SET smooth_hole;
3577 smooth_hole.AddOutline( hole_base );
3578 int smooth_level = aZone->GetHatchSmoothingLevel();
3579
3580 if( smooth_value < pcbIUScale.mmToIU( SMOOTH_SMALL_VAL_MM ) && smooth_level > 1 )
3581 smooth_level = 1;
3582
3583 // Use a larger smooth_value to compensate the outline tickness
3584 // (chamfer is not visible is smooth value < outline thickess)
3585 smooth_value += aZone->GetMinThickness() / 2;
3586
3587 // smooth_value cannot be bigger than the half size oh the hole:
3588 smooth_value = std::min( smooth_value, aZone->GetHatchGap() / 2 );
3589
3590 // the error to approximate a circle by segments when smoothing corners by a arc
3591 maxError = std::max( maxError * 2, smooth_value / 20 );
3592
3593 switch( smooth_level )
3594 {
3595 case 1:
3596 // Chamfer() uses the distance from a corner to create a end point
3597 // for the chamfer.
3598 hole_base = smooth_hole.Chamfer( smooth_value ).Outline( 0 );
3599 break;
3600
3601 default:
3602 if( aZone->GetHatchSmoothingLevel() > 2 )
3603 maxError /= 2; // Force better smoothing
3604
3605 hole_base = smooth_hole.Fillet( smooth_value, maxError ).Outline( 0 );
3606 break;
3607
3608 case 0:
3609 break;
3610 };
3611 }
3612 }
3613
3614 // Build holes
3615 SHAPE_POLY_SET holes;
3616
3617 auto& defaultOffsets = m_board->GetDesignSettings().m_ZoneLayerProperties;
3618 auto& localOffsets = aZone->LayerProperties();
3619
3620 VECTOR2I offset = defaultOffsets[aLayer].hatching_offset.value_or( VECTOR2I() );
3621
3622 if( localOffsets.contains( aLayer ) && localOffsets.at( aLayer ).hatching_offset.has_value() )
3623 offset = localOffsets.at( aLayer ).hatching_offset.value();
3624
3625 int x_offset = bbox.GetX() - ( bbox.GetX() ) % gridsize - gridsize;
3626 int y_offset = bbox.GetY() - ( bbox.GetY() ) % gridsize - gridsize;
3627
3628
3629 for( int xx = x_offset; xx <= bbox.GetRight(); xx += gridsize )
3630 {
3631 for( int yy = y_offset; yy <= bbox.GetBottom(); yy += gridsize )
3632 {
3633 // Generate hole
3634 SHAPE_LINE_CHAIN hole( hole_base );
3635 hole.Move( VECTOR2I( xx, yy ) );
3636
3637 if( !aZone->GetHatchOrientation().IsZero() )
3638 {
3639 hole.Rotate( aZone->GetHatchOrientation() );
3640 }
3641
3642 hole.Move( VECTOR2I( offset.x % gridsize, offset.y % gridsize ) );
3643
3644 holes.AddOutline( hole );
3645 }
3646 }
3647
3648 holes.ClearArcs();
3649
3650 DUMP_POLYS_TO_COPPER_LAYER( holes, In10_Cu, wxT( "hatch-holes" ) );
3651
3652 int deflated_thickness = aZone->GetHatchThickness() - aZone->GetMinThickness();
3653
3654 // Don't let thickness drop below maxError * 2 or it might not get reinflated.
3655 deflated_thickness = std::max( deflated_thickness, maxError * 2 );
3656
3657 // The fill has already been deflated to ensure GetMinThickness() so we just have to
3658 // account for anything beyond that.
3659 SHAPE_POLY_SET deflatedFilledPolys = aFillPolys.CloneDropTriangulation();
3660 deflatedFilledPolys.ClearArcs();
3661 deflatedFilledPolys.Deflate( deflated_thickness, CORNER_STRATEGY::CHAMFER_ALL_CORNERS, maxError );
3662 holes.BooleanIntersection( deflatedFilledPolys );
3663 DUMP_POLYS_TO_COPPER_LAYER( holes, In11_Cu, wxT( "fill-clipped-hatch-holes" ) );
3664
3665 SHAPE_POLY_SET deflatedOutline = aZone->Outline()->CloneDropTriangulation();
3666 deflatedOutline.ClearArcs();
3667 deflatedOutline.Deflate( aZone->GetMinThickness(), CORNER_STRATEGY::CHAMFER_ALL_CORNERS, maxError );
3668 holes.BooleanIntersection( deflatedOutline );
3669 DUMP_POLYS_TO_COPPER_LAYER( holes, In12_Cu, wxT( "outline-clipped-hatch-holes" ) );
3670
3671 // Now filter truncated holes to avoid small holes in pattern
3672 // It happens for holes near the zone outline
3673 for( int ii = 0; ii < holes.OutlineCount(); )
3674 {
3675 double area = holes.Outline( ii ).Area();
3676
3677 if( area < minimal_hole_area ) // The current hole is too small: remove it
3678 holes.DeletePolygon( ii );
3679 else
3680 ++ii;
3681 }
3682
3683 // Drop any holes that completely enclose a thermal ring to ensure thermal reliefs
3684 // stay connected to the hatch webbing. Only drop holes where the thermal ring is
3685 // entirely inside the hole; partial overlaps are kept to preserve the hatch pattern.
3686 if( aThermalRings.OutlineCount() > 0 )
3687 {
3688 BOX2I thermalBBox = aThermalRings.BBox();
3689
3690 // Iterate through holes (backwards since we may delete)
3691 for( int holeIdx = holes.OutlineCount() - 1; holeIdx >= 0; holeIdx-- )
3692 {
3693 const SHAPE_LINE_CHAIN& hole = holes.Outline( holeIdx );
3694 BOX2I holeBBox = hole.BBox();
3695
3696 // Quick rejection: skip if hole bbox doesn't intersect thermal rings bbox
3697 if( !holeBBox.Intersects( thermalBBox ) )
3698 continue;
3699
3700 // Check if ANY thermal ring is completely enclosed by this hole
3701 for( int ringIdx = 0; ringIdx < aThermalRings.OutlineCount(); ringIdx++ )
3702 {
3703 const SHAPE_LINE_CHAIN& ring = aThermalRings.Outline( ringIdx );
3704 BOX2I ringBBox = ring.BBox();
3705 VECTOR2I ringCenter = ringBBox.Centre();
3706
3707 // Quick rejection: hole bbox must contain ring bbox
3708 if( !holeBBox.Contains( ringBBox ) )
3709 continue;
3710
3711 // Check 1: Is the ring center inside the hole?
3712 if( !hole.PointInside( ringCenter ) )
3713 continue;
3714
3715 // Check 2: Is at least one point on the ring inside the hole?
3716 if( ring.PointCount() == 0 || !hole.PointInside( ring.CPoint( 0 ) ) )
3717 continue;
3718
3719 // Check 3: Does the ring outline NOT intersect the hole outline?
3720 // If there's no intersection, the ring is fully enclosed (not touching edges)
3721 SHAPE_LINE_CHAIN::INTERSECTIONS intersections;
3722 ring.Intersect( hole, intersections );
3723
3724 if( intersections.empty() )
3725 {
3726 // This hole completely encloses a ring - drop it
3727 holes.DeletePolygon( holeIdx );
3728 break; // Move to next hole
3729 }
3730 }
3731 }
3732 }
3733
3734 // create grid. Useto
3735 // generate strictly simple polygons needed by Gerber files and Fracture()
3736 aFillPolys.BooleanSubtract( aFillPolys, holes );
3737 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In14_Cu, wxT( "after-hatching" ) );
3738
3739 return true;
3740}
3741
3742
3744{
3745 auto cacheKey = std::make_pair( static_cast<const ZONE*>( aZone ), aLayer );
3746
3747 {
3748 std::lock_guard<std::mutex> lock( m_cacheMutex );
3749 auto it = m_preKnockoutFillCache.find( cacheKey );
3750
3751 if( it == m_preKnockoutFillCache.end() )
3752 {
3753 wxLogTrace( traceZoneFiller, wxT( "Cache miss for zone %s layer %d (cache size: %zu)" ),
3754 aZone->GetNetname(), static_cast<int>( aLayer ), m_preKnockoutFillCache.size() );
3755 return false;
3756 }
3757
3758 wxLogTrace( traceZoneFiller, wxT( "Cache hit for zone %s layer %d: pre-knockout %d outlines" ),
3759 aZone->GetNetname(), static_cast<int>( aLayer ), it->second.OutlineCount() );
3760
3761 // Restore the cached pre-knockout fill
3762 aFillPolys = it->second;
3763 }
3764
3765 // Subtract the FILLED area of higher-priority zones (with clearance for different nets).
3766 // For same-net zones: subtract the filled area directly.
3767 // For different-net zones: subtract the filled area with DRC-evaluated clearance plus
3768 // extra_margin and m_maxError to match the margins used in the initial fill. Without these
3769 // margins, polygon approximation error can produce fills that violate clearance (issue 23053).
3770 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3771 int extra_margin = pcbIUScale.mmToIU( ADVANCED_CFG::GetCfg().m_ExtraClearance );
3772 BOX2I zoneBBox = aZone->GetBoundingBox();
3773 zoneBBox.Inflate( m_worstClearance + extra_margin );
3774
3775 auto evalRulesForItems =
3776 [&bds]( DRC_CONSTRAINT_T aConstraint, const BOARD_ITEM* a, const BOARD_ITEM* b,
3777 PCB_LAYER_ID aEvalLayer ) -> int
3778 {
3779 DRC_CONSTRAINT c = bds.m_DRCEngine->EvalRules( aConstraint, a, b, aEvalLayer );
3780
3781 if( c.IsNull() )
3782 return -1;
3783 else
3784 return c.GetValue().Min();
3785 };
3786
3787 auto knockoutZoneFill =
3788 [&]( ZONE* otherZone )
3789 {
3790 if( otherZone == aZone )
3791 return;
3792
3793 if( !otherZone->GetLayerSet().test( aLayer ) )
3794 return;
3795
3796 if( otherZone->IsTeardropArea() )
3797 return;
3798
3799 if( !otherZone->HigherPriority( aZone ) )
3800 return;
3801
3802 if( !otherZone->GetBoundingBox().Intersects( zoneBBox ) )
3803 return;
3804
3805 if( !otherZone->HasFilledPolysForLayer( aLayer ) )
3806 return;
3807
3808 std::shared_ptr<SHAPE_POLY_SET> otherFill = otherZone->GetFilledPolysList( aLayer );
3809
3810 if( !otherFill || otherFill->OutlineCount() == 0 )
3811 return;
3812
3813 if( otherZone->SameNet( aZone ) )
3814 {
3815 aFillPolys.BooleanSubtract( *otherFill );
3816 }
3817 else
3818 {
3819 int gap = std::max( 0, evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT,
3820 aZone, otherZone, aLayer ) );
3821
3822 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone,
3823 otherZone, aLayer ) );
3824
3825 if( gap < 0 )
3826 return;
3827
3828 SHAPE_POLY_SET inflatedFill = *otherFill;
3829 inflatedFill.Inflate( gap + extra_margin + m_maxError,
3831 aFillPolys.BooleanSubtract( inflatedFill );
3832 }
3833 };
3834
3835 for( ZONE* otherZone : m_board->Zones() )
3836 knockoutZoneFill( otherZone );
3837
3838 for( FOOTPRINT* footprint : m_board->Footprints() )
3839 {
3840 for( ZONE* otherZone : footprint->Zones() )
3841 knockoutZoneFill( otherZone );
3842 }
3843
3844 // Subtract keepout zones (rule areas with do-not-fill)
3845 auto subtractKeepout =
3846 [&]( ZONE* candidate )
3847 {
3848 if( !candidate->GetIsRuleArea() )
3849 return;
3850
3851 if( !candidate->HasKeepoutParametersSet() )
3852 return;
3853
3854 if( candidate->GetDoNotAllowZoneFills() && candidate->IsOnLayer( aLayer ) )
3855 {
3856 if( candidate->GetBoundingBox().Intersects( zoneBBox ) )
3857 {
3858 if( candidate->Outline()->ArcCount() == 0 )
3859 {
3860 aFillPolys.BooleanSubtract( *candidate->Outline() );
3861 }
3862 else
3863 {
3864 SHAPE_POLY_SET keepoutOutline( *candidate->Outline() );
3865 keepoutOutline.ClearArcs();
3866 aFillPolys.BooleanSubtract( keepoutOutline );
3867 }
3868 }
3869 }
3870 };
3871
3872 for( ZONE* keepout : m_board->Zones() )
3873 subtractKeepout( keepout );
3874
3875 for( FOOTPRINT* footprint : m_board->Footprints() )
3876 {
3877 for( ZONE* keepout : footprint->Zones() )
3878 subtractKeepout( keepout );
3879 }
3880
3881 aFillPolys.Fracture();
3882
3883 return true;
3884}
@ ERROR_OUTSIDE
@ ERROR_INSIDE
bool operator==(const wxAuiPaneInfo &aLhs, const wxAuiPaneInfo &aRhs)
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:112
@ ZLO_FORCE_NO_ZONE_CONNECTION
Definition board_item.h:75
@ ZLO_FORCE_FLASHED
Definition board_item.h:74
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:990
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
Container for design settings for a BOARD object.
std::shared_ptr< DRC_ENGINE > m_DRCEngine
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
virtual void TransformShapeToPolygon(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aError, ERROR_LOC aErrorLoc, bool ignoreLineWidth=false) const
Convert the item shape to a closed polygon.
virtual void SetIsKnockout(bool aKnockout)
Definition board_item.h:325
virtual const BOARD * GetBoard() const
Return the BOARD in which this BOARD_ITEM resides, or NULL if none.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:322
int GetCopperLayerCount() const
Definition board.cpp:919
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1082
constexpr int GetSizeMax() const
Definition box2.h:235
constexpr BOX2< Vec > & Inflate(coord_type dx, coord_type dy)
Inflates the rectangle horizontally by dx and vertically by dy.
Definition box2.h:558
constexpr coord_type GetY() const
Definition box2.h:208
constexpr size_type GetWidth() const
Definition box2.h:214
constexpr Vec Centre() const
Definition box2.h:97
constexpr coord_type GetX() const
Definition box2.h:207
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:658
constexpr const Vec GetCenter() const
Definition box2.h:230
constexpr size_type GetHeight() const
Definition box2.h:215
constexpr bool Contains(const Vec &aPoint) const
Definition box2.h:168
constexpr coord_type GetRight() const
Definition box2.h:217
constexpr bool Intersects(const BOX2< Vec > &aRect) const
Definition box2.h:311
constexpr coord_type GetBottom() const
Definition box2.h:222
Represent a set of changes (additions, deletions or modifications) of a data model (e....
Definition commit.h:72
MINOPTMAX< int > & Value()
Definition drc_rule.h:188
const MINOPTMAX< int > & GetValue() const
Definition drc_rule.h:187
ZONE_CONNECTION m_ZoneConnection
Definition drc_rule.h:231
bool IsNull() const
Definition drc_rule.h:182
DRC_CONSTRAINT EvalRules(DRC_CONSTRAINT_T aConstraintType, const BOARD_ITEM *a, const BOARD_ITEM *b, PCB_LAYER_ID aLayer, REPORTER *aReporter=nullptr)
DRC_CONSTRAINT EvalZoneConnection(const BOARD_ITEM *a, const BOARD_ITEM *b, PCB_LAYER_ID aLayer, REPORTER *aReporter=nullptr)
double Sin() const
Definition eda_angle.h:178
double AsDegrees() const
Definition eda_angle.h:116
bool IsZero() const
Definition eda_angle.h:136
double Cos() const
Definition eda_angle.h:197
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:111
Helper class to create more flexible dialogs, including 'do not show again' checkbox handling.
Definition kidialog.h:42
void DoNotShowCheckbox(wxString file, int line)
Shows the 'do not show again' checkbox.
Definition kidialog.cpp:55
bool SetOKCancelLabels(const ButtonLabel &ok, const ButtonLabel &cancel) override
Definition kidialog.h:52
int ShowModal() override
Definition kidialog.cpp:93
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static LSET AllCuMask()
return AllCuMask( MAX_CU_LAYERS );
Definition lset.cpp:608
static const LSET & InternalCuMask()
Return a complete set of internal copper layers which is all Cu layers except F_Cu and B_Cu.
Definition lset.cpp:577
T Min() const
Definition minoptmax.h:33
T Max() const
Definition minoptmax.h:34
T Opt() const
Definition minoptmax.h:35
A PADSTACK defines the characteristics of a single or multi-layer pad, in the IPC sense of the word.
Definition padstack.h:157
UNCONNECTED_LAYER_MODE UnconnectedLayerMode() const
Definition padstack.h:363
DRILL_PROPS & Drill()
Definition padstack.h:348
Definition pad.h:55
const BOX2I GetBoundingBox() const override
The bounding box is cached, so this will be efficient most of the time.
Definition pad.cpp:1332
PAD_SHAPE GetShape(PCB_LAYER_ID aLayer) const
Definition pad.h:196
void SetOffset(PCB_LAYER_ID aLayer, const VECTOR2I &aOffset)
Definition pad.h:323
void SetPosition(const VECTOR2I &aPos) override
Definition pad.h:203
void SetOrientation(const EDA_ANGLE &aAngle)
Set the rotation angle of the pad.
Definition pad.cpp:1429
bool TransformHoleToPolygon(SHAPE_POLY_SET &aBuffer, int aClearance, int aError, ERROR_LOC aErrorLoc=ERROR_INSIDE) const
Build the corner list of the polygonal drill shape in the board coordinate system.
Definition pad.cpp:2491
void GetBoundingHull(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aMaxError, ERROR_LOC aErrorLoc=ERROR_INSIDE) const
Abstract dimension API.
void TransformShapeToPolygon(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aError, ERROR_LOC aErrorLoc, bool aIgnoreLineWidth=false) const override
Convert the item shape to a closed polygon.
void TransformTextToPolySet(SHAPE_POLY_SET &aBuffer, int aClearance, int aMaxError, ERROR_LOC aErrorLoc) const
Function TransformTextToPolySet Convert the text to a polygonSet describing the actual character stro...
Definition pcb_text.cpp:570
void SetPosition(const VECTOR2I &aPoint) override
Definition pcb_track.h:615
const BOX2I GetBoundingBox() const override
Return the orthogonal bounding box of this object for display purposes.
A small class to help profiling.
Definition profile.h:49
double msecs(bool aSinceLast=false)
Definition profile.h:149
A progress reporter interface for use in multi-threaded environments.
int m_vertex2
RESULTS(int aOutline1, int aOutline2, int aVertex1, int aVertex2)
int m_outline2
int m_outline1
int m_vertex1
bool operator<(const RESULTS &aOther) const
Definition seg.h:42
VECTOR2I A
Definition seg.h:49
VECTOR2I::extended_type ecoord
Definition seg.h:44
VECTOR2I B
Definition seg.h:50
static SEG::ecoord Square(int a)
Definition seg.h:123
void Reverse()
Definition seg.h:368
bool PointInside(const VECTOR2I &aPt, int aAccuracy=0, bool aUseBBoxCache=false) const override
Check if point aP lies inside a closed shape.
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
void Move(const VECTOR2I &aVector) override
void SetClosed(bool aClosed)
Mark the line chain as closed (i.e.
int Intersect(const SEG &aSeg, INTERSECTIONS &aIp) const
Find all intersection points between our line chain and the segment aSeg.
int PointCount() const
Return the number of points (vertices) in this line chain.
void Clear()
Remove all points from the line chain.
double Area(bool aAbsolute=true) const
Return the area of this chain.
void Append(int aX, int aY, bool aAllowDuplication=false)
Append a new point at the end of the line chain.
void Rotate(const EDA_ANGLE &aAngle, const VECTOR2I &aCenter={ 0, 0 }) override
Rotate all vertices by a given angle.
const VECTOR2I & CPoint(int aIndex) const
Return a reference to a given point in the line chain.
std::vector< INTERSECTION > INTERSECTIONS
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
Represent a set of closed polygons.
void Rotate(const EDA_ANGLE &aAngle, const VECTOR2I &aCenter={ 0, 0 }) override
Rotate all vertices by a given angle.
SHAPE_POLY_SET Chamfer(int aDistance)
Return a chamfered version of the polygon set.
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.
void DeletePolygon(int aIdx)
Delete aIdx-th polygon from the set.
double Area()
Return the area of this poly set.
bool Collide(const SHAPE *aShape, int aClearance=0, int *aActual=nullptr, VECTOR2I *aLocation=nullptr) const override
Check if the boundary of shape (this) lies closer to the shape aShape than aClearance,...
POLYGON & Polygon(int aIndex)
Return the aIndex-th subpolygon in the 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)
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.
void BuildBBoxCaches() const
Construct BBoxCaches for Contains(), below.
int OutlineCount() const
Return the number of outlines in the set.
SHAPE_POLY_SET Fillet(int aRadius, int aErrorMax)
Return a filleted version of the polygon set.
void Fracture(bool aSimplify=true)
Convert a set of polygons with holes to a single outline with "slits"/"fractures" connecting the oute...
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 BOX2I BBoxFromCaches() const
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
constexpr extended_type SquaredEuclideanNorm() const
Compute the squared euclidean norm of the vector, which is defined as (x ** 2 + y ** 2).
Definition vector2d.h:307
constexpr VECTOR2< T > Perpendicular() const
Compute the perpendicular vector.
Definition vector2d.h:314
VECTOR2< T > Resize(T aNewLength) const
Return a vector of the same direction, but length specified in aNewLength.
Definition vector2d.h:385
VERTEX * getPoint(VERTEX *aPt) const
std::set< RESULTS > GetResults() const
VERTEX_CONNECTOR(const BOX2I &aBBox, const SHAPE_POLY_SET &aPolys, int aDist)
std::set< RESULTS > m_results
std::deque< VERTEX > m_vertices
Definition vertex_set.h:343
friend class VERTEX
Definition vertex_set.h:255
VERTEX * createList(const SHAPE_LINE_CHAIN &points, VERTEX *aTail=nullptr, void *aUserData=nullptr)
Create a list of vertices from a line chain.
void SetBoundingBox(const BOX2I &aBBox)
VERTEX_SET(int aSimplificationLevel)
Definition vertex_set.h:258
uint32_t zOrder(const double aX, const double aY) const
Note that while the inputs are doubles, these are scaled by the size of the bounding box to fit into ...
const double x
Definition vertex_set.h:235
VERTEX * next
Definition vertex_set.h:241
VERTEX * prevZ
Definition vertex_set.h:247
void updateList()
After inserting or changing nodes, this function should be called to remove duplicate vertices and en...
Definition vertex_set.h:121
VERTEX * nextZ
Definition vertex_set.h:248
VERTEX * prev
Definition vertex_set.h:240
const int i
Definition vertex_set.h:234
void * GetUserData() const
Definition vertex_set.h:79
uint32_t z
Definition vertex_set.h:244
bool isEar(bool aMatchUserData=false) const
Check whether the given vertex is in the middle of an ear.
const double y
Definition vertex_set.h:236
COMMIT * m_commit
bool refillZoneFromCache(ZONE *aZone, PCB_LAYER_ID aLayer, SHAPE_POLY_SET &aFillPolys)
Refill a zone from cached pre-knockout fill.
void buildCopperItemClearances(const ZONE *aZone, PCB_LAYER_ID aLayer, const std::vector< PAD * > &aNoConnectionPads, SHAPE_POLY_SET &aHoles, bool aIncludeZoneClearances=true)
Removes clearance from the shape for copper items which share the zone's layer but are not connected ...
int m_worstClearance
bool m_debugZoneFiller
void buildHatchZoneThermalRings(const ZONE *aZone, PCB_LAYER_ID aLayer, const SHAPE_POLY_SET &aSmoothedOutline, const std::vector< BOARD_ITEM * > &aThermalConnectionPads, SHAPE_POLY_SET &aFillPolys, SHAPE_POLY_SET &aThermalRings)
Build thermal rings for pads in hatch zones.
void connect_nearby_polys(SHAPE_POLY_SET &aPolys, double aDistance)
Create strands of zero-width between elements of SHAPE_POLY_SET that are within aDistance of each oth...
void buildThermalSpokes(const ZONE *box, PCB_LAYER_ID aLayer, const std::vector< BOARD_ITEM * > &aSpokedPadsList, std::deque< SHAPE_LINE_CHAIN > &aSpokes)
Function buildThermalSpokes Constructs a list of all thermal spokes for the given zone.
void buildDifferentNetZoneClearances(const ZONE *aZone, PCB_LAYER_ID aLayer, SHAPE_POLY_SET &aHoles)
Build clearance knockout holes for higher-priority zones on different nets.
ZONE_FILLER(BOARD *aBoard, COMMIT *aCommit)
void subtractHigherPriorityZones(const ZONE *aZone, PCB_LAYER_ID aLayer, SHAPE_POLY_SET &aRawFill)
Removes the outlines of higher-proirity zones with the same net.
void knockoutThermalReliefs(const ZONE *aZone, PCB_LAYER_ID aLayer, SHAPE_POLY_SET &aFill, std::vector< BOARD_ITEM * > &aThermalConnectionPads, std::vector< PAD * > &aNoConnectionPads)
Removes thermal reliefs from the shape for any pads connected to the zone.
void addKnockout(BOARD_ITEM *aItem, PCB_LAYER_ID aLayer, int aGap, SHAPE_POLY_SET &aHoles)
Add a knockout for a pad or via.
SHAPE_POLY_SET m_boardOutline
std::map< std::pair< const ZONE *, PCB_LAYER_ID >, SHAPE_POLY_SET > m_preKnockoutFillCache
bool m_brdOutlinesValid
void SetProgressReporter(PROGRESS_REPORTER *aReporter)
std::mutex m_cacheMutex
BOARD * m_board
PROGRESS_REPORTER * m_progressReporter
bool fillCopperZone(const ZONE *aZone, PCB_LAYER_ID aLayer, PCB_LAYER_ID aDebugLayer, const SHAPE_POLY_SET &aSmoothedOutline, const SHAPE_POLY_SET &aMaxExtents, SHAPE_POLY_SET &aFillPolys)
Function fillCopperZone Add non copper areas polygons (pads and tracks with clearance) to a filled co...
void addHoleKnockout(PAD *aPad, int aGap, SHAPE_POLY_SET &aHoles)
Add a knockout for a pad's hole.
bool fillNonCopperZone(const ZONE *candidate, PCB_LAYER_ID aLayer, const SHAPE_POLY_SET &aSmoothedOutline, SHAPE_POLY_SET &aFillPolys)
bool addHatchFillTypeOnZone(const ZONE *aZone, PCB_LAYER_ID aLayer, PCB_LAYER_ID aDebugLayer, SHAPE_POLY_SET &aFillPolys, const SHAPE_POLY_SET &aThermalRings)
for zones having the ZONE_FILL_MODE::ZONE_FILL_MODE::HATCH_PATTERN, create a grid pattern in filled a...
bool fillSingleZone(ZONE *aZone, PCB_LAYER_ID aLayer, SHAPE_POLY_SET &aFillPolys)
Build the filled solid areas polygons from zone outlines (stored in m_Poly) The solid areas can be mo...
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:73
void SetNeedRefill(bool aNeedRefill)
Definition zone.h:301
std::optional< int > GetLocalClearance() const override
Definition zone.cpp:846
ZONE_LAYER_PROPERTIES & LayerProperties(PCB_LAYER_ID aLayer)
Definition zone.h:149
void CacheTriangulation(PCB_LAYER_ID aLayer=UNDEFINED_LAYER)
Create a list of triangles that "fill" the solid areas used for instance to draw these solid areas on...
Definition zone.cpp:1353
const BOX2I GetBoundingBox() const override
Definition zone.cpp:651
SHAPE_POLY_SET * Outline()
Definition zone.h:340
void SetFillFlag(PCB_LAYER_ID aLayer, bool aFlag)
Definition zone.h:291
void SetFilledPolysList(PCB_LAYER_ID aLayer, const SHAPE_POLY_SET &aPolysList)
Set the list of filled polygons.
Definition zone.h:634
int GetMinThickness() const
Definition zone.h:306
bool HigherPriority(const ZONE *aOther) const
Definition zone.cpp:436
int GetHatchThickness() const
Definition zone.h:315
double GetHatchHoleMinArea() const
Definition zone.h:330
bool IsTeardropArea() const
Definition zone.h:694
EDA_ANGLE GetHatchOrientation() const
Definition zone.h:321
bool BuildSmoothedPoly(SHAPE_POLY_SET &aSmoothedPoly, PCB_LAYER_ID aLayer, SHAPE_POLY_SET *aBoardOutline, SHAPE_POLY_SET *aSmoothedPolyWithApron=nullptr) const
Definition zone.cpp:1419
ZONE_FILL_MODE GetFillMode() const
Definition zone.h:224
int GetHatchGap() const
Definition zone.h:318
double GetHatchSmoothingValue() const
Definition zone.h:327
int GetHatchSmoothingLevel() const
Definition zone.h:324
bool IsOnCopperLayer() const override
Definition zone.cpp:543
std::mutex & GetLock()
Definition zone.h:280
unsigned GetAssignedPriority() const
Definition zone.h:125
void TransformRingToPolygon(SHAPE_POLY_SET &aBuffer, const VECTOR2I &aCentre, int aRadius, int aWidth, int aError, ERROR_LOC aErrorLoc)
Convert arcs to multiple straight segments.
void TransformCircleToPolygon(SHAPE_LINE_CHAIN &aBuffer, const VECTOR2I &aCenter, int aRadius, int aError, ERROR_LOC aErrorLoc, int aMinSegCount=0)
Convert a circle to a polygon, using multiple straight lines.
void BuildConvexHull(std::vector< VECTOR2I > &aResult, const std::vector< VECTOR2I > &aPoly)
Calculate the convex hull of a list of points in counter-clockwise order.
CORNER_STRATEGY
define how inflate transform build inflated polygon
@ CHAMFER_ALL_CORNERS
All angles are chamfered.
@ ROUND_ALL_CORNERS
All angles are rounded.
DRC_CONSTRAINT_T
Definition drc_rule.h:47
@ EDGE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:53
@ PHYSICAL_HOLE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:78
@ CLEARANCE_CONSTRAINT
Definition drc_rule.h:49
@ THERMAL_SPOKE_WIDTH_CONSTRAINT
Definition drc_rule.h:64
@ THERMAL_RELIEF_GAP_CONSTRAINT
Definition drc_rule.h:63
@ HOLE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:51
@ PHYSICAL_CLEARANCE_CONSTRAINT
Definition drc_rule.h:77
#define _(s)
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:411
static constexpr EDA_ANGLE ANGLE_90
Definition eda_angle.h:413
@ DEGREES_T
Definition eda_angle.h:31
@ SEGMENT
Definition eda_shape.h:45
a few functions useful in geometry calculations.
bool m_ZoneFillIterativeRefill
Enable iterative zone filling to handle isolated islands in higher priority zones.
bool m_DebugZoneFiller
A mode that dumps the various stages of a F_Cu fill into In1_Cu through In9_Cu.
static const wxChar traceZoneFiller[]
Trace mask for zone filler timing information.
static constexpr std::size_t hash_val(const Types &... args)
Definition hash.h:51
@ ALWAYS_FLASHED
Always flashed for connectivity.
Definition layer_ids.h:186
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:60
@ In11_Cu
Definition layer_ids.h:76
@ In17_Cu
Definition layer_ids.h:82
@ Edge_Cuts
Definition layer_ids.h:112
@ In9_Cu
Definition layer_ids.h:74
@ In7_Cu
Definition layer_ids.h:72
@ In15_Cu
Definition layer_ids.h:80
@ In2_Cu
Definition layer_ids.h:67
@ In10_Cu
Definition layer_ids.h:75
@ Margin
Definition layer_ids.h:113
@ In4_Cu
Definition layer_ids.h:69
@ UNDEFINED_LAYER
Definition layer_ids.h:61
@ In16_Cu
Definition layer_ids.h:81
@ In1_Cu
Definition layer_ids.h:66
@ In8_Cu
Definition layer_ids.h:73
@ In14_Cu
Definition layer_ids.h:79
@ In12_Cu
Definition layer_ids.h:77
@ In6_Cu
Definition layer_ids.h:71
@ In5_Cu
Definition layer_ids.h:70
@ In3_Cu
Definition layer_ids.h:68
@ F_Cu
Definition layer_ids.h:64
@ In18_Cu
Definition layer_ids.h:83
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
@ NPTH
like PAD_PTH, but not plated mechanical use only, no connection allowed
Definition padstack.h:103
@ PTH
Plated through hole pad.
Definition padstack.h:98
PAD_SHAPE
The set of pad shapes, used with PAD::{Set,Get}Shape()
Definition padstack.h:52
BARCODE class definition.
const double epsilon
A storage class for 128-bit hash value.
Definition hash_128.h:36
A struct recording the isolated and single-pad islands within a zone.
Definition zone.h:60
! The properties of a padstack drill. Drill position is always the pad position (origin).
Definition padstack.h:266
PCB_LAYER_ID start
Definition padstack.h:269
PCB_LAYER_ID end
Definition padstack.h:270
VECTOR2I size
Drill diameter (x == y) or slot dimensions (x != y)
Definition padstack.h:267
std::optional< PAD_DRILL_POST_MACHINING_MODE > mode
Definition padstack.h:281
VECTOR2I center
int radius
int clearance
wxString result
Test unit parsing edge cases and error handling.
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
static thread_pool * tp
BS::priority_thread_pool thread_pool
Definition thread_pool.h:31
void RotatePoint(int *pX, int *pY, const EDA_ANGLE &aAngle)
Calculate the new point of coord coord pX, pY, for a rotation center 0, 0.
Definition trigo.cpp:229
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:88
@ PCB_DIM_ORTHOGONAL_T
class PCB_DIM_ORTHOGONAL, a linear dimension constrained to x/y
Definition typeinfo.h:106
@ PCB_DIM_LEADER_T
class PCB_DIM_LEADER, a leader dimension (graphic item)
Definition typeinfo.h:103
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:97
@ PCB_DIM_CENTER_T
class PCB_DIM_CENTER, a center point marking (graphic item)
Definition typeinfo.h:104
@ PCB_TEXTBOX_T
class PCB_TEXTBOX, wrapped text on a layer
Definition typeinfo.h:93
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:92
@ PCB_FIELD_T
class PCB_FIELD, text associated with a footprint property
Definition typeinfo.h:90
@ PCB_BARCODE_T
class PCB_BARCODE, a barcode (graphic item)
Definition typeinfo.h:101
@ PCB_TARGET_T
class PCB_TARGET, a target (graphic item)
Definition typeinfo.h:107
@ PCB_DIM_ALIGNED_T
class PCB_DIM_ALIGNED, a linear dimension (graphic item)
Definition typeinfo.h:102
@ PCB_PAD_T
class PAD, a pad in a footprint
Definition typeinfo.h:87
@ PCB_TABLE_T
class PCB_TABLE, table of PCB_TABLECELLs
Definition typeinfo.h:94
@ PCB_DIM_RADIAL_T
class PCB_DIM_RADIAL, a radius or diameter dimension
Definition typeinfo.h:105
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:695
VECTOR2< double > VECTOR2D
Definition vector2d.h:694
#define SMOOTH_MIN_VAL_MM
int getHatchFillThermalClearance(const ZONE *aZone, BOARD_ITEM *aItem, PCB_LAYER_ID aLayer)
#define DUMP_POLYS_TO_COPPER_LAYER(a, b, c)
#define SMOOTH_SMALL_VAL_MM
ISLAND_REMOVAL_MODE
Whether or not to remove isolated islands from a zone.
ZONE_CONNECTION
How pads are covered by copper in zone.
Definition zones.h:47
@ THERMAL
Use thermal relief for pads.
Definition zones.h:50
@ NONE
Pads are not covered.
Definition zones.h:49
@ FULL
pads are covered by copper
Definition zones.h:51