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