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_map>
31#include <unordered_set>
32#include <core/kicad_algo.h>
33#include <advanced_config.h>
34#include <board.h>
36#include <drc/drc_engine.h>
37#include <zone.h>
38#include <footprint.h>
39#include <pad.h>
40#include <pcb_target.h>
41#include <pcb_track.h>
42#include <pcb_text.h>
43#include <pcb_textbox.h>
44#include <pcb_tablecell.h>
45#include <pcb_table.h>
46#include <pcb_dimension.h>
49#include <board_commit.h>
50#include <progress_reporter.h>
54#include <geometry/vertex_set.h>
56#include <kidialog.h>
57#include <thread_pool.h>
58#include <math/util.h> // for KiROUND
59#include "zone_filler.h"
60#include "project.h"
62#include "pcb_barcode.h"
63
64// Helper classes for connect_nearby_polys
66{
67public:
68 RESULTS( int aOutline1, int aOutline2, int aVertex1, int aVertex2 ) :
69 m_outline1( aOutline1 ), m_outline2( aOutline2 ),
70 m_vertex1( aVertex1 ), m_vertex2( aVertex2 )
71 {
72 }
73
74 bool operator<( const RESULTS& aOther ) const
75 {
76 if( m_outline1 != aOther.m_outline1 )
77 return m_outline1 < aOther.m_outline1;
78 if( m_outline2 != aOther.m_outline2 )
79 return m_outline2 < aOther.m_outline2;
80 if( m_vertex1 != aOther.m_vertex1 )
81 return m_vertex1 < aOther.m_vertex1;
82 return m_vertex2 < aOther.m_vertex2;
83 }
84
89};
90
92{
93public:
94 VERTEX_CONNECTOR( const BOX2I& aBBox, const SHAPE_POLY_SET& aPolys, int aDist ) : VERTEX_SET( 0 )
95 {
96 SetBoundingBox( aBBox );
97 VERTEX* tail = nullptr;
98
99 for( int i = 0; i < aPolys.OutlineCount(); i++ )
100 tail = createList( aPolys.Outline( i ), tail, (void*)( intptr_t )( i ) );
101
102 if( tail )
103 tail->updateList();
104 m_dist = aDist;
105 }
106
107 VERTEX* getPoint( VERTEX* aPt ) const
108 {
109 // z-order range for the current point ± limit bounding box
110 const uint32_t maxZ = zOrder( aPt->x + m_dist, aPt->y + m_dist );
111 const uint32_t minZ = zOrder( aPt->x - m_dist, aPt->y - m_dist );
112 const SEG::ecoord limit2 = SEG::Square( m_dist );
113
114 // first look for points in increasing z-order
115 SEG::ecoord min_dist = std::numeric_limits<SEG::ecoord>::max();
116 VERTEX* retval = nullptr;
117
118 auto check_pt = [&]( VERTEX* p )
119 {
120 VECTOR2D diff( p->x - aPt->x, p->y - aPt->y );
121 SEG::ecoord dist2 = diff.SquaredEuclideanNorm();
122
123 if( dist2 > 0 && dist2 < limit2 && dist2 < min_dist && p->isEar( true ) )
124 {
125 min_dist = dist2;
126 retval = p;
127 }
128 };
129
130 VERTEX* p = aPt->nextZ;
131
132 while( p && p->z <= maxZ )
133 {
134 check_pt( p );
135 p = p->nextZ;
136 }
137
138 p = aPt->prevZ;
139
140 while( p && p->z >= minZ )
141 {
142 check_pt( p );
143 p = p->prevZ;
144 }
145
146 return retval;
147 }
148
150 {
151 if( m_vertices.empty() )
152 return;
153
154 VERTEX* p = m_vertices.front().next;
155 std::set<VERTEX*> visited;
156
157 while( p != &m_vertices.front() )
158 {
159 // Skip points that are concave
160 if( !p->isEar() )
161 {
162 p = p->next;
163 continue;
164 }
165
166 VERTEX* q = nullptr;
167
168 if( ( visited.empty() || !visited.contains( p ) ) && ( q = getPoint( p ) ) )
169 {
170 visited.insert( p );
171
172 if( !visited.contains( q ) &&
173 m_results.emplace( (intptr_t) p->GetUserData(), (intptr_t) q->GetUserData(),
174 p->i, q->i ).second )
175 {
176 // We don't want to connect multiple points in the same vicinity, so skip
177 // 2 points before and after each point and match.
178 visited.insert( p->prev );
179 visited.insert( p->prev->prev );
180 visited.insert( p->next );
181 visited.insert( p->next->next );
182
183 visited.insert( q->prev );
184 visited.insert( q->prev->prev );
185 visited.insert( q->next );
186 visited.insert( q->next->next );
187
188 visited.insert( q );
189 }
190 }
191
192 p = p->next;
193 }
194 }
195
196 std::set<RESULTS> GetResults() const
197 {
198 return m_results;
199 }
200
201private:
202 std::set<RESULTS> m_results;
204};
205
206
212namespace
213{
214
220struct PAD_KNOCKOUT_KEY
221{
222 VECTOR2I position;
223 VECTOR2I effectiveSize; // For circular: max of drill and pad; otherwise pad size
224 int shape; // PAD_SHAPE enum value
225 EDA_ANGLE orientation;
226 int netCode;
227
228 bool operator==( const PAD_KNOCKOUT_KEY& other ) const
229 {
230 return position == other.position && effectiveSize == other.effectiveSize
231 && shape == other.shape && orientation == other.orientation
232 && netCode == other.netCode;
233 }
234};
235
236struct PAD_KNOCKOUT_KEY_HASH
237{
238 size_t operator()( const PAD_KNOCKOUT_KEY& key ) const
239 {
240 return hash_val( key.position.x, key.position.y, key.effectiveSize.x, key.effectiveSize.y,
241 key.shape, key.orientation.AsDegrees(), key.netCode );
242 }
243};
244
248struct VIA_KNOCKOUT_KEY
249{
250 VECTOR2I position;
251 int effectiveSize; // max of drill and via width
252 int netCode;
253
254 bool operator==( const VIA_KNOCKOUT_KEY& other ) const
255 {
256 return position == other.position && effectiveSize == other.effectiveSize
257 && netCode == other.netCode;
258 }
259};
260
261struct VIA_KNOCKOUT_KEY_HASH
262{
263 size_t operator()( const VIA_KNOCKOUT_KEY& key ) const
264 {
265 return hash_val( key.position.x, key.position.y, key.effectiveSize, key.netCode );
266 }
267};
268
271struct TRACK_KNOCKOUT_KEY
272{
273 VECTOR2I start;
274 VECTOR2I end;
275 int width;
276
277 TRACK_KNOCKOUT_KEY( const VECTOR2I& aStart, const VECTOR2I& aEnd, int aWidth ) :
278 width( aWidth )
279 {
280 // Canonicalize endpoint order for consistent hashing
281 if( aStart.x < aEnd.x || ( aStart.x == aEnd.x && aStart.y <= aEnd.y ) )
282 {
283 start = aStart;
284 end = aEnd;
285 }
286 else
287 {
288 start = aEnd;
289 end = aStart;
290 }
291 }
292
293 bool operator==( const TRACK_KNOCKOUT_KEY& other ) const
294 {
295 return start == other.start && end == other.end && width == other.width;
296 }
297};
298
299struct TRACK_KNOCKOUT_KEY_HASH
300{
301 size_t operator()( const TRACK_KNOCKOUT_KEY& key ) const
302 {
303 return hash_val( key.start.x, key.start.y, key.end.x, key.end.y, key.width );
304 }
305};
306
307template<typename Func>
308void forEachBoardAndFootprintZone( BOARD* aBoard, Func&& aFunc )
309{
310 for( ZONE* zone : aBoard->Zones() )
311 aFunc( zone );
312
313 for( FOOTPRINT* footprint : aBoard->Footprints() )
314 {
315 for( ZONE* zone : footprint->Zones() )
316 aFunc( zone );
317 }
318}
319
320bool isZoneFillKeepout( const ZONE* aZone, PCB_LAYER_ID aLayer, const BOX2I& aBBox )
321{
322 return aZone->GetIsRuleArea()
323 && aZone->HasKeepoutParametersSet()
324 && aZone->GetDoNotAllowZoneFills()
325 && aZone->IsOnLayer( aLayer )
326 && aZone->GetBoundingBox().Intersects( aBBox );
327}
328
329void appendZoneOutlineWithoutArcs( const ZONE* aZone, SHAPE_POLY_SET& aPolys )
330{
331 if( aZone->Outline()->ArcCount() == 0 )
332 {
333 aPolys.Append( *aZone->Outline() );
334 return;
335 }
336
337 SHAPE_POLY_SET outline( *aZone->Outline() );
338 outline.ClearArcs();
339 aPolys.Append( outline );
340}
341
342} // anonymous namespace
343
344
346 m_board( aBoard ),
347 m_brdOutlinesValid( false ),
348 m_commit( aCommit ),
349 m_progressReporter( nullptr ),
351{
352 m_maxError = aBoard->GetDesignSettings().m_MaxError;
353
354 // To enable add "DebugZoneFiller=1" to kicad_advanced settings file.
356}
357
358
362
363
365{
366 m_progressReporter = aReporter;
367}
368
369
380bool ZONE_FILLER::Fill( const std::vector<ZONE*>& aZones, bool aCheck, wxWindow* aParent )
381{
382 std::lock_guard<KISPINLOCK> lock( m_board->GetConnectivity()->GetLock() );
383
384 std::vector<std::pair<ZONE*, PCB_LAYER_ID>> toFill;
385 std::map<std::pair<ZONE*, PCB_LAYER_ID>, HASH_128> oldFillHashes;
386 std::map<ZONE*, std::map<PCB_LAYER_ID, ISOLATED_ISLANDS>> isolatedIslandsMap;
387
388 std::shared_ptr<CONNECTIVITY_DATA> connectivity = m_board->GetConnectivity();
389
390 // Ensure that multiple threads don't attempt to initialize the advanced cfg global at the same
391 // time.
393
394 // Rebuild (from scratch, ignoring dirty flags) just in case. This really needs to be reliable.
395 connectivity->ClearRatsnest();
396 connectivity->Build( m_board, m_progressReporter );
397
398 m_worstClearance = m_board->GetMaxClearanceValue();
399
401 {
402 m_progressReporter->Report( aCheck ? _( "Checking zone fills..." )
403 : _( "Building zone fills..." ) );
404 m_progressReporter->SetMaxProgress( aZones.size() );
405 m_progressReporter->KeepRefreshing();
406 }
407
408 // The board outlines is used to clip solid areas inside the board (when outlines are valid)
409 m_boardOutline.RemoveAllContours();
410 m_brdOutlinesValid = m_board->GetBoardPolygonOutlines( m_boardOutline, true );
411
412 // Update and cache zone bounding boxes and pad effective shapes so that we don't have to
413 // make them thread-safe.
414 //
415 for( ZONE* zone : m_board->Zones() )
416 zone->CacheBoundingBox();
417
418 for( FOOTPRINT* footprint : m_board->Footprints() )
419 {
420 for( PAD* pad : footprint->Pads() )
421 {
422 if( pad->IsDirty() )
423 {
424 pad->BuildEffectiveShapes();
425 pad->BuildEffectivePolygon( ERROR_OUTSIDE );
426 }
427 }
428
429 for( ZONE* zone : footprint->Zones() )
430 zone->CacheBoundingBox();
431
432 // Rules may depend on insideCourtyard() or other expressions
433 footprint->BuildCourtyardCaches();
434 footprint->BuildNetTieCache();
435 }
436
437 LSET boardCuMask = LSET::AllCuMask( m_board->GetCopperLayerCount() );
438
439 // Pre-build Y-stripe spatial indices for zone outline containment queries.
440 // Amortizes build cost across the thousands of via/pad flash checks below.
441 std::unordered_map<const ZONE*, POLY_YSTRIPES_INDEX> zoneOutlineIndices;
442
443 for( ZONE* zone : m_board->Zones() )
444 {
445 if( zone->GetNumCorners() <= 2 )
446 continue;
447
448 zoneOutlineIndices[zone].Build( *zone->Outline() );
449 }
450
451 auto findHighestPriorityZone =
452 [&]( const BOX2I& bbox, PCB_LAYER_ID itemLayer, int netcode,
453 const std::function<bool( const ZONE* )>& testFn ) -> ZONE*
454 {
455 unsigned highestPriority = 0;
456 ZONE* highestPriorityZone = nullptr;
457
458 for( ZONE* zone : m_board->Zones() )
459 {
460 // Rule areas are not filled
461 if( zone->GetIsRuleArea() )
462 continue;
463
464 if( zone->GetAssignedPriority() < highestPriority )
465 continue;
466
467 if( !zone->IsOnLayer( itemLayer ) )
468 continue;
469
470 // Degenerate zones will cause trouble; skip them
471 if( zone->GetNumCorners() <= 2 )
472 continue;
473
474 if( !zone->GetBoundingBox().Intersects( bbox ) )
475 continue;
476
477 if( !testFn( zone ) )
478 continue;
479
480 // Prefer highest priority and matching netcode
481 if( zone->GetAssignedPriority() > highestPriority
482 || zone->GetNetCode() == netcode )
483 {
484 highestPriority = zone->GetAssignedPriority();
485 highestPriorityZone = zone;
486 }
487 }
488
489 return highestPriorityZone;
490 };
491
492 auto isInPourKeepoutArea =
493 [&]( const BOX2I& bbox, PCB_LAYER_ID itemLayer, const VECTOR2I& testPoint ) -> bool
494 {
495 for( ZONE* zone : m_board->Zones() )
496 {
497 if( !zone->GetIsRuleArea() )
498 continue;
499
500 if( !zone->HasKeepoutParametersSet() )
501 continue;
502
503 if( !zone->GetDoNotAllowZoneFills() )
504 continue;
505
506 if( !zone->IsOnLayer( itemLayer ) )
507 continue;
508
509 // Degenerate zones will cause trouble; skip them
510 if( zone->GetNumCorners() <= 2 )
511 continue;
512
513 if( !zone->GetBoundingBox().Intersects( bbox ) )
514 continue;
515
516 auto it = zoneOutlineIndices.find( zone );
517
518 if( it != zoneOutlineIndices.end() && it->second.Contains( testPoint ) )
519 return true;
520 }
521
522 return false;
523 };
524
525 // Determine state of conditional via flashing
526 // This is now done completely deterministically prior to filling due to the pathological
527 // case presented in https://gitlab.com/kicad/code/kicad/-/issues/12964.
528 for( PCB_TRACK* track : m_board->Tracks() )
529 {
530 if( track->Type() == PCB_VIA_T )
531 {
532 PCB_VIA* via = static_cast<PCB_VIA*>( track );
533 PADSTACK& padstack = via->Padstack();
534
535 via->ClearZoneLayerOverrides();
536
537 if( !via->GetRemoveUnconnected() )
538 continue;
539
540 BOX2I bbox = via->GetBoundingBox();
541 VECTOR2I center = via->GetPosition();
542 int holeRadius = via->GetDrillValue() / 2 + 1;
543 int netcode = via->GetNetCode();
544 LSET layers = via->GetLayerSet() & boardCuMask;
545
546 // Checking if the via hole touches the zone outline
547 auto viaTestFn =
548 [&]( const ZONE* aZone ) -> bool
549 {
550 return aZone->Outline()->Contains( center, -1, holeRadius );
551 };
552
553 for( PCB_LAYER_ID layer : layers )
554 {
555 if( !via->ConditionallyFlashed( layer ) )
556 continue;
557
558 if( isInPourKeepoutArea( bbox, layer, center ) )
559 {
560 via->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
561 }
562 else
563 {
564 ZONE* zone = findHighestPriorityZone( bbox, layer, netcode, viaTestFn );
565
566 if( zone && zone->GetNetCode() == via->GetNetCode()
568 || layer == padstack.Drill().start
569 || layer == padstack.Drill().end ) )
570 {
571 via->SetZoneLayerOverride( layer, ZLO_FORCE_FLASHED );
572 }
573 else
574 {
575 via->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
576 }
577 }
578 }
579 }
580 }
581
582 // Determine state of conditional pad flashing
583 for( FOOTPRINT* footprint : m_board->Footprints() )
584 {
585 for( PAD* pad : footprint->Pads() )
586 {
587 pad->ClearZoneLayerOverrides();
588
589 if( !pad->GetRemoveUnconnected() )
590 continue;
591
592 BOX2I bbox = pad->GetBoundingBox();
593 VECTOR2I center = pad->GetPosition();
594 int netcode = pad->GetNetCode();
595 LSET layers = pad->GetLayerSet() & boardCuMask;
596
597 auto padTestFn =
598 [&]( const ZONE* aZone ) -> bool
599 {
600 auto it = zoneOutlineIndices.find( aZone );
601
602 if( it != zoneOutlineIndices.end() )
603 return it->second.Contains( center );
604
605 return aZone->Outline()->Contains( center );
606 };
607
608 for( PCB_LAYER_ID layer : layers )
609 {
610 if( !pad->ConditionallyFlashed( layer ) )
611 continue;
612
613 if( isInPourKeepoutArea( bbox, layer, center ) )
614 {
615 pad->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
616 }
617 else
618 {
619 ZONE* zone = findHighestPriorityZone( bbox, layer, netcode, padTestFn );
620
621 if( zone && zone->GetNetCode() == pad->GetNetCode() )
622 pad->SetZoneLayerOverride( layer, ZLO_FORCE_FLASHED );
623 else
624 pad->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
625 }
626 }
627 }
628 }
629
630 for( ZONE* zone : aZones )
631 {
632 // Rule areas are not filled
633 if( zone->GetIsRuleArea() )
634 continue;
635
636 // Degenerate zones will cause trouble; skip them
637 if( zone->GetNumCorners() <= 2 )
638 continue;
639
640 if( m_commit )
641 m_commit->Modify( zone );
642
643 // calculate the hash value for filled areas. it will be used later to know if the
644 // current filled areas are up to date
645 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
646 {
647 zone->BuildHashValue( layer );
648 oldFillHashes[ { zone, layer } ] = zone->GetHashValue( layer );
649
650 // Add the zone to the list of zones to test or refill
651 toFill.emplace_back( std::make_pair( zone, layer ) );
652
653 // Copper-thieving fills are intentionally disconnected stamps; do not
654 // track them through the isolated-islands pass or every stamp gets
655 // classified as removable.
656 if( !zone->IsCopperThieving() )
657 isolatedIslandsMap[zone][layer] = ISOLATED_ISLANDS();
658 }
659
660 // Remove existing fill first to prevent drawing invalid polygons on some platforms
661 zone->UnFill();
662 }
663
664 auto zone_fill_dependency =
665 [&]( ZONE* aZone, PCB_LAYER_ID aLayer, ZONE* aOtherZone,
666 bool aRequireCompletedOtherFill ) -> bool
667 {
668 // Check to see if we have to knock-out the filled areas of a higher-priority
669 // zone. If so we have to wait until said zone is filled before we can fill.
670
671 // If the other zone is already filled on the requested layer then we're
672 // good-to-go
673 if( aRequireCompletedOtherFill && aOtherZone->GetFillFlag( aLayer ) )
674 return false;
675
676 // Even if keepouts exclude copper pours, the exclusion is by outline rather than
677 // filled area, so we're good-to-go here too
678 if( aOtherZone->GetIsRuleArea() )
679 return false;
680
681 // If the other zone is never going to be filled then don't wait for it
682 if( aOtherZone->GetNumCorners() <= 2 )
683 return false;
684
685 // If the zones share no common layers
686 if( !aOtherZone->GetLayerSet().test( aLayer ) )
687 return false;
688
689 if( aZone->HigherPriority( aOtherZone ) )
690 return false;
691
692 // Same-net zones always use outlines to produce determinate results
693 if( aOtherZone->SameNet( aZone ) )
694 return false;
695
696 // A higher priority zone is found: if we intersect and it's not filled yet
697 // then we have to wait.
698 BOX2I inflatedBBox = aZone->GetBoundingBox();
699 inflatedBBox.Inflate( m_worstClearance );
700
701 if( !inflatedBBox.Intersects( aOtherZone->GetBoundingBox() ) )
702 return false;
703
704 return aZone->Outline()->Collide( aOtherZone->Outline(), m_worstClearance );
705 };
706
707 auto check_fill_dependency =
708 [&]( ZONE* aZone, PCB_LAYER_ID aLayer, ZONE* aOtherZone ) -> bool
709 {
710 return zone_fill_dependency( aZone, aLayer, aOtherZone, true );
711 };
712
713 auto fill_item_dependency =
714 [&]( const std::pair<ZONE*, PCB_LAYER_ID>& aWaiter,
715 const std::pair<ZONE*, PCB_LAYER_ID>& aDependency ) -> bool
716 {
717 if( aWaiter.first == aDependency.first || aWaiter.second != aDependency.second )
718 return false;
719
720 return check_fill_dependency( aWaiter.first, aWaiter.second, aDependency.first );
721 };
722
723 auto fill_lambda =
724 [&]( std::pair<ZONE*, PCB_LAYER_ID> aFillItem ) -> int
725 {
726 if( m_progressReporter && m_progressReporter->IsCancelled() )
727 return 0;
728
729 PCB_LAYER_ID layer = aFillItem.second;
730 ZONE* zone = aFillItem.first;
731
732 SHAPE_POLY_SET fillPolys;
733
734 if( !fillSingleZone( zone, layer, fillPolys ) )
735 return 0;
736
737 zone->SetFilledPolysList( layer, fillPolys );
738
740 m_progressReporter->AdvanceProgress();
741
742 return 1;
743 };
744
745 auto tesselate_lambda =
746 [&]( std::pair<ZONE*, PCB_LAYER_ID> aFillItem ) -> int
747 {
748 if( m_progressReporter && m_progressReporter->IsCancelled() )
749 return 0;
750
751 PCB_LAYER_ID layer = aFillItem.second;
752 ZONE* zone = aFillItem.first;
753
754 zone->CacheTriangulation( layer );
755 zone->SetFillFlag( layer, true );
756
757 return 1;
758 };
759
761 std::atomic<bool> cancelled = false;
762
763 auto waitForFutures =
764 [&]( std::vector<std::future<int>>& aFutures, std::vector<int>* aResults = nullptr )
765 {
766 if( aResults )
767 aResults->clear();
768
769 for( auto& future : aFutures )
770 {
771 while( future.wait_for( std::chrono::milliseconds( 100 ) )
772 != std::future_status::ready )
773 {
775 {
776 m_progressReporter->KeepRefreshing();
777
778 if( m_progressReporter->IsCancelled() )
779 cancelled = true;
780 }
781 }
782
783 int result = future.get();
784
785 if( aResults )
786 aResults->push_back( result );
787 }
788 };
789
790 struct FILL_DAG
791 {
792 std::vector<std::vector<size_t>> successors;
793 std::vector<int> inDegree;
794 std::vector<size_t> currentWave;
795 };
796
797 auto build_fill_dag = [&]( const std::vector<std::pair<ZONE*, PCB_LAYER_ID>>& aFillItems, auto&& aHasDependency,
798 bool aAnyDependencies ) -> FILL_DAG
799 {
800 FILL_DAG dag;
801
802 dag.successors.resize( aFillItems.size() );
803 dag.inDegree.assign( aFillItems.size(), 0 );
804 dag.currentWave.reserve( aFillItems.size() );
805
806 // Skip the O(N²) dependency scan when the caller guarantees no deps.
807 // All items become the initial wave.
808 if( aAnyDependencies )
809 {
810 for( size_t i = 0; i < aFillItems.size(); ++i )
811 {
812 for( size_t j = 0; j < aFillItems.size(); ++j )
813 {
814 if( i == j )
815 continue;
816
817 if( aHasDependency( aFillItems[j], aFillItems[i] ) )
818 {
819 dag.successors[i].push_back( j );
820 dag.inDegree[j]++;
821 }
822 }
823 }
824 }
825
826 for( size_t i = 0; i < aFillItems.size(); ++i )
827 {
828 if( dag.inDegree[i] == 0 )
829 dag.currentWave.push_back( i );
830 }
831
832 return dag;
833 };
834
835 auto run_fill_waves = [&]( const std::vector<std::pair<ZONE*, PCB_LAYER_ID>>& aFillItems, auto&& aFillFn,
836 auto&& aTessFn, auto&& aHasDependency, bool aAnyDependencies )
837 {
838 FILL_DAG dag = build_fill_dag( aFillItems, aHasDependency, aAnyDependencies );
839
840 while( !dag.currentWave.empty() && !cancelled.load() )
841 {
842 std::vector<std::future<int>> fillFutures;
843 std::vector<int> fillResults;
844
845 fillFutures.reserve( dag.currentWave.size() );
846
847 for( size_t idx : dag.currentWave )
848 {
849 fillFutures.emplace_back( tp.submit_task(
850 [&aFillFn, &aFillItems, idx]()
851 {
852 return aFillFn( aFillItems[idx] );
853 } ) );
854 }
855
856 waitForFutures( fillFutures, &fillResults );
857
858 std::vector<std::future<int>> tessFutures;
859
860 tessFutures.reserve( dag.currentWave.size() );
861
862 for( size_t ii = 0; ii < fillResults.size(); ++ii )
863 {
864 if( fillResults[ii] == 0 )
865 continue;
866
867 size_t idx = dag.currentWave[ii];
868
869 tessFutures.emplace_back( tp.submit_task(
870 [&aTessFn, &aFillItems, idx]()
871 {
872 return aTessFn( aFillItems[idx] );
873 } ) );
874 }
875
876 waitForFutures( tessFutures );
877
878 if( cancelled.load() )
879 break;
880
881 std::vector<size_t> nextWave;
882
883 for( size_t idx : dag.currentWave )
884 {
885 for( size_t succ : dag.successors[idx] )
886 {
887 if( --dag.inDegree[succ] == 0 )
888 nextWave.push_back( succ );
889 }
890 }
891
892 dag.currentWave = std::move( nextWave );
893 }
894 };
895
896 run_fill_waves( toFill, fill_lambda, tesselate_lambda, fill_item_dependency, true );
897
898 // Now update the connectivity to check for isolated copper islands
899 // (NB: FindIsolatedCopperIslands() is multi-threaded)
901 {
902 if( m_progressReporter->IsCancelled() )
903 return false;
904
905 m_progressReporter->AdvancePhase();
906 m_progressReporter->Report( _( "Removing isolated copper islands..." ) );
907 m_progressReporter->KeepRefreshing();
908 }
909
910 connectivity->SetProgressReporter( m_progressReporter );
911 connectivity->FillIsolatedIslandsMap( isolatedIslandsMap );
912 connectivity->SetProgressReporter( nullptr );
913
914 if( m_progressReporter && m_progressReporter->IsCancelled() )
915 return false;
916
917 for( ZONE* zone : aZones )
918 {
919 // Keepout zones are not filled
920 if( zone->GetIsRuleArea() )
921 continue;
922
923 zone->SetIsFilled( true );
924 }
925
926 // Now remove isolated copper islands according to the isolated islands strategy assigned
927 // by the user (always, never, below-certain-size).
928 //
929 // Track zone-layer pairs that had islands removed for potential iterative refill.
930 // Per-layer granularity lets the iterative loop re-refill only the layers that actually
931 // changed, instead of every layer of every changed zone.
932 std::set<std::pair<ZONE*, PCB_LAYER_ID>> zonesWithRemovedIslandLayers;
933
934 // Per-layer tracking: a zone-layer pair is "initially fully isolated" when every fill
935 // outline on that layer was an island in the initial pass (i.e. the zone has no pad
936 // connectivity on that layer). Used in the iterative loop to distinguish legitimately
937 // unconnected pours — which must be preserved — from zones that became fully isolated
938 // only because other fills changed.
939 std::set<std::pair<ZONE*, PCB_LAYER_ID>> initiallyFullyIsolatedLayers;
940
941 for( const auto& [ zone, zoneIslands ] : isolatedIslandsMap )
942 {
943 // Track per-layer isolation, and skip island removal on layers where every
944 // outline is an island (unconnected pour — must be preserved as-is).
945 bool allLayersFullyIsolated = true;
946
947 for( const auto& [ layer, layerIslands ] : zoneIslands )
948 {
949 bool layerFullyIsolated = ( layerIslands.m_IsolatedOutlines.size()
950 == static_cast<size_t>( zone->GetFilledPolysList( layer )->OutlineCount() ) );
951
952 if( layerFullyIsolated )
953 initiallyFullyIsolatedLayers.insert( { zone, layer } );
954 else
955 allLayersFullyIsolated = false;
956 }
957
958 if( allLayersFullyIsolated )
959 continue;
960
961 for( const auto& [ layer, layerIslands ] : zoneIslands )
962 {
963 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( layer ) )
964 continue;
965
966 if( layerIslands.m_IsolatedOutlines.empty() )
967 continue;
968
969 std::vector<int> islands = layerIslands.m_IsolatedOutlines;
970
971 // The list of polygons to delete must be explored from last to first in list,
972 // to allow deleting a polygon from list without breaking the remaining of the list
973 std::sort( islands.begin(), islands.end(), std::greater<int>() );
974
975 std::shared_ptr<SHAPE_POLY_SET> poly = zone->GetFilledPolysList( layer );
976 long long int minArea = zone->GetMinIslandArea();
977 ISLAND_REMOVAL_MODE mode = zone->GetIslandRemovalMode();
978
979 for( int idx : islands )
980 {
981 SHAPE_LINE_CHAIN& outline = poly->Outline( idx );
982
983 if( mode == ISLAND_REMOVAL_MODE::ALWAYS )
984 {
985 poly->DeletePolygonAndTriangulationData( idx, false );
986 zonesWithRemovedIslandLayers.insert( { zone, layer } );
987 }
988 else if ( mode == ISLAND_REMOVAL_MODE::AREA && outline.Area( true ) < minArea )
989 {
990 poly->DeletePolygonAndTriangulationData( idx, false );
991 zonesWithRemovedIslandLayers.insert( { zone, layer } );
992 }
993 else
994 {
995 zone->SetIsIsland( layer, idx );
996 }
997 }
998
999 poly->UpdateTriangulationDataHash();
1000 zone->CalculateFilledArea();
1001
1002 if( m_progressReporter && m_progressReporter->IsCancelled() )
1003 return false;
1004 }
1005 }
1006
1007 // Iterative refill: when islands are removed, overlapping zones may be able to reclaim
1008 // the freed space. Repeat until fills stabilise (convergence), up to a safety limit.
1009 //
1010 // Each wave captures a snapshot of all zone fills before running. Every task in the wave
1011 // reads knockouts from the snapshot rather than from the live zone objects. This guarantees
1012 // that all tasks see the same pre-wave fill state regardless of the order in which parallel
1013 // tasks complete — preventing a fast-finishing task's expanded fill from blocking a
1014 // slower task from claiming the same freed area.
1015 const bool iterativeRefill = ADVANCED_CFG::GetCfg().m_ZoneFillIterativeRefill;
1016
1017 if( iterativeRefill && !zonesWithRemovedIslandLayers.empty() )
1018 {
1019 const int maxIterations = 8;
1020 bool progressReported = false;
1021 bool hitIterationLimit = false;
1022
1023 // Seed: zone-layer pairs whose fills changed due to initial island removal.
1024 std::set<std::pair<ZONE*, PCB_LAYER_ID>> changedZoneLayers( zonesWithRemovedIslandLayers );
1025
1026 auto cached_refill_tessellate_lambda = [&]( const std::pair<ZONE*, PCB_LAYER_ID>& aFillItem ) -> int
1027 {
1028 ZONE* zone = aFillItem.first;
1029 PCB_LAYER_ID layer = aFillItem.second;
1030 zone->CacheTriangulation( layer );
1031 zone->SetFillFlag( layer, true );
1032 return 1;
1033 };
1034
1035 auto no_dependency = []( const std::pair<ZONE*, PCB_LAYER_ID>&, const std::pair<ZONE*, PCB_LAYER_ID>& ) -> bool
1036 {
1037 return false;
1038 };
1039
1040 for( int iteration = 0; iteration < maxIterations; ++iteration )
1041 {
1042 // Candidate selection: only re-refill (zone, layer) pairs where `layer` is the
1043 // same layer that changed on some seed zone and whose bbox touches it.
1044 // Per-layer narrowing skips the N-1 other layers of each changed zone.
1045 std::vector<std::pair<ZONE*, PCB_LAYER_ID>> zonesToRefill;
1046 std::set<std::pair<ZONE*, PCB_LAYER_ID>> zonesToRefillSet;
1047
1048 for( const auto& [changedZone, changedLayer] : changedZoneLayers )
1049 {
1050 BOX2I bbox = changedZone->GetBoundingBox();
1051 bbox.Inflate( m_worstClearance );
1052
1053 for( ZONE* zone : aZones )
1054 {
1055 if( zone->GetIsRuleArea() )
1056 continue;
1057
1058 if( !zone->GetLayerSet().test( changedLayer ) )
1059 continue;
1060
1061 // A candidate only needs re-evaluation when the changed zone can
1062 // affect it in one of two ways:
1063 // 1. Fill shape: changed zone is a higher-priority knockout of
1064 // candidate — candidate's refill may now claim freed space.
1065 // 2. Connectivity cluster: changed zone is same-net as candidate —
1066 // even if candidate's fill shape is unchanged, refilling from
1067 // cache restores outlines that were previously removed as
1068 // islands, and island detection re-evaluates with the new
1069 // same-net bridging geometry. This is what drives cascading
1070 // island refills: a low-priority same-net zone growing can
1071 // un-orphan a higher-priority zone's standalone outline.
1072 // Zones that are neither higher-priority knockouts nor same-net have
1073 // no fill or connectivity dependency on the changed zone — skip.
1074 if( zone != changedZone && !changedZone->HigherPriority( zone ) && !changedZone->SameNet( zone ) )
1075 {
1076 continue;
1077 }
1078
1079 if( !zone->GetBoundingBox().Intersects( bbox ) )
1080 continue;
1081
1082 auto fillItem = std::make_pair( zone, changedLayer );
1083
1084 if( zonesToRefillSet.insert( fillItem ).second )
1085 zonesToRefill.push_back( fillItem );
1086 }
1087 }
1088
1089 if( zonesToRefill.empty() )
1090 break;
1091
1092 if( !progressReported )
1093 {
1094 if( m_progressReporter )
1095 {
1096 m_progressReporter->AdvancePhase();
1097 m_progressReporter->Report( _( "Refilling zones after island removal..." ) );
1098 m_progressReporter->KeepRefreshing();
1099 }
1100
1101 progressReported = true;
1102 }
1103
1104 // Snapshot hashes before the wave for convergence detection. Only zones in
1105 // zonesToRefill can change their fill this wave (refill writes them; subsequent
1106 // island removal also only touches them), so we only need pre-hashes for those.
1107 std::map<std::pair<ZONE*, PCB_LAYER_ID>, HASH_128> iterHashes;
1108
1109 for( const auto& fillItem : zonesToRefill )
1110 {
1111 fillItem.first->BuildHashValue( fillItem.second );
1112 iterHashes[fillItem] = fillItem.first->GetHashValue( fillItem.second );
1113 }
1114
1115 // Snapshot fills before the wave. Every refill task reads knockouts from this
1116 // snapshot so all tasks see the same pre-wave state regardless of completion
1117 // order — preventing a fast-finishing task's expanded fill from blocking a
1118 // slower task from claiming the same freed area.
1119 //
1120 // refillZoneFromCache only reads knockouts on the layer being refilled, so we
1121 // only need to clone fills on layers that appear in zonesToRefill. On boards
1122 // with many layers and few changed layers this avoids most of the snapshot cost.
1123 LSET snapshotLayers;
1124
1125 for( const auto& [zone, layer] : zonesToRefill )
1126 snapshotLayers.set( layer );
1127
1128 FillSnapshot snapshot;
1129
1130 forEachBoardAndFootprintZone( m_board,
1131 [&]( ZONE* zone )
1132 {
1133 if( zone->GetIsRuleArea() )
1134 return;
1135
1136 LSET copperLayers = zone->GetLayerSet()
1137 & LSET::AllCuMask( m_board->GetCopperLayerCount() )
1138 & snapshotLayers;
1139
1140 for( PCB_LAYER_ID layer : copperLayers )
1141 {
1142 if( !zone->HasFilledPolysForLayer( layer ) )
1143 continue;
1144
1145 auto sp = zone->GetFilledPolysList( layer );
1146
1147 if( sp && sp->OutlineCount() > 0 )
1148 snapshot[{ zone, layer }] = sp->CloneDropTriangulation();
1149 }
1150 } );
1151
1152 auto cached_refill_fill_lambda =
1153 [&]( const std::pair<ZONE*, PCB_LAYER_ID>& aFillItem ) -> int
1154 {
1155 ZONE* zone = aFillItem.first;
1156 PCB_LAYER_ID layer = aFillItem.second;
1157 SHAPE_POLY_SET fillPolys;
1158
1159 if( !refillZoneFromCache( zone, layer, fillPolys, &snapshot ) )
1160 return 0;
1161
1162 zone->SetFilledPolysList( layer, fillPolys );
1163 zone->SetFillFlag( layer, false );
1164 return 1;
1165 };
1166
1167 run_fill_waves( zonesToRefill, cached_refill_fill_lambda, cached_refill_tessellate_lambda, no_dependency,
1168 /* aAnyDependencies */ false );
1169
1170 // Island detection on the refilled zones only. Zones that grew into freed space
1171 // can still develop islands if they are simultaneously blocked on one side by a
1172 // higher-priority zone that grew in a prior wave.
1173 std::map<ZONE*, std::map<PCB_LAYER_ID, ISOLATED_ISLANDS>> refillIslandsMap;
1174
1175 for( const auto& [zone, layer] : zonesToRefill )
1176 {
1177 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( layer ) )
1178 continue;
1179
1180 // Mirrors the initial isolatedIslandsMap build above: thieving stamps
1181 // are intentionally disconnected and must not be tracked as islands,
1182 // or the iterative refill will delete them on the next pass.
1183 if( zone->IsCopperThieving() )
1184 continue;
1185
1186 refillIslandsMap[zone][layer] = ISOLATED_ISLANDS();
1187 }
1188
1189 connectivity->FillIsolatedIslandsMap( refillIslandsMap );
1190
1191 for( const auto& [zone, zoneIslands] : refillIslandsMap )
1192 {
1193 for( const auto& [layer, layerIslands] : zoneIslands )
1194 {
1195 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( layer ) )
1196 continue;
1197
1198 if( layerIslands.m_IsolatedOutlines.empty() )
1199 continue;
1200
1201 // Preserve layers that were initially fully isolated (unconnected pours):
1202 // if every outline on this layer is still an island, keep them as-is.
1203 if( initiallyFullyIsolatedLayers.count( { zone, layer } ) > 0 )
1204 {
1205 if( layerIslands.m_IsolatedOutlines.size()
1206 == static_cast<size_t>( zone->GetFilledPolysList( layer )->OutlineCount() ) )
1207 {
1208 continue;
1209 }
1210 }
1211
1212 std::vector<int> islands = layerIslands.m_IsolatedOutlines;
1213 std::sort( islands.begin(), islands.end(), std::greater<int>() );
1214
1215 std::shared_ptr<SHAPE_POLY_SET> poly = zone->GetFilledPolysList( layer );
1216 long long int minArea = zone->GetMinIslandArea();
1218
1219 for( int idx : islands )
1220 {
1221 SHAPE_LINE_CHAIN& outline = poly->Outline( idx );
1222
1223 if( mode == ISLAND_REMOVAL_MODE::ALWAYS )
1224 poly->DeletePolygonAndTriangulationData( idx, false );
1225 else if( mode == ISLAND_REMOVAL_MODE::AREA && outline.Area( true ) < minArea )
1226 poly->DeletePolygonAndTriangulationData( idx, false );
1227 else
1228 zone->SetIsIsland( layer, idx );
1229 }
1230
1231 poly->UpdateTriangulationDataHash();
1232 zone->CalculateFilledArea();
1233 }
1234 }
1235
1236 // Convergence check: collect zone-layer pairs whose fill changed (refill or
1237 // island removal) compared to the pre-wave hash snapshot. These seed the next
1238 // iteration. Only zonesToRefill entries can have changed, so we only scan those.
1239 changedZoneLayers.clear();
1240
1241 for( const auto& fillItem : zonesToRefill )
1242 {
1243 fillItem.first->BuildHashValue( fillItem.second );
1244
1245 auto hashIt = iterHashes.find( fillItem );
1246 HASH_128 oldHash = ( hashIt != iterHashes.end() ) ? hashIt->second : HASH_128{};
1247
1248 if( fillItem.first->GetHashValue( fillItem.second ) != oldHash )
1249 changedZoneLayers.insert( fillItem );
1250 }
1251
1252 if( changedZoneLayers.empty() )
1253 break; // Stable — converged.
1254
1255 if( iteration + 1 >= maxIterations )
1256 {
1257 hitIterationLimit = true;
1258 break;
1259 }
1260 }
1261
1262 if( hitIterationLimit )
1263 {
1264 wxString msg = wxString::Format( _( "Zone fills may be incorrect: iterative refill did not converge "
1265 "after %d passes.\n\n"
1266 "This can happen with complex overlapping zones. "
1267 "Consider simplifying your zones." ),
1268 maxIterations );
1269
1270 if( aParent )
1271 {
1272 KIDIALOG dlg( aParent, msg, _( "Warning" ), wxOK | wxICON_WARNING );
1273 dlg.DoNotShowCheckbox( __FILE__, __LINE__ );
1274 dlg.ShowModal();
1275 }
1276 else
1277 {
1278 wxLogWarning( msg );
1279 }
1280 }
1281 }
1282
1283 // Now remove islands which are either outside the board edge or fail to meet the minimum
1284 // area requirements
1285 using island_check_return = std::vector<std::pair<std::shared_ptr<SHAPE_POLY_SET>, int>>;
1286
1287 std::vector<std::pair<std::shared_ptr<SHAPE_POLY_SET>, double>> polys_to_check;
1288
1289 // rough estimate to save re-allocation time
1290 polys_to_check.reserve( m_board->GetCopperLayerCount() * aZones.size() );
1291
1292 for( ZONE* zone : aZones )
1293 {
1294 // Don't check for connections on layers that only exist in the zone but
1295 // were disabled in the board
1296 BOARD* board = zone->GetBoard();
1297 LSET zoneCopperLayers = zone->GetLayerSet() & LSET::AllCuMask( board->GetCopperLayerCount() );
1298
1299 // Min-thickness is the web thickness. On the other hand, a blob min-thickness by
1300 // min-thickness is not useful. Since there's no obvious definition of web vs. blob, we
1301 // arbitrarily choose "at least 3X the area".
1302 double minArea = (double) zone->GetMinThickness() * zone->GetMinThickness() * 3;
1303
1304 for( PCB_LAYER_ID layer : zoneCopperLayers )
1305 {
1306 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( layer ) )
1307 continue;
1308
1309 polys_to_check.emplace_back( zone->GetFilledPolysList( layer ), minArea );
1310 }
1311 }
1312
1313 auto island_lambda =
1314 [&]( int aStart, int aEnd ) -> island_check_return
1315 {
1316 island_check_return retval;
1317
1318 for( int ii = aStart; ii < aEnd && !cancelled.load(); ++ii )
1319 {
1320 auto [poly, minArea] = polys_to_check[ii];
1321
1322 for( int jj = poly->OutlineCount() - 1; jj >= 0; jj-- )
1323 {
1324 SHAPE_POLY_SET island;
1325 SHAPE_POLY_SET intersection;
1326 const SHAPE_LINE_CHAIN& test_poly = poly->Polygon( jj ).front();
1327 double island_area = test_poly.Area();
1328
1329 if( island_area < minArea )
1330 continue;
1331
1332
1333 island.AddOutline( test_poly );
1334 intersection.BooleanIntersection( m_boardOutline, island );
1335
1336 // Nominally, all of these areas should be either inside or outside the
1337 // board outline. So this test should be able to just compare areas (if
1338 // they are equal, you are inside). But in practice, we sometimes have
1339 // slight overlap at the edges, so testing against half-size area acts as
1340 // a fail-safe.
1341 if( intersection.Area() < island_area / 2.0 )
1342 retval.emplace_back( poly, jj );
1343 }
1344 }
1345
1346 return retval;
1347 };
1348
1349 auto island_returns = tp.submit_blocks( 0, polys_to_check.size(), island_lambda );
1350 cancelled = false;
1351
1352 // Allow island removal threads to finish
1353 for( size_t ii = 0; ii < island_returns.size(); ++ii )
1354 {
1355 std::future<island_check_return>& ret = island_returns[ii];
1356
1357 if( ret.valid() )
1358 {
1359 std::future_status status = ret.wait_for( std::chrono::seconds( 0 ) );
1360
1361 while( status != std::future_status::ready )
1362 {
1363 if( m_progressReporter )
1364 {
1365 m_progressReporter->KeepRefreshing();
1366
1367 if( m_progressReporter->IsCancelled() )
1368 cancelled = true;
1369 }
1370
1371 status = ret.wait_for( std::chrono::milliseconds( 100 ) );
1372 }
1373 }
1374 }
1375
1376 if( cancelled.load() )
1377 return false;
1378
1379 for( size_t ii = 0; ii < island_returns.size(); ++ii )
1380 {
1381 std::future<island_check_return>& ret = island_returns[ii];
1382
1383 if( ret.valid() )
1384 {
1385 for( auto& action_item : ret.get() )
1386 action_item.first->DeletePolygonAndTriangulationData( action_item.second, true );
1387 }
1388 }
1389
1390 for( ZONE* zone : aZones )
1391 zone->CalculateFilledArea();
1392
1393 // Second pass: Re-evaluate via flashing based on actual filled polygons.
1394 // The first pass (before filling) marks vias as ZLO_FORCE_FLASHED if they're within the
1395 // zone outline. However, if the fill doesn't actually reach the via (due to obstacles like
1396 // tracks), we should not flash the via. See https://gitlab.com/kicad/code/kicad/-/issues/22010
1397 //
1398 // Build a spatial index per filled zone-layer for O(log V) containment queries instead of
1399 // O(V) ray-casting. This is critical for boards with large zone fills (many vertices) and
1400 // many vias/pads.
1401 struct INDEXED_ZONE
1402 {
1403 BOX2I bbox;
1404 std::unique_ptr<POLY_YSTRIPES_INDEX> index;
1405 };
1406
1407 struct NET_LAYER_HASH
1408 {
1409 size_t operator()( const std::pair<int, PCB_LAYER_ID>& k ) const
1410 {
1411 return std::hash<int>()( k.first ) ^ ( std::hash<int>()( k.second ) << 16 );
1412 }
1413 };
1414
1415 std::unordered_map<std::pair<int, PCB_LAYER_ID>, std::vector<INDEXED_ZONE>, NET_LAYER_HASH>
1416 filledZonesByNetLayer;
1417
1418 for( ZONE* zone : m_board->Zones() )
1419 {
1420 if( zone->GetIsRuleArea() )
1421 continue;
1422
1423 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
1424 {
1425 if( !zone->HasFilledPolysForLayer( layer ) )
1426 continue;
1427
1428 const std::shared_ptr<SHAPE_POLY_SET>& fill = zone->GetFilledPolysList( layer );
1429
1430 if( fill->IsEmpty() )
1431 continue;
1432
1433 INDEXED_ZONE iz;
1434 iz.bbox = fill->BBox();
1435 iz.index = std::make_unique<POLY_YSTRIPES_INDEX>();
1436 iz.index->Build( *fill );
1437 filledZonesByNetLayer[{ zone->GetNetCode(), layer }].push_back( std::move( iz ) );
1438 }
1439 }
1440
1441 auto zoneReachesPoint =
1442 [&]( int aNetcode, PCB_LAYER_ID aLayer, const VECTOR2I& aCenter, int aRadius ) -> bool
1443 {
1444 auto it = filledZonesByNetLayer.find( { aNetcode, aLayer } );
1445
1446 if( it == filledZonesByNetLayer.end() )
1447 return false;
1448
1449 for( const INDEXED_ZONE& iz : it->second )
1450 {
1451 if( !iz.bbox.GetInflated( aRadius ).Contains( aCenter ) )
1452 continue;
1453
1454 if( iz.index->Contains( aCenter, aRadius ) )
1455 return true;
1456 }
1457
1458 return false;
1459 };
1460
1461 for( PCB_TRACK* track : m_board->Tracks() )
1462 {
1463 if( track->Type() != PCB_VIA_T )
1464 continue;
1465
1466 PCB_VIA* via = static_cast<PCB_VIA*>( track );
1467 VECTOR2I center = via->GetPosition();
1468 int holeRadius = via->GetDrillValue() / 2;
1469 int netcode = via->GetNetCode();
1470 LSET layers = via->GetLayerSet() & boardCuMask;
1471
1472 for( PCB_LAYER_ID layer : layers )
1473 {
1474 if( via->GetZoneLayerOverride( layer ) != ZLO_FORCE_FLASHED )
1475 continue;
1476
1477 if( !zoneReachesPoint( netcode, layer, center, holeRadius ) )
1478 via->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
1479 }
1480 }
1481
1482 for( FOOTPRINT* footprint : m_board->Footprints() )
1483 {
1484 for( PAD* pad : footprint->Pads() )
1485 {
1486 VECTOR2I center = pad->GetPosition();
1487 int netcode = pad->GetNetCode();
1488 LSET layers = pad->GetLayerSet() & boardCuMask;
1489
1490 int holeRadius = 0;
1491
1492 if( pad->HasHole() )
1493 holeRadius = std::min( pad->GetDrillSizeX(), pad->GetDrillSizeY() ) / 2;
1494
1495 for( PCB_LAYER_ID layer : layers )
1496 {
1497 if( pad->GetZoneLayerOverride( layer ) != ZLO_FORCE_FLASHED )
1498 continue;
1499
1500 if( !zoneReachesPoint( netcode, layer, center, holeRadius ) )
1501 pad->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
1502 }
1503 }
1504 }
1505
1506 if( aCheck )
1507 {
1508 bool outOfDate = false;
1509
1510 for( ZONE* zone : aZones )
1511 {
1512 // Keepout zones are not filled
1513 if( zone->GetIsRuleArea() )
1514 continue;
1515
1516 for( PCB_LAYER_ID layer : zone->GetLayerSet() )
1517 {
1518 zone->BuildHashValue( layer );
1519
1520 if( oldFillHashes[ { zone, layer } ] != zone->GetHashValue( layer ) )
1521 outOfDate = true;
1522 }
1523 }
1524
1525 if( ( m_board->GetProject()
1526 && m_board->GetProject()->GetLocalSettings().m_PrototypeZoneFill ) )
1527 {
1528 KIDIALOG dlg( aParent, _( "Prototype zone fill enabled. Disable setting and refill?" ), _( "Confirmation" ),
1529 wxOK | wxCANCEL | wxICON_WARNING );
1530 dlg.SetOKCancelLabels( _( "Disable and refill" ), _( "Continue without Refill" ) );
1531 dlg.DoNotShowCheckbox( __FILE__, __LINE__ );
1532
1533 if( dlg.ShowModal() == wxID_OK )
1534 {
1535 m_board->GetProject()->GetLocalSettings().m_PrototypeZoneFill = false;
1536 }
1537 else if( !outOfDate )
1538 {
1539 return false;
1540 }
1541 }
1542
1543 if( outOfDate )
1544 {
1545 KIDIALOG dlg( aParent, _( "Zone fills are out-of-date. Refill?" ), _( "Confirmation" ),
1546 wxOK | wxCANCEL | wxICON_WARNING );
1547 dlg.SetOKCancelLabels( _( "Refill" ), _( "Continue without Refill" ) );
1548 dlg.DoNotShowCheckbox( __FILE__, __LINE__ );
1549
1550 if( dlg.ShowModal() == wxID_CANCEL )
1551 return false;
1552 }
1553 else
1554 {
1555 // No need to commit something that hasn't changed (and committing will set
1556 // the modified flag).
1557 return false;
1558 }
1559 }
1560
1561 if( m_progressReporter )
1562 {
1563 if( m_progressReporter->IsCancelled() )
1564 return false;
1565
1566 m_progressReporter->AdvancePhase();
1567 m_progressReporter->KeepRefreshing();
1568 }
1569
1570 return true;
1571}
1572
1573
1578void ZONE_FILLER::addKnockout( BOARD_ITEM* aItem, PCB_LAYER_ID aLayer, int aGap, SHAPE_POLY_SET& aHoles )
1579{
1580 if( aItem->Type() == PCB_PAD_T && static_cast<PAD*>( aItem )->GetShape( aLayer ) == PAD_SHAPE::CUSTOM )
1581 {
1582 PAD* pad = static_cast<PAD*>( aItem );
1583 SHAPE_POLY_SET poly;
1584 pad->TransformShapeToPolygon( poly, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1585
1586 // the pad shape in zone can be its convex hull or the shape itself
1587 if( pad->GetCustomShapeInZoneOpt() == CUSTOM_SHAPE_ZONE_MODE::CONVEXHULL )
1588 {
1589 std::vector<VECTOR2I> convex_hull;
1590 BuildConvexHull( convex_hull, poly );
1591
1592 aHoles.NewOutline();
1593
1594 for( const VECTOR2I& pt : convex_hull )
1595 aHoles.Append( pt );
1596 }
1597 else
1598 {
1599 aHoles.Append( poly );
1600 }
1601 }
1602 else
1603 {
1604 aItem->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1605 }
1606}
1607
1608
1612void ZONE_FILLER::addHoleKnockout( PAD* aPad, int aGap, SHAPE_POLY_SET& aHoles )
1613{
1614 aPad->TransformHoleToPolygon( aHoles, aGap, m_maxError, ERROR_OUTSIDE );
1615}
1616
1617
1618
1623void ZONE_FILLER::addKnockout( BOARD_ITEM* aItem, PCB_LAYER_ID aLayer, int aGap,
1624 bool aIgnoreLineWidth, SHAPE_POLY_SET& aHoles )
1625{
1626 switch( aItem->Type() )
1627 {
1628 case PCB_FIELD_T:
1629 case PCB_TEXT_T:
1630 {
1631 PCB_TEXT* text = static_cast<PCB_TEXT*>( aItem );
1632
1633 if( text->IsVisible() )
1634 {
1635 if( text->IsKnockout() )
1636 {
1637 // Knockout text should only leave holes where the text is, not where the copper fill
1638 // around it would be.
1639 PCB_TEXT textCopy = *text;
1640 textCopy.SetIsKnockout( false );
1641 textCopy.TransformTextToPolySet( aHoles, 0, m_maxError, ERROR_INSIDE );
1642 }
1643 else
1644 {
1645 text->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1646 }
1647 }
1648
1649 break;
1650 }
1651
1652 case PCB_TEXTBOX_T:
1653 case PCB_TABLE_T:
1654 case PCB_SHAPE_T:
1655 case PCB_TARGET_T:
1656 aItem->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE, aIgnoreLineWidth );
1657 break;
1658
1659 case PCB_BARCODE_T:
1660 {
1661 PCB_BARCODE* barcode = static_cast<PCB_BARCODE*>( aItem );
1662 barcode->GetBoundingHull( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1663 break;
1664 }
1665
1666 case PCB_DIM_ALIGNED_T:
1667 case PCB_DIM_LEADER_T:
1668 case PCB_DIM_CENTER_T:
1669 case PCB_DIM_RADIAL_T:
1671 {
1672 PCB_DIMENSION_BASE* dim = static_cast<PCB_DIMENSION_BASE*>( aItem );
1673
1674 dim->TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE, false );
1675 dim->PCB_TEXT::TransformShapeToPolygon( aHoles, aLayer, aGap, m_maxError, ERROR_OUTSIDE );
1676 break;
1677 }
1678
1679 default:
1680 break;
1681 }
1682}
1683
1684
1690 SHAPE_POLY_SET& aFill,
1691 std::vector<BOARD_ITEM*>& aThermalConnectionPads,
1692 std::vector<PAD*>& aNoConnectionPads )
1693{
1694 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
1695 ZONE_CONNECTION connection;
1696 DRC_CONSTRAINT constraint;
1697 int padClearance;
1698 std::shared_ptr<SHAPE> padShape;
1699 int holeClearance;
1700 SHAPE_POLY_SET holes;
1701
1702 // Deduplication sets for coincident pads and vias
1703 std::unordered_set<PAD_KNOCKOUT_KEY, PAD_KNOCKOUT_KEY_HASH> processedPads;
1704 std::unordered_set<VIA_KNOCKOUT_KEY, VIA_KNOCKOUT_KEY_HASH> processedVias;
1705
1706 for( FOOTPRINT* footprint : m_board->Footprints() )
1707 {
1708 for( PAD* pad : footprint->Pads() )
1709 {
1710 // NPTH pads with a drill hole affect all copper layers even when they carry no copper
1711 // on that layer (e.g. layers limited to "*.Mask"). The physical hole still requires
1712 // a clearance knockout, so skip only pads that are truly irrelevant to this layer.
1713 bool npthWithHole = pad->GetAttribute() == PAD_ATTRIB::NPTH
1714 && pad->GetDrillSize().x > 0;
1715
1716 if( !pad->IsOnLayer( aLayer ) && !npthWithHole )
1717 continue;
1718
1719 BOX2I padBBox = pad->GetBoundingBox();
1720 padBBox.Inflate( m_worstClearance );
1721
1722 if( !padBBox.Intersects( aZone->GetBoundingBox() ) )
1723 continue;
1724
1725 // Deduplicate coincident pads (skip custom pads - they have complex shapes)
1726 PAD_SHAPE padShapeType = pad->GetShape( aLayer );
1727
1728 if( padShapeType != PAD_SHAPE::CUSTOM )
1729 {
1730 // For circular pads: use max of drill and pad size; otherwise just pad size
1731 VECTOR2I padSize = pad->GetSize( aLayer );
1732 VECTOR2I effectiveSize;
1733
1734 if( padShapeType == PAD_SHAPE::CIRCLE )
1735 {
1736 int drill = std::max( pad->GetDrillSize().x, pad->GetDrillSize().y );
1737 int maxDim = std::max( { padSize.x, padSize.y, drill } );
1738 effectiveSize = VECTOR2I( maxDim, maxDim );
1739 }
1740 else
1741 {
1742 effectiveSize = padSize;
1743 }
1744
1745 PAD_KNOCKOUT_KEY padKey{ pad->GetPosition(), effectiveSize,
1746 static_cast<int>( padShapeType ),
1747 pad->GetOrientation(), pad->GetNetCode() };
1748
1749 if( !processedPads.insert( padKey ).second )
1750 continue;
1751 }
1752
1753 bool noConnection = pad->GetNetCode() != aZone->GetNetCode();
1754
1755 if( !aZone->IsTeardropArea() )
1756 {
1757 if( aZone->GetNetCode() == 0
1758 || pad->GetZoneLayerOverride( aLayer ) == ZLO_FORCE_NO_ZONE_CONNECTION )
1759 {
1760 noConnection = true;
1761 }
1762 }
1763
1764 // Check if the pad is backdrilled or post-machined on this layer
1765 if( pad->IsBackdrilledOrPostMachined( aLayer ) )
1766 noConnection = true;
1767
1768 if( noConnection )
1769 {
1770 // collect these for knockout in buildCopperItemClearances()
1771 aNoConnectionPads.push_back( pad );
1772 continue;
1773 }
1774
1775 // For hatch zones, respect the zone connection type just like solid zones
1776 // Pads with THERMAL connection get thermal rings; FULL connections get no knockout;
1777 // NONE connections get handled later in buildCopperItemClearances.
1779 {
1780 constraint = bds.m_DRCEngine->EvalZoneConnection( pad, aZone, aLayer );
1781 connection = constraint.m_ZoneConnection;
1782
1783 if( connection == ZONE_CONNECTION::THERMAL && !pad->CanFlashLayer( aLayer ) )
1784 connection = ZONE_CONNECTION::NONE;
1785
1786 switch( connection )
1787 {
1789 {
1790 padShape = pad->GetEffectiveShape( aLayer, FLASHING::ALWAYS_FLASHED );
1791
1792 if( aFill.Collide( padShape.get(), 0 ) )
1793 {
1794 // Get the thermal relief gap
1796 aZone, aLayer );
1797 int thermalGap = constraint.GetValue().Min();
1798
1799 // Knock out the thermal gap only - the thermal ring will be added separately
1800 aThermalConnectionPads.push_back( pad );
1801 addKnockout( pad, aLayer, thermalGap, holes );
1802 }
1803
1804 break;
1805 }
1806
1808 // Will be handled by buildCopperItemClearances
1809 aNoConnectionPads.push_back( pad );
1810 break;
1811
1813 default:
1814 // No knockout - pad connects directly to the hatch
1815 break;
1816 }
1817
1818 continue;
1819 }
1820
1821 if( aZone->IsTeardropArea() )
1822 {
1823 connection = ZONE_CONNECTION::FULL;
1824 }
1825 else
1826 {
1827 constraint = bds.m_DRCEngine->EvalZoneConnection( pad, aZone, aLayer );
1828 connection = constraint.m_ZoneConnection;
1829 }
1830
1831 if( connection == ZONE_CONNECTION::THERMAL && !pad->CanFlashLayer( aLayer ) )
1832 connection = ZONE_CONNECTION::NONE;
1833
1834 switch( connection )
1835 {
1837 padShape = pad->GetEffectiveShape( aLayer, FLASHING::ALWAYS_FLASHED );
1838
1839 if( aFill.Collide( padShape.get(), 0 ) )
1840 {
1841 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, pad, aZone, aLayer );
1842 padClearance = constraint.GetValue().Min();
1843
1844 aThermalConnectionPads.push_back( pad );
1845 addKnockout( pad, aLayer, padClearance, holes );
1846 }
1847
1848 break;
1849
1851 constraint = bds.m_DRCEngine->EvalRules( PHYSICAL_CLEARANCE_CONSTRAINT, pad, aZone, aLayer );
1852
1853 if( constraint.GetValue().Min() > aZone->GetLocalClearance().value() )
1854 padClearance = constraint.GetValue().Min();
1855 else
1856 padClearance = aZone->GetLocalClearance().value();
1857
1858 if( pad->FlashLayer( aLayer ) )
1859 {
1860 addKnockout( pad, aLayer, padClearance, holes );
1861 }
1862 else if( pad->GetDrillSize().x > 0 )
1863 {
1864 constraint = bds.m_DRCEngine->EvalRules( PHYSICAL_HOLE_CLEARANCE_CONSTRAINT, pad, aZone, aLayer );
1865
1866 if( constraint.GetValue().Min() > padClearance )
1867 holeClearance = constraint.GetValue().Min();
1868 else
1869 holeClearance = padClearance;
1870
1871 pad->TransformHoleToPolygon( holes, holeClearance, m_maxError, ERROR_OUTSIDE );
1872 }
1873
1874 break;
1875
1876 default:
1877 // No knockout
1878 continue;
1879 }
1880 }
1881 }
1882
1883 // For hatch zones, vias also need thermal treatment to prevent isolation inside hatch holes.
1884 // We respect the zone connection type just like pads: THERMAL gets a relief knockout,
1885 // FULL connects directly to the webbing, NONE is handled in buildCopperItemClearances.
1887 {
1888 for( PCB_TRACK* track : m_board->Tracks() )
1889 {
1890 if( track->Type() != PCB_VIA_T )
1891 continue;
1892
1893 PCB_VIA* via = static_cast<PCB_VIA*>( track );
1894
1895 if( !via->IsOnLayer( aLayer ) )
1896 continue;
1897
1898 BOX2I viaBBox = via->GetBoundingBox();
1899 viaBBox.Inflate( m_worstClearance );
1900
1901 if( !viaBBox.Intersects( aZone->GetBoundingBox() ) )
1902 continue;
1903
1904 // Deduplicate coincident vias (circular, so use max of drill and width)
1905 int viaEffectiveSize = std::max( via->GetDrillValue(), via->GetWidth( aLayer ) );
1906 VIA_KNOCKOUT_KEY viaKey{ via->GetPosition(), viaEffectiveSize, via->GetNetCode() };
1907
1908 if( !processedVias.insert( viaKey ).second )
1909 continue;
1910
1911 bool noConnection = via->GetNetCode() != aZone->GetNetCode()
1912 || ( via->Padstack().UnconnectedLayerMode() == UNCONNECTED_LAYER_MODE::START_END_ONLY
1913 && aLayer != via->Padstack().Drill().start
1914 && aLayer != via->Padstack().Drill().end );
1915
1916 if( via->GetZoneLayerOverride( aLayer ) == ZLO_FORCE_NO_ZONE_CONNECTION )
1917 noConnection = true;
1918
1919 // Check if this layer is affected by backdrill or post-machining
1920 if( via->IsBackdrilledOrPostMachined( aLayer ) )
1921 {
1922 noConnection = true;
1923
1924 // Add knockout for backdrill/post-machining hole
1925 int pmSize = 0;
1926 int bdSize = 0;
1927
1928 const PADSTACK::POST_MACHINING_PROPS& frontPM = via->Padstack().FrontPostMachining();
1929 const PADSTACK::POST_MACHINING_PROPS& backPM = via->Padstack().BackPostMachining();
1930
1933 {
1934 pmSize = std::max( pmSize, frontPM.size );
1935 }
1936
1939 {
1940 pmSize = std::max( pmSize, backPM.size );
1941 }
1942
1943 const PADSTACK::DRILL_PROPS& secDrill = via->Padstack().SecondaryDrill();
1944
1945 if( secDrill.start != UNDEFINED_LAYER && secDrill.end != UNDEFINED_LAYER )
1946 bdSize = secDrill.size.x;
1947
1948 int knockoutSize = std::max( pmSize, bdSize );
1949
1950 if( knockoutSize > 0 )
1951 {
1952 int clearance = aZone->GetLocalClearance().value_or( 0 );
1953
1954 TransformCircleToPolygon( holes, via->GetPosition(), knockoutSize / 2 + clearance,
1956 }
1957 }
1958
1959 if( noConnection )
1960 continue;
1961
1962 constraint = bds.m_DRCEngine->EvalZoneConnection( via, aZone, aLayer );
1963 connection = constraint.m_ZoneConnection;
1964
1965 switch( connection )
1966 {
1968 {
1970 aZone, aLayer );
1971 int thermalGap = constraint.GetValue().Min();
1972
1973 // Only force thermal if the via is small enough to be isolated in a hatch hole.
1974 // A via wider than the hole width will always touch the webbing naturally.
1975 if( thermalGap > 0 )
1976 {
1977 aThermalConnectionPads.push_back( via );
1978 addKnockout( via, aLayer, thermalGap, holes );
1979 }
1980
1981 break;
1982 }
1983
1985 // Will be handled by buildCopperItemClearances
1986 break;
1987
1989 default:
1990 // No knockout - via connects directly to the hatch webbing
1991 break;
1992 }
1993 }
1994 }
1995
1996 aFill.BooleanSubtract( holes );
1997}
1998
1999
2005 const std::vector<PAD*>& aNoConnectionPads,
2006 SHAPE_POLY_SET& aHoles,
2007 bool aIncludeZoneClearances )
2008{
2009 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
2010 long ticker = 0;
2011
2012 // Deduplication sets for coincident items
2013 std::unordered_set<PAD_KNOCKOUT_KEY, PAD_KNOCKOUT_KEY_HASH> processedPads;
2014 std::unordered_set<VIA_KNOCKOUT_KEY, VIA_KNOCKOUT_KEY_HASH> processedVias;
2015 std::unordered_set<TRACK_KNOCKOUT_KEY, TRACK_KNOCKOUT_KEY_HASH> processedTracks;
2016
2017 auto checkForCancel =
2018 [&ticker]( PROGRESS_REPORTER* aReporter ) -> bool
2019 {
2020 return aReporter && ( ticker++ % 50 ) == 0 && aReporter->IsCancelled();
2021 };
2022
2023 // A small extra clearance to be sure actual track clearances are not smaller than
2024 // requested clearance due to many approximations in calculations, like arc to segment
2025 // approx, rounding issues, etc.
2026 BOX2I zone_boundingbox = aZone->GetBoundingBox();
2027 int extra_margin = pcbIUScale.mmToIU( ADVANCED_CFG::GetCfg().m_ExtraClearance );
2028
2029 // Items outside the zone bounding box are skipped, so it needs to be inflated by the
2030 // largest clearance value found in the netclasses and rules
2031 zone_boundingbox.Inflate( m_worstClearance + extra_margin );
2032
2033 auto evalRulesForItems =
2034 [&bds]( DRC_CONSTRAINT_T aConstraint, const BOARD_ITEM* a, const BOARD_ITEM* b,
2035 PCB_LAYER_ID aEvalLayer ) -> int
2036 {
2037 DRC_CONSTRAINT c = bds.m_DRCEngine->EvalRules( aConstraint, a, b, aEvalLayer );
2038
2039 if( c.IsNull() )
2040 return -1;
2041 else
2042 return c.GetValue().Min();
2043 };
2044
2045 // Add non-connected pad clearances
2046 //
2047 auto knockoutPadClearance =
2048 [&]( PAD* aPad )
2049 {
2050 int init_gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aPad, aLayer );
2051 int gap = init_gap;
2052 bool hasHole = aPad->GetDrillSize().x > 0;
2053 bool flashLayer = aPad->FlashLayer( aLayer );
2054 bool platedHole = hasHole && aPad->GetAttribute() == PAD_ATTRIB::PTH;
2055
2056 if( flashLayer || platedHole )
2057 {
2058 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aPad, aLayer ) );
2059 }
2060
2061 if( flashLayer && gap >= 0 )
2062 addKnockout( aPad, aLayer, gap + extra_margin, aHoles );
2063
2064 if( hasHole )
2065 {
2066 // NPTH do not need copper clearance gaps to their holes
2067 if( aPad->GetAttribute() == PAD_ATTRIB::NPTH )
2068 gap = init_gap;
2069
2070 gap = std::max( gap, evalRulesForItems( PHYSICAL_HOLE_CLEARANCE_CONSTRAINT, aZone, aPad, aLayer ) );
2071
2072 gap = std::max( gap, evalRulesForItems( HOLE_CLEARANCE_CONSTRAINT, aZone, aPad, aLayer ) );
2073
2074 // Oblong NPTH holes are milled rather than drilled, so they need
2075 // edge clearance in addition to hole clearance
2076 if( aPad->GetAttribute() == PAD_ATTRIB::NPTH
2077 && aPad->GetDrillSize().x != aPad->GetDrillSize().y )
2078 {
2079 gap = std::max( gap, evalRulesForItems( EDGE_CLEARANCE_CONSTRAINT, aZone,
2080 aPad, aLayer ) );
2081 }
2082
2083 if( gap >= 0 )
2084 addHoleKnockout( aPad, gap + extra_margin, aHoles );
2085 }
2086
2087 // Handle backdrill and post-machining knockouts
2088 if( aPad->IsBackdrilledOrPostMachined( aLayer ) )
2089 {
2090 int pmSize = 0;
2091 int bdSize = 0;
2092
2093 const PADSTACK::POST_MACHINING_PROPS& frontPM = aPad->Padstack().FrontPostMachining();
2094 const PADSTACK::POST_MACHINING_PROPS& backPM = aPad->Padstack().BackPostMachining();
2095
2098 {
2099 pmSize = std::max( pmSize, frontPM.size );
2100 }
2101
2104 {
2105 pmSize = std::max( pmSize, backPM.size );
2106 }
2107
2108 const PADSTACK::DRILL_PROPS& secDrill = aPad->Padstack().SecondaryDrill();
2109
2110 if( secDrill.start != UNDEFINED_LAYER && secDrill.end != UNDEFINED_LAYER )
2111 bdSize = secDrill.size.x;
2112
2113 int knockoutSize = std::max( pmSize, bdSize );
2114
2115 if( knockoutSize > 0 )
2116 {
2117 int clearance = std::max( gap, 0 ) + extra_margin;
2118
2119 TransformCircleToPolygon( aHoles, aPad->GetPosition(), knockoutSize / 2 + clearance,
2121 }
2122 }
2123 };
2124
2125 for( PAD* pad : aNoConnectionPads )
2126 {
2127 if( checkForCancel( m_progressReporter ) )
2128 return;
2129
2130 // Deduplicate coincident pads (skip custom pads - they have complex shapes)
2131 PAD_SHAPE padShape = pad->GetShape( aLayer );
2132
2133 if( padShape != PAD_SHAPE::CUSTOM )
2134 {
2135 // For circular pads: use max of drill and pad size; otherwise just pad size
2136 VECTOR2I padSize = pad->GetSize( aLayer );
2137 VECTOR2I effectiveSize;
2138
2139 if( padShape == PAD_SHAPE::CIRCLE )
2140 {
2141 int drill = std::max( pad->GetDrillSize().x, pad->GetDrillSize().y );
2142 int maxDim = std::max( { padSize.x, padSize.y, drill } );
2143 effectiveSize = VECTOR2I( maxDim, maxDim );
2144 }
2145 else
2146 {
2147 effectiveSize = padSize;
2148 }
2149
2150 PAD_KNOCKOUT_KEY padKey{ pad->GetPosition(), effectiveSize,
2151 static_cast<int>( padShape ), pad->GetOrientation(),
2152 pad->GetNetCode() };
2153
2154 if( !processedPads.insert( padKey ).second )
2155 continue;
2156 }
2157
2158 knockoutPadClearance( pad );
2159 }
2160
2161 // Add non-connected track clearances
2162 //
2163 auto knockoutTrackClearance =
2164 [&]( PCB_TRACK* aTrack )
2165 {
2166 if( aTrack->GetBoundingBox().Intersects( zone_boundingbox ) )
2167 {
2168 bool sameNet = aTrack->GetNetCode() == aZone->GetNetCode();
2169
2170 if( !aZone->IsTeardropArea() && aZone->GetNetCode() == 0 )
2171 sameNet = false;
2172
2173 int gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aTrack, aLayer );
2174
2175 if( aTrack->Type() == PCB_VIA_T )
2176 {
2177 PCB_VIA* via = static_cast<PCB_VIA*>( aTrack );
2178
2179 if( via->GetZoneLayerOverride( aLayer ) == ZLO_FORCE_NO_ZONE_CONNECTION )
2180 sameNet = false;
2181 }
2182
2183 if( !sameNet )
2184 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aTrack, aLayer ) );
2185
2186 if( aTrack->Type() == PCB_VIA_T )
2187 {
2188 PCB_VIA* via = static_cast<PCB_VIA*>( aTrack );
2189
2190 if( via->FlashLayer( aLayer ) && gap > 0 )
2191 {
2192 via->TransformShapeToPolygon( aHoles, aLayer, gap + extra_margin, m_maxError,
2193 ERROR_OUTSIDE );
2194 }
2195
2196 gap = std::max( gap, evalRulesForItems( PHYSICAL_HOLE_CLEARANCE_CONSTRAINT, aZone, via,
2197 aLayer ) );
2198
2199 if( !sameNet )
2200 gap = std::max( gap, evalRulesForItems( HOLE_CLEARANCE_CONSTRAINT, aZone, via, aLayer ) );
2201
2202 if( gap >= 0 )
2203 {
2204 int radius = via->GetDrillValue() / 2;
2205
2206 TransformCircleToPolygon( aHoles, via->GetPosition(), radius + gap + extra_margin,
2208 }
2209
2210 // Handle backdrill and post-machining knockouts
2211 if( via->IsBackdrilledOrPostMachined( aLayer ) )
2212 {
2213 int pmSize = 0;
2214 int bdSize = 0;
2215
2216 const PADSTACK::POST_MACHINING_PROPS& frontPM = via->Padstack().FrontPostMachining();
2217 const PADSTACK::POST_MACHINING_PROPS& backPM = via->Padstack().BackPostMachining();
2218
2221 {
2222 pmSize = std::max( pmSize, frontPM.size );
2223 }
2224
2227 {
2228 pmSize = std::max( pmSize, backPM.size );
2229 }
2230
2231 const PADSTACK::DRILL_PROPS& secDrill = via->Padstack().SecondaryDrill();
2232
2233 if( secDrill.start != UNDEFINED_LAYER && secDrill.end != UNDEFINED_LAYER )
2234 bdSize = secDrill.size.x;
2235
2236 int knockoutSize = std::max( pmSize, bdSize );
2237
2238 if( knockoutSize > 0 )
2239 {
2240 int clearance = std::max( gap, 0 ) + extra_margin;
2241
2242 TransformCircleToPolygon( aHoles, via->GetPosition(), knockoutSize / 2 + clearance,
2244 }
2245 }
2246 }
2247 else
2248 {
2249 if( gap >= 0 )
2250 {
2251 aTrack->TransformShapeToPolygon( aHoles, aLayer, gap + extra_margin, m_maxError,
2252 ERROR_OUTSIDE );
2253 }
2254 }
2255 }
2256 };
2257
2258 for( PCB_TRACK* track : m_board->Tracks() )
2259 {
2260 if( !track->IsOnLayer( aLayer ) )
2261 continue;
2262
2263 if( checkForCancel( m_progressReporter ) )
2264 return;
2265
2266 // Deduplicate coincident tracks and vias
2267 if( track->Type() == PCB_VIA_T )
2268 {
2269 PCB_VIA* via = static_cast<PCB_VIA*>( track );
2270 int viaEffectiveSize = std::max( via->GetDrillValue(), via->GetWidth( aLayer ) );
2271 VIA_KNOCKOUT_KEY viaKey{ via->GetPosition(), viaEffectiveSize, via->GetNetCode() };
2272
2273 if( !processedVias.insert( viaKey ).second )
2274 continue;
2275 }
2276 else
2277 {
2278 TRACK_KNOCKOUT_KEY trackKey( track->GetStart(), track->GetEnd(), track->GetWidth() );
2279
2280 if( !processedTracks.insert( trackKey ).second )
2281 continue;
2282 }
2283
2284 knockoutTrackClearance( track );
2285 }
2286
2287 // Add graphic item clearances.
2288 //
2289 auto knockoutGraphicClearance =
2290 [&]( BOARD_ITEM* aItem )
2291 {
2292 int shapeNet = -1;
2293
2294 if( aItem->Type() == PCB_SHAPE_T )
2295 shapeNet = static_cast<PCB_SHAPE*>( aItem )->GetNetCode();
2296
2297 bool sameNet = shapeNet == aZone->GetNetCode();
2298
2299 if( !aZone->IsTeardropArea() && aZone->GetNetCode() == 0 )
2300 sameNet = false;
2301
2302 // A item on the Edge_Cuts or Margin is always seen as on any layer:
2303 if( aItem->IsOnLayer( aLayer )
2304 || aItem->IsOnLayer( Edge_Cuts )
2305 || aItem->IsOnLayer( Margin ) )
2306 {
2307 if( aItem->GetBoundingBox().Intersects( zone_boundingbox ) )
2308 {
2309 bool ignoreLineWidths = false;
2310 int gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aItem, aLayer );
2311
2312 if( aItem->IsOnLayer( aLayer ) && !sameNet )
2313 {
2314 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aItem, aLayer ) );
2315 }
2316 else if( aItem->IsOnLayer( Edge_Cuts ) )
2317 {
2318 gap = std::max( gap, evalRulesForItems( EDGE_CLEARANCE_CONSTRAINT, aZone, aItem, aLayer ) );
2319 ignoreLineWidths = true;
2320 }
2321 else if( aItem->IsOnLayer( Margin ) )
2322 {
2323 gap = std::max( gap, evalRulesForItems( EDGE_CLEARANCE_CONSTRAINT, aZone, aItem, aLayer ) );
2324 }
2325
2326 if( gap >= 0 )
2327 {
2328 gap += extra_margin;
2329 addKnockout( aItem, aLayer, gap, ignoreLineWidths, aHoles );
2330 }
2331 }
2332 }
2333 };
2334
2335 auto knockoutCourtyardClearance =
2336 [&]( FOOTPRINT* aFootprint )
2337 {
2338 if( aFootprint->GetBoundingBox().Intersects( zone_boundingbox ) )
2339 {
2340 int gap = evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aFootprint, aLayer );
2341
2342 // For internal copper layers, GetCourtyard( aLayer ) always returns the
2343 // front courtyard because IsBackLayer() is false for all internal layers.
2344 // Use the footprint's own layer to select the correct courtyard instead.
2345 PCB_LAYER_ID courtyardSide = IsInnerCopperLayer( aLayer ) ? aFootprint->GetLayer() : aLayer;
2346
2347 if( gap == 0 )
2348 {
2349 aHoles.Append( aFootprint->GetCourtyard( courtyardSide ) );
2350 }
2351 else if( gap > 0 )
2352 {
2353 SHAPE_POLY_SET hole = aFootprint->GetCourtyard( courtyardSide );
2355 aHoles.Append( hole );
2356 }
2357 }
2358 };
2359
2360 for( FOOTPRINT* footprint : m_board->Footprints() )
2361 {
2362 knockoutCourtyardClearance( footprint );
2363 knockoutGraphicClearance( &footprint->Reference() );
2364 knockoutGraphicClearance( &footprint->Value() );
2365
2366 std::set<PAD*> allowedNetTiePads;
2367
2368 // Don't knock out holes for graphic items which implement a net-tie to the zone's net
2369 // on the layer being filled.
2370 if( footprint->IsNetTie() )
2371 {
2372 for( PAD* pad : footprint->Pads() )
2373 {
2374 bool sameNet = pad->GetNetCode() == aZone->GetNetCode();
2375
2376 if( !aZone->IsTeardropArea() && aZone->GetNetCode() == 0 )
2377 sameNet = false;
2378
2379 if( sameNet )
2380 {
2381 if( pad->IsOnLayer( aLayer ) )
2382 allowedNetTiePads.insert( pad );
2383
2384 for( PAD* other : footprint->GetNetTiePads( pad ) )
2385 {
2386 if( other->IsOnLayer( aLayer ) )
2387 allowedNetTiePads.insert( other );
2388 }
2389 }
2390 }
2391 }
2392
2393 for( BOARD_ITEM* item : footprint->GraphicalItems() )
2394 {
2395 if( checkForCancel( m_progressReporter ) )
2396 return;
2397
2398 BOX2I itemBBox = item->GetBoundingBox();
2399
2400 if( !zone_boundingbox.Intersects( itemBBox ) )
2401 continue;
2402
2403 bool skipItem = false;
2404
2405 if( item->IsOnLayer( aLayer ) )
2406 {
2407 std::shared_ptr<SHAPE> itemShape = item->GetEffectiveShape();
2408
2409 for( PAD* pad : allowedNetTiePads )
2410 {
2411 if( pad->GetBoundingBox().Intersects( itemBBox )
2412 && pad->GetEffectiveShape( aLayer )->Collide( itemShape.get() ) )
2413 {
2414 skipItem = true;
2415 break;
2416 }
2417 }
2418 }
2419
2420 if( !skipItem )
2421 knockoutGraphicClearance( item );
2422 }
2423 }
2424
2425 for( BOARD_ITEM* item : m_board->Drawings() )
2426 {
2427 if( checkForCancel( m_progressReporter ) )
2428 return;
2429
2430 knockoutGraphicClearance( item );
2431 }
2432
2433 // Add non-connected zone clearances
2434 //
2435 auto knockoutZoneClearance =
2436 [&]( ZONE* aKnockout )
2437 {
2438 // If the zones share no common layers
2439 if( !aKnockout->GetLayerSet().test( aLayer ) )
2440 return;
2441
2442 if( aKnockout->GetBoundingBox().Intersects( zone_boundingbox ) )
2443 {
2444 if( aKnockout->GetIsRuleArea() )
2445 {
2446 if( aKnockout->GetDoNotAllowZoneFills() && !aZone->IsTeardropArea() )
2447 {
2448 // Keepouts use outline with no clearance
2449 aKnockout->TransformSmoothedOutlineToPolygon( aHoles, 0, m_maxError, ERROR_OUTSIDE,
2450 nullptr );
2451 }
2452 }
2453 else
2454 {
2455 if( aKnockout->HigherPriority( aZone ) && !aKnockout->SameNet( aZone ) )
2456 {
2457 int gap = std::max( 0, evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT, aZone, aKnockout,
2458 aLayer ) );
2459
2460 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone, aKnockout, aLayer ) );
2461
2462 // Negative clearance permits zones to short
2463 if( gap < 0 )
2464 return;
2465
2466 SHAPE_POLY_SET poly;
2467 aKnockout->TransformShapeToPolygon( poly, aLayer, gap + extra_margin, m_maxError,
2468 ERROR_OUTSIDE );
2469 aHoles.Append( poly );
2470 }
2471 }
2472 }
2473 };
2474
2475 if( aIncludeZoneClearances )
2476 {
2477 for( ZONE* otherZone : m_board->Zones() )
2478 {
2479 if( checkForCancel( m_progressReporter ) )
2480 return;
2481
2482 knockoutZoneClearance( otherZone );
2483 }
2484
2485 for( FOOTPRINT* footprint : m_board->Footprints() )
2486 {
2487 for( ZONE* otherZone : footprint->Zones() )
2488 {
2489 if( checkForCancel( m_progressReporter ) )
2490 return;
2491
2492 knockoutZoneClearance( otherZone );
2493 }
2494 }
2495 }
2496
2497 aHoles.Simplify();
2498}
2499
2500
2506{
2507 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
2508 int extra_margin = pcbIUScale.mmToIU( ADVANCED_CFG::GetCfg().m_ExtraClearance );
2509
2510 BOX2I zone_boundingbox = aZone->GetBoundingBox();
2511 zone_boundingbox.Inflate( m_worstClearance + extra_margin );
2512
2513 auto evalRulesForItems =
2514 [&bds]( DRC_CONSTRAINT_T aConstraint, const BOARD_ITEM* a, const BOARD_ITEM* b,
2515 PCB_LAYER_ID aEvalLayer ) -> int
2516 {
2517 DRC_CONSTRAINT c = bds.m_DRCEngine->EvalRules( aConstraint, a, b, aEvalLayer );
2518
2519 if( c.IsNull() )
2520 return -1;
2521 else
2522 return c.GetValue().Min();
2523 };
2524
2525 // Keepout zones (rule areas) are excluded here because they are subtracted earlier in the
2526 // fill process, before the deflate/inflate min-width cycle. Subtracting them here would
2527 // trigger a second deflate/inflate pass that creates artifacts along curved keepout
2528 // boundaries (issue 23515).
2529 auto knockoutZoneClearance =
2530 [&]( ZONE* aKnockout )
2531 {
2532 if( aKnockout->GetIsRuleArea() )
2533 return;
2534
2535 if( !aKnockout->GetLayerSet().test( aLayer ) )
2536 return;
2537
2538 if( aKnockout->GetBoundingBox().Intersects( zone_boundingbox ) )
2539 {
2540 if( aKnockout->HigherPriority( aZone ) && !aKnockout->SameNet( aZone ) )
2541 {
2542 int gap = std::max( 0, evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT,
2543 aZone, aKnockout, aLayer ) );
2544
2545 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone,
2546 aKnockout, aLayer ) );
2547
2548 if( gap < 0 )
2549 return;
2550
2551 SHAPE_POLY_SET poly;
2552 aKnockout->TransformShapeToPolygon( poly, aLayer, gap + extra_margin,
2554 aHoles.Append( poly );
2555 }
2556 }
2557 };
2558
2559 forEachBoardAndFootprintZone( m_board, knockoutZoneClearance );
2560
2561 aHoles.Simplify();
2562}
2563
2564
2570 SHAPE_POLY_SET& aRawFill )
2571{
2572 BOX2I zoneBBox = aZone->GetBoundingBox();
2573 SHAPE_POLY_SET knockouts;
2574
2575 auto collectZoneOutline =
2576 [&]( ZONE* aKnockout )
2577 {
2578 if( !aKnockout->GetLayerSet().test( aLayer ) )
2579 return;
2580
2581 if( aKnockout->GetBoundingBox().Intersects( zoneBBox ) )
2582 appendZoneOutlineWithoutArcs( aKnockout, knockouts );
2583 };
2584
2585 forEachBoardAndFootprintZone(
2586 m_board,
2587 [&]( ZONE* otherZone )
2588 {
2589 // Don't use `HigherPriority()` here because we only want explicitly-higher
2590 // priorities, not equal-priority zones.
2591 bool higherPrioritySameNet =
2592 otherZone->SameNet( aZone )
2593 && otherZone->GetAssignedPriority() > aZone->GetAssignedPriority();
2594
2595 if( higherPrioritySameNet && !otherZone->IsTeardropArea() )
2596 collectZoneOutline( otherZone );
2597 } );
2598
2599 if( knockouts.OutlineCount() > 0 )
2600 aRawFill.BooleanSubtract( knockouts );
2601}
2602
2603
2604void ZONE_FILLER::connect_nearby_polys( SHAPE_POLY_SET& aPolys, double aDistance )
2605{
2606 if( aPolys.OutlineCount() < 1 )
2607 return;
2608
2609 VERTEX_CONNECTOR vs( aPolys.BBoxFromCaches(), aPolys, aDistance );
2610
2611 vs.FindResults();
2612
2613 // This cannot be a reference because we need to do the comparison below while
2614 // changing the values
2615 std::map<int, std::vector<std::pair<int, VECTOR2I>>> insertion_points;
2616
2617 for( const RESULTS& result : vs.GetResults() )
2618 {
2619 SHAPE_LINE_CHAIN& line1 = aPolys.Outline( result.m_outline1 );
2620 SHAPE_LINE_CHAIN& line2 = aPolys.Outline( result.m_outline2 );
2621
2622 VECTOR2I pt1 = line1.CPoint( result.m_vertex1 );
2623 VECTOR2I pt2 = line2.CPoint( result.m_vertex2 );
2624
2625 // We want to insert the existing point first so that we can place the new point
2626 // between the two points at the same location.
2627 insertion_points[result.m_outline1].push_back( { result.m_vertex1, pt1 } );
2628 insertion_points[result.m_outline1].push_back( { result.m_vertex1, pt2 } );
2629 }
2630
2631 for( auto& [outline, vertices] : insertion_points )
2632 {
2633 SHAPE_LINE_CHAIN& line = aPolys.Outline( outline );
2634
2635 // Stable sort here because we want to make sure that we are inserting pt1 first and
2636 // pt2 second but still sorting the rest of the indices from highest to lowest.
2637 // This allows us to insert into the existing polygon without modifying the future
2638 // insertion points.
2639 std::stable_sort( vertices.begin(), vertices.end(),
2640 []( const std::pair<int, VECTOR2I>& a, const std::pair<int, VECTOR2I>& b )
2641 {
2642 return a.first > b.first;
2643 } );
2644
2645 for( const auto& [vertex, pt] : vertices )
2646 line.Insert( vertex + 1, pt );
2647 }
2648}
2649
2650
2652{
2653 int half_min_width = aZone->GetMinThickness() / 2;
2654 int epsilon = pcbIUScale.mmToIU( 0.001 );
2655
2656 if( half_min_width - epsilon <= epsilon )
2657 return;
2658
2659 SHAPE_POLY_SET preDeflate = aFillPolys.CloneDropTriangulation();
2660
2661 aFillPolys.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS,
2662 m_maxError );
2663
2664 aFillPolys.Fracture();
2665 connect_nearby_polys( aFillPolys, aZone->GetMinThickness() );
2666
2667 for( int ii = aFillPolys.OutlineCount() - 1; ii >= 0; ii-- )
2668 {
2669 std::vector<SHAPE_LINE_CHAIN>& island = aFillPolys.Polygon( ii );
2670 BOX2I islandExtents;
2671
2672 for( const VECTOR2I& pt : island.front().CPoints() )
2673 {
2674 islandExtents.Merge( pt );
2675
2676 if( islandExtents.GetSizeMax() > aZone->GetMinThickness() )
2677 break;
2678 }
2679
2680 if( islandExtents.GetSizeMax() < aZone->GetMinThickness() )
2681 aFillPolys.DeletePolygon( ii );
2682 }
2683
2684 aFillPolys.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS, m_maxError,
2685 true );
2686 aFillPolys.BooleanIntersection( preDeflate );
2687}
2688
2689
2690#define DUMP_POLYS_TO_COPPER_LAYER( a, b, c ) \
2691 { if( m_debugZoneFiller && aDebugLayer == b ) \
2692 { \
2693 m_board->SetLayerName( b, c ); \
2694 SHAPE_POLY_SET d = a; \
2695 d.Fracture(); \
2696 aFillPolys = d; \
2697 return false; \
2698 } \
2699 }
2700
2701
2702/*
2703 * Note that aSmoothedOutline is larger than the zone where it intersects with other, same-net
2704 * zones. This is to prevent the re-inflation post min-width trimming from createing divots
2705 * between adjacent zones. The final aMaxExtents trimming will remove these areas from the final
2706 * fill.
2707 */
2708bool ZONE_FILLER::fillCopperZone( const ZONE* aZone, PCB_LAYER_ID aLayer, PCB_LAYER_ID aDebugLayer,
2709 const SHAPE_POLY_SET& aSmoothedOutline,
2710 const SHAPE_POLY_SET& aMaxExtents, SHAPE_POLY_SET& aFillPolys )
2711{
2712 // m_maxError is initialized in the constructor. Don't reassign here to avoid data races
2713 // when multiple threads call this function concurrently.
2714
2715 // Features which are min_width should survive pruning; features that are *less* than
2716 // min_width should not. Therefore we subtract epsilon from the min_width when
2717 // deflating/inflating.
2718 int half_min_width = aZone->GetMinThickness() / 2;
2719 int epsilon = pcbIUScale.mmToIU( 0.001 );
2720
2721 // Solid polygons are deflated and inflated during calculations. Deflating doesn't cause
2722 // issues, but inflate is tricky as it can create excessively long and narrow spikes for
2723 // acute angles.
2724 // ALLOW_ACUTE_CORNERS cannot be used due to the spike problem.
2725 // CHAMFER_ACUTE_CORNERS is tempting, but can still produce spikes in some unusual
2726 // circumstances (https://gitlab.com/kicad/code/kicad/-/issues/5581).
2727 // It's unclear if ROUND_ACUTE_CORNERS would have the same issues, but is currently avoided
2728 // as a "less-safe" option.
2729 // ROUND_ALL_CORNERS produces the uniformly nicest shapes, but also a lot of segments.
2730 // CHAMFER_ALL_CORNERS improves the segment count.
2733
2734 std::vector<BOARD_ITEM*> thermalConnectionPads;
2735 std::vector<PAD*> noConnectionPads;
2736 std::deque<SHAPE_LINE_CHAIN> thermalSpokes;
2737 SHAPE_POLY_SET clearanceHoles;
2738
2739 aFillPolys = aSmoothedOutline;
2740 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In1_Cu, wxT( "smoothed-outline" ) );
2741
2742 if( m_progressReporter && m_progressReporter->IsCancelled() )
2743 return false;
2744
2745 /* -------------------------------------------------------------------------------------
2746 * Knockout thermal reliefs.
2747 */
2748
2749 knockoutThermalReliefs( aZone, aLayer, aFillPolys, thermalConnectionPads, noConnectionPads );
2750 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In2_Cu, wxT( "minus-thermal-reliefs" ) );
2751
2752 if( m_progressReporter && m_progressReporter->IsCancelled() )
2753 return false;
2754
2755 /* -------------------------------------------------------------------------------------
2756 * For hatch zones, add thermal rings around pads with thermal relief.
2757 * The rings are clipped to the zone boundary and provide the connection point
2758 * for the hatch webbing instead of connecting directly to the pad.
2759 */
2760
2761 SHAPE_POLY_SET thermalRings;
2762
2764 {
2765 buildHatchZoneThermalRings( aZone, aLayer, aSmoothedOutline, thermalConnectionPads,
2766 aFillPolys, thermalRings );
2767 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In2_Cu, wxT( "plus-thermal-rings" ) );
2768 }
2769
2770 if( m_progressReporter && m_progressReporter->IsCancelled() )
2771 return false;
2772
2773 /* -------------------------------------------------------------------------------------
2774 * Knockout electrical clearances.
2775 */
2776
2777 // When iterative refill is enabled, we build zone-to-zone clearances separately so we can
2778 // cache the fill before zone knockouts are applied (issue 21746). Keepout zones are always
2779 // included in clearanceHoles regardless of the iterative refill setting so they are
2780 // subtracted before the deflate/inflate min-width cycle. Subtracting keepouts after that
2781 // cycle and running a second deflate/inflate pass creates artifacts along curved keepout
2782 // boundaries (issue 23515).
2783 const bool iterativeRefill = ADVANCED_CFG::GetCfg().m_ZoneFillIterativeRefill;
2784
2785 buildCopperItemClearances( aZone, aLayer, noConnectionPads, clearanceHoles,
2786 !iterativeRefill /* include zone clearances only if not iterative */ );
2787
2788 if( iterativeRefill )
2789 {
2790 BOX2I zone_boundingbox = aZone->GetBoundingBox();
2791 bool addedKeepoutHoles = false;
2792
2793 auto collectKeepoutHoles =
2794 [&]( ZONE* candidate )
2795 {
2796 if( aZone->IsTeardropArea() )
2797 return;
2798
2799 if( !isZoneFillKeepout( candidate, aLayer, zone_boundingbox ) )
2800 return;
2801
2802 candidate->TransformSmoothedOutlineToPolygon( clearanceHoles, 0, m_maxError,
2803 ERROR_OUTSIDE, nullptr );
2804 addedKeepoutHoles = true;
2805 };
2806
2807 forEachBoardAndFootprintZone( m_board, collectKeepoutHoles );
2808
2809 if( addedKeepoutHoles )
2810 clearanceHoles.Simplify();
2811 }
2812
2813 DUMP_POLYS_TO_COPPER_LAYER( clearanceHoles, In3_Cu, wxT( "clearance-holes" ) );
2814
2815 if( m_progressReporter && m_progressReporter->IsCancelled() )
2816 return false;
2817
2818 /* -------------------------------------------------------------------------------------
2819 * Add thermal relief spokes.
2820 */
2821
2822 buildThermalSpokes( aZone, aLayer, thermalConnectionPads, thermalSpokes );
2823
2824 if( m_progressReporter && m_progressReporter->IsCancelled() )
2825 return false;
2826
2827 // Create a temporary zone that we can hit-test spoke-ends against. It's only temporary
2828 // because the "real" subtract-clearance-holes has to be done after the spokes are added.
2829 SHAPE_POLY_SET testAreas = aFillPolys.CloneDropTriangulation();
2830 testAreas.BooleanSubtract( clearanceHoles );
2831
2832 // When iterative refill is enabled, zone-to-zone clearances are not included in
2833 // clearanceHoles (they're applied later to allow pre-knockout caching). But we still
2834 // need to account for them when testing spoke endpoints, otherwise spokes will be kept
2835 // that point into areas that will be knocked out by higher-priority zones.
2836 SHAPE_POLY_SET zoneClearances;
2837
2838 if( iterativeRefill )
2839 {
2840 buildDifferentNetZoneClearances( aZone, aLayer, zoneClearances );
2841
2842 if( zoneClearances.OutlineCount() > 0 )
2843 testAreas.BooleanSubtract( zoneClearances );
2844 }
2845
2846 DUMP_POLYS_TO_COPPER_LAYER( testAreas, In4_Cu, wxT( "minus-clearance-holes" ) );
2847
2848 // Prune features that don't meet minimum-width criteria
2849 if( half_min_width - epsilon > epsilon )
2850 {
2851 testAreas.Deflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2852 DUMP_POLYS_TO_COPPER_LAYER( testAreas, In5_Cu, wxT( "spoke-test-deflated" ) );
2853
2854 testAreas.Inflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2855 DUMP_POLYS_TO_COPPER_LAYER( testAreas, In6_Cu, wxT( "spoke-test-reinflated" ) );
2856 }
2857
2858 if( m_progressReporter && m_progressReporter->IsCancelled() )
2859 return false;
2860
2861 // Build a Y-stripe spatial index for O(sqrt(V)) spoke endpoint containment queries
2862 // instead of O(V) brute-force ray-casting with bbox caches.
2863 POLY_YSTRIPES_INDEX spokeTestIndex;
2864 spokeTestIndex.Build( testAreas );
2865 int interval = 0;
2866
2867 SHAPE_POLY_SET debugSpokes;
2868
2869 for( const SHAPE_LINE_CHAIN& spoke : thermalSpokes )
2870 {
2871 const VECTOR2I& testPt = spoke.CPoint( 3 );
2872
2873 // Hit-test against zone body
2874 if( spokeTestIndex.Contains( testPt, 1 ) )
2875 {
2876 if( m_debugZoneFiller )
2877 debugSpokes.AddOutline( spoke );
2878
2879 aFillPolys.AddOutline( spoke );
2880 continue;
2881 }
2882
2883 if( interval++ > 400 )
2884 {
2885 if( m_progressReporter && m_progressReporter->IsCancelled() )
2886 return false;
2887
2888 interval = 0;
2889 }
2890
2891 // Hit-test against other spokes
2892 for( const SHAPE_LINE_CHAIN& other : thermalSpokes )
2893 {
2894 // Hit test in both directions to avoid interactions with round-off errors.
2895 // (See https://gitlab.com/kicad/code/kicad/-/issues/13316.)
2896 if( &other != &spoke
2897 && other.PointInside( testPt, 1 )
2898 && spoke.PointInside( other.CPoint( 3 ), 1 ) )
2899 {
2900 if( m_debugZoneFiller )
2901 debugSpokes.AddOutline( spoke );
2902
2903 aFillPolys.AddOutline( spoke );
2904 break;
2905 }
2906 }
2907 }
2908
2909 DUMP_POLYS_TO_COPPER_LAYER( debugSpokes, In7_Cu, wxT( "spokes" ) );
2910
2911 if( m_progressReporter && m_progressReporter->IsCancelled() )
2912 return false;
2913
2914 aFillPolys.BooleanSubtract( clearanceHoles );
2915 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In8_Cu, wxT( "after-spoke-trimming" ) );
2916
2917 /* -------------------------------------------------------------------------------------
2918 * Prune features that don't meet minimum-width criteria
2919 */
2920
2921 if( half_min_width - epsilon > epsilon )
2922 {
2923 aFillPolys.Deflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2924
2925 // Also deflate thermal rings to match, for correct hatch hole notching
2926 if( thermalRings.OutlineCount() > 0 )
2927 thermalRings.Deflate( half_min_width - epsilon, fastCornerStrategy, m_maxError );
2928 }
2929
2930 // Min-thickness is the web thickness. On the other hand, a blob min-thickness by
2931 // min-thickness is not useful. Since there's no obvious definition of web vs. blob, we
2932 // arbitrarily choose "at least 2X min-thickness on one axis". (Since we're doing this
2933 // during the deflated state, that means we test for "at least min-thickness".)
2934 for( int ii = aFillPolys.OutlineCount() - 1; ii >= 0; ii-- )
2935 {
2936 std::vector<SHAPE_LINE_CHAIN>& island = aFillPolys.Polygon( ii );
2937 BOX2I islandExtents;
2938
2939 for( const VECTOR2I& pt : island.front().CPoints() )
2940 {
2941 islandExtents.Merge( pt );
2942
2943 if( islandExtents.GetSizeMax() > aZone->GetMinThickness() )
2944 break;
2945 }
2946
2947 if( islandExtents.GetSizeMax() < aZone->GetMinThickness() )
2948 aFillPolys.DeletePolygon( ii );
2949 }
2950
2951 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In9_Cu, wxT( "deflated" ) );
2952
2953 if( m_progressReporter && m_progressReporter->IsCancelled() )
2954 return false;
2955
2956 /* -------------------------------------------------------------------------------------
2957 * Process the hatch pattern (note that we do this while deflated)
2958 */
2959
2961 && ( !m_board->GetProject()
2962 || !m_board->GetProject()->GetLocalSettings().m_PrototypeZoneFill ) )
2963 {
2964 // Combine thermal rings with clearance holes (non-connected pad clearances) so that
2965 // the hatch hole-dropping logic considers both types of rings
2966 SHAPE_POLY_SET ringsToProtect = thermalRings;
2967 ringsToProtect.BooleanAdd( clearanceHoles );
2968
2969 if( !addHatchFillTypeOnZone( aZone, aLayer, aDebugLayer, aFillPolys, ringsToProtect ) )
2970 return false;
2971 }
2972 else if( aZone->GetFillMode() == ZONE_FILL_MODE::COPPER_THIEVING )
2973 {
2974 if( !addCopperThievingPattern( aZone, aLayer, aFillPolys ) )
2975 return false;
2976 }
2977 else
2978 {
2979 /* ---------------------------------------------------------------------------------
2980 * Connect nearby polygons with zero-width lines in order to ensure correct
2981 * re-inflation.
2982 */
2983 aFillPolys.Fracture();
2984 connect_nearby_polys( aFillPolys, aZone->GetMinThickness() );
2985
2986 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In10_Cu, wxT( "connected-nearby-polys" ) );
2987 }
2988
2989 if( m_progressReporter && m_progressReporter->IsCancelled() )
2990 return false;
2991
2992 /* -------------------------------------------------------------------------------------
2993 * Finish minimum-width pruning by re-inflating
2994 */
2995
2996 if( half_min_width - epsilon > epsilon )
2997 aFillPolys.Inflate( half_min_width - epsilon, cornerStrategy, m_maxError, true );
2998
2999 // The deflation/inflation process can leave notches in the outline. Remove these by
3000 // doing a union with the original ring
3001 aFillPolys.BooleanAdd( thermalRings );
3002
3003 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In15_Cu, wxT( "after-reinflating" ) );
3004
3005 /* -------------------------------------------------------------------------------------
3006 * Ensure additive changes (thermal stubs and inflating acute corners) do not add copper
3007 * outside the zone boundary, inside the clearance holes, or between otherwise isolated
3008 * islands
3009 */
3010
3011 for( BOARD_ITEM* item : thermalConnectionPads )
3012 {
3013 if( item->Type() == PCB_PAD_T )
3014 addHoleKnockout( static_cast<PAD*>( item ), 0, clearanceHoles );
3015 }
3016
3017 aFillPolys.BooleanIntersection( aMaxExtents );
3018 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In16_Cu, wxT( "after-trim-to-outline" ) );
3019 aFillPolys.BooleanSubtract( clearanceHoles );
3020 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In17_Cu, wxT( "after-trim-to-clearance-holes" ) );
3021
3022 // Cache the pre-knockout fill for iterative refill optimization (issue 21746).
3023 // The cache stores the fill BEFORE zone-to-zone knockouts so the iterative refill can
3024 // reclaim space when higher-priority zones have islands removed.
3025 bool knockoutsApplied = false;
3026
3027 if( iterativeRefill )
3028 {
3029 {
3030 std::lock_guard<std::mutex> lock( m_cacheMutex );
3031 m_preKnockoutFillCache[{ aZone, aLayer }] = aFillPolys;
3032 }
3033
3034 // Reuse the zone clearances already computed for spoke endpoint testing
3035 if( zoneClearances.OutlineCount() > 0 )
3036 {
3037 aFillPolys.BooleanSubtract( zoneClearances );
3038 knockoutsApplied = true;
3039 }
3040 }
3041
3042 /* -------------------------------------------------------------------------------------
3043 * Re-prune minimum-width violations introduced by different-net zone knockouts.
3044 *
3045 * This must run BEFORE subtracting same-net higher-priority zones. At this point the
3046 * fill still extends into overlapping same-net zone areas, which provides a natural
3047 * buffer that prevents the deflate/inflate cycle from creating divots at same-net
3048 * zone boundaries (the same role aSmoothedOutline plays in the initial min-width pass).
3049 */
3050
3051 if( knockoutsApplied )
3052 postKnockoutMinWidthPrune( aZone, aFillPolys );
3053
3054 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In18_Cu, wxT( "after-post-knockout-min-width" ) );
3055
3056 /* -------------------------------------------------------------------------------------
3057 * Lastly give any same-net but higher-priority zones control over their own area.
3058 */
3059
3060 subtractHigherPriorityZones( aZone, aLayer, aFillPolys );
3061 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In19_Cu, wxT( "minus-higher-priority-zones" ) );
3062
3063 aFillPolys.Fracture();
3064 return true;
3065}
3066
3067
3069 const SHAPE_POLY_SET& aSmoothedOutline,
3070 SHAPE_POLY_SET& aFillPolys )
3071{
3072 BOX2I zone_boundingbox = aZone->GetBoundingBox();
3073 SHAPE_POLY_SET clearanceHoles;
3074 long ticker = 0;
3075
3076 auto checkForCancel =
3077 [&ticker]( PROGRESS_REPORTER* aReporter ) -> bool
3078 {
3079 return aReporter && ( ticker++ % 50 ) == 0 && aReporter->IsCancelled();
3080 };
3081
3082 auto knockoutGraphicItem =
3083 [&]( BOARD_ITEM* aItem )
3084 {
3085 if( aItem->IsKnockout() && aItem->IsOnLayer( aLayer )
3086 && aItem->GetBoundingBox().Intersects( zone_boundingbox ) )
3087 {
3088 addKnockout( aItem, aLayer, 0, true, clearanceHoles );
3089 }
3090 };
3091
3092 for( FOOTPRINT* footprint : m_board->Footprints() )
3093 {
3094 if( checkForCancel( m_progressReporter ) )
3095 return false;
3096
3097 knockoutGraphicItem( &footprint->Reference() );
3098 knockoutGraphicItem( &footprint->Value() );
3099
3100 for( BOARD_ITEM* item : footprint->GraphicalItems() )
3101 knockoutGraphicItem( item );
3102 }
3103
3104 for( BOARD_ITEM* item : m_board->Drawings() )
3105 {
3106 if( checkForCancel( m_progressReporter ) )
3107 return false;
3108
3109 knockoutGraphicItem( item );
3110 }
3111
3112 aFillPolys = aSmoothedOutline;
3113 aFillPolys.BooleanSubtract( clearanceHoles );
3114
3115 SHAPE_POLY_SET keepoutHoles;
3116
3117 auto collectKeepout =
3118 [&]( ZONE* candidate )
3119 {
3120 if( !isZoneFillKeepout( candidate, aLayer, zone_boundingbox ) )
3121 return;
3122
3123 appendZoneOutlineWithoutArcs( candidate, keepoutHoles );
3124 };
3125
3126 bool cancelledKeepoutScan = false;
3127
3128 forEachBoardAndFootprintZone(
3129 m_board,
3130 [&]( ZONE* keepout )
3131 {
3132 if( cancelledKeepoutScan )
3133 return;
3134
3135 if( checkForCancel( m_progressReporter ) )
3136 {
3137 cancelledKeepoutScan = true;
3138 return;
3139 }
3140
3141 collectKeepout( keepout );
3142 } );
3143
3144 if( cancelledKeepoutScan )
3145 return false;
3146
3147 if( keepoutHoles.OutlineCount() > 0 )
3148 aFillPolys.BooleanSubtract( keepoutHoles );
3149
3150 // Features which are min_width should survive pruning; features that are *less* than
3151 // min_width should not. Therefore we subtract epsilon from the min_width when
3152 // deflating/inflating.
3153 int half_min_width = aZone->GetMinThickness() / 2;
3154 int epsilon = pcbIUScale.mmToIU( 0.001 );
3155
3156 aFillPolys.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS, m_maxError );
3157
3158 // Remove the non filled areas due to the hatch pattern
3160 {
3161 SHAPE_POLY_SET noThermalRings; // Non-copper zones have no thermal reliefs
3162
3163 if( !addHatchFillTypeOnZone( aZone, aLayer, aLayer, aFillPolys, noThermalRings ) )
3164 return false;
3165 }
3166 else if( aZone->GetFillMode() == ZONE_FILL_MODE::COPPER_THIEVING )
3167 {
3168 if( !addCopperThievingPattern( aZone, aLayer, aFillPolys ) )
3169 return false;
3170 }
3171
3172 // Re-inflate after pruning of areas that don't meet minimum-width criteria
3173 if( half_min_width - epsilon > epsilon )
3174 aFillPolys.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS, m_maxError );
3175
3176 aFillPolys.Fracture();
3177 return true;
3178}
3179
3180
3181/*
3182 * Build the filled solid areas data from real outlines (stored in m_Poly)
3183 * The solid areas can be more than one on copper layers, and do not have holes
3184 * ( holes are linked by overlapping segments to the main outline)
3185 */
3187{
3188 SHAPE_POLY_SET* boardOutline = m_brdOutlinesValid ? &m_boardOutline : nullptr;
3189 SHAPE_POLY_SET maxExtents;
3190 SHAPE_POLY_SET smoothedPoly;
3191 PCB_LAYER_ID debugLayer = UNDEFINED_LAYER;
3192
3193 if( m_debugZoneFiller && LSET::InternalCuMask().Contains( aLayer ) )
3194 {
3195 debugLayer = aLayer;
3196 aLayer = F_Cu;
3197 }
3198
3199 if( !aZone->BuildSmoothedPoly( maxExtents, aLayer, boardOutline, &smoothedPoly ) )
3200 return false;
3201
3202 if( m_progressReporter && m_progressReporter->IsCancelled() )
3203 return false;
3204
3205 if( aZone->IsOnCopperLayer() )
3206 {
3207 if( fillCopperZone( aZone, aLayer, debugLayer, smoothedPoly, maxExtents, aFillPolys ) )
3208 aZone->SetNeedRefill( false );
3209 }
3210 else
3211 {
3212 if( fillNonCopperZone( aZone, aLayer, smoothedPoly, aFillPolys ) )
3213 aZone->SetNeedRefill( false );
3214 }
3215
3216 return true;
3217}
3218
3219
3224 const std::vector<BOARD_ITEM*>& aSpokedPadsList,
3225 std::deque<SHAPE_LINE_CHAIN>& aSpokesList )
3226{
3227 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3228 BOX2I zoneBB = aZone->GetBoundingBox();
3229 DRC_CONSTRAINT constraint;
3230 int zone_half_width = aZone->GetMinThickness() / 2;
3231
3233 zone_half_width = aZone->GetHatchThickness() / 2;
3234
3235 zoneBB.Inflate( std::max( bds.GetBiggestClearanceValue(), aZone->GetLocalClearance().value() ) );
3236
3237 // Is a point on the boundary of the polygon inside or outside?
3238 // The boundary may be off by MaxError
3239 int epsilon = bds.m_MaxError;
3240
3241 for( BOARD_ITEM* item : aSpokedPadsList )
3242 {
3243 // We currently only connect to pads, not pad holes
3244 if( !item->IsOnLayer( aLayer ) )
3245 continue;
3246
3247 int thermalReliefGap = 0;
3248 int spoke_w = 0;
3249 PAD* pad = nullptr;
3250 PCB_VIA* via = nullptr;
3251 bool circular = false;
3252
3253 if( item->Type() == PCB_PAD_T )
3254 {
3255 pad = static_cast<PAD*>( item );
3256 VECTOR2I padSize = pad->GetSize( aLayer );
3257
3258 if( pad->GetShape( aLayer) == PAD_SHAPE::CIRCLE
3259 || ( pad->GetShape( aLayer ) == PAD_SHAPE::OVAL && padSize.x == padSize.y ) )
3260 {
3261 circular = true;
3262 }
3263 }
3264 else if( item->Type() == PCB_VIA_T )
3265 {
3266 via = static_cast<PCB_VIA*>( item );
3267 circular = true;
3268 }
3269
3270 // For hatch zones, use proper DRC constraints for thermal gap and spoke width,
3271 // just like solid zones. This ensures consistent thermal relief appearance and
3272 // respects pad-specific thermal spoke settings.
3274 {
3275 if( pad )
3276 {
3278 aZone, aLayer );
3279 thermalReliefGap = constraint.GetValue().Min();
3280
3282 aZone, aLayer );
3283 spoke_w = constraint.GetValue().Opt();
3284
3285 int spoke_max_allowed_w = std::min( pad->GetSize( aLayer ).x, pad->GetSize( aLayer ).y );
3286 spoke_w = std::clamp( spoke_w, constraint.Value().Min(), constraint.Value().Max() );
3287 spoke_w = std::min( spoke_w, spoke_max_allowed_w );
3288
3289 if( spoke_w < aZone->GetMinThickness() )
3290 continue;
3291 }
3292 else if( via )
3293 {
3295 aZone, aLayer );
3296 thermalReliefGap = constraint.GetValue().Min();
3297
3299 aZone, aLayer );
3300 spoke_w = constraint.GetValue().Opt();
3301
3302 spoke_w = std::min( spoke_w, via->GetWidth( aLayer ) );
3303
3304 if( spoke_w < aZone->GetMinThickness() )
3305 continue;
3306 }
3307 else
3308 {
3309 continue;
3310 }
3311 }
3312 else if( pad )
3313 {
3314 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, pad, aZone, aLayer );
3315 thermalReliefGap = constraint.GetValue().Min();
3316
3317 constraint = bds.m_DRCEngine->EvalRules( THERMAL_SPOKE_WIDTH_CONSTRAINT, pad, aZone, aLayer );
3318 spoke_w = constraint.GetValue().Opt();
3319
3320 // Spoke width should ideally be smaller than the pad minor axis.
3321 // Otherwise the thermal shape is not really a thermal relief,
3322 // and the algo to count the actual number of spokes can fail
3323 int spoke_max_allowed_w = std::min( pad->GetSize( aLayer ).x, pad->GetSize( aLayer ).y );
3324
3325 spoke_w = std::clamp( spoke_w, constraint.Value().Min(), constraint.Value().Max() );
3326
3327 // ensure the spoke width is smaller than the pad minor size
3328 spoke_w = std::min( spoke_w, spoke_max_allowed_w );
3329
3330 // Cannot create stubs having a width < zone min thickness
3331 if( spoke_w < aZone->GetMinThickness() )
3332 continue;
3333 }
3334 else
3335 {
3336 // We don't currently support via thermal connections *except* in a hatched zone.
3337 continue;
3338 }
3339
3340 int spoke_half_w = spoke_w / 2;
3341
3342 // Quick test here to possibly save us some work
3343 BOX2I itemBB = item->GetBoundingBox();
3344 itemBB.Inflate( thermalReliefGap + epsilon );
3345
3346 if( !( itemBB.Intersects( zoneBB ) ) )
3347 continue;
3348
3349 bool customSpokes = false;
3350
3351 if( pad && pad->GetShape( aLayer ) == PAD_SHAPE::CUSTOM )
3352 {
3353 for( const std::shared_ptr<PCB_SHAPE>& primitive : pad->GetPrimitives( aLayer ) )
3354 {
3355 if( primitive->IsProxyItem() && primitive->GetShape() == SHAPE_T::SEGMENT )
3356 {
3357 customSpokes = true;
3358 break;
3359 }
3360 }
3361 }
3362
3363 // Thermal spokes consist of square-ended segments from the pad center to points just
3364 // outside the thermal relief. The outside end has an extra center point (which must be
3365 // at idx 3) which is used for testing whether or not the spoke connects to copper in the
3366 // parent zone.
3367
3368 auto buildSpokesFromOrigin =
3369 [&]( const BOX2I& box, EDA_ANGLE angle )
3370 {
3371 VECTOR2I center = box.GetCenter();
3372 VECTOR2I half_size = KiROUND( box.GetWidth() / 2.0, box.GetHeight() / 2.0 );
3373
3374 // Function to find intersection of line with box edge
3375 auto intersectBBox =
3376 [&]( const EDA_ANGLE& spokeAngle, VECTOR2I* spoke_side ) -> VECTOR2I
3377 {
3378 double dx = spokeAngle.Cos();
3379 double dy = spokeAngle.Sin();
3380
3381 // Short-circuit the axis cases because they will be degenerate in the
3382 // intersection test
3383 if( dx == 0 )
3384 {
3385 *spoke_side = VECTOR2I( spoke_half_w, 0 );
3386 return KiROUND( 0.0, dy * half_size.y );
3387 }
3388 else if( dy == 0 )
3389 {
3390 *spoke_side = VECTOR2I( 0, spoke_half_w );
3391 return KiROUND( dx * half_size.x, 0.0 );
3392 }
3393
3394 // We are going to intersect with one side or the other. Whichever
3395 // we hit first is the fraction of the spoke length we keep
3396 double dist_x = half_size.x / std::abs( dx );
3397 double dist_y = half_size.y / std::abs( dy );
3398
3399 if( dist_x < dist_y )
3400 {
3401 *spoke_side = KiROUND( 0.0, spoke_half_w / ( ANGLE_90 - spokeAngle ).Sin() );
3402 return KiROUND( dx * dist_x, dy * dist_x );
3403 }
3404 else
3405 {
3406 *spoke_side = KiROUND( spoke_half_w / spokeAngle.Sin(), 0.0 );
3407 return KiROUND( dx * dist_y, dy * dist_y );
3408 }
3409 };
3410
3411 // Precalculate angles for four cardinal directions
3412 const EDA_ANGLE angles[4] = {
3413 EDA_ANGLE( 0.0, DEGREES_T ) + angle, // Right
3414 EDA_ANGLE( 90.0, DEGREES_T ) + angle, // Up
3415 EDA_ANGLE( 180.0, DEGREES_T ) + angle, // Left
3416 EDA_ANGLE( 270.0, DEGREES_T ) + angle // Down
3417 };
3418
3419 // Generate four spokes in cardinal directions
3420 for( const EDA_ANGLE& spokeAngle : angles )
3421 {
3422 VECTOR2I spoke_side;
3423 VECTOR2I intersection = intersectBBox( spokeAngle, &spoke_side );
3424
3425 SHAPE_LINE_CHAIN spoke;
3426 spoke.Append( center + spoke_side );
3427 spoke.Append( center - spoke_side );
3428 spoke.Append( center + intersection - spoke_side );
3429 spoke.Append( center + intersection ); // test pt
3430 spoke.Append( center + intersection + spoke_side );
3431 spoke.SetClosed( true );
3432 aSpokesList.push_back( std::move( spoke ) );
3433 }
3434 };
3435
3436 if( customSpokes )
3437 {
3438 SHAPE_POLY_SET thermalPoly;
3439 SHAPE_LINE_CHAIN thermalOutline;
3440
3441 pad->TransformShapeToPolygon( thermalPoly, aLayer, thermalReliefGap + epsilon, m_maxError, ERROR_OUTSIDE );
3442
3443 if( thermalPoly.OutlineCount() )
3444 thermalOutline = thermalPoly.Outline( 0 );
3445
3446 SHAPE_LINE_CHAIN padOutline = pad->GetEffectivePolygon( aLayer, ERROR_OUTSIDE )->Outline( 0 );
3447
3448 auto trimToOutline = [&]( SEG& aSegment )
3449 {
3450 SHAPE_LINE_CHAIN::INTERSECTIONS intersections;
3451
3452 if( padOutline.Intersect( aSegment, intersections ) )
3453 {
3454 intersections.clear();
3455
3456 // Trim the segment to the thermal outline
3457 if( thermalOutline.Intersect( aSegment, intersections ) )
3458 {
3459 aSegment.B = intersections.front().p;
3460 return true;
3461 }
3462 }
3463 return false;
3464 };
3465
3466 for( const std::shared_ptr<PCB_SHAPE>& primitive : pad->GetPrimitives( aLayer ) )
3467 {
3468 if( primitive->IsProxyItem() && primitive->GetShape() == SHAPE_T::SEGMENT )
3469 {
3470 SEG seg( primitive->GetStart(), primitive->GetEnd() );
3471 SHAPE_LINE_CHAIN::INTERSECTIONS intersections;
3472
3473 RotatePoint( seg.A, pad->GetOrientation() );
3474 RotatePoint( seg.B, pad->GetOrientation() );
3475 seg.A += pad->ShapePos( aLayer );
3476 seg.B += pad->ShapePos( aLayer );
3477
3478 // Make sure seg.A is the origin
3479 if( !pad->GetEffectivePolygon( aLayer, ERROR_OUTSIDE )->Contains( seg.A ) )
3480 {
3481 // Do not create this spoke if neither point is in the pad.
3482 if( !pad->GetEffectivePolygon( aLayer, ERROR_OUTSIDE )->Contains( seg.B ) )
3483 continue;
3484
3485 seg.Reverse();
3486 }
3487
3488 // Trim segment to pad and thermal outline polygon.
3489 // If there is no intersection with the pad, don't create the spoke.
3490 if( trimToOutline( seg ) )
3491 {
3492 VECTOR2I direction = ( seg.B - seg.A ).Resize( spoke_half_w );
3493 VECTOR2I offset = direction.Perpendicular().Resize( spoke_half_w );
3494 // Extend the spoke edges by half the spoke width to capture convex pad shapes
3495 // with a maximum of 45 degrees.
3496 SEG segL( seg.A - direction - offset, seg.B + direction - offset );
3497 SEG segR( seg.A - direction + offset, seg.B + direction + offset );
3498
3499 // Only create this spoke if both edges intersect the pad and thermal outline
3500 if( trimToOutline( segL ) && trimToOutline( segR ) )
3501 {
3502 // Extend the spoke by the minimum thickness for the zone to ensure full
3503 // connection width
3504 direction = direction.Resize( aZone->GetMinThickness() );
3505
3506 SHAPE_LINE_CHAIN spoke;
3507
3508 spoke.Append( seg.A + offset );
3509 spoke.Append( seg.A - offset );
3510
3511 spoke.Append( segL.B + direction );
3512 spoke.Append( seg.B + direction ); // test pt at index 3.
3513 spoke.Append( segR.B + direction );
3514
3515 spoke.SetClosed( true );
3516 aSpokesList.push_back( std::move( spoke ) );
3517 }
3518 }
3519 }
3520 }
3521 }
3522 else
3523 {
3524 EDA_ANGLE thermalSpokeAngle;
3525
3526 // Use pad's thermal spoke angle for both solid and hatch zones.
3527 // This ensures custom thermal spoke templates are respected.
3528 if( pad )
3529 thermalSpokeAngle = pad->GetThermalSpokeAngle();
3530
3531 BOX2I spokesBox;
3532 VECTOR2I position;
3533 EDA_ANGLE orientation;
3534
3535 // Since the bounding-box needs to be correclty rotated we use a dummy pad to keep
3536 // from dirtying the real pad's cached shapes.
3537 if( pad )
3538 {
3539 PAD dummy_pad( *pad );
3540 dummy_pad.SetOrientation( ANGLE_0 );
3541
3542 // Spokes are from center of pad shape, not from hole. So the dummy pad has no shape
3543 // offset and is at position 0,0
3544 dummy_pad.SetPosition( VECTOR2I( 0, 0 ) );
3545 dummy_pad.SetOffset( aLayer, VECTOR2I( 0, 0 ) );
3546
3547 spokesBox = dummy_pad.GetBoundingBox( aLayer );
3548 position = pad->ShapePos( aLayer );
3549 orientation = pad->GetOrientation();
3550 }
3551 else if( via )
3552 {
3553 PCB_VIA dummy_via( *via );
3554 dummy_via.SetPosition( VECTOR2I( 0, 0 ) );
3555
3556 spokesBox = dummy_via.GetBoundingBox( aLayer );
3557 position = via->GetPosition();
3558 }
3559
3560 // Add half the zone mininum width to the inflate amount to account for the fact that
3561 // the deflation procedure will shrink the results by half the half the zone min width.
3562 spokesBox.Inflate( thermalReliefGap + epsilon + zone_half_width );
3563
3564 // Yet another wrinkle: the bounding box for circles will overshoot the mark considerably
3565 // when the spokes are near a 45 degree increment. So we build the spokes at 0 degrees
3566 // and then rotate them to the correct position.
3567 if( circular )
3568 {
3569 buildSpokesFromOrigin( spokesBox, ANGLE_0 );
3570
3571 if( thermalSpokeAngle != ANGLE_0 )
3572 {
3573 // Rotate the last four elements of aspokeslist
3574 for( auto it = aSpokesList.rbegin(); it != aSpokesList.rbegin() + 4; ++it )
3575 it->Rotate( thermalSpokeAngle );
3576 }
3577 }
3578 else
3579 {
3580 buildSpokesFromOrigin( spokesBox, thermalSpokeAngle );
3581 }
3582
3583 auto spokeIter = aSpokesList.rbegin();
3584
3585 for( int ii = 0; ii < 4; ++ii, ++spokeIter )
3586 {
3587 spokeIter->Rotate( orientation );
3588 spokeIter->Move( position );
3589 }
3590 }
3591 }
3592
3593 for( size_t ii = 0; ii < aSpokesList.size(); ++ii )
3594 aSpokesList[ii].GenerateBBoxCache();
3595}
3596
3597
3599 const SHAPE_POLY_SET& aSmoothedOutline,
3600 const std::vector<BOARD_ITEM*>& aThermalConnectionPads,
3601 SHAPE_POLY_SET& aFillPolys,
3602 SHAPE_POLY_SET& aThermalRings )
3603{
3604 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
3605 DRC_CONSTRAINT constraint;
3606
3607 for( BOARD_ITEM* item : aThermalConnectionPads )
3608 {
3609 if( !item->IsOnLayer( aLayer ) )
3610 continue;
3611
3612 PAD* pad = nullptr;
3613 PCB_VIA* via = nullptr;
3614 bool isCircular = false;
3615 int thermalGap = 0;
3616 int spokeWidth = 0;
3617 VECTOR2I position;
3618 int padRadius = 0;
3619
3620 if( item->Type() == PCB_PAD_T )
3621 {
3622 pad = static_cast<PAD*>( item );
3623 VECTOR2I padSize = pad->GetSize( aLayer );
3624 position = pad->ShapePos( aLayer );
3625
3626 isCircular = ( pad->GetShape( aLayer ) == PAD_SHAPE::CIRCLE
3627 || ( pad->GetShape( aLayer ) == PAD_SHAPE::OVAL && padSize.x == padSize.y ) );
3628
3629 if( isCircular )
3630 padRadius = std::max( padSize.x, padSize.y ) / 2;
3631
3632 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, pad, aZone, aLayer );
3633 thermalGap = constraint.GetValue().Min();
3634
3635 constraint = bds.m_DRCEngine->EvalRules( THERMAL_SPOKE_WIDTH_CONSTRAINT, pad, aZone, aLayer );
3636 spokeWidth = constraint.GetValue().Opt();
3637
3638 // Clamp spoke width to pad size
3639 int spokeMaxWidth = std::min( padSize.x, padSize.y );
3640 spokeWidth = std::min( spokeWidth, spokeMaxWidth );
3641 }
3642 else if( item->Type() == PCB_VIA_T )
3643 {
3644 via = static_cast<PCB_VIA*>( item );
3645 position = via->GetPosition();
3646 isCircular = true;
3647 padRadius = via->GetWidth( aLayer ) / 2;
3648
3649 constraint = bds.m_DRCEngine->EvalRules( THERMAL_RELIEF_GAP_CONSTRAINT, via, aZone, aLayer );
3650 thermalGap = constraint.GetValue().Min();
3651
3652 constraint = bds.m_DRCEngine->EvalRules( THERMAL_SPOKE_WIDTH_CONSTRAINT, via, aZone, aLayer );
3653 spokeWidth = constraint.GetValue().Opt();
3654
3655 // Clamp spoke width to via diameter
3656 spokeWidth = std::min( spokeWidth, padRadius * 2 );
3657 }
3658 else
3659 {
3660 continue;
3661 }
3662
3663 // Don't create a ring if spoke width is too small
3664 if( spokeWidth < aZone->GetMinThickness() )
3665 continue;
3666
3667 SHAPE_POLY_SET thermalRing;
3668
3669 if( isCircular )
3670 {
3671 // For circular pads/vias: create an arc ring
3672 // Ring inner radius = pad radius + thermal gap
3673 // Ring width = spoke width
3674 int ringInnerRadius = padRadius + thermalGap;
3675 int ringWidth = spokeWidth;
3676
3677 TransformRingToPolygon( thermalRing, position, ringInnerRadius + ringWidth / 2,
3678 ringWidth, m_maxError, ERROR_OUTSIDE );
3679 }
3680 else
3681 {
3682 // For non-circular pads: create ring by inflating pad to outer radius,
3683 // then subtracting pad inflated to inner radius
3684 SHAPE_POLY_SET outerShape;
3685 SHAPE_POLY_SET innerShape;
3686
3687 // Outer ring edge = pad + thermal gap + spoke width
3688 pad->TransformShapeToPolygon( outerShape, aLayer, thermalGap + spokeWidth,
3690
3691 // Inner ring edge = pad + thermal gap (this is already knocked out)
3692 pad->TransformShapeToPolygon( innerShape, aLayer, thermalGap,
3694
3695 thermalRing = outerShape;
3696 thermalRing.BooleanSubtract( innerShape );
3697 }
3698
3699 // Clip the thermal ring to the zone boundary so it doesn't overflow
3700 thermalRing.BooleanIntersection( aSmoothedOutline );
3701
3702 // Add the thermal ring to the fill
3703 aFillPolys.BooleanAdd( thermalRing );
3704
3705 // Also collect thermal rings for hatch hole notching to ensure connectivity
3706 aThermalRings.BooleanAdd( thermalRing );
3707 }
3708}
3709
3710
3712 SHAPE_POLY_SET& aFillPolys )
3713{
3714 wxCHECK( aZone->IsCopperThieving(), false );
3715
3716 const THIEVING_SETTINGS& settings = aZone->GetThievingSettings();
3717
3718 // Constructor defaults are positive but a malformed file or test board could still
3719 // produce a zero gap, which would deadlock the grid loop below. Bail out without
3720 // touching aFillPolys so the zone simply has no fill, matching POLYGONS-with-bad-poly.
3721 // element_size is meaningful for dots and squares only. Hatch uses line_width.
3722 const bool needsElementSize = ( settings.pattern != THIEVING_PATTERN::HATCH );
3723 const bool needsLineWidth = ( settings.pattern == THIEVING_PATTERN::HATCH );
3724
3725 if( settings.gap <= 0
3726 || ( needsElementSize && settings.element_size <= 0 )
3727 || ( needsLineWidth && settings.line_width <= 0 ) )
3728 {
3729 aFillPolys.RemoveAllContours();
3730 return true;
3731 }
3732
3733 SHAPE_POLY_SET filledRegion = aFillPolys.CloneDropTriangulation();
3734
3735 if( filledRegion.OutlineCount() == 0 )
3736 {
3737 aFillPolys.RemoveAllContours();
3738 return true;
3739 }
3740
3741 // Rotate the clip region into the pattern's local frame so the grid iterates
3742 // axis-aligned; the resulting stamps get rotated back into the zone's frame below.
3743 if( !settings.orientation.IsZero() )
3744 filledRegion.Rotate( -settings.orientation );
3745
3746 // BBox() over all outlines — the post-clearance fill region may be split
3747 // into several pieces (e.g. by a track cutting across the zone) and the
3748 // void grid has to cover every piece.
3749 BOX2I bbox = filledRegion.BBox();
3750
3751 // Per-layer phase offset (hatching_offset) — same lookup the hatch generator uses
3752 // so thieving on multiple copper layers can be de-correlated through the stack-up.
3753 // Board-default offsets apply first; per-zone local offsets override.
3754 const auto& defaultOffsets = m_board->GetDesignSettings().m_ZoneLayerProperties;
3755 const auto& localOffsets = aZone->LayerProperties();
3756 VECTOR2I offset;
3757
3758 if( auto it = defaultOffsets.find( aLayer ); it != defaultOffsets.end() )
3759 offset = it->second.hatching_offset.value_or( VECTOR2I() );
3760
3761 if( localOffsets.contains( aLayer ) && localOffsets.at( aLayer ).hatching_offset.has_value() )
3762 offset = localOffsets.at( aLayer ).hatching_offset.value();
3763
3764 if( !settings.orientation.IsZero() )
3765 RotatePoint( offset, -settings.orientation );
3766
3767 // Gap is edge-to-edge; grid stride is element_size + gap (dots/squares) or
3768 // line_width + gap (crosshatch).
3769 const int dotStride = settings.element_size + settings.gap;
3770
3771 // The filler stamps thieving shapes while aFillPolys is deflated by
3772 // half_min_width and then later re-inflates by the same amount. Pre-compensate
3773 // the dot radius so the final stamp matches element_size exactly. If the
3774 // user's element_size is smaller than min_thickness, fall back to a 1 IU
3775 // radius so the reinflate produces approximately min_thickness diameter.
3776 const int halfMinWidth = aZone->GetMinThickness() / 2;
3777 const int dotRadius = std::max( settings.element_size / 2 - halfMinWidth, 1 );
3778 const int maxError = m_board->GetDesignSettings().m_MaxError;
3779
3780 // Collect every stamp into a single SHAPE_POLY_SET, then BooleanIntersect once.
3781 // Per-stamp boolean ops would explode in cost on a 10k-dot zone.
3782 SHAPE_POLY_SET stamps;
3783
3784 int xStart = bbox.GetLeft() - ( bbox.GetLeft() % dotStride ) + offset.x;
3785 int yStart = bbox.GetTop() - ( bbox.GetTop() % dotStride ) + offset.y;
3786
3787 while( xStart > bbox.GetLeft() )
3788 xStart -= dotStride;
3789
3790 while( yStart > bbox.GetTop() )
3791 yStart -= dotStride;
3792
3793 // Hatch is subtractive: keep the zone outline as a perimeter border around
3794 // the mesh by carving voids out of aFillPolys. Dots and squares are
3795 // additive: replace aFillPolys with the stamp set, clipped to the zone.
3796 if( settings.pattern == THIEVING_PATTERN::HATCH )
3797 {
3798 // Void size in the deflated frame is gap + min_thickness so that the
3799 // generic reinflate at the end of fillCopperZone shrinks the void by
3800 // min_thickness and the final edge-to-edge spacing equals user gap.
3801 const int voidSize = settings.gap + aZone->GetMinThickness();
3802 const int lineStride = settings.line_width + settings.gap;
3803
3804 // Deflate aFillPolys by line_width to define an interior region that
3805 // can receive voids. The unaltered annulus between aFillPolys and
3806 // interior becomes the perimeter outline of the mesh, matching how
3807 // the existing HATCH_PATTERN fill mode produces a border. This also
3808 // protects narrow post-clearance fragments (e.g. a thin strip on the
3809 // opposite side of a track) from being entirely consumed by voids.
3810 SHAPE_POLY_SET interior = aFillPolys.CloneDropTriangulation();
3812
3813 if( interior.OutlineCount() == 0 )
3814 return true;
3815
3816 // Walk a starting position backwards into the bbox so we never miss a
3817 // void on the negative side after the modulo step. bbox already
3818 // contains the rotated filledRegion bounds, which slightly overcover
3819 // the interior; extra voids get clipped to interior below.
3820 int xVoid = bbox.GetLeft() - ( bbox.GetLeft() % lineStride ) + offset.x
3821 + lineStride / 2;
3822 int yVoid = bbox.GetTop() - ( bbox.GetTop() % lineStride ) + offset.y
3823 + lineStride / 2;
3824
3825 while( xVoid - voidSize / 2 > bbox.GetLeft() )
3826 xVoid -= lineStride;
3827
3828 while( yVoid - voidSize / 2 > bbox.GetTop() )
3829 yVoid -= lineStride;
3830
3831 SHAPE_POLY_SET voids;
3832
3833 for( int yy = yVoid; yy <= bbox.GetBottom() + voidSize; yy += lineStride )
3834 {
3835 for( int xx = xVoid; xx <= bbox.GetRight() + voidSize; xx += lineStride )
3836 {
3837 SHAPE_LINE_CHAIN rect;
3838 rect.Append( xx - voidSize / 2, yy - voidSize / 2 );
3839 rect.Append( xx + voidSize / 2, yy - voidSize / 2 );
3840 rect.Append( xx + voidSize / 2, yy + voidSize / 2 );
3841 rect.Append( xx - voidSize / 2, yy + voidSize / 2 );
3842 rect.SetClosed( true );
3843 voids.AddOutline( rect );
3844 }
3845 }
3846
3847 if( !settings.orientation.IsZero() )
3848 voids.Rotate( settings.orientation );
3849
3850 // Clip voids to interior so the perimeter border survives the
3851 // subtraction. Without this clamp, voids on the edge punch through
3852 // the border, and narrow post-clearance pieces of aFillPolys are
3853 // consumed entirely.
3854 voids.BooleanIntersection( interior );
3855
3856 // Carve the voids out of the zone fill region. No island removal: the
3857 // hatch mesh is a single connected piece with its zone-outline border.
3858 aFillPolys.BooleanSubtract( voids );
3859 return true;
3860 }
3861
3862 // Dots and squares: drop any stamp transected by an obstacle or touching the
3863 // zone outline. Deflating the fill region by stampHalfExtent + 1 IU yields
3864 // the set of centres where a full stamp fits without touching the boundary.
3865 const int sideLen = std::max( settings.element_size - aZone->GetMinThickness(), 1 );
3866 const VECTOR2I squareSize( sideLen, sideLen );
3867
3868 const int containmentInset =
3869 ( ( settings.pattern == THIEVING_PATTERN::SQUARES ) ? sideLen / 2 : dotRadius ) + 1;
3870
3871 filledRegion.Deflate( containmentInset, CORNER_STRATEGY::CHAMFER_ALL_CORNERS, maxError );
3872
3873 if( filledRegion.OutlineCount() == 0 )
3874 {
3875 aFillPolys.RemoveAllContours();
3876 return true;
3877 }
3878
3879 filledRegion.BuildBBoxCaches();
3880
3881 int rowIndex = 0;
3882
3883 for( int yy = yStart; yy <= bbox.GetBottom() + dotRadius; yy += dotStride )
3884 {
3885 const int rowOffset = ( settings.stagger && ( rowIndex & 1 ) ) ? dotStride / 2 : 0;
3886
3887 for( int xx = xStart + rowOffset; xx <= bbox.GetRight() + dotRadius; xx += dotStride )
3888 {
3889 VECTOR2I centre( xx, yy );
3890
3891 if( !filledRegion.Contains( centre, -1, 0, true ) )
3892 continue;
3893
3894 if( settings.pattern == THIEVING_PATTERN::SQUARES )
3895 {
3896 TransformTrapezoidToPolygon( stamps, centre, squareSize, ANGLE_0, 0, 0, 0,
3897 maxError, ERROR_OUTSIDE );
3898 }
3899 else
3900 {
3901 TransformCircleToPolygon( stamps, centre, dotRadius, maxError, ERROR_OUTSIDE );
3902 }
3903 }
3904
3905 ++rowIndex;
3906 }
3907
3908 if( !settings.orientation.IsZero() )
3909 stamps.Rotate( settings.orientation );
3910
3911 aFillPolys = stamps;
3912 return true;
3913}
3914
3915
3917 PCB_LAYER_ID aDebugLayer, SHAPE_POLY_SET& aFillPolys,
3918 const SHAPE_POLY_SET& aThermalRings )
3919{
3920 // Build grid:
3921
3922 // obviously line thickness must be > zone min thickness.
3923 // It can happens if a board file was edited by hand by a python script
3924 // Use 1 micron margin to be *sure* there is no issue in Gerber files
3925 // (Gbr file unit = 1 or 10 nm) due to some truncation in coordinates or calculations
3926 // This margin also avoid problems due to rounding coordinates in next calculations
3927 // that can create incorrect polygons
3928 int thickness = std::max( aZone->GetHatchThickness(),
3929 aZone->GetMinThickness() + pcbIUScale.mmToIU( 0.001 ) );
3930
3931 int gridsize = thickness + aZone->GetHatchGap();
3932 int maxError = m_board->GetDesignSettings().m_MaxError;
3933
3934 SHAPE_POLY_SET filledPolys = aFillPolys.CloneDropTriangulation();
3935 // Use a area that contains the rotated bbox by orientation, and after rotate the result
3936 // by -orientation.
3937 if( !aZone->GetHatchOrientation().IsZero() )
3938 filledPolys.Rotate( - aZone->GetHatchOrientation() );
3939
3940 BOX2I bbox = filledPolys.BBox( 0 );
3941
3942 // Build hole shape
3943 // the hole size is aZone->GetHatchGap(), but because the outline thickness
3944 // is aZone->GetMinThickness(), the hole shape size must be larger
3945 SHAPE_LINE_CHAIN hole_base;
3946 int hole_size = aZone->GetHatchGap() + aZone->GetMinThickness();
3947 VECTOR2I corner( 0, 0 );;
3948 hole_base.Append( corner );
3949 corner.x += hole_size;
3950 hole_base.Append( corner );
3951 corner.y += hole_size;
3952 hole_base.Append( corner );
3953 corner.x = 0;
3954 hole_base.Append( corner );
3955 hole_base.SetClosed( true );
3956
3957 // Calculate minimal area of a grid hole.
3958 // All holes smaller than a threshold will be removed
3959 double minimal_hole_area = hole_base.Area() * aZone->GetHatchHoleMinArea();
3960
3961 // Now convert this hole to a smoothed shape:
3962 if( aZone->GetHatchSmoothingLevel() > 0 )
3963 {
3964 // the actual size of chamfer, or rounded corner radius is the half size
3965 // of the HatchFillTypeGap scaled by aZone->GetHatchSmoothingValue()
3966 // aZone->GetHatchSmoothingValue() = 1.0 is the max value for the chamfer or the
3967 // radius of corner (radius = half size of the hole)
3968 int smooth_value = KiROUND( aZone->GetHatchGap()
3969 * aZone->GetHatchSmoothingValue() / 2 );
3970
3971 // Minimal optimization:
3972 // make smoothing only for reasonable smooth values, to avoid a lot of useless segments
3973 // and if the smooth value is small, use chamfer even if fillet is requested
3974 #define SMOOTH_MIN_VAL_MM 0.02
3975 #define SMOOTH_SMALL_VAL_MM 0.04
3976
3977 if( smooth_value > pcbIUScale.mmToIU( SMOOTH_MIN_VAL_MM ) )
3978 {
3979 SHAPE_POLY_SET smooth_hole;
3980 smooth_hole.AddOutline( hole_base );
3981 int smooth_level = aZone->GetHatchSmoothingLevel();
3982
3983 if( smooth_value < pcbIUScale.mmToIU( SMOOTH_SMALL_VAL_MM ) && smooth_level > 1 )
3984 smooth_level = 1;
3985
3986 // Use a larger smooth_value to compensate the outline tickness
3987 // (chamfer is not visible is smooth value < outline thickess)
3988 smooth_value += aZone->GetMinThickness() / 2;
3989
3990 // smooth_value cannot be bigger than the half size oh the hole:
3991 smooth_value = std::min( smooth_value, aZone->GetHatchGap() / 2 );
3992
3993 // the error to approximate a circle by segments when smoothing corners by a arc
3994 maxError = std::max( maxError * 2, smooth_value / 20 );
3995
3996 switch( smooth_level )
3997 {
3998 case 1:
3999 // Chamfer() uses the distance from a corner to create a end point
4000 // for the chamfer.
4001 hole_base = smooth_hole.Chamfer( smooth_value ).Outline( 0 );
4002 break;
4003
4004 default:
4005 if( aZone->GetHatchSmoothingLevel() > 2 )
4006 maxError /= 2; // Force better smoothing
4007
4008 hole_base = smooth_hole.Fillet( smooth_value, maxError ).Outline( 0 );
4009 break;
4010
4011 case 0:
4012 break;
4013 };
4014 }
4015 }
4016
4017 // Build holes
4018 SHAPE_POLY_SET holes;
4019
4020 const auto& defaultOffsets = m_board->GetDesignSettings().m_ZoneLayerProperties;
4021 const auto& localOffsets = aZone->LayerProperties();
4022
4023 VECTOR2I offset;
4024
4025 if( auto it = defaultOffsets.find( aLayer ); it != defaultOffsets.end() )
4026 offset = it->second.hatching_offset.value_or( VECTOR2I() );
4027
4028 if( localOffsets.contains( aLayer ) && localOffsets.at( aLayer ).hatching_offset.has_value() )
4029 offset = localOffsets.at( aLayer ).hatching_offset.value();
4030
4031 int x_offset = bbox.GetX() - ( bbox.GetX() ) % gridsize - gridsize;
4032 int y_offset = bbox.GetY() - ( bbox.GetY() ) % gridsize - gridsize;
4033
4034
4035 for( int xx = x_offset; xx <= bbox.GetRight(); xx += gridsize )
4036 {
4037 for( int yy = y_offset; yy <= bbox.GetBottom(); yy += gridsize )
4038 {
4039 // Generate hole
4040 SHAPE_LINE_CHAIN hole( hole_base );
4041 hole.Move( VECTOR2I( xx, yy ) );
4042
4043 if( !aZone->GetHatchOrientation().IsZero() )
4044 {
4045 hole.Rotate( aZone->GetHatchOrientation() );
4046 }
4047
4048 hole.Move( VECTOR2I( offset.x % gridsize, offset.y % gridsize ) );
4049
4050 holes.AddOutline( hole );
4051 }
4052 }
4053
4054 holes.ClearArcs();
4055
4056 DUMP_POLYS_TO_COPPER_LAYER( holes, In10_Cu, wxT( "hatch-holes" ) );
4057
4058 int deflated_thickness = aZone->GetHatchThickness() - aZone->GetMinThickness();
4059
4060 // Don't let thickness drop below maxError * 2 or it might not get reinflated.
4061 deflated_thickness = std::max( deflated_thickness, maxError * 2 );
4062
4063 // The fill has already been deflated to ensure GetMinThickness() so we just have to
4064 // account for anything beyond that.
4065 SHAPE_POLY_SET deflatedFilledPolys = aFillPolys.CloneDropTriangulation();
4066 deflatedFilledPolys.ClearArcs();
4067 deflatedFilledPolys.Deflate( deflated_thickness, CORNER_STRATEGY::CHAMFER_ALL_CORNERS, maxError );
4068 holes.BooleanIntersection( deflatedFilledPolys );
4069 DUMP_POLYS_TO_COPPER_LAYER( holes, In11_Cu, wxT( "fill-clipped-hatch-holes" ) );
4070
4071 SHAPE_POLY_SET deflatedOutline = aZone->Outline()->CloneDropTriangulation();
4072 deflatedOutline.ClearArcs();
4073 deflatedOutline.Deflate( aZone->GetMinThickness(), CORNER_STRATEGY::CHAMFER_ALL_CORNERS, maxError );
4074 holes.BooleanIntersection( deflatedOutline );
4075 DUMP_POLYS_TO_COPPER_LAYER( holes, In12_Cu, wxT( "outline-clipped-hatch-holes" ) );
4076
4077 // Now filter truncated holes to avoid small holes in pattern
4078 // It happens for holes near the zone outline
4079 for( int ii = 0; ii < holes.OutlineCount(); )
4080 {
4081 double area = holes.Outline( ii ).Area();
4082
4083 if( area < minimal_hole_area ) // The current hole is too small: remove it
4084 holes.DeletePolygon( ii );
4085 else
4086 ++ii;
4087 }
4088
4089 // Drop any holes that completely enclose a thermal ring to ensure thermal reliefs
4090 // stay connected to the hatch webbing. Only drop holes where the thermal ring is
4091 // entirely inside the hole; partial overlaps are kept to preserve the hatch pattern.
4092 if( aThermalRings.OutlineCount() > 0 )
4093 {
4094 BOX2I thermalBBox = aThermalRings.BBox();
4095
4096 // Iterate through holes (backwards since we may delete)
4097 for( int holeIdx = holes.OutlineCount() - 1; holeIdx >= 0; holeIdx-- )
4098 {
4099 const SHAPE_LINE_CHAIN& hole = holes.Outline( holeIdx );
4100 BOX2I holeBBox = hole.BBox();
4101
4102 // Quick rejection: skip if hole bbox doesn't intersect thermal rings bbox
4103 if( !holeBBox.Intersects( thermalBBox ) )
4104 continue;
4105
4106 // Check if ANY thermal ring is completely enclosed by this hole
4107 for( int ringIdx = 0; ringIdx < aThermalRings.OutlineCount(); ringIdx++ )
4108 {
4109 const SHAPE_LINE_CHAIN& ring = aThermalRings.Outline( ringIdx );
4110 BOX2I ringBBox = ring.BBox();
4111 VECTOR2I ringCenter = ringBBox.Centre();
4112
4113 // Quick rejection: hole bbox must contain ring bbox
4114 if( !holeBBox.Contains( ringBBox ) )
4115 continue;
4116
4117 // Check 1: Is the ring center inside the hole?
4118 if( !hole.PointInside( ringCenter ) )
4119 continue;
4120
4121 // Check 2: Is at least one point on the ring inside the hole?
4122 if( ring.PointCount() == 0 || !hole.PointInside( ring.CPoint( 0 ) ) )
4123 continue;
4124
4125 // Check 3: Does the ring outline NOT intersect the hole outline?
4126 // If there's no intersection, the ring is fully enclosed (not touching edges)
4127 SHAPE_LINE_CHAIN::INTERSECTIONS intersections;
4128 ring.Intersect( hole, intersections );
4129
4130 if( intersections.empty() )
4131 {
4132 // This hole completely encloses a ring - drop it
4133 holes.DeletePolygon( holeIdx );
4134 break; // Move to next hole
4135 }
4136 }
4137 }
4138 }
4139
4140 // create grid. Useto
4141 // generate strictly simple polygons needed by Gerber files and Fracture()
4142 aFillPolys.BooleanSubtract( aFillPolys, holes );
4143 DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In14_Cu, wxT( "after-hatching" ) );
4144
4145 return true;
4146}
4147
4148
4150 const FillSnapshot* aSnapshot )
4151{
4152 auto cacheKey = std::make_pair( static_cast<const ZONE*>( aZone ), aLayer );
4153
4154 {
4155 std::lock_guard<std::mutex> lock( m_cacheMutex );
4156 auto it = m_preKnockoutFillCache.find( cacheKey );
4157
4158 if( it == m_preKnockoutFillCache.end() )
4159 return false;
4160
4161 // Restore the cached pre-knockout fill
4162 aFillPolys = it->second;
4163 }
4164
4165 // Subtract the FILLED area of higher-priority zones (with clearance for different nets).
4166 // For same-net zones: subtract the filled area directly.
4167 // For different-net zones: subtract the filled area with DRC-evaluated clearance plus
4168 // extra_margin and m_maxError to match the margins used in the initial fill. Without these
4169 // margins, polygon approximation error can produce fills that violate clearance (issue 23053).
4170 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
4171 int extra_margin = pcbIUScale.mmToIU( ADVANCED_CFG::GetCfg().m_ExtraClearance );
4172 BOX2I zoneBBox = aZone->GetBoundingBox();
4173 zoneBBox.Inflate( m_worstClearance + extra_margin );
4174
4175 auto evalRulesForItems =
4176 [&bds]( DRC_CONSTRAINT_T aConstraint, const BOARD_ITEM* a, const BOARD_ITEM* b,
4177 PCB_LAYER_ID aEvalLayer ) -> int
4178 {
4179 DRC_CONSTRAINT c = bds.m_DRCEngine->EvalRules( aConstraint, a, b, aEvalLayer );
4180
4181 if( c.IsNull() )
4182 return -1;
4183 else
4184 return c.GetValue().Min();
4185 };
4186
4187 bool knockoutsApplied = false;
4188 SHAPE_POLY_SET diffNetKnockouts;
4189 SHAPE_POLY_SET sameNetKnockouts;
4190
4191 auto collectZoneKnockout =
4192 [&]( ZONE* otherZone )
4193 {
4194 if( otherZone == aZone )
4195 return;
4196
4197 if( !otherZone->GetLayerSet().test( aLayer ) )
4198 return;
4199
4200 if( otherZone->IsTeardropArea() && otherZone->SameNet( aZone ) )
4201 return;
4202
4203 if( !otherZone->HigherPriority( aZone ) )
4204 return;
4205
4206 if( !otherZone->GetBoundingBox().Intersects( zoneBBox ) )
4207 return;
4208
4209 // Resolve the fill to use: from the snapshot when provided, otherwise the live fill.
4210 // The snapshot ensures all parallel tasks in a wave read a consistent pre-wave state
4211 // so no task can block another by writing a larger fill first.
4212 const SHAPE_POLY_SET* fillPtr = nullptr;
4213 std::shared_ptr<SHAPE_POLY_SET> fillShared; // keeps live fill shared_ptr alive
4214
4215 if( aSnapshot )
4216 {
4217 auto it = aSnapshot->find( { static_cast<const ZONE*>( otherZone ), aLayer } );
4218
4219 if( it == aSnapshot->end() )
4220 return; // not filled at snapshot time; skip
4221
4222 fillPtr = &it->second;
4223 }
4224 else
4225 {
4226 if( !otherZone->HasFilledPolysForLayer( aLayer ) )
4227 return;
4228
4229 fillShared = otherZone->GetFilledPolysList( aLayer );
4230
4231 if( !fillShared )
4232 return;
4233
4234 fillPtr = fillShared.get();
4235 }
4236
4237 if( fillPtr->OutlineCount() == 0 )
4238 return;
4239
4240 if( otherZone->SameNet( aZone ) )
4241 {
4242 sameNetKnockouts.Append( *fillPtr );
4243 }
4244 else
4245 {
4246 int gap = std::max( 0, evalRulesForItems( PHYSICAL_CLEARANCE_CONSTRAINT,
4247 aZone, otherZone, aLayer ) );
4248
4249 gap = std::max( gap, evalRulesForItems( CLEARANCE_CONSTRAINT, aZone,
4250 otherZone, aLayer ) );
4251
4252 if( gap < 0 )
4253 return;
4254
4255 SHAPE_POLY_SET inflatedFill = *fillPtr;
4256 inflatedFill.Inflate( gap + extra_margin + m_maxError,
4258 diffNetKnockouts.Append( inflatedFill );
4259 knockoutsApplied = true;
4260 }
4261 };
4262
4263 forEachBoardAndFootprintZone( m_board, collectZoneKnockout );
4264
4265 // Keepout zones are not collected here because they are already baked into the cached
4266 // pre-knockout fill. They were subtracted before the initial deflate/inflate min-width
4267 // cycle so the cached fill already reflects keepout boundaries (issue 23515).
4268
4269 // Subtract different-net knockouts first, then re-prune min-width
4270 // violations BEFORE subtracting same-net knockouts. The fill still extends into
4271 // overlapping same-net zone areas at this point, which provides a natural buffer
4272 // that prevents the deflate/inflate cycle from creating divots at same-net
4273 // zone boundaries.
4274 if( diffNetKnockouts.OutlineCount() > 0 )
4275 aFillPolys.BooleanSubtract( diffNetKnockouts );
4276
4277 if( knockoutsApplied )
4278 postKnockoutMinWidthPrune( aZone, aFillPolys );
4279
4280 if( sameNetKnockouts.OutlineCount() > 0 )
4281 aFillPolys.BooleanSubtract( sameNetKnockouts );
4282
4283 aFillPolys.Fracture();
4284
4285 return true;
4286}
int index
@ ERROR_OUTSIDE
@ ERROR_INSIDE
bool operator==(const wxAuiPaneInfo &aLhs, const wxAuiPaneInfo &aRhs)
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:125
@ ZLO_FORCE_NO_ZONE_CONNECTION
Definition board_item.h:75
@ ZLO_FORCE_FLASHED
Definition board_item.h:74
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:990
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
BASE_SET & set(size_t pos)
Definition base_set.h:116
Container for design settings for a BOARD object.
std::shared_ptr< DRC_ENGINE > m_DRCEngine
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
virtual void TransformShapeToPolygon(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aError, ERROR_LOC aErrorLoc, bool ignoreLineWidth=false) const
Convert the item shape to a closed polygon.
virtual void SetIsKnockout(bool aKnockout)
Definition board_item.h:356
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:323
const ZONES & Zones() const
Definition board.h:368
int GetCopperLayerCount() const
Definition board.cpp:937
const FOOTPRINTS & Footprints() const
Definition board.h:364
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1101
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 coord_type GetLeft() const
Definition box2.h:228
constexpr bool Contains(const Vec &aPoint) const
Definition box2.h:168
constexpr coord_type GetRight() const
Definition box2.h:217
constexpr coord_type GetTop() const
Definition box2.h:229
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:201
const MINOPTMAX< int > & GetValue() const
Definition drc_rule.h:200
ZONE_CONNECTION m_ZoneConnection
Definition drc_rule.h:246
bool IsNull() const
Definition drc_rule.h:195
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:112
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 const 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:366
DRILL_PROPS & Drill()
Definition padstack.h:351
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:1354
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:1451
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:2592
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:615
void SetPosition(const VECTOR2I &aPoint) override
Definition pcb_track.h:558
const BOX2I GetBoundingBox() const override
Return the orthogonal bounding box of this object for display purposes.
Y-stripe spatial index for efficient point-in-polygon containment testing.
bool Contains(const VECTOR2I &aPt, int aAccuracy=0) const
Test whether a point is inside the indexed polygon set.
void Build(const SHAPE_POLY_SET &aPolySet)
Build the spatial index from a SHAPE_POLY_SET's outlines and holes.
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
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.
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.
void Insert(size_t aVertex, const VECTOR2I &aP)
bool PointInside(const VECTOR2I &aPt, int aAccuracy=0, bool aUseBBoxCache=false) const override
Check if point aP lies inside a closed shape.
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.
void RemoveAllContours()
Remove all outlines & holes (clears) the polygon set.
SHAPE_POLY_SET Chamfer(int aDistance)
Return a chamfered version of the polygon set.
void BooleanAdd(const SHAPE_POLY_SET &b)
Perform boolean polyset union.
void ClearArcs()
Removes all arc references from all the outlines and holes in the polyset.
int AddOutline(const SHAPE_LINE_CHAIN &aOutline)
Adds a new outline to the set and returns its index.
void DeletePolygon(int aIdx)
Delete aIdx-th polygon from the set.
double Area()
Return the area of this poly set.
bool Collide(const SHAPE *aShape, int aClearance=0, int *aActual=nullptr, VECTOR2I *aLocation=nullptr) const override
Check if the boundary of shape (this) lies closer to the shape aShape than aClearance,...
POLYGON & Polygon(int aIndex)
Return the aIndex-th subpolygon in the set.
void Inflate(int aAmount, CORNER_STRATEGY aCornerStrategy, int aMaxError, bool aSimplify=false)
Perform outline inflation/deflation.
int Append(int x, int y, int aOutline=-1, int aHole=-1, bool aAllowDuplication=false)
Appends a vertex at the end of the given outline/hole (default: the last outline)
void Simplify()
Simplify the polyset (merges overlapping polys, eliminates degeneracy/self-intersections)
int ArcCount() const
Count the number of arc shapes present.
SHAPE_LINE_CHAIN & Outline(int aIndex)
Return the reference to aIndex-th outline in the set.
int NewOutline()
Creates a new empty polygon in the set and returns its index.
void Deflate(int aAmount, CORNER_STRATEGY aCornerStrategy, int aMaxError)
void BooleanIntersection(const SHAPE_POLY_SET &b)
Perform boolean polyset intersection.
void BuildBBoxCaches() const
Construct BBoxCaches for Contains(), below.
int OutlineCount() const
Return the number of outlines in the set.
SHAPE_POLY_SET Fillet(int aRadius, int aErrorMax)
Return a filleted version of the polygon set.
void Fracture(bool aSimplify=true)
Convert a set of polygons with holes to a single outline with "slits"/"fractures" connecting the oute...
bool Contains(const VECTOR2I &aP, int aSubpolyIndex=-1, int aAccuracy=0, bool aUseBBoxCaches=false) const
Return true if a given subpolygon contains the point aP.
SHAPE_POLY_SET CloneDropTriangulation() const
void BooleanSubtract(const SHAPE_POLY_SET &b)
Perform boolean polyset difference.
const BOX2I BBoxFromCaches() const
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
constexpr extended_type SquaredEuclideanNorm() const
Compute the squared euclidean norm of the vector, which is defined as (x ** 2 + y ** 2).
Definition vector2d.h:307
constexpr VECTOR2< T > Perpendicular() const
Compute the perpendicular vector.
Definition vector2d.h:314
VECTOR2< T > Resize(T aNewLength) const
Return a vector of the same direction, but length specified in aNewLength.
Definition vector2d.h:385
VERTEX * getPoint(VERTEX *aPt) const
std::set< RESULTS > GetResults() const
VERTEX_CONNECTOR(const BOX2I &aBBox, const SHAPE_POLY_SET &aPolys, int aDist)
std::set< RESULTS > m_results
std::deque< VERTEX > m_vertices
Definition vertex_set.h:343
friend class VERTEX
Definition vertex_set.h:255
VERTEX * createList(const SHAPE_LINE_CHAIN &points, VERTEX *aTail=nullptr, void *aUserData=nullptr)
Create a list of vertices from a line chain.
void SetBoundingBox(const BOX2I &aBBox)
VERTEX_SET(int aSimplificationLevel)
Definition vertex_set.h:258
uint32_t zOrder(const double aX, const double aY) const
Note that while the inputs are doubles, these are scaled by the size of the bounding box to fit into ...
const double x
Definition vertex_set.h:235
VERTEX * next
Definition vertex_set.h:241
VERTEX * prevZ
Definition vertex_set.h:247
void updateList()
After inserting or changing nodes, this function should be called to remove duplicate vertices and en...
Definition vertex_set.h:121
VERTEX * nextZ
Definition vertex_set.h:248
VERTEX * prev
Definition vertex_set.h:240
const int i
Definition vertex_set.h:234
void * GetUserData() const
Definition vertex_set.h:79
uint32_t z
Definition vertex_set.h:244
bool isEar(bool aMatchUserData=false) const
Check whether the given vertex is in the middle of an ear.
const double y
Definition vertex_set.h:236
COMMIT * m_commit
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.
std::map< std::pair< const ZONE *, PCB_LAYER_ID >, SHAPE_POLY_SET > FillSnapshot
Snapshot of zone fill polygons captured before an iterative refill wave.
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 refillZoneFromCache(ZONE *aZone, PCB_LAYER_ID aLayer, SHAPE_POLY_SET &aFillPolys, const FillSnapshot *aSnapshot=nullptr)
Refill a zone from cached pre-knockout fill.
bool addCopperThievingPattern(const ZONE *aZone, PCB_LAYER_ID aLayer, SHAPE_POLY_SET &aFillPolys)
Stamp a regular grid of pattern shapes onto a zone's filled area for copper thieving.
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)
void postKnockoutMinWidthPrune(const ZONE *aZone, SHAPE_POLY_SET &aFillPolys)
Remove minimum-width violations introduced by zone-to-zone knockouts.
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 CacheTriangulation(PCB_LAYER_ID aLayer=UNDEFINED_LAYER, const SHAPE_POLY_SET::TASK_SUBMITTER &aSubmitter={})
Create a list of triangles that "fill" the solid areas used for instance to draw these solid areas on...
Definition zone.cpp:1450
void SetNeedRefill(bool aNeedRefill)
Definition zone.h:314
bool GetIsRuleArea() const
Accessors to parameters used in Rule Area zones:
Definition zone.h:802
std::optional< int > GetLocalClearance() const override
Definition zone.cpp:936
const THIEVING_SETTINGS & GetThievingSettings() const
Definition zone.h:355
ZONE_LAYER_PROPERTIES & LayerProperties(PCB_LAYER_ID aLayer)
Definition zone.h:150
std::shared_ptr< SHAPE_POLY_SET > GetFilledPolysList(PCB_LAYER_ID aLayer) const
Definition zone.h:688
const BOX2I GetBoundingBox() const override
Definition zone.cpp:741
ISLAND_REMOVAL_MODE GetIslandRemovalMode() const
Definition zone.h:824
SHAPE_POLY_SET * Outline()
Definition zone.h:422
void SetFillFlag(PCB_LAYER_ID aLayer, bool aFlag)
Definition zone.h:304
bool IsCopperThieving() const
Definition zone.h:353
long long int GetMinIslandArea() const
Definition zone.h:827
void SetFilledPolysList(PCB_LAYER_ID aLayer, const SHAPE_POLY_SET &aPolysList)
Set the list of filled polygons.
Definition zone.h:717
int GetMinThickness() const
Definition zone.h:319
bool HigherPriority(const ZONE *aOther) const
Definition zone.cpp:471
bool HasFilledPolysForLayer(PCB_LAYER_ID aLayer) const
Definition zone.h:679
int GetHatchThickness() const
Definition zone.h:329
double GetHatchHoleMinArea() const
Definition zone.h:344
virtual bool IsOnLayer(PCB_LAYER_ID) const override
Test to see if this object is on the given layer.
Definition zone.cpp:734
bool IsTeardropArea() const
Definition zone.h:777
EDA_ANGLE GetHatchOrientation() const
Definition zone.h:335
bool BuildSmoothedPoly(SHAPE_POLY_SET &aSmoothedPoly, PCB_LAYER_ID aLayer, SHAPE_POLY_SET *aBoardOutline, SHAPE_POLY_SET *aSmoothedPolyWithApron=nullptr) const
Definition zone.cpp:1530
ZONE_FILL_MODE GetFillMode() const
Definition zone.h:242
virtual LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition zone.h:137
bool HasKeepoutParametersSet() const
Accessor to determine if any keepout parameters are set.
Definition zone.h:793
int GetHatchGap() const
Definition zone.h:332
double GetHatchSmoothingValue() const
Definition zone.h:341
bool GetDoNotAllowZoneFills() const
Definition zone.h:812
int GetHatchSmoothingLevel() const
Definition zone.h:338
void SetIsIsland(PCB_LAYER_ID aLayer, int aPolyIdx)
Definition zone.h:732
bool IsOnCopperLayer() const override
Definition zone.cpp:578
double CalculateFilledArea()
Compute the area currently occupied by the zone fill.
Definition zone.cpp:1690
unsigned GetAssignedPriority() const
Definition zone.h:126
bool SameNet(const ZONE *aOther) const
Definition zone.cpp:485
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 TransformTrapezoidToPolygon(SHAPE_POLY_SET &aBuffer, const VECTOR2I &aPosition, const VECTOR2I &aSize, const EDA_ANGLE &aRotation, int aDeltaX, int aDeltaY, int aInflate, int aError, ERROR_LOC aErrorLoc)
Convert a rectangle or trapezoid to a polygon.
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:53
@ EDGE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:59
@ PHYSICAL_HOLE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:87
@ CLEARANCE_CONSTRAINT
Definition drc_rule.h:55
@ THERMAL_SPOKE_WIDTH_CONSTRAINT
Definition drc_rule.h:70
@ THERMAL_RELIEF_GAP_CONSTRAINT
Definition drc_rule.h:69
@ HOLE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:57
@ PHYSICAL_CLEARANCE_CONSTRAINT
Definition drc_rule.h:86
#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:50
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 constexpr std::size_t hash_val(const Types &... args)
Definition hash.h:51
@ ALWAYS_FLASHED
Always flashed for connectivity.
Definition layer_ids.h:186
bool IsInnerCopperLayer(int aLayerId)
Test whether a layer is an inner (In1_Cu to In30_Cu) copper layer.
Definition layer_ids.h:701
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
@ In19_Cu
Definition layer_ids.h:84
@ 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
Parameters that drive copper-thieving fill generation.
EDA_ANGLE orientation
THIEVING_PATTERN pattern
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:85
@ PCB_DIM_ORTHOGONAL_T
class PCB_DIM_ORTHOGONAL, a linear dimension constrained to x/y
Definition typeinfo.h:103
@ PCB_DIM_LEADER_T
class PCB_DIM_LEADER, a leader dimension (graphic item)
Definition typeinfo.h:100
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:94
@ PCB_DIM_CENTER_T
class PCB_DIM_CENTER, a center point marking (graphic item)
Definition typeinfo.h:101
@ PCB_TEXTBOX_T
class PCB_TEXTBOX, wrapped text on a layer
Definition typeinfo.h:90
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:89
@ PCB_FIELD_T
class PCB_FIELD, text associated with a footprint property
Definition typeinfo.h:87
@ PCB_BARCODE_T
class PCB_BARCODE, a barcode (graphic item)
Definition typeinfo.h:98
@ PCB_TARGET_T
class PCB_TARGET, a target (graphic item)
Definition typeinfo.h:104
@ PCB_DIM_ALIGNED_T
class PCB_DIM_ALIGNED, a linear dimension (graphic item)
Definition typeinfo.h:99
@ PCB_PAD_T
class PAD, a pad in a footprint
Definition typeinfo.h:84
@ PCB_TABLE_T
class PCB_TABLE, table of PCB_TABLECELLs
Definition typeinfo.h:91
@ PCB_DIM_RADIAL_T
class PCB_DIM_RADIAL, a radius or diameter dimension
Definition typeinfo.h:102
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:687
VECTOR2< double > VECTOR2D
Definition vector2d.h:686
#define SMOOTH_MIN_VAL_MM
#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