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