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