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