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