KiCad PCB EDA Suite
Loading...
Searching...
No Matches
ratsnest_data.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) 2013-2017 CERN
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * @author Maciej Suminski <[email protected]>
8 * @author Tomasz Wlostowski <[email protected]>
9 *
10 * This program is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU General Public License
12 * as published by the Free Software Foundation; either version 2
13 * of the License, or (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 */
23
28
29#ifdef PROFILE
30#include <core/profile.h>
31#endif
32
34#include <functional>
35using namespace std::placeholders;
36
37#include <algorithm>
38#include <cassert>
39#include <limits>
40#include <list>
41
42#include <delaunator.hpp>
43
45{
46
47public:
48 disjoint_set( size_t size )
49 {
50 m_data.resize( size );
51 m_depth.resize( size, 0 );
52
53 for( size_t i = 0; i < size; i++ )
54 m_data[i] = i;
55 }
56
57 int find( int aVal )
58 {
59 int root = aVal;
60
61 while( m_data[root] != root )
62 root = m_data[root];
63
64 // Compress the path
65 while( m_data[aVal] != aVal )
66 {
67 auto& tmp = m_data[aVal];
68 aVal = tmp;
69 tmp = root;
70 }
71
72 return root;
73 }
74
75
76 bool unite( int aVal1, int aVal2 )
77 {
78 aVal1 = find( aVal1 );
79 aVal2 = find( aVal2 );
80
81 if( aVal1 != aVal2 )
82 {
83 if( m_depth[aVal1] < m_depth[aVal2] )
84 {
85 m_data[aVal1] = aVal2;
86 }
87 else
88 {
89 m_data[aVal2] = aVal1;
90
91 if( m_depth[aVal1] == m_depth[aVal2] )
92 m_depth[aVal1]++;
93 }
94
95 return true;
96 }
97
98 return false;
99 }
100
101private:
102 std::vector<int> m_data;
103 std::vector<int> m_depth;
104};
105
106
107void RN_NET::kruskalMST( const std::vector<CN_EDGE> &aEdges )
108{
109 disjoint_set dset( m_nodes.size() );
110
111 m_rnEdges.clear();
112
113 int i = 0;
114
115 for( const std::shared_ptr<CN_ANCHOR>& node : m_nodes )
116 node->SetTag( i++ );
117
118 for( const CN_EDGE& tmp : aEdges )
119 {
120 const std::shared_ptr<const CN_ANCHOR>& source = tmp.GetSourceNode();
121 const std::shared_ptr<const CN_ANCHOR>& target = tmp.GetTargetNode();
122
123 wxCHECK2( source && !source->Dirty() && target && !target->Dirty(), continue );
124
125 if( dset.unite( source->GetTag(), target->GetTag() ) )
126 {
127 if( tmp.GetWeight() > 0 )
128 m_rnEdges.push_back( tmp );
129 }
130 }
131}
132
133
135{
136private:
137 std::multiset<std::shared_ptr<CN_ANCHOR>, CN_PTR_CMP> m_allNodes;
138
139
140 // Checks if all nodes in aNodes lie on a single line. Requires the nodes to
141 // have unique coordinates!
142 bool areNodesColinear( const std::vector<std::shared_ptr<CN_ANCHOR>>& aNodes ) const
143 {
144 if ( aNodes.size() <= 2 )
145 return true;
146
147 const VECTOR2I p0( aNodes[0]->Pos() );
148 const VECTOR2I v0( aNodes[1]->Pos() - p0 );
149
150 for( unsigned i = 2; i < aNodes.size(); i++ )
151 {
152 const VECTOR2I v1 = aNodes[i]->Pos() - p0;
153
154 if( v0.Cross( v1 ) != 0 )
155 return false;
156 }
157
158 return true;
159 }
160
161public:
162
163 void Clear()
164 {
165 m_allNodes.clear();
166 }
167
168 void AddNode( const std::shared_ptr<CN_ANCHOR>& aNode )
169 {
170 m_allNodes.insert( aNode );
171 }
172
173 void Triangulate( std::vector<CN_EDGE>& mstEdges )
174 {
175 std::vector<double> node_pts;
176 std::vector<std::shared_ptr<CN_ANCHOR>> anchors;
177 std::vector< std::vector<std::shared_ptr<CN_ANCHOR>> > anchorChains( m_allNodes.size() );
178
179 node_pts.reserve( 2 * m_allNodes.size() );
180 anchors.reserve( m_allNodes.size() );
181
182 auto addEdge =
183 [&]( const std::shared_ptr<CN_ANCHOR>& src, const std::shared_ptr<CN_ANCHOR>& dst )
184 {
185 mstEdges.emplace_back( src, dst, src->Dist( *dst ) );
186 };
187
188 std::shared_ptr<CN_ANCHOR> prev = nullptr;
189
190 for( const std::shared_ptr<CN_ANCHOR>& n : m_allNodes )
191 {
192 if( !prev || prev->Pos() != n->Pos() )
193 {
194 node_pts.push_back( n->Pos().x );
195 node_pts.push_back( n->Pos().y );
196 anchors.push_back( n );
197 prev = n;
198 }
199
200 anchorChains[anchors.size() - 1].push_back( n );
201 }
202
203 if( anchors.empty() )
204 {
205 return;
206 }
207 else if( anchors.size() == 1 )
208 {
209 // The anchors all have the same position, but may not have overlapping layers.
210 prev = nullptr;
211
212 for( const std::shared_ptr<CN_ANCHOR>& n : m_allNodes )
213 {
214 if( prev && !( prev->Parent()->GetLayerSet() & n->Parent()->GetLayerSet() ).any() )
215 {
216 // Use a minimal but non-zero distance or the edge will be ignored
217 mstEdges.emplace_back( prev, n, 1 );
218 }
219
220 prev = n;
221 }
222
223 return;
224 }
225 else if( areNodesColinear( anchors ) )
226 {
227 // special case: all nodes are on the same line - there's no
228 // triangulation for such set. In this case, we sort along any coordinate
229 // and chain the nodes together.
230 for( size_t i = 0; i < anchors.size() - 1; i++ )
231 addEdge( anchors[i], anchors[i + 1] );
232 }
233 else
234 {
235 delaunator::Delaunator delaunator( node_pts );
236 auto& triangles = delaunator.triangles;
237
238 for( size_t i = 0; i < triangles.size(); i += 3 )
239 {
240 addEdge( anchors[triangles[i]], anchors[triangles[i + 1]] );
241 addEdge( anchors[triangles[i + 1]], anchors[triangles[i + 2]] );
242 addEdge( anchors[triangles[i + 2]], anchors[triangles[i]] );
243 }
244
245 for( size_t i = 0; i < delaunator.halfedges.size(); i++ )
246 {
247 if( delaunator.halfedges[i] == delaunator::INVALID_INDEX )
248 continue;
249
250 addEdge( anchors[triangles[i]], anchors[triangles[delaunator.halfedges[i]]] );
251 }
252 }
253
254 for( size_t i = 0; i < anchorChains.size(); i++ )
255 {
256 std::vector<std::shared_ptr<CN_ANCHOR>>& chain = anchorChains[i];
257
258 if( chain.size() < 2 )
259 continue;
260
261 std::sort( chain.begin(), chain.end(),
262 [] ( const std::shared_ptr<CN_ANCHOR>& a, const std::shared_ptr<CN_ANCHOR>& b )
263 {
264 return a->GetCluster().get() < b->GetCluster().get();
265 } );
266
267 for( unsigned int j = 1; j < chain.size(); j++ )
268 {
269 const std::shared_ptr<CN_ANCHOR>& prevNode = chain[j - 1];
270 const std::shared_ptr<CN_ANCHOR>& curNode = chain[j];
271 int weight = prevNode->GetCluster() != curNode->GetCluster() ? 1 : 0;
272 mstEdges.emplace_back( prevNode, curNode, weight );
273 }
274 }
275 }
276};
277
278
280{
282}
283
284
286{
287 // Special cases do not need complicated algorithms (actually, it does not work well with
288 // the Delaunay triangulator)
289 if( m_nodes.size() <= 2 )
290 {
291 m_rnEdges.clear();
292
293 // Check if the only possible connection exists
294 if( m_boardEdges.size() == 0 && m_nodes.size() == 2 )
295 {
296 // There can be only one possible connection, but it is missing
297 auto it = m_nodes.begin();
298 const std::shared_ptr<CN_ANCHOR>& source = *it++;
299 const std::shared_ptr<CN_ANCHOR>& target = *it;
300
301 source->SetTag( 0 );
302 target->SetTag( 1 );
303 m_rnEdges.emplace_back( source, target );
304 }
305 else
306 {
307 // Set tags to m_nodes as connected
308 for( const std::shared_ptr<CN_ANCHOR>& node : m_nodes )
309 node->SetTag( 0 );
310 }
311
312 return;
313 }
314
315
316 m_triangulator->Clear();
317
318 for( const std::shared_ptr<CN_ANCHOR>& n : m_nodes )
319 m_triangulator->AddNode( n );
320
321 std::vector<CN_EDGE> triangEdges;
322 triangEdges.reserve( m_nodes.size() + m_boardEdges.size() );
323
324#ifdef PROFILE
325 PROF_TIMER cnt( "triangulate" );
326#endif
327 m_triangulator->Triangulate( triangEdges );
328#ifdef PROFILE
329 cnt.Show();
330#endif
331
332 for( const CN_EDGE& e : m_boardEdges )
333 triangEdges.emplace_back( e );
334
335 std::sort( triangEdges.begin(), triangEdges.end() );
336
337// Get the minimal spanning tree
338#ifdef PROFILE
339 PROF_TIMER cnt2( "mst" );
340#endif
341 kruskalMST( triangEdges );
342#ifdef PROFILE
343 cnt2.Show();
344#endif
345}
346
347
349{
350 auto optimizeZoneAnchor =
351 [&]( const VECTOR2I& aPos, const LSET& aLayerSet,
352 const std::shared_ptr<const CN_ANCHOR>& aAnchor,
353 const std::function<void( std::shared_ptr<const CN_ANCHOR> )>& setOptimizedTo )
354 {
355 SEG::ecoord closest_dist_sq = ( aAnchor->Pos() - aPos ).SquaredEuclideanNorm();
356 VECTOR2I closest_pt;
357 CN_ITEM* closest_item = nullptr;
358
359 for( CN_ITEM* item : aAnchor->Item()->ConnectedItems() )
360 {
361 // Don't consider shorted items
362 if( aAnchor->Item()->Net() != item->Net() )
363 continue;
364
365 CN_ZONE_LAYER* zoneLayer = dynamic_cast<CN_ZONE_LAYER*>( item );
366
367 if( zoneLayer && aLayerSet.test( zoneLayer->GetBoardLayer() ) )
368 {
369 const std::vector<VECTOR2I>& pts = zoneLayer->GetOutline().CPoints();
370
371 for( const VECTOR2I& pt : pts )
372 {
373 SEG::ecoord dist_sq = ( pt - aPos ).SquaredEuclideanNorm();
374
375 if( dist_sq < closest_dist_sq )
376 {
377 closest_pt = pt;
378 closest_item = zoneLayer;
379 closest_dist_sq = dist_sq;
380 }
381 }
382 }
383 }
384
385 if( closest_item )
386 setOptimizedTo( std::make_shared<CN_ANCHOR>( closest_pt, closest_item ) );
387 };
388
389 auto optimizeZoneToZoneAnchors =
390 [&]( const std::shared_ptr<const CN_ANCHOR>& a,
391 const std::shared_ptr<const CN_ANCHOR>& b,
392 const std::function<void( const std::shared_ptr<const CN_ANCHOR>& )>&
393 setOptimizedATo,
394 const std::function<void( const std::shared_ptr<const CN_ANCHOR>& )>&
395 setOptimizedBTo )
396 {
397 struct CENTER
398 {
399 VECTOR2I pt;
400 bool valid = false;
401 };
402
403 struct DIST_PAIR
404 {
405 DIST_PAIR( int64_t aDistSq, size_t aIdA, size_t aIdB )
406 : dist_sq( aDistSq ), idA( aIdA ), idB( aIdB )
407 {}
408
409 int64_t dist_sq;
410 size_t idA;
411 size_t idB;
412 };
413
414 const std::vector<CN_ITEM*>& connectedItemsA = a->Item()->ConnectedItems();
415 const std::vector<CN_ITEM*>& connectedItemsB = b->Item()->ConnectedItems();
416
417 std::vector<CENTER> centersA( connectedItemsA.size() );
418 std::vector<CENTER> centersB( connectedItemsB.size() );
419
420 for( size_t i = 0; i < connectedItemsA.size(); i++ )
421 {
422 CN_ITEM* itemA = connectedItemsA[i];
423 CN_ZONE_LAYER* zoneLayerA = dynamic_cast<CN_ZONE_LAYER*>( itemA );
424
425 if( !zoneLayerA )
426 continue;
427
428 const SHAPE_LINE_CHAIN& shapeA = zoneLayerA->GetOutline();
429 centersA[i].pt = shapeA.BBox().GetCenter();
430 centersA[i].valid = true;
431 }
432
433 for( size_t i = 0; i < connectedItemsB.size(); i++ )
434 {
435 CN_ITEM* itemB = connectedItemsB[i];
436 CN_ZONE_LAYER* zoneLayerB = dynamic_cast<CN_ZONE_LAYER*>( itemB );
437
438 if( !zoneLayerB )
439 continue;
440
441 const SHAPE_LINE_CHAIN& shapeB = zoneLayerB->GetOutline();
442 centersB[i].pt = shapeB.BBox().GetCenter();
443 centersB[i].valid = true;
444 }
445
446 std::vector<DIST_PAIR> pairsToTest;
447
448 for( size_t ia = 0; ia < centersA.size(); ia++ )
449 {
450 for( size_t ib = 0; ib < centersB.size(); ib++ )
451 {
452 const CENTER& ca = centersA[ia];
453 const CENTER& cb = centersB[ib];
454
455 if( !ca.valid || !cb.valid )
456 continue;
457
458 VECTOR2L pA( ca.pt );
459 VECTOR2L pB( cb.pt );
460
461 int64_t dist_sq = ( pB - pA ).SquaredEuclideanNorm();
462 pairsToTest.emplace_back( dist_sq, ia, ib );
463 }
464 }
465
466 std::sort( pairsToTest.begin(), pairsToTest.end(),
467 []( const DIST_PAIR& dp_a, const DIST_PAIR& dp_b )
468 {
469 return dp_a.dist_sq < dp_b.dist_sq;
470 } );
471
472 const int c_polyPairsLimit = 3;
473
474 for( size_t i = 0; i < pairsToTest.size() && i < c_polyPairsLimit; i++ )
475 {
476 const DIST_PAIR& pair = pairsToTest[i];
477
478 CN_ZONE_LAYER* zoneLayerA = static_cast<CN_ZONE_LAYER*>( connectedItemsA[pair.idA] );
479 CN_ZONE_LAYER* zoneLayerB = static_cast<CN_ZONE_LAYER*>( connectedItemsB[pair.idB] );
480
481 if( zoneLayerA == zoneLayerB )
482 continue;
483
484 const SHAPE_LINE_CHAIN& shapeA = zoneLayerA->GetOutline();
485 const SHAPE_LINE_CHAIN& shapeB = zoneLayerB->GetOutline();
486
487 VECTOR2I ptA;
488 VECTOR2I ptB;
489
490 if( shapeA.ClosestSegmentsFast( shapeB, ptA, ptB ) )
491 {
492 setOptimizedATo( std::make_shared<CN_ANCHOR>( ptA, zoneLayerA ) );
493 setOptimizedBTo( std::make_shared<CN_ANCHOR>( ptB, zoneLayerB ) );
494 }
495 }
496 };
497
498 for( CN_EDGE& edge : m_rnEdges )
499 {
500 const std::shared_ptr<const CN_ANCHOR>& source = edge.GetSourceNode();
501 const std::shared_ptr<const CN_ANCHOR>& target = edge.GetTargetNode();
502
503 wxCHECK2( source && !source->Dirty() && target && !target->Dirty(), continue );
504
505 if( source->ConnectedItemsCount() == 0 )
506 {
507 optimizeZoneAnchor( source->Pos(), source->Parent()->GetLayerSet(), target,
508 [&]( const std::shared_ptr<const CN_ANCHOR>& optimized )
509 {
510 edge.SetTargetNode( optimized );
511 } );
512 }
513 else if( target->ConnectedItemsCount() == 0 )
514 {
515 optimizeZoneAnchor( target->Pos(), target->Parent()->GetLayerSet(), source,
516 [&]( const std::shared_ptr<const CN_ANCHOR>& optimized )
517 {
518 edge.SetSourceNode( optimized );
519 } );
520 }
521 else
522 {
523 optimizeZoneToZoneAnchors( source, target,
524 [&]( const std::shared_ptr<const CN_ANCHOR>& optimized )
525 {
526 edge.SetSourceNode( optimized );
527 },
528 [&]( const std::shared_ptr<const CN_ANCHOR>& optimized )
529 {
530 edge.SetTargetNode( optimized );
531 } );
532 }
533 }
534}
535
536
538{
539 compute();
540
541 m_dirty = false;
542}
543
544
546{
547 for( CN_EDGE& edge : m_rnEdges )
548 edge.RemoveInvalidRefs();
549
550 for( CN_EDGE& edge : m_boardEdges )
551 edge.RemoveInvalidRefs();
552
553 auto is_invalid = []( const CN_EDGE& edge )
554 {
555 return !edge.GetSourceNode() || !edge.GetTargetNode();
556 };
557
558 m_rnEdges.erase( std::remove_if( m_rnEdges.begin(), m_rnEdges.end(), is_invalid ), m_rnEdges.end() );
559 m_boardEdges.erase( std::remove_if( m_boardEdges.begin(), m_boardEdges.end(), is_invalid ),
560 m_boardEdges.end() );
561}
562
563
565{
566 m_rnEdges.clear();
567 m_boardEdges.clear();
568 m_nodes.clear();
569
570 m_dirty = true;
571}
572
573
574void RN_NET::AddCluster( std::shared_ptr<CN_CLUSTER> aCluster )
575{
576 std::shared_ptr<CN_ANCHOR> firstAnchor;
577
578 for( CN_ITEM* item : *aCluster )
579 {
580 std::vector<std::shared_ptr<CN_ANCHOR>>& anchors = item->Anchors();
581 unsigned int nAnchors = dynamic_cast<CN_ZONE_LAYER*>( item ) ? 1 : anchors.size();
582
583 if( nAnchors > anchors.size() )
584 nAnchors = anchors.size();
585
586 for( unsigned int i = 0; i < nAnchors; i++ )
587 {
588 anchors[i]->SetCluster( aCluster );
589 m_nodes.insert( anchors[i] );
590
591 if( firstAnchor )
592 {
593 if( firstAnchor != anchors[i] )
594 m_boardEdges.emplace_back( firstAnchor, anchors[i], 0 );
595 }
596 else
597 {
598 firstAnchor = anchors[i];
599 }
600 }
601 }
602}
603
604
605bool RN_NET::NearestBicoloredPair( RN_NET* aOtherNet, VECTOR2I& aPos1, VECTOR2I& aPos2 ) const
606{
607 bool rv = false;
608
610
611 auto verify =
612 [&]( const std::shared_ptr<CN_ANCHOR>& aTestNode1,
613 const std::shared_ptr<CN_ANCHOR>& aTestNode2 )
614 {
615 VECTOR2I diff = aTestNode1->Pos() - aTestNode2->Pos();
616 SEG::ecoord dist_sq = diff.SquaredEuclideanNorm();
617
618 if( dist_sq < distMax_sq )
619 {
620 rv = true;
621 distMax_sq = dist_sq;
622 aPos1 = aTestNode1->Pos();
623 aPos2 = aTestNode2->Pos();
624 }
625 };
626
630 for( const std::shared_ptr<CN_ANCHOR>& nodeA : aOtherNet->m_nodes )
631 {
632
633 if( nodeA->GetNoLine() )
634 continue;
635
639 auto fwd_it = m_nodes.lower_bound( nodeA );
640 auto rev_it = std::make_reverse_iterator( fwd_it );
641
642 for( ; fwd_it != m_nodes.end(); ++fwd_it )
643 {
644 const std::shared_ptr<CN_ANCHOR>& nodeB = *fwd_it;
645
646 if( nodeB->GetNoLine() )
647 continue;
648
649 SEG::ecoord distX_sq = SEG::Square( nodeA->Pos().x - nodeB->Pos().x );
650
653 if( distX_sq > distMax_sq )
654 break;
655
656 verify( nodeA, nodeB );
657 }
658
660 for( ; rev_it != m_nodes.rend(); ++rev_it )
661 {
662 const std::shared_ptr<CN_ANCHOR>& nodeB = *rev_it;
663
664 if( nodeB->GetNoLine() )
665 continue;
666
667 SEG::ecoord distX_sq = SEG::Square( nodeA->Pos().x - nodeB->Pos().x );
668
669 if( distX_sq > distMax_sq )
670 break;
671
672 verify( nodeA, nodeB );
673 }
674 }
675
676 return rv;
677}
constexpr const Vec GetCenter() const
Definition box2.h:226
CN_EDGE represents a point-to-point connection, whether realized or unrealized (ie: tracks etc.
CN_ITEM represents a BOARD_CONNETED_ITEM in the connectivity system (ie: a pad, track/arc/via,...
const std::vector< CN_ITEM * > & ConnectedItems() const
PCB_LAYER_ID GetBoardLayer() const
When using CN_ITEM layers to compare against board items, use this function which correctly remaps th...
const SHAPE_LINE_CHAIN & GetOutline() const
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
A small class to help profiling.
Definition profile.h:46
void Show(std::ostream &aStream=std::cerr)
Print the elapsed time (in a suitable unit) to a stream.
Definition profile.h:103
void AddNode(const std::shared_ptr< CN_ANCHOR > &aNode)
std::multiset< std::shared_ptr< CN_ANCHOR >, CN_PTR_CMP > m_allNodes
bool areNodesColinear(const std::vector< std::shared_ptr< CN_ANCHOR > > &aNodes) const
void Triangulate(std::vector< CN_EDGE > &mstEdges)
std::shared_ptr< TRIANGULATOR_STATE > m_triangulator
std::multiset< std::shared_ptr< CN_ANCHOR >, CN_PTR_CMP > m_nodes
< Vector of nodes
void RemoveInvalidRefs()
void kruskalMST(const std::vector< CN_EDGE > &aEdges)
void UpdateNet()
Recompute ratsnest for a net.
std::vector< CN_EDGE > m_rnEdges
Flag indicating necessity of recalculation of ratsnest for a net.
void OptimizeRNEdges()
Find optimal ends of RNEdges.
bool NearestBicoloredPair(RN_NET *aOtherNet, VECTOR2I &aPos1, VECTOR2I &aPos2) const
bool m_dirty
std::vector< CN_EDGE > m_boardEdges
Vector of edges that makes ratsnest for a given net.
void compute()
< Recompute ratsnest from scratch.
void Clear()
void AddCluster(std::shared_ptr< CN_CLUSTER > aCluster)
VECTOR2I::extended_type ecoord
Definition seg.h:40
static SEG::ecoord Square(int a)
Definition seg.h:119
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
bool ClosestSegmentsFast(const SHAPE_LINE_CHAIN &aOther, VECTOR2I &aPt0, VECTOR2I &aPt1) const
Finds closest points between segments of this and the other line chain.
const std::vector< VECTOR2I > & CPoints() 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 Cross(const VECTOR2< T > &aVector) const
Compute cross product of self with aVector.
Definition vector2d.h:534
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
static constexpr extended_type ECOORD_MAX
Definition vector2d.h:72
disjoint_set(size_t size)
std::vector< int > m_data
std::vector< int > m_depth
int find(int aVal)
bool unite(int aVal1, int aVal2)
Class that computes missing connections on a PCB.
VECTOR3I v1(5, 5, 5)
const SHAPE_LINE_CHAIN chain
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
VECTOR2< int64_t > VECTOR2L
Definition vector2d.h:684