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 <zone.h>
44#include <footprint.h>
45#include <pad.h>
46#include <pcb_target.h>
47#include <pcb_track.h>
48#include <pcb_text.h>
49#include <pcb_textbox.h>
50#include <pcb_tablecell.h>
51#include <pcb_table.h>
52#include <pcb_dimension.h>
55#include <board_commit.h>
56#include <progress_reporter.h>
60#include <geometry/vertex_set.h>
61#include <kidialog.h>
62#include <thread_pool.h>
63#include <math/util.h> // for KiROUND
64#include "zone_filler.h"
65#include "project.h"
67#include "pcb_barcode.h"
68
69// Helper classes for connect_nearby_polys
71{
72public:
73 RESULTS( int aOutline1, int aOutline2, int aVertex1, int aVertex2 ) :
74 m_outline1( aOutline1 ), m_outline2( aOutline2 ),
75 m_vertex1( aVertex1 ), m_vertex2( aVertex2 )
76 {
77 }
78
79 bool operator<( const RESULTS& aOther ) const
80 {
81 if( m_outline1 != aOther.m_outline1 )
82 return m_outline1 < aOther.m_outline1;
83 if( m_outline2 != aOther.m_outline2 )
84 return m_outline2 < aOther.m_outline2;
85 if( m_vertex1 != aOther.m_vertex1 )
86 return m_vertex1 < aOther.m_vertex1;
87 return m_vertex2 < aOther.m_vertex2;
88 }
89
94};
95
97{
98public:
99 VERTEX_CONNECTOR( const BOX2I& aBBox, const SHAPE_POLY_SET& aPolys, int aDist ) : VERTEX_SET( 0 )
100 {
101 SetBoundingBox( aBBox );
102 VERTEX* tail = nullptr;
103
104 for( int i = 0; i < aPolys.OutlineCount(); i++ )
105 tail = createList( aPolys.Outline( i ), tail, (void*)( intptr_t )( i ) );
106
107 if( tail )
108 tail->updateList();
109 m_dist = aDist;
110 }
111
112 VERTEX* getPoint( VERTEX* aPt ) const
113 {
114 // z-order range for the current point ± limit bounding box
115 const uint32_t maxZ = zOrder( aPt->x + m_dist, aPt->y + m_dist );
116 const uint32_t minZ = zOrder( aPt->x - m_dist, aPt->y - m_dist );
117 const SEG::ecoord limit2 = SEG::Square( m_dist );
118
119 // first look for points in increasing z-order
120 SEG::ecoord min_dist = std::numeric_limits<SEG::ecoord>::max();
121 VERTEX* retval = nullptr;
122
123 auto check_pt = [&]( VERTEX* p )
124 {
125 VECTOR2D diff( p->x - aPt->x, p->y - aPt->y );
126 SEG::ecoord dist2 = diff.SquaredEuclideanNorm();
127
128 if( dist2 > 0 && dist2 < limit2 && dist2 < min_dist && p->isEar( true ) )
129 {
130 min_dist = dist2;
131 retval = p;
132 }
133 };
134
135 VERTEX* p = aPt->nextZ;
136
137 while( p && p->z <= maxZ )
138 {
139 check_pt( p );
140 p = p->nextZ;
141 }
142
143 p = aPt->prevZ;
144
145 while( p && p->z >= minZ )
146 {
147 check_pt( p );
148 p = p->prevZ;
149 }
150
151 return retval;
152 }
153
155 {
156 if( m_vertices.empty() )
157 return;
158
159 VERTEX* p = m_vertices.front().next;
160 std::set<VERTEX*> visited;
161
162 while( p != &m_vertices.front() )
163 {
164 // Skip points that are concave
165 if( !p->isEar() )
166 {
167 p = p->next;
168 continue;
169 }
170
171 VERTEX* q = nullptr;
172
173 if( ( visited.empty() || !visited.contains( p ) ) && ( q = getPoint( p ) ) )
174 {
175 visited.insert( p );
176
177 if( !visited.contains( q ) &&
178 m_results.emplace( (intptr_t) p->GetUserData(), (intptr_t) q->GetUserData(),
179 p->i, q->i ).second )
180 {
181 // We don't want to connect multiple points in the same vicinity, so skip
182 // 2 points before and after each point and match.
183 visited.insert( p->prev );
184 visited.insert( p->prev->prev );
185 visited.insert( p->next );
186 visited.insert( p->next->next );
187
188 visited.insert( q->prev );
189 visited.insert( q->prev->prev );
190 visited.insert( q->next );
191 visited.insert( q->next->next );
192
193 visited.insert( q );
194 }
195 }
196
197 p = p->next;
198 }
199 }
200
201 std::set<RESULTS> GetResults() const
202 {
203 return m_results;
204 }
205
206private:
207 std::set<RESULTS> m_results;
209};
210
211
217namespace
218{
219
223struct PAD_KNOCKOUT_KEY
224{
225 VECTOR2I position;
226 VECTOR2I effectiveSize; // For circular: max of drill and pad; otherwise pad size
227 int shape; // PAD_SHAPE enum value
228 EDA_ANGLE orientation;
229
230 bool operator==( const PAD_KNOCKOUT_KEY& other ) const
231 {
232 return position == other.position && effectiveSize == other.effectiveSize
233 && shape == other.shape && orientation == other.orientation;
234 }
235};
236
237struct PAD_KNOCKOUT_KEY_HASH
238{
239 size_t operator()( const PAD_KNOCKOUT_KEY& key ) const
240 {
241 return hash_val( key.position.x, key.position.y, key.effectiveSize.x, key.effectiveSize.y,
242 key.shape, key.orientation.AsDegrees() );
243 }
244};
245
247struct VIA_KNOCKOUT_KEY
248{
249 VECTOR2I position;
250 int effectiveSize; // max of drill and via width
251
252 bool operator==( const VIA_KNOCKOUT_KEY& other ) const
253 {
254 return position == other.position && effectiveSize == other.effectiveSize;
255 }
256};
257
258struct VIA_KNOCKOUT_KEY_HASH
259{
260 size_t operator()( const VIA_KNOCKOUT_KEY& key ) const
261 {
262 return hash_val( key.position.x, key.position.y, key.effectiveSize );
263 }
264};
265
268struct TRACK_KNOCKOUT_KEY
269{
270 VECTOR2I start;
271 VECTOR2I end;
272 int width;
273
274 TRACK_KNOCKOUT_KEY( const VECTOR2I& aStart, const VECTOR2I& aEnd, int aWidth ) :
275 width( aWidth )
276 {
277 // Canonicalize endpoint order for consistent hashing
278 if( aStart.x < aEnd.x || ( aStart.x == aEnd.x && aStart.y <= aEnd.y ) )
279 {
280 start = aStart;
281 end = aEnd;
282 }
283 else
284 {
285 start = aEnd;
286 end = aStart;
287 }
288 }
289
290 bool operator==( const TRACK_KNOCKOUT_KEY& other ) const
291 {
292 return start == other.start && end == other.end && width == other.width;
293 }
294};
295
296struct TRACK_KNOCKOUT_KEY_HASH
297{
298 size_t operator()( const TRACK_KNOCKOUT_KEY& key ) const
299 {
300 return hash_val( key.start.x, key.start.y, key.end.x, key.end.y, key.width );
301 }
302};
303
304} // anonymous namespace
305
306
308 m_board( aBoard ),
309 m_brdOutlinesValid( false ),
310 m_commit( aCommit ),
311 m_progressReporter( nullptr ),
313{
314 m_maxError = aBoard->GetDesignSettings().m_MaxError;
315
316 // To enable add "DebugZoneFiller=1" to kicad_advanced settings file.
318}
319
320
324
325
327{
328 m_progressReporter = aReporter;
329 wxASSERT_MSG( m_commit, wxT( "ZONE_FILLER must have a valid commit to call SetProgressReporter" ) );
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,
1003 wxT( "Cached refill for zone %s: %d outlines, area %.0f" ),
1004 zone->GetNetname(), fillPolys.OutlineCount(), fillPolys.Area() );
1005
1006 zone->SetFilledPolysList( layer, fillPolys );
1007 zone->SetFillFlag( layer, true );
1008 return 1;
1009 };
1010
1011 std::vector<std::pair<std::future<int>, int>> refillReturns;
1012 refillReturns.reserve( zonesToRefill.size() );
1013 size_t refillFinished = 0;
1014
1015 for( const auto& fillItem : zonesToRefill )
1016 {
1017 refillReturns.emplace_back( std::make_pair( tp.submit_task(
1018 [&, fillItem]()
1019 {
1020 return cached_refill_lambda( fillItem );
1021 } ), 0 ) );
1022 }
1023
1024 while( !cancelled && refillFinished != 2 * zonesToRefill.size() )
1025 {
1026 for( size_t ii = 0; ii < refillReturns.size(); ++ii )
1027 {
1028 auto& ret = refillReturns[ii];
1029
1030 if( ret.second > 1 )
1031 continue;
1032
1033 std::future_status status = ret.first.wait_for( std::chrono::seconds( 0 ) );
1034
1035 if( status == std::future_status::ready )
1036 {
1037 if( ret.first.get() )
1038 {
1039 ++refillFinished;
1040 ret.second++;
1041 }
1042
1043 if( !cancelled )
1044 {
1045 if( ret.second == 0 )
1046 {
1047 refillReturns[ii].first = tp.submit_task(
1048 [&, idx = ii]()
1049 {
1050 return cached_refill_lambda( zonesToRefill[idx] );
1051 } );
1052 }
1053 else if( ret.second == 1 )
1054 {
1055 refillReturns[ii].first = tp.submit_task(
1056 [&, idx = ii]()
1057 {
1058 return tesselate_lambda( zonesToRefill[idx] );
1059 } );
1060 }
1061 }
1062 }
1063 }
1064
1065 std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
1066
1067 if( m_progressReporter )
1068 {
1069 m_progressReporter->KeepRefreshing();
1070
1071 if( m_progressReporter->IsCancelled() )
1072 cancelled = true;
1073 }
1074 }
1075
1076 // Wait for all refill tasks to complete
1077 for( auto& ret : refillReturns )
1078 {
1079 if( ret.first.valid() )
1080 {
1081 std::future_status status = ret.first.wait_for( std::chrono::seconds( 0 ) );
1082
1083 while( status != std::future_status::ready )
1084 {
1085 if( m_progressReporter )
1086 m_progressReporter->KeepRefreshing();
1087
1088 status = ret.first.wait_for( std::chrono::milliseconds( 100 ) );
1089 }
1090 }
1091 }
1092
1093 // Re-run island detection for refilled zones
1094 std::map<ZONE*, std::map<PCB_LAYER_ID, ISOLATED_ISLANDS>> refillIslandsMap;
1095 std::set<ZONE*> refillZones;
1096
1097 for( const auto& [zone, layer] : zonesToRefill )
1098 refillZones.insert( zone );
1099
1100 for( ZONE* zone : refillZones )
1101 refillIslandsMap[zone] = std::map<PCB_LAYER_ID, ISOLATED_ISLANDS>();
1102
1103 connectivity->FillIsolatedIslandsMap( refillIslandsMap );
1104
1105 // Remove islands from refilled zones
1106 for( const auto& [ zone, zoneIslands ] : refillIslandsMap )
1107 {
1108 bool allIslands = true;
1109
1110 for( const auto& [ layer, layerIslands ] : zoneIslands )
1111 {
1112 if( layerIslands.m_IsolatedOutlines.size()
1113 != static_cast<size_t>( zone->GetFilledPolysList( layer )->OutlineCount() ) )
1114 {
1115 allIslands = false;
1116 break;
1117 }
1118 }
1119
1120 if( allIslands )
1121 continue;
1122
1123 for( const auto& [ layer, layerIslands ] : zoneIslands )
1124 {
1125 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( layer ) )
1126 continue;
1127
1128 if( layerIslands.m_IsolatedOutlines.empty() )
1129 continue;
1130
1131 std::vector<int> islands = layerIslands.m_IsolatedOutlines;
1132 std::sort( islands.begin(), islands.end(), std::greater<int>() );
1133
1134 std::shared_ptr<SHAPE_POLY_SET> poly = zone->GetFilledPolysList( layer );
1135 long long int minArea = zone->GetMinIslandArea();
1136 ISLAND_REMOVAL_MODE mode = zone->GetIslandRemovalMode();
1137
1138 for( int idx : islands )
1139 {
1140 SHAPE_LINE_CHAIN& outline = poly->Outline( idx );
1141
1142 if( mode == ISLAND_REMOVAL_MODE::ALWAYS )
1143 poly->DeletePolygonAndTriangulationData( idx, false );
1144 else if( mode == ISLAND_REMOVAL_MODE::AREA && outline.Area( true ) < minArea )
1145 poly->DeletePolygonAndTriangulationData( idx, false );
1146 else
1147 zone->SetIsIsland( layer, idx );
1148 }
1149
1150 poly->UpdateTriangulationDataHash();
1151 zone->CalculateFilledArea();
1152 }
1153 }
1154
1155 wxLogTrace( traceZoneFiller, wxT( "Iterative refill completed in %0.3f ms" ),
1156 timer.msecs() );
1157 }
1158 }
1159
1160 // Now remove islands which are either outside the board edge or fail to meet the minimum
1161 // area requirements
1162 using island_check_return = std::vector<std::pair<std::shared_ptr<SHAPE_POLY_SET>, int>>;
1163
1164 std::vector<std::pair<std::shared_ptr<SHAPE_POLY_SET>, double>> polys_to_check;
1165
1166 // rough estimate to save re-allocation time
1167 polys_to_check.reserve( m_board->GetCopperLayerCount() * aZones.size() );
1168
1169 for( ZONE* zone : aZones )
1170 {
1171 // Don't check for connections on layers that only exist in the zone but
1172 // were disabled in the board
1173 BOARD* board = zone->GetBoard();
1174 LSET zoneCopperLayers = zone->GetLayerSet() & LSET::AllCuMask( board->GetCopperLayerCount() );
1175
1176 // Min-thickness is the web thickness. On the other hand, a blob min-thickness by
1177 // min-thickness is not useful. Since there's no obvious definition of web vs. blob, we
1178 // arbitrarily choose "at least 3X the area".
1179 double minArea = (double) zone->GetMinThickness() * zone->GetMinThickness() * 3;
1180
1181 for( PCB_LAYER_ID layer : zoneCopperLayers )
1182 {
1183 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( layer ) )
1184 continue;
1185
1186 polys_to_check.emplace_back( zone->GetFilledPolysList( layer ), minArea );
1187 }
1188 }
1189
1190 auto island_lambda =
1191 [&]( int aStart, int aEnd ) -> island_check_return
1192 {
1193 island_check_return retval;
1194
1195 for( int ii = aStart; ii < aEnd && !cancelled; ++ii )
1196 {
1197 auto [poly, minArea] = polys_to_check[ii];
1198
1199 for( int jj = poly->OutlineCount() - 1; jj >= 0; jj-- )
1200 {
1201 SHAPE_POLY_SET island;
1202 SHAPE_POLY_SET intersection;
1203 const SHAPE_LINE_CHAIN& test_poly = poly->Polygon( jj ).front();
1204 double island_area = test_poly.Area();
1205
1206 if( island_area < minArea )
1207 continue;
1208
1209
1210 island.AddOutline( test_poly );
1211 intersection.BooleanIntersection( m_boardOutline, island );
1212
1213 // Nominally, all of these areas should be either inside or outside the
1214 // board outline. So this test should be able to just compare areas (if
1215 // they are equal, you are inside). But in practice, we sometimes have
1216 // slight overlap at the edges, so testing against half-size area acts as
1217 // a fail-safe.
1218 if( intersection.Area() < island_area / 2.0 )
1219 retval.emplace_back( poly, jj );
1220 }
1221 }
1222
1223 return retval;
1224 };
1225
1226 auto island_returns = tp.submit_blocks( 0, polys_to_check.size(), island_lambda );
1227 cancelled = false;
1228
1229 // Allow island removal threads to finish
1230 for( size_t ii = 0; ii < island_returns.size(); ++ii )
1231 {
1232 std::future<island_check_return>& ret = island_returns[ii];
1233
1234 if( ret.valid() )
1235 {
1236 std::future_status status = ret.wait_for( std::chrono::seconds( 0 ) );
1237
1238 while( status != std::future_status::ready )
1239 {
1240 if( m_progressReporter )
1241 {
1242 m_progressReporter->KeepRefreshing();
1243
1244 if( m_progressReporter->IsCancelled() )
1245 cancelled = true;
1246 }
1247
1248 status = ret.wait_for( std::chrono::milliseconds( 100 ) );
1249 }
1250 }
1251 }
1252
1253 if( cancelled )
1254 return false;
1255
1256 for( size_t ii = 0; ii < island_returns.size(); ++ii )
1257 {
1258 std::future<island_check_return>& ret = island_returns[ii];
1259
1260 if( ret.valid() )
1261 {
1262 for( auto& action_item : ret.get() )
1263 action_item.first->DeletePolygonAndTriangulationData( action_item.second, true );
1264 }
1265 }
1266
1267 for( ZONE* zone : aZones )
1268 zone->CalculateFilledArea();
1269
1270 // Second pass: Re-evaluate via flashing based on actual filled polygons.
1271 // The first pass (before filling) marks vias as ZLO_FORCE_FLASHED if they're within the
1272 // zone outline. However, if the fill doesn't actually reach the via (due to obstacles like
1273 // tracks), we should not flash the via. See https://gitlab.com/kicad/code/kicad/-/issues/22010
1274 for( PCB_TRACK* track : m_board->Tracks() )
1275 {
1276 if( track->Type() != PCB_VIA_T )
1277 continue;
1278
1279 PCB_VIA* via = static_cast<PCB_VIA*>( track );
1280 VECTOR2I center = via->GetPosition();
1281 int holeRadius = via->GetDrillValue() / 2;
1282 int netcode = via->GetNetCode();
1283 LSET layers = via->GetLayerSet() & boardCuMask;
1284
1285 for( PCB_LAYER_ID layer : layers )
1286 {
1287 if( via->GetZoneLayerOverride( layer ) != ZLO_FORCE_FLASHED )
1288 continue;
1289
1290 bool zoneReachesVia = false;
1291
1292 for( ZONE* zone : m_board->Zones() )
1293 {
1294 if( zone->GetIsRuleArea() )
1295 continue;
1296
1297 if( zone->GetNetCode() != netcode )
1298 continue;
1299
1300 if( !zone->IsOnLayer( layer ) )
1301 continue;
1302
1303 if( !zone->HasFilledPolysForLayer( layer ) )
1304 continue;
1305
1306 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
1307
1308 if( fill->IsEmpty() )
1309 continue;
1310
1311 // Check if the filled zone reaches the via hole. Use holeRadius as reach distance
1312 // to match the pre-fill check logic.
1313 if( fill->Contains( center, -1, holeRadius ) )
1314 {
1315 zoneReachesVia = true;
1316 break;
1317 }
1318 }
1319
1320 if( !zoneReachesVia )
1321 via->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
1322 }
1323 }
1324
1325 // Same logic for pads
1326 for( FOOTPRINT* footprint : m_board->Footprints() )
1327 {
1328 for( PAD* pad : footprint->Pads() )
1329 {
1330 VECTOR2I center = pad->GetPosition();
1331 int netcode = pad->GetNetCode();
1332 LSET layers = pad->GetLayerSet() & boardCuMask;
1333
1334 // For TH pads, use the hole radius as tolerance since the filled zone creates a
1335 // thermal relief around the pad hole, similar to vias.
1336 int holeRadius = 0;
1337
1338 if( pad->HasHole() )
1339 holeRadius = std::min( pad->GetDrillSizeX(), pad->GetDrillSizeY() ) / 2;
1340
1341 for( PCB_LAYER_ID layer : layers )
1342 {
1343 if( pad->GetZoneLayerOverride( layer ) != ZLO_FORCE_FLASHED )
1344 continue;
1345
1346 bool zoneReachesPad = false;
1347
1348 for( ZONE* zone : m_board->Zones() )
1349 {
1350 if( zone->GetIsRuleArea() )
1351 continue;
1352
1353 if( zone->GetNetCode() != netcode )
1354 continue;
1355
1356 if( !zone->IsOnLayer( layer ) )
1357 continue;
1358
1359 if( !zone->HasFilledPolysForLayer( layer ) )
1360 continue;
1361
1362 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
1363
1364 if( fill->IsEmpty() )
1365 continue;
1366
1367 if( fill->Contains( center, -1, holeRadius ) )
1368 {
1369 zoneReachesPad = true;
1370 break;
1371 }
1372 }
1373
1374 if( !zoneReachesPad )
1375 pad->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
1376 }
1377 }
1378 }
1379
1380 if( aCheck )
1381 {
1382 bool outOfDate = false;
1383
1384 for( ZONE* zone : aZones )
1385 {
1386 // Keepout zones are not filled
1387 if( zone->GetIsRuleArea() )
1388 continue;
1389
1390 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
1391 {
1392 zone->BuildHashValue( layer );
1393
1394 if( oldFillHashes[ { zone, layer } ] != zone->GetHashValue( layer ) )
1395 outOfDate = true;
1396 }
1397 }
1398
1399 if( ( m_board->GetProject()
1400 && m_board->GetProject()->GetLocalSettings().m_PrototypeZoneFill ) )
1401 {
1402 KIDIALOG dlg( aParent, _( "Prototype zone fill enabled. Disable setting and refill?" ), _( "Confirmation" ),
1403 wxOK | wxCANCEL | wxICON_WARNING );
1404 dlg.SetOKCancelLabels( _( "Disable and refill" ), _( "Continue without Refill" ) );
1405 dlg.DoNotShowCheckbox( __FILE__, __LINE__ );
1406
1407 if( dlg.ShowModal() == wxID_OK )
1408 {
1409 m_board->GetProject()->GetLocalSettings().m_PrototypeZoneFill = false;
1410 }
1411 else if( !outOfDate )
1412 {
1413 return false;
1414 }
1415 }
1416
1417 if( outOfDate )
1418 {
1419 KIDIALOG dlg( aParent, _( "Zone fills are out-of-date. Refill?" ), _( "Confirmation" ),
1420 wxOK | wxCANCEL | wxICON_WARNING );
1421 dlg.SetOKCancelLabels( _( "Refill" ), _( "Continue without Refill" ) );
1422 dlg.DoNotShowCheckbox( __FILE__, __LINE__ );
1423
1424 if( dlg.ShowModal() == wxID_CANCEL )
1425 return false;
1426 }
1427 else
1428 {
1429 // No need to commit something that hasn't changed (and committing will set
1430 // the modified flag).
1431 return false;
1432 }
1433 }
1434
1435 if( m_progressReporter )
1436 {
1437 if( m_progressReporter->IsCancelled() )
1438 return false;
1439
1440 m_progressReporter->AdvancePhase();
1441 m_progressReporter->KeepRefreshing();
1442 }
1443
1444 return true;
1445}
1446
1447
1452void ZONE_FILLER::addKnockout( BOARD_ITEM* aItem, PCB_LAYER_ID aLayer, int aGap, SHAPE_POLY_SET& aHoles )
1453{
1454 if( aItem->Type() == PCB_PAD_T && static_cast<PAD*>( aItem )->GetShape( aLayer ) == PAD_SHAPE::CUSTOM )
1455 {
1456 PAD* pad = static_cast<PAD*>( aItem );
1457 SHAPE_POLY_SET poly;
1458 pad->TransformShapeToPolygon( poly, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1459
1460 // the pad shape in zone can be its convex hull or the shape itself
1461 if( pad->GetCustomShapeInZoneOpt() == CUSTOM_SHAPE_ZONE_MODE::CONVEXHULL )
1462 {
1463 std::vector<VECTOR2I> convex_hull;
1464 BuildConvexHull( convex_hull, poly );
1465
1466 aHoles.NewOutline();
1467
1468 for( const VECTOR2I& pt : convex_hull )
1469 aHoles.Append( pt );
1470 }
1471 else
1472 {
1473 aHoles.Append( poly );
1474 }
1475 }
1476 else
1477 {
1478 aItem->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1479 }
1480}
1481
1482
1486void ZONE_FILLER::addHoleKnockout( PAD* aPad, int aGap, SHAPE_POLY_SET& aHoles )
1487{
1488 aPad->TransformHoleToPolygon( aHoles, aGap, m_maxError, ERROR_OUTSIDE );
1489}
1490
1491
1493{
1494 int minorAxis = 0;
1495
1496 if( aItem->Type() == PCB_PAD_T )
1497 {
1498 PAD* pad = static_cast<PAD*>( aItem );
1499 VECTOR2I padSize = pad->GetSize( aLayer );
1500
1501 minorAxis = std::min( padSize.x, padSize.y );
1502 }
1503 else if( aItem->Type() == PCB_VIA_T )
1504 {
1505 PCB_VIA* via = static_cast<PCB_VIA*>( aItem );
1506
1507 minorAxis = via->GetWidth( aLayer );
1508 }
1509
1510 return ( aZone->GetHatchGap() - aZone->GetHatchThickness() - minorAxis ) / 2;
1511}
1512
1513
1518void ZONE_FILLER::addKnockout( BOARD_ITEM* aItem, PCB_LAYER_ID aLayer, int aGap,
1519 bool aIgnoreLineWidth, SHAPE_POLY_SET& aHoles )
1520{
1521 switch( aItem->Type() )
1522 {
1523 case PCB_FIELD_T:
1524 case PCB_TEXT_T:
1525 {
1526 PCB_TEXT* text = static_cast<PCB_TEXT*>( aItem );
1527
1528 if( text->IsVisible() )
1529 {
1530 if( text->IsKnockout() )
1531 {
1532 // Knockout text should only leave holes where the text is, not where the copper fill
1533 // around it would be.
1534 PCB_TEXT textCopy = *text;
1535 textCopy.SetIsKnockout( false );
1536 textCopy.TransformTextToPolySet( aHoles, 0, m_maxError, ERROR_INSIDE );
1537 }
1538 else
1539 {
1540 text->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1541 }
1542 }
1543
1544 break;
1545 }
1546
1547 case PCB_TEXTBOX_T:
1548 case PCB_TABLE_T:
1549 case PCB_SHAPE_T:
1550 case PCB_TARGET_T:
1551 aItem->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE, aIgnoreLineWidth );
1552 break;
1553
1554 case PCB_BARCODE_T:
1555 {
1556 PCB_BARCODE* barcode = static_cast<PCB_BARCODE*>( aItem );
1557 barcode->GetBoundingHull( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1558 break;
1559 }
1560
1561 case PCB_DIM_ALIGNED_T:
1562 case PCB_DIM_LEADER_T:
1563 case PCB_DIM_CENTER_T:
1564 case PCB_DIM_RADIAL_T:
1566 {
1567 PCB_DIMENSION_BASE* dim = static_cast<PCB_DIMENSION_BASE*>( aItem );
1568
1569 dim->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE, false );
1570 dim->PCB_TEXT::TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1571 break;
1572 }
1573
1574 default:
1575 break;
1576 }
1577}
1578
1579
1585 SHAPE_POLY_SET& aFill,
1586 std::vector<BOARD_ITEM*>& aThermalConnectionPads,
1587 std::vector<PAD*>& aNoConnectionPads )
1588{
1589 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1590 ZONE_CONNECTION connection;
1591 DRC_CONSTRAINT constraint;
1592 int padClearance;
1593 std::shared_ptr<SHAPE> padShape;
1594 int holeClearance;
1595 SHAPE_POLY_SET holes;
1596
1597 // Deduplication sets for coincident pads and vias
1598 std::unordered_set<PAD_KNOCKOUT_KEY, PAD_KNOCKOUT_KEY_HASH> processedPads;
1599 std::unordered_set<VIA_KNOCKOUT_KEY, VIA_KNOCKOUT_KEY_HASH> processedVias;
1600
1601 for( FOOTPRINT* footprint : m_board->Footprints() )
1602 {
1603 for( PAD* pad : footprint->Pads() )
1604 {
1605 if( !pad->IsOnLayer( aLayer ) )
1606 continue;
1607
1608 BOX2I padBBox = pad->GetBoundingBox();
1609 padBBox.Inflate( m_worstClearance );
1610
1611 if( !padBBox.Intersects( aZone->GetBoundingBox() ) )
1612 continue;
1613
1614 // Deduplicate coincident pads (skip custom pads - they have complex shapes)
1615 PAD_SHAPE padShapeType = pad->GetShape( aLayer );
1616
1617 if( padShapeType != PAD_SHAPE::CUSTOM )
1618 {
1619 // For circular pads: use max of drill and pad size; otherwise just pad size
1620 VECTOR2I padSize = pad->GetSize( aLayer );
1621 VECTOR2I effectiveSize;
1622
1623 if( padShapeType == PAD_SHAPE::CIRCLE )
1624 {
1625 int drill = std::max( pad->GetDrillSize().x, pad->GetDrillSize().y );
1626 int maxDim = std::max( { padSize.x, padSize.y, drill } );
1627 effectiveSize = VECTOR2I( maxDim, maxDim );
1628 }
1629 else
1630 {
1631 effectiveSize = padSize;
1632 }
1633
1634 PAD_KNOCKOUT_KEY padKey{ pad->GetPosition(), effectiveSize,
1635 static_cast<int>( padShapeType ), pad->GetOrientation() };
1636
1637 if( !processedPads.insert( padKey ).second )
1638 continue;
1639 }
1640
1641 bool noConnection = pad->GetNetCode() != aZone->GetNetCode();
1642
1643 if( !aZone->IsTeardropArea() )
1644 {
1645 if( aZone->GetNetCode() == 0
1646 || pad->GetZoneLayerOverride( aLayer ) == ZLO_FORCE_NO_ZONE_CONNECTION )
1647 {
1648 noConnection = true;
1649 }
1650 }
1651
1652 // Check if the pad is backdrilled or post-machined on this layer
1653 if( pad->IsBackdrilledOrPostMachined( aLayer ) )
1654 noConnection = true;
1655
1656 if( noConnection )
1657 {
1658 // collect these for knockout in buildCopperItemClearances()
1659 aNoConnectionPads.push_back( pad );
1660 continue;
1661 }
1662
1663 // For hatch zones, respect the zone connection type just like solid zones
1664 // Pads with THERMAL connection get thermal rings; FULL connections get no knockout;
1665 // NONE connections get handled later in buildCopperItemClearances.
1667 {
1668 constraint = bds.m_DRCEngine->EvalZoneConnection( pad, aZone, aLayer );
1669 connection = constraint.m_ZoneConnection;
1670
1671 if( connection == ZONE_CONNECTION::THERMAL && !pad->CanFlashLayer( aLayer ) )
1672 connection = ZONE_CONNECTION::NONE;
1673
1674 switch( connection )
1675 {
1677 {
1678 padShape = pad->GetEffectiveShape( aLayer, FLASHING::ALWAYS_FLASHED );
1679
1680 if( aFill.Collide( padShape.get(), 0 ) )
1681 {
1682 // Get the thermal relief gap
1684 aZone, aLayer );
1685 int thermalGap = constraint.GetValue().Min();
1686
1687 // Knock out the thermal gap only - the thermal ring will be added separately
1688 aThermalConnectionPads.push_back( pad );
1689 addKnockout( pad, aLayer, thermalGap, holes );
1690 }
1691
1692 break;
1693 }
1694
1696 // Will be handled by buildCopperItemClearances
1697 aNoConnectionPads.push_back( pad );
1698 break;
1699
1701 default:
1702 // No knockout - pad connects directly to the hatch
1703 break;
1704 }
1705
1706 continue;
1707 }
1708
1709 if( aZone->IsTeardropArea() )
1710 {
1711 connection = ZONE_CONNECTION::FULL;
1712 }
1713 else
1714 {
1715 constraint = bds.m_DRCEngine->EvalZoneConnection( pad, aZone, aLayer );
1716 connection = constraint.m_ZoneConnection;
1717 }
1718
1719 if( connection == ZONE_CONNECTION::THERMAL && !pad->CanFlashLayer( aLayer ) )
1720 connection = ZONE_CONNECTION::NONE;
1721
1722 switch( connection )
1723 {
1725 padShape = pad->GetEffectiveShape( aLayer, FLASHING::ALWAYS_FLASHED );
1726
1727 if( aFill.Collide( padShape.get(), 0 ) )
1728 {
1729 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, pad, aZone, aLayer );
1730 padClearance = constraint.GetValue().Min();
1731
1732 aThermalConnectionPads.push_back( pad );
1733 addKnockout( pad, aLayer, padClearance, holes );
1734 }
1735
1736 break;
1737
1739 constraint = bds.m_DRCEngine->EvalRules( PHYSICAL_CLEARANCE_CONSTRAINT, pad, aZone, aLayer );
1740
1741 if( constraint.GetValue().Min() > aZone->GetLocalClearance().value() )
1742 padClearance = constraint.GetValue().Min();
1743 else
1744 padClearance = aZone->GetLocalClearance().value();
1745
1746 if( pad->FlashLayer( aLayer ) )
1747 {
1748 addKnockout( pad, aLayer, padClearance, holes );
1749 }
1750 else if( pad->GetDrillSize().x > 0 )
1751 {
1752 constraint = bds.m_DRCEngine->EvalRules( PHYSICAL_HOLE_CLEARANCE_CONSTRAINT, pad, aZone, aLayer );
1753
1754 if( constraint.GetValue().Min() > padClearance )
1755 holeClearance = constraint.GetValue().Min();
1756 else
1757 holeClearance = padClearance;
1758
1759 pad->TransformHoleToPolygon( holes, holeClearance, m_maxError, ERROR_OUTSIDE );
1760 }
1761
1762 break;
1763
1764 default:
1765 // No knockout
1766 continue;
1767 }
1768 }
1769 }
1770
1771 // For hatch zones, vias also get proper thermal treatment. They always use thermal connection
1772 // since vias don't have zone connection settings like pads do.
1774 {
1775 for( PCB_TRACK* track : m_board->Tracks() )
1776 {
1777 if( track->Type() == PCB_VIA_T )
1778 {
1779 PCB_VIA* via = static_cast<PCB_VIA*>( track );
1780
1781 if( !via->IsOnLayer( aLayer ) )
1782 continue;
1783
1784 BOX2I viaBBox = via->GetBoundingBox();
1785 viaBBox.Inflate( m_worstClearance );
1786
1787 if( !viaBBox.Intersects( aZone->GetBoundingBox() ) )
1788 continue;
1789
1790 // Deduplicate coincident vias (circular, so use max of drill and width)
1791 int viaEffectiveSize = std::max( via->GetDrillValue(), via->GetWidth( aLayer ) );
1792 VIA_KNOCKOUT_KEY viaKey{ via->GetPosition(), viaEffectiveSize };
1793
1794 if( !processedVias.insert( viaKey ).second )
1795 continue;
1796
1797 bool noConnection = via->GetNetCode() != aZone->GetNetCode()
1798 || ( via->Padstack().UnconnectedLayerMode() == UNCONNECTED_LAYER_MODE::START_END_ONLY
1799 && aLayer != via->Padstack().Drill().start
1800 && aLayer != via->Padstack().Drill().end );
1801
1802 // Check if this layer is affected by backdrill or post-machining
1803 if( via->IsBackdrilledOrPostMachined( aLayer ) )
1804 {
1805 noConnection = true;
1806
1807 // Add knockout for backdrill/post-machining hole
1808 int pmSize = 0;
1809 int bdSize = 0;
1810
1811 const PADSTACK::POST_MACHINING_PROPS& frontPM = via->Padstack().FrontPostMachining();
1812 const PADSTACK::POST_MACHINING_PROPS& backPM = via->Padstack().BackPostMachining();
1813
1816 {
1817 pmSize = std::max( pmSize, frontPM.size );
1818 }
1819
1822 {
1823 pmSize = std::max( pmSize, backPM.size );
1824 }
1825
1826 const PADSTACK::DRILL_PROPS& secDrill = via->Padstack().SecondaryDrill();
1827
1828 if( secDrill.start != UNDEFINED_LAYER && secDrill.end != UNDEFINED_LAYER )
1829 bdSize = secDrill.size.x;
1830
1831 int knockoutSize = std::max( pmSize, bdSize );
1832
1833 if( knockoutSize > 0 )
1834 {
1835 int clearance = aZone->GetLocalClearance().value_or( 0 );
1836
1837 TransformCircleToPolygon( holes, via->GetPosition(), knockoutSize / 2 + clearance,
1839 }
1840 }
1841
1842 if( noConnection )
1843 continue;
1844
1845 // Use proper thermal gap from DRC constraints
1846 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, via, aZone, aLayer );
1847 int thermalGap = constraint.GetValue().Min();
1848
1849 aThermalConnectionPads.push_back( via );
1850 addKnockout( via, aLayer, thermalGap, holes );
1851 }
1852 }
1853 }
1854
1855 aFill.BooleanSubtract( holes );
1856}
1857
1858
1864 const std::vector<PAD*>& aNoConnectionPads,
1865 SHAPE_POLY_SET& aHoles,
1866 bool aIncludeZoneClearances )
1867{
1868 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1869 long ticker = 0;
1870
1871 // Deduplication sets for coincident items
1872 std::unordered_set<PAD_KNOCKOUT_KEY, PAD_KNOCKOUT_KEY_HASH> processedPads;
1873 std::unordered_set<VIA_KNOCKOUT_KEY, VIA_KNOCKOUT_KEY_HASH> processedVias;
1874 std::unordered_set<TRACK_KNOCKOUT_KEY, TRACK_KNOCKOUT_KEY_HASH> processedTracks;
1875
1876 auto checkForCancel =
1877 [&ticker]( PROGRESS_REPORTER* aReporter ) -> bool
1878 {
1879 return aReporter && ( ticker++ % 50 ) == 0 && aReporter->IsCancelled();
1880 };
1881
1882 // A small extra clearance to be sure actual track clearances are not smaller than
1883 // requested clearance due to many approximations in calculations, like arc to segment
1884 // approx, rounding issues, etc.
1885 BOX2I zone_boundingbox = aZone->GetBoundingBox();
1886 int extra_margin = pcbIUScale.mmToIU( ADVANCED_CFG::GetCfg().m_ExtraClearance );
1887
1888 // Items outside the zone bounding box are skipped, so it needs to be inflated by the
1889 // largest clearance value found in the netclasses and rules
1890 zone_boundingbox.Inflate( m_worstClearance + extra_margin );
1891
1892 auto evalRulesForItems =
1893 [&bds]( DRC_CONSTRAINT_T aConstraint, const BOARD_ITEM* a, const BOARD_ITEM* b,
1894 PCB_LAYER_ID aEvalLayer ) -> int
1895 {
1896 DRC_CONSTRAINT c = bds.m_DRCEngine->EvalRules( aConstraint, a, b, aEvalLayer );
1897
1898 if( c.IsNull() )
1899 return -1;
1900 else
1901 return c.GetValue().Min();
1902 };
1903
1904 // Add non-connected pad clearances
1905 //
1906 auto knockoutPadClearance =
1907 [&]( PAD* aPad )
1908 {
1909 int init_gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aPad, aLayer );
1910 int gap = init_gap;
1911 bool hasHole = aPad->GetDrillSize().x > 0;
1912 bool flashLayer = aPad->FlashLayer( aLayer );
1913 bool platedHole = hasHole && aPad->GetAttribute() == PAD_ATTRIB::PTH;
1914
1915 if( flashLayer || platedHole )
1916 {
1917 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aPad, aLayer ) );
1918 }
1919
1920 if( flashLayer && gap >= 0 )
1921 addKnockout( aPad, aLayer, gap + extra_margin, aHoles );
1922
1923 if( hasHole )
1924 {
1925 // NPTH do not need copper clearance gaps to their holes
1926 if( aPad->GetAttribute() == PAD_ATTRIB::NPTH )
1927 gap = init_gap;
1928
1929 gap = std::max( gap, evalRulesForItems( PHYSICAL_HOLE_CLEARANCE_CONSTRAINT, aZone, aPad, aLayer ) );
1930
1931 gap = std::max( gap, evalRulesForItems( HOLE_CLEARANCE_CONSTRAINT, aZone, aPad, aLayer ) );
1932
1933 if( gap >= 0 )
1934 addHoleKnockout( aPad, gap + extra_margin, aHoles );
1935 }
1936
1937 // Handle backdrill and post-machining knockouts
1938 if( aPad->IsBackdrilledOrPostMachined( aLayer ) )
1939 {
1940 int pmSize = 0;
1941 int bdSize = 0;
1942
1943 const PADSTACK::POST_MACHINING_PROPS& frontPM = aPad->Padstack().FrontPostMachining();
1944 const PADSTACK::POST_MACHINING_PROPS& backPM = aPad->Padstack().BackPostMachining();
1945
1948 {
1949 pmSize = std::max( pmSize, frontPM.size );
1950 }
1951
1954 {
1955 pmSize = std::max( pmSize, backPM.size );
1956 }
1957
1958 const PADSTACK::DRILL_PROPS& secDrill = aPad->Padstack().SecondaryDrill();
1959
1960 if( secDrill.start != UNDEFINED_LAYER && secDrill.end != UNDEFINED_LAYER )
1961 bdSize = secDrill.size.x;
1962
1963 int knockoutSize = std::max( pmSize, bdSize );
1964
1965 if( knockoutSize > 0 )
1966 {
1967 int clearance = std::max( gap, 0 ) + extra_margin;
1968
1969 TransformCircleToPolygon( aHoles, aPad->GetPosition(), knockoutSize / 2 + clearance,
1971 }
1972 }
1973 };
1974
1975 for( PAD* pad : aNoConnectionPads )
1976 {
1977 if( checkForCancel( m_progressReporter ) )
1978 return;
1979
1980 // Deduplicate coincident pads (skip custom pads - they have complex shapes)
1981 PAD_SHAPE padShape = pad->GetShape( aLayer );
1982
1983 if( padShape != PAD_SHAPE::CUSTOM )
1984 {
1985 // For circular pads: use max of drill and pad size; otherwise just pad size
1986 VECTOR2I padSize = pad->GetSize( aLayer );
1987 VECTOR2I effectiveSize;
1988
1989 if( padShape == PAD_SHAPE::CIRCLE )
1990 {
1991 int drill = std::max( pad->GetDrillSize().x, pad->GetDrillSize().y );
1992 int maxDim = std::max( { padSize.x, padSize.y, drill } );
1993 effectiveSize = VECTOR2I( maxDim, maxDim );
1994 }
1995 else
1996 {
1997 effectiveSize = padSize;
1998 }
1999
2000 PAD_KNOCKOUT_KEY padKey{ pad->GetPosition(), effectiveSize,
2001 static_cast<int>( padShape ), pad->GetOrientation() };
2002
2003 if( !processedPads.insert( padKey ).second )
2004 continue;
2005 }
2006
2007 knockoutPadClearance( pad );
2008 }
2009
2010 // Add non-connected track clearances
2011 //
2012 auto knockoutTrackClearance =
2013 [&]( PCB_TRACK* aTrack )
2014 {
2015 if( aTrack->GetBoundingBox().Intersects( zone_boundingbox ) )
2016 {
2017 bool sameNet = aTrack->GetNetCode() == aZone->GetNetCode();
2018
2019 if( !aZone->IsTeardropArea() && aZone->GetNetCode() == 0 )
2020 sameNet = false;
2021
2022 int gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aTrack, aLayer );
2023
2024 if( aTrack->Type() == PCB_VIA_T )
2025 {
2026 PCB_VIA* via = static_cast<PCB_VIA*>( aTrack );
2027
2028 if( via->GetZoneLayerOverride( aLayer ) == ZLO_FORCE_NO_ZONE_CONNECTION )
2029 sameNet = false;
2030 }
2031
2032 if( !sameNet )
2033 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aTrack, aLayer ) );
2034
2035 if( aTrack->Type() == PCB_VIA_T )
2036 {
2037 PCB_VIA* via = static_cast<PCB_VIA*>( aTrack );
2038
2039 if( via->FlashLayer( aLayer ) && gap > 0 )
2040 {
2041 via->TransformShapeToPolygon( aHoles, aLayer, gap + extra_margin, m_maxError,
2042 ERROR_OUTSIDE );
2043 }
2044
2045 gap = std::max( gap, evalRulesForItems( PHYSICAL_HOLE_CLEARANCE_CONSTRAINT, aZone, via,
2046 aLayer ) );
2047
2048 if( !sameNet )
2049 gap = std::max( gap, evalRulesForItems( HOLE_CLEARANCE_CONSTRAINT, aZone, via, aLayer ) );
2050
2051 if( gap >= 0 )
2052 {
2053 int radius = via->GetDrillValue() / 2;
2054
2055 TransformCircleToPolygon( aHoles, via->GetPosition(), radius + gap + extra_margin,
2057 }
2058
2059 // Handle backdrill and post-machining knockouts
2060 if( via->IsBackdrilledOrPostMachined( aLayer ) )
2061 {
2062 int pmSize = 0;
2063 int bdSize = 0;
2064
2065 const PADSTACK::POST_MACHINING_PROPS& frontPM = via->Padstack().FrontPostMachining();
2066 const PADSTACK::POST_MACHINING_PROPS& backPM = via->Padstack().BackPostMachining();
2067
2070 {
2071 pmSize = std::max( pmSize, frontPM.size );
2072 }
2073
2076 {
2077 pmSize = std::max( pmSize, backPM.size );
2078 }
2079
2080 const PADSTACK::DRILL_PROPS& secDrill = via->Padstack().SecondaryDrill();
2081
2082 if( secDrill.start != UNDEFINED_LAYER && secDrill.end != UNDEFINED_LAYER )
2083 bdSize = secDrill.size.x;
2084
2085 int knockoutSize = std::max( pmSize, bdSize );
2086
2087 if( knockoutSize > 0 )
2088 {
2089 int clearance = std::max( gap, 0 ) + extra_margin;
2090
2091 TransformCircleToPolygon( aHoles, via->GetPosition(), knockoutSize / 2 + clearance,
2093 }
2094 }
2095 }
2096 else
2097 {
2098 if( gap >= 0 )
2099 {
2100 aTrack->TransformShapeToPolygon( aHoles, aLayer, gap + extra_margin, m_maxError,
2101 ERROR_OUTSIDE );
2102 }
2103 }
2104 }
2105 };
2106
2107 for( PCB_TRACK* track : m_board->Tracks() )
2108 {
2109 if( !track->IsOnLayer( aLayer ) )
2110 continue;
2111
2112 if( checkForCancel( m_progressReporter ) )
2113 return;
2114
2115 // Deduplicate coincident tracks and vias
2116 if( track->Type() == PCB_VIA_T )
2117 {
2118 PCB_VIA* via = static_cast<PCB_VIA*>( track );
2119 int viaEffectiveSize = std::max( via->GetDrillValue(), via->GetWidth( aLayer ) );
2120 VIA_KNOCKOUT_KEY viaKey{ via->GetPosition(), viaEffectiveSize };
2121
2122 if( !processedVias.insert( viaKey ).second )
2123 continue;
2124 }
2125 else
2126 {
2127 TRACK_KNOCKOUT_KEY trackKey( track->GetStart(), track->GetEnd(), track->GetWidth() );
2128
2129 if( !processedTracks.insert( trackKey ).second )
2130 continue;
2131 }
2132
2133 knockoutTrackClearance( track );
2134 }
2135
2136 // Add graphic item clearances.
2137 //
2138 auto knockoutGraphicClearance =
2139 [&]( BOARD_ITEM* aItem )
2140 {
2141 int shapeNet = -1;
2142
2143 if( aItem->Type() == PCB_SHAPE_T )
2144 shapeNet = static_cast<PCB_SHAPE*>( aItem )->GetNetCode();
2145
2146 bool sameNet = shapeNet == aZone->GetNetCode();
2147
2148 if( !aZone->IsTeardropArea() && aZone->GetNetCode() == 0 )
2149 sameNet = false;
2150
2151 // A item on the Edge_Cuts or Margin is always seen as on any layer:
2152 if( aItem->IsOnLayer( aLayer )
2153 || aItem->IsOnLayer( Edge_Cuts )
2154 || aItem->IsOnLayer( Margin ) )
2155 {
2156 if( aItem->GetBoundingBox().Intersects( zone_boundingbox ) )
2157 {
2158 bool ignoreLineWidths = false;
2159 int gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aItem, aLayer );
2160
2161 if( aItem->IsOnLayer( aLayer ) && !sameNet )
2162 {
2163 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aItem, aLayer ) );
2164 }
2165 else if( aItem->IsOnLayer( Edge_Cuts ) )
2166 {
2167 gap = std::max( gap, evalRulesForItems( EDGE_CLEARANCE_CONSTRAINT, aZone, aItem, aLayer ) );
2168 ignoreLineWidths = true;
2169 }
2170 else if( aItem->IsOnLayer( Margin ) )
2171 {
2172 gap = std::max( gap, evalRulesForItems( EDGE_CLEARANCE_CONSTRAINT, aZone, aItem, aLayer ) );
2173 }
2174
2175 if( gap >= 0 )
2176 {
2177 gap += extra_margin;
2178 addKnockout( aItem, aLayer, gap, ignoreLineWidths, aHoles );
2179 }
2180 }
2181 }
2182 };
2183
2184 auto knockoutCourtyardClearance =
2185 [&]( FOOTPRINT* aFootprint )
2186 {
2187 if( aFootprint->GetBoundingBox().Intersects( zone_boundingbox ) )
2188 {
2189 int gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aFootprint, aLayer );
2190
2191 if( gap == 0 )
2192 {
2193 aHoles.Append( aFootprint->GetCourtyard( aLayer ) );
2194 }
2195 else if( gap > 0 )
2196 {
2197 SHAPE_POLY_SET hole = aFootprint->GetCourtyard( aLayer );
2199 aHoles.Append( hole );
2200 }
2201 }
2202 };
2203
2204 for( FOOTPRINT* footprint : m_board->Footprints() )
2205 {
2206 knockoutCourtyardClearance( footprint );
2207 knockoutGraphicClearance( &footprint->Reference() );
2208 knockoutGraphicClearance( &footprint->Value() );
2209
2210 std::set<PAD*> allowedNetTiePads;
2211
2212 // Don't knock out holes for graphic items which implement a net-tie to the zone's net
2213 // on the layer being filled.
2214 if( footprint->IsNetTie() )
2215 {
2216 for( PAD* pad : footprint->Pads() )
2217 {
2218 bool sameNet = pad->GetNetCode() == aZone->GetNetCode();
2219
2220 if( !aZone->IsTeardropArea() && aZone->GetNetCode() == 0 )
2221 sameNet = false;
2222
2223 if( sameNet )
2224 {
2225 if( pad->IsOnLayer( aLayer ) )
2226 allowedNetTiePads.insert( pad );
2227
2228 for( PAD* other : footprint->GetNetTiePads( pad ) )
2229 {
2230 if( other->IsOnLayer( aLayer ) )
2231 allowedNetTiePads.insert( other );
2232 }
2233 }
2234 }
2235 }
2236
2237 for( BOARD_ITEM* item : footprint->GraphicalItems() )
2238 {
2239 if( checkForCancel( m_progressReporter ) )
2240 return;
2241
2242 BOX2I itemBBox = item->GetBoundingBox();
2243
2244 if( !zone_boundingbox.Intersects( itemBBox ) )
2245 continue;
2246
2247 bool skipItem = false;
2248
2249 if( item->IsOnLayer( aLayer ) )
2250 {
2251 std::shared_ptr<SHAPE> itemShape = item->GetEffectiveShape();
2252
2253 for( PAD* pad : allowedNetTiePads )
2254 {
2255 if( pad->GetBoundingBox().Intersects( itemBBox )
2256 && pad->GetEffectiveShape( aLayer )->Collide( itemShape.get() ) )
2257 {
2258 skipItem = true;
2259 break;
2260 }
2261 }
2262 }
2263
2264 if( !skipItem )
2265 knockoutGraphicClearance( item );
2266 }
2267 }
2268
2269 for( BOARD_ITEM* item : m_board->Drawings() )
2270 {
2271 if( checkForCancel( m_progressReporter ) )
2272 return;
2273
2274 knockoutGraphicClearance( item );
2275 }
2276
2277 // Add non-connected zone clearances
2278 //
2279 auto knockoutZoneClearance =
2280 [&]( ZONE* aKnockout )
2281 {
2282 // If the zones share no common layers
2283 if( !aKnockout->GetLayerSet().test( aLayer ) )
2284 return;
2285
2286 if( aKnockout->GetBoundingBox().Intersects( zone_boundingbox ) )
2287 {
2288 if( aKnockout->GetIsRuleArea() )
2289 {
2290 // Keepouts use outline with no clearance
2291 aKnockout->TransformSmoothedOutlineToPolygon( aHoles, 0, m_maxError, ERROR_OUTSIDE, nullptr );
2292 }
2293 else
2294 {
2295 int gap = std::max( 0, evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aKnockout,
2296 aLayer ) );
2297
2298 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aKnockout, aLayer ) );
2299
2300 SHAPE_POLY_SET poly;
2301 aKnockout->TransformShapeToPolygon( poly, aLayer, gap + extra_margin, m_maxError,
2302 ERROR_OUTSIDE );
2303 aHoles.Append( poly );
2304 }
2305 }
2306 };
2307
2308 if( aIncludeZoneClearances )
2309 {
2310 for( ZONE* otherZone : m_board->Zones() )
2311 {
2312 if( checkForCancel( m_progressReporter ) )
2313 return;
2314
2315 // Only check zones whose bounding box overlaps the max clearance
2316 if( !otherZone->GetBoundingBox().Intersects( zone_boundingbox ) )
2317 continue;
2318
2319 // Negative clearance permits zones to short
2320 if( evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, otherZone, aLayer ) < 0 )
2321 continue;
2322
2323 if( otherZone->GetIsRuleArea() )
2324 {
2325 if( otherZone->GetDoNotAllowZoneFills() && !aZone->IsTeardropArea() )
2326 knockoutZoneClearance( otherZone );
2327 }
2328 else if( otherZone->HigherPriority( aZone ) )
2329 {
2330 if( !otherZone->SameNet( aZone ) )
2331 knockoutZoneClearance( otherZone );
2332 }
2333 }
2334
2335 for( FOOTPRINT* footprint : m_board->Footprints() )
2336 {
2337 for( ZONE* otherZone : footprint->Zones() )
2338 {
2339 if( checkForCancel( m_progressReporter ) )
2340 return;
2341
2342 // Only check zones whose bounding box overlaps
2343 if( !otherZone->GetBoundingBox().Intersects( zone_boundingbox ) )
2344 continue;
2345
2346 if( otherZone->GetIsRuleArea() )
2347 {
2348 if( otherZone->GetDoNotAllowZoneFills() && !aZone->IsTeardropArea() )
2349 knockoutZoneClearance( otherZone );
2350 }
2351 else if( otherZone->HigherPriority( aZone ) )
2352 {
2353 if( !otherZone->SameNet( aZone ) )
2354 knockoutZoneClearance( otherZone );
2355 }
2356 }
2357 }
2358 }
2359
2360 aHoles.Simplify();
2361}
2362
2363
2369 SHAPE_POLY_SET& aHoles )
2370{
2371 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
2372 int extra_margin = pcbIUScale.mmToIU( ADVANCED_CFG::GetCfg().m_ExtraClearance );
2373
2374 BOX2I zone_boundingbox = aZone->GetBoundingBox();
2375 zone_boundingbox.Inflate( m_worstClearance + extra_margin );
2376
2377 auto evalRulesForItems =
2378 [&bds]( DRC_CONSTRAINT_T aConstraint, const BOARD_ITEM* a, const BOARD_ITEM* b,
2379 PCB_LAYER_ID aEvalLayer ) -> int
2380 {
2381 DRC_CONSTRAINT c = bds.m_DRCEngine->EvalRules( aConstraint, a, b, aEvalLayer );
2382
2383 if( c.IsNull() )
2384 return -1;
2385 else
2386 return c.GetValue().Min();
2387 };
2388
2389 auto knockoutZoneClearance =
2390 [&]( ZONE* aKnockout )
2391 {
2392 if( !aKnockout->GetLayerSet().test( aLayer ) )
2393 return;
2394
2395 if( aKnockout->GetBoundingBox().Intersects( zone_boundingbox ) )
2396 {
2397 if( aKnockout->GetIsRuleArea() )
2398 {
2399 aKnockout->TransformSmoothedOutlineToPolygon( aHoles, 0, m_maxError,
2400 ERROR_OUTSIDE, nullptr );
2401 }
2402 else
2403 {
2404 int gap = std::max( 0, evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT,
2405 aZone, aKnockout, aLayer ) );
2406
2407 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone,
2408 aKnockout, aLayer ) );
2409
2410 SHAPE_POLY_SET poly;
2411 aKnockout->TransformShapeToPolygon( poly, aLayer, gap + extra_margin,
2413 aHoles.Append( poly );
2414 }
2415 }
2416 };
2417
2418 for( ZONE* otherZone : m_board->Zones() )
2419 {
2420 if( !otherZone->GetBoundingBox().Intersects( zone_boundingbox ) )
2421 continue;
2422
2423 if( evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, otherZone, aLayer ) < 0 )
2424 continue;
2425
2426 if( otherZone->GetIsRuleArea() )
2427 {
2428 if( otherZone->GetDoNotAllowZoneFills() && !aZone->IsTeardropArea() )
2429 knockoutZoneClearance( otherZone );
2430 }
2431 else if( otherZone->HigherPriority( aZone ) )
2432 {
2433 if( !otherZone->SameNet( aZone ) )
2434 knockoutZoneClearance( otherZone );
2435 }
2436 }
2437
2438 for( FOOTPRINT* footprint : m_board->Footprints() )
2439 {
2440 for( ZONE* otherZone : footprint->Zones() )
2441 {
2442 if( !otherZone->GetBoundingBox().Intersects( zone_boundingbox ) )
2443 continue;
2444
2445 if( otherZone->GetIsRuleArea() )
2446 {
2447 if( otherZone->GetDoNotAllowZoneFills() && !aZone->IsTeardropArea() )
2448 knockoutZoneClearance( otherZone );
2449 }
2450 else if( otherZone->HigherPriority( aZone ) )
2451 {
2452 if( !otherZone->SameNet( aZone ) )
2453 knockoutZoneClearance( otherZone );
2454 }
2455 }
2456 }
2457
2458 aHoles.Simplify();
2459}
2460
2461
2467 SHAPE_POLY_SET& aRawFill )
2468{
2469 BOX2I zoneBBox = aZone->GetBoundingBox();
2470
2471 auto knockoutZoneOutline =
2472 [&]( ZONE* aKnockout )
2473 {
2474 // If the zones share no common layers
2475 if( !aKnockout->GetLayerSet().test( aLayer ) )
2476 return;
2477
2478 if( aKnockout->GetBoundingBox().Intersects( zoneBBox ) )
2479 {
2480 // Processing of arc shapes in zones is not yet supported because Clipper
2481 // can't do boolean operations on them. The poly outline must be converted to
2482 // segments first.
2483 SHAPE_POLY_SET outline = aKnockout->Outline()->CloneDropTriangulation();
2484 outline.ClearArcs();
2485
2486 aRawFill.BooleanSubtract( outline );
2487 }
2488 };
2489
2490 for( ZONE* otherZone : m_board->Zones() )
2491 {
2492 // Don't use the `HigherPriority()` check here because we _only_ want to knock out zones
2493 // with explicitly higher priorities, not those with equal priorities
2494 if( otherZone->SameNet( aZone )
2495 && otherZone->GetAssignedPriority() > aZone->GetAssignedPriority() )
2496 {
2497 // Do not remove teardrop area: it is not useful and not good
2498 if( !otherZone->IsTeardropArea() )
2499 knockoutZoneOutline( otherZone );
2500 }
2501 }
2502
2503 for( FOOTPRINT* footprint : m_board->Footprints() )
2504 {
2505 for( ZONE* otherZone : footprint->Zones() )
2506 {
2507 if( otherZone->SameNet( aZone ) && otherZone->HigherPriority( aZone ) )
2508 {
2509 // Do not remove teardrop area: it is not useful and not good
2510 if( !otherZone->IsTeardropArea() )
2511 knockoutZoneOutline( otherZone );
2512 }
2513 }
2514 }
2515}
2516
2517
2518void ZONE_FILLER::connect_nearby_polys( SHAPE_POLY_SET& aPolys, double aDistance )
2519{
2520 if( aPolys.OutlineCount() < 1 )
2521 return;
2522
2523 VERTEX_CONNECTOR vs( aPolys.BBoxFromCaches(), aPolys, aDistance );
2524
2525 vs.FindResults();
2526
2527 // This cannot be a reference because we need to do the comparison below while
2528 // changing the values
2529 std::map<int, std::vector<std::pair<int, VECTOR2I>>> insertion_points;
2530
2531 for( const RESULTS& result : vs.GetResults() )
2532 {
2533 SHAPE_LINE_CHAIN& line1 = aPolys.Outline( result.m_outline1 );
2534 SHAPE_LINE_CHAIN& line2 = aPolys.Outline( result.m_outline2 );
2535
2536 VECTOR2I pt1 = line1.CPoint( result.m_vertex1 );
2537 VECTOR2I pt2 = line2.CPoint( result.m_vertex2 );
2538
2539 // We want to insert the existing point first so that we can place the new point
2540 // between the two points at the same location.
2541 insertion_points[result.m_outline1].push_back( { result.m_vertex1, pt1 } );
2542 insertion_points[result.m_outline1].push_back( { result.m_vertex1, pt2 } );
2543 }
2544
2545 for( auto& [outline, vertices] : insertion_points )
2546 {
2547 SHAPE_LINE_CHAIN& line = aPolys.Outline( outline );
2548
2549 if( vertices.empty() )
2550 continue;
2551
2552 // Stable sort here because we want to make sure that we are inserting pt1 first and
2553 // pt2 second but still sorting the rest of the indices
2554 std::stable_sort( vertices.begin(), vertices.end(),
2555 []( const std::pair<int, VECTOR2I>& a, const std::pair<int, VECTOR2I>& b )
2556 {
2557 return a.first < b.first;
2558 } );
2559
2560 std::vector<VECTOR2I> new_points;
2561 new_points.reserve( line.PointCount() + vertices.size() );
2562
2563 size_t vertex_idx = 0;
2564
2565 for( int i = 0; i < line.PointCount(); ++i )
2566 {
2567 new_points.push_back( line.CPoint( i ) );
2568
2569 // Insert all points that should come after position i
2570 while( vertex_idx < vertices.size() && vertices[vertex_idx].first == i )
2571 {
2572 new_points.push_back( vertices[vertex_idx].second );
2573 vertex_idx++;
2574 }
2575 }
2576
2577 line.Clear();
2578
2579 for( const auto& pt : new_points )
2580 line.Append( pt );
2581 }
2582}
2583
2584
2585#define DUMP_POLYS_TO_COPPER_LAYER( a, b, c ) \
2586 { if( m_debugZoneFiller && aDebugLayer == b ) \
2587 { \
2588 m_board->SetLayerName( b, c ); \
2589 SHAPE_POLY_SET d = a; \
2590 d.Fracture(); \
2591 aFillPolys = d; \
2592 return false; \
2593 } \
2594 }
2595
2596
2597/*
2598 * Note that aSmoothedOutline is larger than the zone where it intersects with other, same-net
2599 * zones. This is to prevent the re-inflation post min-width trimming from createing divots
2600 * between adjacent zones. The final aMaxExtents trimming will remove these areas from the final
2601 * fill.
2602 */
2603bool ZONE_FILLER::fillCopperZone( const ZONE* aZone, PCB_LAYER_ID aLayer, PCB_LAYER_ID aDebugLayer,
2604 const SHAPE_POLY_SET& aSmoothedOutline,
2605 const SHAPE_POLY_SET& aMaxExtents, SHAPE_POLY_SET& aFillPolys )
2606{
2607 // m_maxError is initialized in the constructor. Don't reassign here to avoid data races
2608 // when multiple threads call this function concurrently.
2609
2610 // Features which are min_width should survive pruning; features that are *less* than
2611 // min_width should not. Therefore we subtract epsilon from the min_width when
2612 // deflating/inflating.
2613 int half_min_width = aZone->GetMinThickness() / 2;
2614 int epsilon = pcbIUScale.mmToIU( 0.001 );
2615
2616 // Solid polygons are deflated and inflated during calculations. Deflating doesn't cause
2617 // issues, but inflate is tricky as it can create excessively long and narrow spikes for
2618 // acute angles.
2619 // ALLOW_ACUTE_CORNERS cannot be used due to the spike problem.
2620 // CHAMFER_ACUTE_CORNERS is tempting, but can still produce spikes in some unusual
2621 // circumstances (https://gitlab.com/kicad/code/kicad/-/issues/5581).
2622 // It's unclear if ROUND_ACUTE_CORNERS would have the same issues, but is currently avoided
2623 // as a "less-safe" option.
2624 // ROUND_ALL_CORNERS produces the uniformly nicest shapes, but also a lot of segments.
2625 // CHAMFER_ALL_CORNERS improves the segment count.
2628
2629 std::vector<BOARD_ITEM*> thermalConnectionPads;
2630 std::vector<PAD*> noConnectionPads;
2631 std::deque<SHAPE_LINE_CHAIN> thermalSpokes;
2632 SHAPE_POLY_SET clearanceHoles;
2633
2634 aFillPolys = aSmoothedOutline;
2635 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In1_Cu, wxT( "smoothed-outline" ) );
2636
2637 if( m_progressReporter && m_progressReporter->IsCancelled() )
2638 return false;
2639
2640 /* -------------------------------------------------------------------------------------
2641 * Knockout thermal reliefs.
2642 */
2643
2644 knockoutThermalReliefs( aZone, aLayer, aFillPolys, thermalConnectionPads, noConnectionPads );
2645 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In2_Cu, wxT( "minus-thermal-reliefs" ) );
2646
2647 if( m_progressReporter && m_progressReporter->IsCancelled() )
2648 return false;
2649
2650 /* -------------------------------------------------------------------------------------
2651 * For hatch zones, add thermal rings around pads with thermal relief.
2652 * The rings are clipped to the zone boundary and provide the connection point
2653 * for the hatch webbing instead of connecting directly to the pad.
2654 */
2655
2656 SHAPE_POLY_SET thermalRings;
2657
2659 {
2660 buildHatchZoneThermalRings( aZone, aLayer, aSmoothedOutline, thermalConnectionPads,
2661 aFillPolys, thermalRings );
2662 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In2_Cu, wxT( "plus-thermal-rings" ) );
2663 }
2664
2665 if( m_progressReporter && m_progressReporter->IsCancelled() )
2666 return false;
2667
2668 /* -------------------------------------------------------------------------------------
2669 * Knockout electrical clearances.
2670 */
2671
2672 // When iterative refill is enabled, we build zone clearances separately so we can cache
2673 // the fill before zone knockouts are applied (issue 21746).
2674 const bool iterativeRefill = ADVANCED_CFG::GetCfg().m_ZoneFillIterativeRefill;
2675
2676 buildCopperItemClearances( aZone, aLayer, noConnectionPads, clearanceHoles,
2677 !iterativeRefill /* include zone clearances only if not iterative */ );
2678 DUMP_POLYS_TO_COPPER_LAYER( clearanceHoles, In3_Cu, wxT( "clearance-holes" ) );
2679
2680 if( m_progressReporter && m_progressReporter->IsCancelled() )
2681 return false;
2682
2683 /* -------------------------------------------------------------------------------------
2684 * Add thermal relief spokes.
2685 */
2686
2687 buildThermalSpokes( aZone, aLayer, thermalConnectionPads, thermalSpokes );
2688
2689 if( m_progressReporter && m_progressReporter->IsCancelled() )
2690 return false;
2691
2692 // Create a temporary zone that we can hit-test spoke-ends against. It's only temporary
2693 // because the "real" subtract-clearance-holes has to be done after the spokes are added.
2694 static const bool USE_BBOX_CACHES = true;
2695 SHAPE_POLY_SET testAreas = aFillPolys.CloneDropTriangulation();
2696 testAreas.BooleanSubtract( clearanceHoles );
2697 DUMP_POLYS_TO_COPPER_LAYER( testAreas, In4_Cu, wxT( "minus-clearance-holes" ) );
2698
2699 // Prune features that don't meet minimum-width criteria
2700 if( half_min_width - epsilon > epsilon )
2701 {
2702 testAreas.Deflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2703 DUMP_POLYS_TO_COPPER_LAYER( testAreas, In5_Cu, wxT( "spoke-test-deflated" ) );
2704
2705 testAreas.Inflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2706 DUMP_POLYS_TO_COPPER_LAYER( testAreas, In6_Cu, wxT( "spoke-test-reinflated" ) );
2707 }
2708
2709 if( m_progressReporter && m_progressReporter->IsCancelled() )
2710 return false;
2711
2712 // Spoke-end-testing is hugely expensive so we generate cached bounding-boxes to speed
2713 // things up a bit.
2714 testAreas.BuildBBoxCaches();
2715 int interval = 0;
2716
2717 SHAPE_POLY_SET debugSpokes;
2718
2719 for( const SHAPE_LINE_CHAIN& spoke : thermalSpokes )
2720 {
2721 const VECTOR2I& testPt = spoke.CPoint( 3 );
2722
2723 // Hit-test against zone body
2724 if( testAreas.Contains( testPt, -1, 1, USE_BBOX_CACHES ) )
2725 {
2726 if( m_debugZoneFiller )
2727 debugSpokes.AddOutline( spoke );
2728
2729 aFillPolys.AddOutline( spoke );
2730 continue;
2731 }
2732
2733 if( interval++ > 400 )
2734 {
2735 if( m_progressReporter && m_progressReporter->IsCancelled() )
2736 return false;
2737
2738 interval = 0;
2739 }
2740
2741 // Hit-test against other spokes
2742 for( const SHAPE_LINE_CHAIN& other : thermalSpokes )
2743 {
2744 // Hit test in both directions to avoid interactions with round-off errors.
2745 // (See https://gitlab.com/kicad/code/kicad/-/issues/13316.)
2746 if( &other != &spoke
2747 && other.PointInside( testPt, 1, USE_BBOX_CACHES )
2748 && spoke.PointInside( other.CPoint( 3 ), 1, USE_BBOX_CACHES ) )
2749 {
2750 if( m_debugZoneFiller )
2751 debugSpokes.AddOutline( spoke );
2752
2753 aFillPolys.AddOutline( spoke );
2754 break;
2755 }
2756 }
2757 }
2758
2759 DUMP_POLYS_TO_COPPER_LAYER( debugSpokes, In7_Cu, wxT( "spokes" ) );
2760
2761 if( m_progressReporter && m_progressReporter->IsCancelled() )
2762 return false;
2763
2764 aFillPolys.BooleanSubtract( clearanceHoles );
2765 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In8_Cu, wxT( "after-spoke-trimming" ) );
2766
2767 /* -------------------------------------------------------------------------------------
2768 * Prune features that don't meet minimum-width criteria
2769 */
2770
2771 if( half_min_width - epsilon > epsilon )
2772 {
2773 aFillPolys.Deflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2774
2775 // Also deflate thermal rings to match, for correct hatch hole notching
2776 if( thermalRings.OutlineCount() > 0 )
2777 thermalRings.Deflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2778 }
2779
2780 // Min-thickness is the web thickness. On the other hand, a blob min-thickness by
2781 // min-thickness is not useful. Since there's no obvious definition of web vs. blob, we
2782 // arbitrarily choose "at least 2X min-thickness on one axis". (Since we're doing this
2783 // during the deflated state, that means we test for "at least min-thickness".)
2784 for( int ii = aFillPolys.OutlineCount() - 1; ii >= 0; ii-- )
2785 {
2786 std::vector<SHAPE_LINE_CHAIN>& island = aFillPolys.Polygon( ii );
2787 BOX2I islandExtents;
2788
2789 for( const VECTOR2I& pt : island.front().CPoints() )
2790 {
2791 islandExtents.Merge( pt );
2792
2793 if( islandExtents.GetSizeMax() > aZone->GetMinThickness() )
2794 break;
2795 }
2796
2797 if( islandExtents.GetSizeMax() < aZone->GetMinThickness() )
2798 aFillPolys.DeletePolygon( ii );
2799 }
2800
2801 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In9_Cu, wxT( "deflated" ) );
2802
2803 if( m_progressReporter && m_progressReporter->IsCancelled() )
2804 return false;
2805
2806 /* -------------------------------------------------------------------------------------
2807 * Process the hatch pattern (note that we do this while deflated)
2808 */
2809
2811 && ( !m_board->GetProject()
2812 || !m_board->GetProject()->GetLocalSettings().m_PrototypeZoneFill ) )
2813 {
2814 // Combine thermal rings with clearance holes (non-connected pad clearances) so that
2815 // the hatch hole-dropping logic considers both types of rings
2816 SHAPE_POLY_SET ringsToProtect = thermalRings;
2817 ringsToProtect.BooleanAdd( clearanceHoles );
2818
2819 if( !addHatchFillTypeOnZone( aZone, aLayer, aDebugLayer, aFillPolys, ringsToProtect ) )
2820 return false;
2821 }
2822 else
2823 {
2824 /* ---------------------------------------------------------------------------------
2825 * Connect nearby polygons with zero-width lines in order to ensure correct
2826 * re-inflation.
2827 */
2828 aFillPolys.Fracture();
2829 connect_nearby_polys( aFillPolys, aZone->GetMinThickness() );
2830
2831 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In10_Cu, wxT( "connected-nearby-polys" ) );
2832 }
2833
2834 if( m_progressReporter && m_progressReporter->IsCancelled() )
2835 return false;
2836
2837 /* -------------------------------------------------------------------------------------
2838 * Finish minimum-width pruning by re-inflating
2839 */
2840
2841 if( half_min_width - epsilon > epsilon )
2842 aFillPolys.Inflate( half_min_width - epsilon, cornerStrategy, m_maxError, true );
2843
2844 // The deflation/inflation process can leave notches in the outline. Remove these by
2845 // doing a union with the original ring
2846 aFillPolys.BooleanAdd( thermalRings );
2847
2848 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In15_Cu, wxT( "after-reinflating" ) );
2849
2850 /* -------------------------------------------------------------------------------------
2851 * Ensure additive changes (thermal stubs and inflating acute corners) do not add copper
2852 * outside the zone boundary, inside the clearance holes, or between otherwise isolated
2853 * islands
2854 */
2855
2856 for( BOARD_ITEM* item : thermalConnectionPads )
2857 {
2858 if( item->Type() == PCB_PAD_T )
2859 addHoleKnockout( static_cast<PAD*>( item ), 0, clearanceHoles );
2860 }
2861
2862 aFillPolys.BooleanIntersection( aMaxExtents );
2863 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In16_Cu, wxT( "after-trim-to-outline" ) );
2864 aFillPolys.BooleanSubtract( clearanceHoles );
2865 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In17_Cu, wxT( "after-trim-to-clearance-holes" ) );
2866
2867 /* -------------------------------------------------------------------------------------
2868 * Lastly give any same-net but higher-priority zones control over their own area.
2869 */
2870
2871 // Cache the pre-knockout fill for iterative refill optimization (issue 21746)
2872 // At this point, the fill has NOT been knocked out by higher-priority zones on different nets
2873 if( iterativeRefill )
2874 {
2875 {
2876 std::lock_guard<std::mutex> lock( m_cacheMutex );
2877 m_preKnockoutFillCache[{ aZone, aLayer }] = aFillPolys;
2878 }
2879
2880 BOX2I cacheBBox = aFillPolys.BBox();
2881
2882 wxLogTrace( traceZoneFiller,
2883 wxT( "Cached pre-knockout fill for zone %s layer %d: %d outlines, area %.0f, "
2884 "bbox (%d,%d)-(%d,%d)" ),
2885 aZone->GetNetname(), static_cast<int>( aLayer ),
2886 aFillPolys.OutlineCount(), aFillPolys.Area(),
2887 cacheBBox.GetX(), cacheBBox.GetY(), cacheBBox.GetRight(), cacheBBox.GetBottom() );
2888
2889 // Now apply zone-to-zone knockouts for different-net zones
2890 SHAPE_POLY_SET zoneClearances;
2891 buildDifferentNetZoneClearances( aZone, aLayer, zoneClearances );
2892
2893 if( zoneClearances.OutlineCount() > 0 )
2894 aFillPolys.BooleanSubtract( zoneClearances );
2895 }
2896
2897 subtractHigherPriorityZones( aZone, aLayer, aFillPolys );
2898 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In18_Cu, wxT( "minus-higher-priority-zones" ) );
2899
2900 aFillPolys.Fracture();
2901 return true;
2902}
2903
2904
2906 const SHAPE_POLY_SET& aSmoothedOutline,
2907 SHAPE_POLY_SET& aFillPolys )
2908{
2909 BOX2I zone_boundingbox = aZone->GetBoundingBox();
2910 SHAPE_POLY_SET clearanceHoles;
2911 long ticker = 0;
2912
2913 auto checkForCancel =
2914 [&ticker]( PROGRESS_REPORTER* aReporter ) -> bool
2915 {
2916 return aReporter && ( ticker++ % 50 ) == 0 && aReporter->IsCancelled();
2917 };
2918
2919 auto knockoutGraphicItem =
2920 [&]( BOARD_ITEM* aItem )
2921 {
2922 if( aItem->IsKnockout() && aItem->IsOnLayer( aLayer )
2923 && aItem->GetBoundingBox().Intersects( zone_boundingbox ) )
2924 {
2925 addKnockout( aItem, aLayer, 0, true, clearanceHoles );
2926 }
2927 };
2928
2929 for( FOOTPRINT* footprint : m_board->Footprints() )
2930 {
2931 if( checkForCancel( m_progressReporter ) )
2932 return false;
2933
2934 knockoutGraphicItem( &footprint->Reference() );
2935 knockoutGraphicItem( &footprint->Value() );
2936
2937 for( BOARD_ITEM* item : footprint->GraphicalItems() )
2938 knockoutGraphicItem( item );
2939 }
2940
2941 for( BOARD_ITEM* item : m_board->Drawings() )
2942 {
2943 if( checkForCancel( m_progressReporter ) )
2944 return false;
2945
2946 knockoutGraphicItem( item );
2947 }
2948
2949 aFillPolys = aSmoothedOutline;
2950 aFillPolys.BooleanSubtract( clearanceHoles );
2951
2952 auto subtractKeepout =
2953 [&]( ZONE* candidate )
2954 {
2955 if( !candidate->GetIsRuleArea() )
2956 return;
2957
2958 if( !candidate->HasKeepoutParametersSet() )
2959 return;
2960
2961 if( candidate->GetDoNotAllowZoneFills() && candidate->IsOnLayer( aLayer ) )
2962 {
2963 if( candidate->GetBoundingBox().Intersects( zone_boundingbox ) )
2964 {
2965 if( candidate->Outline()->ArcCount() == 0 )
2966 {
2967 aFillPolys.BooleanSubtract( *candidate->Outline() );
2968 }
2969 else
2970 {
2971 SHAPE_POLY_SET keepoutOutline( *candidate->Outline() );
2972 keepoutOutline.ClearArcs();
2973 aFillPolys.BooleanSubtract( keepoutOutline );
2974 }
2975 }
2976 }
2977 };
2978
2979 for( ZONE* keepout : m_board->Zones() )
2980 {
2981 if( checkForCancel( m_progressReporter ) )
2982 return false;
2983
2984 subtractKeepout( keepout );
2985 }
2986
2987 for( FOOTPRINT* footprint : m_board->Footprints() )
2988 {
2989 if( checkForCancel( m_progressReporter ) )
2990 return false;
2991
2992 for( ZONE* keepout : footprint->Zones() )
2993 subtractKeepout( keepout );
2994 }
2995
2996 // Features which are min_width should survive pruning; features that are *less* than
2997 // min_width should not. Therefore we subtract epsilon from the min_width when
2998 // deflating/inflating.
2999 int half_min_width = aZone->GetMinThickness() / 2;
3000 int epsilon = pcbIUScale.mmToIU( 0.001 );
3001
3002 aFillPolys.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS, m_maxError );
3003
3004 // Remove the non filled areas due to the hatch pattern
3006 {
3007 SHAPE_POLY_SET noThermalRings; // Non-copper zones have no thermal reliefs
3008
3009 if( !addHatchFillTypeOnZone( aZone, aLayer, aLayer, aFillPolys, noThermalRings ) )
3010 return false;
3011 }
3012
3013 // Re-inflate after pruning of areas that don't meet minimum-width criteria
3014 if( half_min_width - epsilon > epsilon )
3015 aFillPolys.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS, m_maxError );
3016
3017 aFillPolys.Fracture();
3018 return true;
3019}
3020
3021
3022/*
3023 * Build the filled solid areas data from real outlines (stored in m_Poly)
3024 * The solid areas can be more than one on copper layers, and do not have holes
3025 * ( holes are linked by overlapping segments to the main outline)
3026 */
3028{
3029 SHAPE_POLY_SET* boardOutline = m_brdOutlinesValid ? &m_boardOutline : nullptr;
3030 SHAPE_POLY_SET maxExtents;
3031 SHAPE_POLY_SET smoothedPoly;
3032 PCB_LAYER_ID debugLayer = UNDEFINED_LAYER;
3033
3034 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( aLayer ) )
3035 {
3036 debugLayer = aLayer;
3037 aLayer = F_Cu;
3038 }
3039
3040 if( !aZone->BuildSmoothedPoly( maxExtents, aLayer, boardOutline, &smoothedPoly ) )
3041 return false;
3042
3043 if( m_progressReporter && m_progressReporter->IsCancelled() )
3044 return false;
3045
3046 if( aZone->IsOnCopperLayer() )
3047 {
3048 if( fillCopperZone( aZone, aLayer, debugLayer, smoothedPoly, maxExtents, aFillPolys ) )
3049 aZone->SetNeedRefill( false );
3050 }
3051 else
3052 {
3053 if( fillNonCopperZone( aZone, aLayer, smoothedPoly, aFillPolys ) )
3054 aZone->SetNeedRefill( false );
3055 }
3056
3057 return true;
3058}
3059
3060
3065 const std::vector<BOARD_ITEM*>& aSpokedPadsList,
3066 std::deque<SHAPE_LINE_CHAIN>& aSpokesList )
3067{
3068 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3069 BOX2I zoneBB = aZone->GetBoundingBox();
3070 DRC_CONSTRAINT constraint;
3071 int zone_half_width = aZone->GetMinThickness() / 2;
3072
3074 zone_half_width = aZone->GetHatchThickness() / 2;
3075
3076 zoneBB.Inflate( std::max( bds.GetBiggestClearanceValue(), aZone->GetLocalClearance().value() ) );
3077
3078 // Is a point on the boundary of the polygon inside or outside?
3079 // The boundary may be off by MaxError
3080 int epsilon = bds.m_MaxError;
3081
3082 for( BOARD_ITEM* item : aSpokedPadsList )
3083 {
3084 // We currently only connect to pads, not pad holes
3085 if( !item->IsOnLayer( aLayer ) )
3086 continue;
3087
3088 int thermalReliefGap = 0;
3089 int spoke_w = 0;
3090 PAD* pad = nullptr;
3091 PCB_VIA* via = nullptr;
3092 bool circular = false;
3093
3094 if( item->Type() == PCB_PAD_T )
3095 {
3096 pad = static_cast<PAD*>( item );
3097 VECTOR2I padSize = pad->GetSize( aLayer );
3098
3099 if( pad->GetShape( aLayer) == PAD_SHAPE::CIRCLE
3100 || ( pad->GetShape( aLayer ) == PAD_SHAPE::OVAL && padSize.x == padSize.y ) )
3101 {
3102 circular = true;
3103 }
3104 }
3105 else if( item->Type() == PCB_VIA_T )
3106 {
3107 via = static_cast<PCB_VIA*>( item );
3108 circular = true;
3109 }
3110
3111 // For hatch zones, use proper DRC constraints for thermal gap and spoke width,
3112 // just like solid zones. This ensures consistent thermal relief appearance and
3113 // respects pad-specific thermal spoke settings.
3115 {
3116 if( pad )
3117 {
3119 aZone, aLayer );
3120 thermalReliefGap = constraint.GetValue().Min();
3121
3123 aZone, aLayer );
3124 spoke_w = constraint.GetValue().Opt();
3125
3126 int spoke_max_allowed_w = std::min( pad->GetSize( aLayer ).x, pad->GetSize( aLayer ).y );
3127 spoke_w = std::clamp( spoke_w, constraint.Value().Min(), constraint.Value().Max() );
3128 spoke_w = std::min( spoke_w, spoke_max_allowed_w );
3129
3130 if( spoke_w < aZone->GetMinThickness() )
3131 continue;
3132 }
3133 else if( via )
3134 {
3136 aZone, aLayer );
3137 thermalReliefGap = constraint.GetValue().Min();
3138
3140 aZone, aLayer );
3141 spoke_w = constraint.GetValue().Opt();
3142
3143 spoke_w = std::min( spoke_w, via->GetWidth( aLayer ) );
3144
3145 if( spoke_w < aZone->GetMinThickness() )
3146 continue;
3147 }
3148 else
3149 {
3150 continue;
3151 }
3152 }
3153 else if( pad )
3154 {
3155 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, pad, aZone, aLayer );
3156 thermalReliefGap = constraint.GetValue().Min();
3157
3158 constraint = bds.m_DRCEngine->EvalRules( THERMAL_SPOKE_WIDTH_CONSTRAINT, pad, aZone, aLayer );
3159 spoke_w = constraint.GetValue().Opt();
3160
3161 // Spoke width should ideally be smaller than the pad minor axis.
3162 // Otherwise the thermal shape is not really a thermal relief,
3163 // and the algo to count the actual number of spokes can fail
3164 int spoke_max_allowed_w = std::min( pad->GetSize( aLayer ).x, pad->GetSize( aLayer ).y );
3165
3166 spoke_w = std::clamp( spoke_w, constraint.Value().Min(), constraint.Value().Max() );
3167
3168 // ensure the spoke width is smaller than the pad minor size
3169 spoke_w = std::min( spoke_w, spoke_max_allowed_w );
3170
3171 // Cannot create stubs having a width < zone min thickness
3172 if( spoke_w < aZone->GetMinThickness() )
3173 continue;
3174 }
3175 else
3176 {
3177 // We don't currently support via thermal connections *except* in a hatched zone.
3178 continue;
3179 }
3180
3181 int spoke_half_w = spoke_w / 2;
3182
3183 // Quick test here to possibly save us some work
3184 BOX2I itemBB = item->GetBoundingBox();
3185 itemBB.Inflate( thermalReliefGap + epsilon );
3186
3187 if( !( itemBB.Intersects( zoneBB ) ) )
3188 continue;
3189
3190 bool customSpokes = false;
3191
3192 if( pad && pad->GetShape( aLayer ) == PAD_SHAPE::CUSTOM )
3193 {
3194 for( const std::shared_ptr<PCB_SHAPE>& primitive : pad->GetPrimitives( aLayer ) )
3195 {
3196 if( primitive->IsProxyItem() && primitive->GetShape() == SHAPE_T::SEGMENT )
3197 {
3198 customSpokes = true;
3199 break;
3200 }
3201 }
3202 }
3203
3204 // Thermal spokes consist of square-ended segments from the pad center to points just
3205 // outside the thermal relief. The outside end has an extra center point (which must be
3206 // at idx 3) which is used for testing whether or not the spoke connects to copper in the
3207 // parent zone.
3208
3209 auto buildSpokesFromOrigin =
3210 [&]( const BOX2I& box, EDA_ANGLE angle )
3211 {
3212 VECTOR2I center = box.GetCenter();
3213 VECTOR2I half_size = KiROUND( box.GetWidth() / 2.0, box.GetHeight() / 2.0 );
3214
3215 // Function to find intersection of line with box edge
3216 auto intersectBBox =
3217 [&]( const EDA_ANGLE& spokeAngle, VECTOR2I* spoke_side ) -> VECTOR2I
3218 {
3219 double dx = spokeAngle.Cos();
3220 double dy = spokeAngle.Sin();
3221
3222 // Short-circuit the axis cases because they will be degenerate in the
3223 // intersection test
3224 if( dx == 0 )
3225 {
3226 *spoke_side = VECTOR2I( spoke_half_w, 0 );
3227 return KiROUND( 0.0, dy * half_size.y );
3228 }
3229 else if( dy == 0 )
3230 {
3231 *spoke_side = VECTOR2I( 0, spoke_half_w );
3232 return KiROUND( dx * half_size.x, 0.0 );
3233 }
3234
3235 // We are going to intersect with one side or the other. Whichever
3236 // we hit first is the fraction of the spoke length we keep
3237 double dist_x = half_size.x / std::abs( dx );
3238 double dist_y = half_size.y / std::abs( dy );
3239
3240 if( dist_x < dist_y )
3241 {
3242 *spoke_side = KiROUND( 0.0, spoke_half_w / ( ANGLE_90 - spokeAngle ).Sin() );
3243 return KiROUND( dx * dist_x, dy * dist_x );
3244 }
3245 else
3246 {
3247 *spoke_side = KiROUND( spoke_half_w / spokeAngle.Sin(), 0.0 );
3248 return KiROUND( dx * dist_y, dy * dist_y );
3249 }
3250 };
3251
3252 // Precalculate angles for four cardinal directions
3253 const EDA_ANGLE angles[4] = {
3254 EDA_ANGLE( 0.0, DEGREES_T ) + angle, // Right
3255 EDA_ANGLE( 90.0, DEGREES_T ) + angle, // Up
3256 EDA_ANGLE( 180.0, DEGREES_T ) + angle, // Left
3257 EDA_ANGLE( 270.0, DEGREES_T ) + angle // Down
3258 };
3259
3260 // Generate four spokes in cardinal directions
3261 for( const EDA_ANGLE& spokeAngle : angles )
3262 {
3263 VECTOR2I spoke_side;
3264 VECTOR2I intersection = intersectBBox( spokeAngle, &spoke_side );
3265
3266 SHAPE_LINE_CHAIN spoke;
3267 spoke.Append( center + spoke_side );
3268 spoke.Append( center - spoke_side );
3269 spoke.Append( center + intersection - spoke_side );
3270 spoke.Append( center + intersection ); // test pt
3271 spoke.Append( center + intersection + spoke_side );
3272 spoke.SetClosed( true );
3273 aSpokesList.push_back( std::move( spoke ) );
3274 }
3275 };
3276
3277 if( customSpokes )
3278 {
3279 SHAPE_POLY_SET thermalPoly;
3280 SHAPE_LINE_CHAIN thermalOutline;
3281
3282 pad->TransformShapeToPolygon( thermalPoly, aLayer, thermalReliefGap + epsilon, m_maxError, ERROR_OUTSIDE );
3283
3284 if( thermalPoly.OutlineCount() )
3285 thermalOutline = thermalPoly.Outline( 0 );
3286
3287 SHAPE_LINE_CHAIN padOutline = pad->GetEffectivePolygon( aLayer, ERROR_OUTSIDE )->Outline( 0 );
3288
3289 auto trimToOutline = [&]( SEG& aSegment )
3290 {
3291 SHAPE_LINE_CHAIN::INTERSECTIONS intersections;
3292
3293 if( padOutline.Intersect( aSegment, intersections ) )
3294 {
3295 intersections.clear();
3296
3297 // Trim the segment to the thermal outline
3298 if( thermalOutline.Intersect( aSegment, intersections ) )
3299 {
3300 aSegment.B = intersections.front().p;
3301 return true;
3302 }
3303 }
3304 return false;
3305 };
3306
3307 for( const std::shared_ptr<PCB_SHAPE>& primitive : pad->GetPrimitives( aLayer ) )
3308 {
3309 if( primitive->IsProxyItem() && primitive->GetShape() == SHAPE_T::SEGMENT )
3310 {
3311 SEG seg( primitive->GetStart(), primitive->GetEnd() );
3312 SHAPE_LINE_CHAIN::INTERSECTIONS intersections;
3313
3314 RotatePoint( seg.A, pad->GetOrientation() );
3315 RotatePoint( seg.B, pad->GetOrientation() );
3316 seg.A += pad->ShapePos( aLayer );
3317 seg.B += pad->ShapePos( aLayer );
3318
3319 // Make sure seg.A is the origin
3320 if( !pad->GetEffectivePolygon( aLayer, ERROR_OUTSIDE )->Contains( seg.A ) )
3321 {
3322 // Do not create this spoke if neither point is in the pad.
3323 if( !pad->GetEffectivePolygon( aLayer, ERROR_OUTSIDE )->Contains( seg.B ) )
3324 continue;
3325
3326 seg.Reverse();
3327 }
3328
3329 // Trim segment to pad and thermal outline polygon.
3330 // If there is no intersection with the pad, don't create the spoke.
3331 if( trimToOutline( seg ) )
3332 {
3333 VECTOR2I direction = ( seg.B - seg.A ).Resize( spoke_half_w );
3334 VECTOR2I offset = direction.Perpendicular().Resize( spoke_half_w );
3335 // Extend the spoke edges by half the spoke width to capture convex pad shapes
3336 // with a maximum of 45 degrees.
3337 SEG segL( seg.A - direction - offset, seg.B + direction - offset );
3338 SEG segR( seg.A - direction + offset, seg.B + direction + offset );
3339
3340 // Only create this spoke if both edges intersect the pad and thermal outline
3341 if( trimToOutline( segL ) && trimToOutline( segR ) )
3342 {
3343 // Extend the spoke by the minimum thickness for the zone to ensure full
3344 // connection width
3345 direction = direction.Resize( aZone->GetMinThickness() );
3346
3347 SHAPE_LINE_CHAIN spoke;
3348
3349 spoke.Append( seg.A + offset );
3350 spoke.Append( seg.A - offset );
3351
3352 spoke.Append( segL.B + direction );
3353 spoke.Append( seg.B + direction ); // test pt at index 3.
3354 spoke.Append( segR.B + direction );
3355
3356 spoke.SetClosed( true );
3357 aSpokesList.push_back( std::move( spoke ) );
3358 }
3359 }
3360 }
3361 }
3362 }
3363 else
3364 {
3365 EDA_ANGLE thermalSpokeAngle;
3366
3367 // Use pad's thermal spoke angle for both solid and hatch zones.
3368 // This ensures custom thermal spoke templates are respected.
3369 if( pad )
3370 thermalSpokeAngle = pad->GetThermalSpokeAngle();
3371
3372 BOX2I spokesBox;
3373 VECTOR2I position;
3374 EDA_ANGLE orientation;
3375
3376 // Since the bounding-box needs to be correclty rotated we use a dummy pad to keep
3377 // from dirtying the real pad's cached shapes.
3378 if( pad )
3379 {
3380 PAD dummy_pad( *pad );
3381 dummy_pad.SetOrientation( ANGLE_0 );
3382
3383 // Spokes are from center of pad shape, not from hole. So the dummy pad has no shape
3384 // offset and is at position 0,0
3385 dummy_pad.SetPosition( VECTOR2I( 0, 0 ) );
3386 dummy_pad.SetOffset( aLayer, VECTOR2I( 0, 0 ) );
3387
3388 spokesBox = dummy_pad.GetBoundingBox( aLayer );
3389 position = pad->ShapePos( aLayer );
3390 orientation = pad->GetOrientation();
3391 }
3392 else if( via )
3393 {
3394 PCB_VIA dummy_via( *via );
3395 dummy_via.SetPosition( VECTOR2I( 0, 0 ) );
3396
3397 spokesBox = dummy_via.GetBoundingBox( aLayer );
3398 position = via->GetPosition();
3399 }
3400
3401 // Add half the zone mininum width to the inflate amount to account for the fact that
3402 // the deflation procedure will shrink the results by half the half the zone min width.
3403 spokesBox.Inflate( thermalReliefGap + epsilon + zone_half_width );
3404
3405 // Yet another wrinkle: the bounding box for circles will overshoot the mark considerably
3406 // when the spokes are near a 45 degree increment. So we build the spokes at 0 degrees
3407 // and then rotate them to the correct position.
3408 if( circular )
3409 {
3410 buildSpokesFromOrigin( spokesBox, ANGLE_0 );
3411
3412 if( thermalSpokeAngle != ANGLE_0 )
3413 {
3414 // Rotate the last four elements of aspokeslist
3415 for( auto it = aSpokesList.rbegin(); it != aSpokesList.rbegin() + 4; ++it )
3416 it->Rotate( thermalSpokeAngle );
3417 }
3418 }
3419 else
3420 {
3421 buildSpokesFromOrigin( spokesBox, thermalSpokeAngle );
3422 }
3423
3424 auto spokeIter = aSpokesList.rbegin();
3425
3426 for( int ii = 0; ii < 4; ++ii, ++spokeIter )
3427 {
3428 spokeIter->Rotate( orientation );
3429 spokeIter->Move( position );
3430 }
3431 }
3432 }
3433
3434 for( size_t ii = 0; ii < aSpokesList.size(); ++ii )
3435 aSpokesList[ii].GenerateBBoxCache();
3436}
3437
3438
3440 const SHAPE_POLY_SET& aSmoothedOutline,
3441 const std::vector<BOARD_ITEM*>& aThermalConnectionPads,
3442 SHAPE_POLY_SET& aFillPolys,
3443 SHAPE_POLY_SET& aThermalRings )
3444{
3445 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3446 DRC_CONSTRAINT constraint;
3447
3448 for( BOARD_ITEM* item : aThermalConnectionPads )
3449 {
3450 if( !item->IsOnLayer( aLayer ) )
3451 continue;
3452
3453 PAD* pad = nullptr;
3454 PCB_VIA* via = nullptr;
3455 bool isCircular = false;
3456 int thermalGap = 0;
3457 int spokeWidth = 0;
3458 VECTOR2I position;
3459 int padRadius = 0;
3460
3461 if( item->Type() == PCB_PAD_T )
3462 {
3463 pad = static_cast<PAD*>( item );
3464 VECTOR2I padSize = pad->GetSize( aLayer );
3465 position = pad->ShapePos( aLayer );
3466
3467 isCircular = ( pad->GetShape( aLayer ) == PAD_SHAPE::CIRCLE
3468 || ( pad->GetShape( aLayer ) == PAD_SHAPE::OVAL && padSize.x == padSize.y ) );
3469
3470 if( isCircular )
3471 padRadius = std::max( padSize.x, padSize.y ) / 2;
3472
3473 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, pad, aZone, aLayer );
3474 thermalGap = constraint.GetValue().Min();
3475
3476 constraint = bds.m_DRCEngine->EvalRules( THERMAL_SPOKE_WIDTH_CONSTRAINT, pad, aZone, aLayer );
3477 spokeWidth = constraint.GetValue().Opt();
3478
3479 // Clamp spoke width to pad size
3480 int spokeMaxWidth = std::min( padSize.x, padSize.y );
3481 spokeWidth = std::min( spokeWidth, spokeMaxWidth );
3482 }
3483 else if( item->Type() == PCB_VIA_T )
3484 {
3485 via = static_cast<PCB_VIA*>( item );
3486 position = via->GetPosition();
3487 isCircular = true;
3488 padRadius = via->GetWidth( aLayer ) / 2;
3489
3490 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, via, aZone, aLayer );
3491 thermalGap = constraint.GetValue().Min();
3492
3493 constraint = bds.m_DRCEngine->EvalRules( THERMAL_SPOKE_WIDTH_CONSTRAINT, via, aZone, aLayer );
3494 spokeWidth = constraint.GetValue().Opt();
3495
3496 // Clamp spoke width to via diameter
3497 spokeWidth = std::min( spokeWidth, padRadius * 2 );
3498 }
3499 else
3500 {
3501 continue;
3502 }
3503
3504 // Don't create a ring if spoke width is too small
3505 if( spokeWidth < aZone->GetMinThickness() )
3506 continue;
3507
3508 SHAPE_POLY_SET thermalRing;
3509
3510 if( isCircular )
3511 {
3512 // For circular pads/vias: create an arc ring
3513 // Ring inner radius = pad radius + thermal gap
3514 // Ring width = spoke width
3515 int ringInnerRadius = padRadius + thermalGap;
3516 int ringWidth = spokeWidth;
3517
3518 TransformRingToPolygon( thermalRing, position, ringInnerRadius + ringWidth / 2,
3519 ringWidth, m_maxError, ERROR_OUTSIDE );
3520 }
3521 else
3522 {
3523 // For non-circular pads: create ring by inflating pad to outer radius,
3524 // then subtracting pad inflated to inner radius
3525 SHAPE_POLY_SET outerShape;
3526 SHAPE_POLY_SET innerShape;
3527
3528 // Outer ring edge = pad + thermal gap + spoke width
3529 pad->TransformShapeToPolygon( outerShape, aLayer, thermalGap + spokeWidth,
3531
3532 // Inner ring edge = pad + thermal gap (this is already knocked out)
3533 pad->TransformShapeToPolygon( innerShape, aLayer, thermalGap,
3535
3536 thermalRing = outerShape;
3537 thermalRing.BooleanSubtract( innerShape );
3538 }
3539
3540 // Clip the thermal ring to the zone boundary so it doesn't overflow
3541 thermalRing.BooleanIntersection( aSmoothedOutline );
3542
3543 // Add the thermal ring to the fill
3544 aFillPolys.BooleanAdd( thermalRing );
3545
3546 // Also collect thermal rings for hatch hole notching to ensure connectivity
3547 aThermalRings.BooleanAdd( thermalRing );
3548 }
3549}
3550
3551
3553 PCB_LAYER_ID aDebugLayer, SHAPE_POLY_SET& aFillPolys,
3554 const SHAPE_POLY_SET& aThermalRings )
3555{
3556 // Build grid:
3557
3558 // obviously line thickness must be > zone min thickness.
3559 // It can happens if a board file was edited by hand by a python script
3560 // Use 1 micron margin to be *sure* there is no issue in Gerber files
3561 // (Gbr file unit = 1 or 10 nm) due to some truncation in coordinates or calculations
3562 // This margin also avoid problems due to rounding coordinates in next calculations
3563 // that can create incorrect polygons
3564 int thickness = std::max( aZone->GetHatchThickness(),
3565 aZone->GetMinThickness() + pcbIUScale.mmToIU( 0.001 ) );
3566
3567 int gridsize = thickness + aZone->GetHatchGap();
3568 int maxError = m_board->GetDesignSettings().m_MaxError;
3569
3570 SHAPE_POLY_SET filledPolys = aFillPolys.CloneDropTriangulation();
3571 // Use a area that contains the rotated bbox by orientation, and after rotate the result
3572 // by -orientation.
3573 if( !aZone->GetHatchOrientation().IsZero() )
3574 filledPolys.Rotate( - aZone->GetHatchOrientation() );
3575
3576 BOX2I bbox = filledPolys.BBox( 0 );
3577
3578 // Build hole shape
3579 // the hole size is aZone->GetHatchGap(), but because the outline thickness
3580 // is aZone->GetMinThickness(), the hole shape size must be larger
3581 SHAPE_LINE_CHAIN hole_base;
3582 int hole_size = aZone->GetHatchGap() + aZone->GetMinThickness();
3583 VECTOR2I corner( 0, 0 );;
3584 hole_base.Append( corner );
3585 corner.x += hole_size;
3586 hole_base.Append( corner );
3587 corner.y += hole_size;
3588 hole_base.Append( corner );
3589 corner.x = 0;
3590 hole_base.Append( corner );
3591 hole_base.SetClosed( true );
3592
3593 // Calculate minimal area of a grid hole.
3594 // All holes smaller than a threshold will be removed
3595 double minimal_hole_area = hole_base.Area() * aZone->GetHatchHoleMinArea();
3596
3597 // Now convert this hole to a smoothed shape:
3598 if( aZone->GetHatchSmoothingLevel() > 0 )
3599 {
3600 // the actual size of chamfer, or rounded corner radius is the half size
3601 // of the HatchFillTypeGap scaled by aZone->GetHatchSmoothingValue()
3602 // aZone->GetHatchSmoothingValue() = 1.0 is the max value for the chamfer or the
3603 // radius of corner (radius = half size of the hole)
3604 int smooth_value = KiROUND( aZone->GetHatchGap()
3605 * aZone->GetHatchSmoothingValue() / 2 );
3606
3607 // Minimal optimization:
3608 // make smoothing only for reasonable smooth values, to avoid a lot of useless segments
3609 // and if the smooth value is small, use chamfer even if fillet is requested
3610 #define SMOOTH_MIN_VAL_MM 0.02
3611 #define SMOOTH_SMALL_VAL_MM 0.04
3612
3613 if( smooth_value > pcbIUScale.mmToIU( SMOOTH_MIN_VAL_MM ) )
3614 {
3615 SHAPE_POLY_SET smooth_hole;
3616 smooth_hole.AddOutline( hole_base );
3617 int smooth_level = aZone->GetHatchSmoothingLevel();
3618
3619 if( smooth_value < pcbIUScale.mmToIU( SMOOTH_SMALL_VAL_MM ) && smooth_level > 1 )
3620 smooth_level = 1;
3621
3622 // Use a larger smooth_value to compensate the outline tickness
3623 // (chamfer is not visible is smooth value < outline thickess)
3624 smooth_value += aZone->GetMinThickness() / 2;
3625
3626 // smooth_value cannot be bigger than the half size oh the hole:
3627 smooth_value = std::min( smooth_value, aZone->GetHatchGap() / 2 );
3628
3629 // the error to approximate a circle by segments when smoothing corners by a arc
3630 maxError = std::max( maxError * 2, smooth_value / 20 );
3631
3632 switch( smooth_level )
3633 {
3634 case 1:
3635 // Chamfer() uses the distance from a corner to create a end point
3636 // for the chamfer.
3637 hole_base = smooth_hole.Chamfer( smooth_value ).Outline( 0 );
3638 break;
3639
3640 default:
3641 if( aZone->GetHatchSmoothingLevel() > 2 )
3642 maxError /= 2; // Force better smoothing
3643
3644 hole_base = smooth_hole.Fillet( smooth_value, maxError ).Outline( 0 );
3645 break;
3646
3647 case 0:
3648 break;
3649 };
3650 }
3651 }
3652
3653 // Build holes
3654 SHAPE_POLY_SET holes;
3655
3656 auto& defaultOffsets = m_board->GetDesignSettings().m_ZoneLayerProperties;
3657 auto& localOffsets = aZone->LayerProperties();
3658
3659 VECTOR2I offset = defaultOffsets[aLayer].hatching_offset.value_or( VECTOR2I() );
3660
3661 if( localOffsets.contains( aLayer ) && localOffsets.at( aLayer ).hatching_offset.has_value() )
3662 offset = localOffsets.at( aLayer ).hatching_offset.value();
3663
3664 int x_offset = bbox.GetX() - ( bbox.GetX() ) % gridsize - gridsize;
3665 int y_offset = bbox.GetY() - ( bbox.GetY() ) % gridsize - gridsize;
3666
3667
3668 for( int xx = x_offset; xx <= bbox.GetRight(); xx += gridsize )
3669 {
3670 for( int yy = y_offset; yy <= bbox.GetBottom(); yy += gridsize )
3671 {
3672 // Generate hole
3673 SHAPE_LINE_CHAIN hole( hole_base );
3674 hole.Move( VECTOR2I( xx, yy ) );
3675
3676 if( !aZone->GetHatchOrientation().IsZero() )
3677 {
3678 hole.Rotate( aZone->GetHatchOrientation() );
3679 }
3680
3681 hole.Move( VECTOR2I( offset.x % gridsize, offset.y % gridsize ) );
3682
3683 holes.AddOutline( hole );
3684 }
3685 }
3686
3687 holes.ClearArcs();
3688
3689 DUMP_POLYS_TO_COPPER_LAYER( holes, In10_Cu, wxT( "hatch-holes" ) );
3690
3691 int deflated_thickness = aZone->GetHatchThickness() - aZone->GetMinThickness();
3692
3693 // Don't let thickness drop below maxError * 2 or it might not get reinflated.
3694 deflated_thickness = std::max( deflated_thickness, maxError * 2 );
3695
3696 // The fill has already been deflated to ensure GetMinThickness() so we just have to
3697 // account for anything beyond that.
3698 SHAPE_POLY_SET deflatedFilledPolys = aFillPolys.CloneDropTriangulation();
3699 deflatedFilledPolys.ClearArcs();
3700 deflatedFilledPolys.Deflate( deflated_thickness, CORNER_STRATEGY::CHAMFER_ALL_CORNERS, maxError );
3701 holes.BooleanIntersection( deflatedFilledPolys );
3702 DUMP_POLYS_TO_COPPER_LAYER( holes, In11_Cu, wxT( "fill-clipped-hatch-holes" ) );
3703
3704 SHAPE_POLY_SET deflatedOutline = aZone->Outline()->CloneDropTriangulation();
3705 deflatedOutline.ClearArcs();
3706 deflatedOutline.Deflate( aZone->GetMinThickness(), CORNER_STRATEGY::CHAMFER_ALL_CORNERS, maxError );
3707 holes.BooleanIntersection( deflatedOutline );
3708 DUMP_POLYS_TO_COPPER_LAYER( holes, In12_Cu, wxT( "outline-clipped-hatch-holes" ) );
3709
3710 // Now filter truncated holes to avoid small holes in pattern
3711 // It happens for holes near the zone outline
3712 for( int ii = 0; ii < holes.OutlineCount(); )
3713 {
3714 double area = holes.Outline( ii ).Area();
3715
3716 if( area < minimal_hole_area ) // The current hole is too small: remove it
3717 holes.DeletePolygon( ii );
3718 else
3719 ++ii;
3720 }
3721
3722 // Drop any holes that completely enclose a thermal ring to ensure thermal reliefs
3723 // stay connected to the hatch webbing. Only drop holes where the thermal ring is
3724 // entirely inside the hole; partial overlaps are kept to preserve the hatch pattern.
3725 if( aThermalRings.OutlineCount() > 0 )
3726 {
3727 BOX2I thermalBBox = aThermalRings.BBox();
3728
3729 // Iterate through holes (backwards since we may delete)
3730 for( int holeIdx = holes.OutlineCount() - 1; holeIdx >= 0; holeIdx-- )
3731 {
3732 const SHAPE_LINE_CHAIN& hole = holes.Outline( holeIdx );
3733 BOX2I holeBBox = hole.BBox();
3734
3735 // Quick rejection: skip if hole bbox doesn't intersect thermal rings bbox
3736 if( !holeBBox.Intersects( thermalBBox ) )
3737 continue;
3738
3739 // Check if ANY thermal ring is completely enclosed by this hole
3740 for( int ringIdx = 0; ringIdx < aThermalRings.OutlineCount(); ringIdx++ )
3741 {
3742 const SHAPE_LINE_CHAIN& ring = aThermalRings.Outline( ringIdx );
3743 BOX2I ringBBox = ring.BBox();
3744 VECTOR2I ringCenter = ringBBox.Centre();
3745
3746 // Quick rejection: hole bbox must contain ring bbox
3747 if( !holeBBox.Contains( ringBBox ) )
3748 continue;
3749
3750 // Check 1: Is the ring center inside the hole?
3751 if( !hole.PointInside( ringCenter ) )
3752 continue;
3753
3754 // Check 2: Is at least one point on the ring inside the hole?
3755 if( ring.PointCount() == 0 || !hole.PointInside( ring.CPoint( 0 ) ) )
3756 continue;
3757
3758 // Check 3: Does the ring outline NOT intersect the hole outline?
3759 // If there's no intersection, the ring is fully enclosed (not touching edges)
3760 SHAPE_LINE_CHAIN::INTERSECTIONS intersections;
3761 ring.Intersect( hole, intersections );
3762
3763 if( intersections.empty() )
3764 {
3765 // This hole completely encloses a ring - drop it
3766 holes.DeletePolygon( holeIdx );
3767 break; // Move to next hole
3768 }
3769 }
3770 }
3771 }
3772
3773 // create grid. Useto
3774 // generate strictly simple polygons needed by Gerber files and Fracture()
3775 aFillPolys.BooleanSubtract( aFillPolys, holes );
3776 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In14_Cu, wxT( "after-hatching" ) );
3777
3778 return true;
3779}
3780
3781
3783{
3784 auto cacheKey = std::make_pair( static_cast<const ZONE*>( aZone ), aLayer );
3785
3786 {
3787 std::lock_guard<std::mutex> lock( m_cacheMutex );
3788 auto it = m_preKnockoutFillCache.find( cacheKey );
3789
3790 if( it == m_preKnockoutFillCache.end() )
3791 {
3792 wxLogTrace( traceZoneFiller, wxT( "Cache miss for zone %s layer %d (cache size: %zu)" ),
3793 aZone->GetNetname(), static_cast<int>( aLayer ), m_preKnockoutFillCache.size() );
3794 return false;
3795 }
3796
3797 wxLogTrace( traceZoneFiller, wxT( "Cache hit for zone %s layer %d: pre-knockout %d outlines" ),
3798 aZone->GetNetname(), static_cast<int>( aLayer ), it->second.OutlineCount() );
3799
3800 // Restore the cached pre-knockout fill
3801 aFillPolys = it->second;
3802 }
3803
3804 // Subtract the FILLED area of higher-priority zones (with clearance for different nets).
3805 // For same-net zones: subtract the filled area directly
3806 // For different-net zones: subtract the filled area with appropriate clearance
3807 // This replaces the original knockout logic to use actual fills after island removal.
3808 BOX2I zoneBBox = aZone->GetBoundingBox();
3809 int zone_clearance = aZone->GetLocalClearance().value();
3810 int board_clearance = m_board->GetDesignSettings().m_MinClearance;
3811
3812 for( ZONE* otherZone : m_board->Zones() )
3813 {
3814 if( otherZone == aZone )
3815 continue;
3816
3817 if( !otherZone->GetLayerSet().test( aLayer ) )
3818 continue;
3819
3820 if( otherZone->IsTeardropArea() )
3821 continue;
3822
3823 if( !otherZone->HigherPriority( aZone ) )
3824 continue;
3825
3826 if( !otherZone->GetBoundingBox().Intersects( zoneBBox ) )
3827 continue;
3828
3829 std::shared_ptr<SHAPE_POLY_SET> otherFill = otherZone->GetFilledPolysList( aLayer );
3830
3831 if( !otherFill || otherFill->OutlineCount() == 0 )
3832 continue;
3833
3834 if( otherZone->SameNet( aZone ) )
3835 {
3836 // Same net: subtract filled area directly
3837 aFillPolys.BooleanSubtract( *otherFill );
3838 }
3839 else
3840 {
3841 // Different net: subtract filled area with clearance
3842 int clearance = std::max( zone_clearance, otherZone->GetLocalClearance().value() );
3843 clearance = std::max( clearance, board_clearance );
3844
3845 SHAPE_POLY_SET inflatedFill = *otherFill;
3847 aFillPolys.BooleanSubtract( inflatedFill );
3848 }
3849 }
3850
3851 for( FOOTPRINT* footprint : m_board->Footprints() )
3852 {
3853 for( ZONE* otherZone : footprint->Zones() )
3854 {
3855 if( !otherZone->GetLayerSet().test( aLayer ) )
3856 continue;
3857
3858 if( otherZone->IsTeardropArea() )
3859 continue;
3860
3861 if( !otherZone->HigherPriority( aZone ) )
3862 continue;
3863
3864 if( !otherZone->GetBoundingBox().Intersects( zoneBBox ) )
3865 continue;
3866
3867 std::shared_ptr<SHAPE_POLY_SET> otherFill = otherZone->GetFilledPolysList( aLayer );
3868
3869 if( !otherFill || otherFill->OutlineCount() == 0 )
3870 continue;
3871
3872 if( otherZone->SameNet( aZone ) )
3873 {
3874 aFillPolys.BooleanSubtract( *otherFill );
3875 }
3876 else
3877 {
3878 int clearance = std::max( zone_clearance, otherZone->GetLocalClearance().value() );
3879 clearance = std::max( clearance, board_clearance );
3880
3881 SHAPE_POLY_SET inflatedFill = *otherFill;
3883 aFillPolys.BooleanSubtract( inflatedFill );
3884 }
3885 }
3886 }
3887
3888 // Subtract keepout zones (rule areas with do-not-fill)
3889 auto subtractKeepout =
3890 [&]( ZONE* candidate )
3891 {
3892 if( !candidate->GetIsRuleArea() )
3893 return;
3894
3895 if( !candidate->HasKeepoutParametersSet() )
3896 return;
3897
3898 if( candidate->GetDoNotAllowZoneFills() && candidate->IsOnLayer( aLayer ) )
3899 {
3900 if( candidate->GetBoundingBox().Intersects( zoneBBox ) )
3901 {
3902 if( candidate->Outline()->ArcCount() == 0 )
3903 {
3904 aFillPolys.BooleanSubtract( *candidate->Outline() );
3905 }
3906 else
3907 {
3908 SHAPE_POLY_SET keepoutOutline( *candidate->Outline() );
3909 keepoutOutline.ClearArcs();
3910 aFillPolys.BooleanSubtract( keepoutOutline );
3911 }
3912 }
3913 }
3914 };
3915
3916 for( ZONE* keepout : m_board->Zones() )
3917 subtractKeepout( keepout );
3918
3919 for( FOOTPRINT* footprint : m_board->Footprints() )
3920 {
3921 for( ZONE* keepout : footprint->Zones() )
3922 subtractKeepout( keepout );
3923 }
3924
3925 aFillPolys.Fracture();
3926
3927 return true;
3928}
@ 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:74
@ ZLO_FORCE_FLASHED
Definition board_item.h:73
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:83
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:324
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:921
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1084
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:187
const MINOPTMAX< int > & GetValue() const
Definition drc_rule.h:186
ZONE_CONNECTION m_ZoneConnection
Definition drc_rule.h:230
bool IsNull() const
Definition drc_rule.h:181
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:110
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:1300
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:1395
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:2455
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:567
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.
void Fracture()
Convert a set of polygons with holes to a single outline with "slits"/"fractures" connecting the oute...
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.
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:74
void SetNeedRefill(bool aNeedRefill)
Definition zone.h:302
std::optional< int > GetLocalClearance() const override
Definition zone.cpp:841
ZONE_LAYER_PROPERTIES & LayerProperties(PCB_LAYER_ID aLayer)
Definition zone.h:150
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:1348
const BOX2I GetBoundingBox() const override
Definition zone.cpp:646
SHAPE_POLY_SET * Outline()
Definition zone.h:341
void SetFillFlag(PCB_LAYER_ID aLayer, bool aFlag)
Definition zone.h:292
void SetFilledPolysList(PCB_LAYER_ID aLayer, const SHAPE_POLY_SET &aPolysList)
Set the list of filled polygons.
Definition zone.h:630
int GetMinThickness() const
Definition zone.h:307
bool HigherPriority(const ZONE *aOther) const
Definition zone.cpp:434
int GetHatchThickness() const
Definition zone.h:316
double GetHatchHoleMinArea() const
Definition zone.h:331
bool IsTeardropArea() const
Definition zone.h:690
EDA_ANGLE GetHatchOrientation() const
Definition zone.h:322
bool BuildSmoothedPoly(SHAPE_POLY_SET &aSmoothedPoly, PCB_LAYER_ID aLayer, SHAPE_POLY_SET *aBoardOutline, SHAPE_POLY_SET *aSmoothedPolyWithApron=nullptr) const
Definition zone.cpp:1414
ZONE_FILL_MODE GetFillMode() const
Definition zone.h:225
int GetHatchGap() const
Definition zone.h:319
double GetHatchSmoothingValue() const
Definition zone.h:328
int GetHatchSmoothingLevel() const
Definition zone.h:325
bool IsOnCopperLayer() const override
Definition zone.cpp:538
std::mutex & GetLock()
Definition zone.h:281
unsigned GetAssignedPriority() const
Definition zone.h:126
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:61
! 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