KiCad PCB EDA Suite
Loading...
Searching...
No Matches
3d_model_align.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 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7#include "3d_model_align.h"
8#include <core/union_find.h>
10#include <Eigen/Dense>
11#include <algorithm>
12#include <cmath>
13#include <functional>
14#include <limits>
15#include <map>
16#include <numeric>
17#include <utility>
18
19namespace MODEL_ALIGN
20{
21namespace
22{
23 using V2 = Eigen::Vector2d;
24 using V3 = Eigen::Vector3d;
25 using M3 = Eigen::Matrix3d;
26 constexpr double PI = 3.14159265358979323846;
27 constexpr double INF = std::numeric_limits<double>::infinity();
28
29 V3 vec( const glm::dvec3& aValue )
30 {
31 return V3( aValue.x, aValue.y, aValue.z );
32 }
33 V2 vec( const glm::dvec2& aValue )
34 {
35 return V2( aValue.x, aValue.y );
36 }
37 glm::dvec3 vec( const V3& aValue )
38 {
39 return { aValue.x(), aValue.y(), aValue.z() };
40 }
41
42 using BOX2 = Eigen::AlignedBox2d;
43 using BOX3 = Eigen::AlignedBox3d;
44
45 struct CONTACT
46 {
47 V3 position;
48 double weight;
49 };
50
51 struct ALIGN_CANDIDATE
52 {
53 M3 rotation;
54 V3 offset;
55 unsigned int matched;
56 unsigned int total;
57 SOLUTION_KIND kind;
58 double copper = 0;
59 double turn = 0;
60 };
61
62 void describe( REGION& aRegion )
63 {
64 V3 centroid = V3::Zero();
65 V3 normal = V3::Zero();
66
67 for( const auto& triangle : aRegion.triangles )
68 {
69 V3 a = vec( aRegion.vertices[triangle[0]] );
70 V3 b = vec( aRegion.vertices[triangle[1]] );
71 V3 c = vec( aRegion.vertices[triangle[2]] );
72 V3 n = ( b - a ).cross( c - a ) / 2;
73 double area = n.norm();
74 aRegion.area += area;
75 centroid += area * ( a + b + c ) / 3;
76 normal += n;
77 }
78
79 if( aRegion.area <= 0 || !std::isfinite( aRegion.area ) || !centroid.allFinite() || !normal.allFinite() )
80 {
81 aRegion.area = 0;
82 return;
83 }
84
85 centroid /= aRegion.area;
86 M3 covariance = M3::Zero();
87
88 for( const auto& vertex : aRegion.vertices )
89 {
90 V3 delta = vec( vertex ) - centroid;
91 covariance += delta * delta.transpose();
92 }
93
94 if( !covariance.allFinite() )
95 {
96 aRegion.area = 0;
97 return;
98 }
99
100 Eigen::SelfAdjointEigenSolver<M3> eigen( covariance );
101
102 if( eigen.info() != Eigen::Success )
103 {
104 aRegion.area = 0;
105 return;
106 }
107
108 BOX3 box;
109
110 for( const auto& vertex : aRegion.vertices )
111 box.extend( V3( eigen.eigenvectors().transpose() * ( vec( vertex ) - centroid ) ) );
112
113 V3 extents = box.sizes();
114 std::sort( extents.data(), extents.data() + 3, std::greater<double>() );
115 aRegion.centroid = vec( centroid );
116 aRegion.extents = vec( extents );
117 aRegion.planar = extents[2] < 1e-3 + 1e-3 * extents[0];
118 aRegion.normalMagnitude = normal.norm() / aRegion.area;
119
120 if( normal.norm() > 0 )
121 aRegion.normal = vec( V3( normal.normalized() ) );
122 }
123
124 bool congruent( const REGION& aLeft, const REGION& aRight, double aLength )
125 {
126 if( aLeft.material != aRight.material || aLeft.planar != aRight.planar
127 || std::abs( aLeft.area - aRight.area ) > 0.05 * std::max( aLeft.area, aRight.area ) )
128 return false;
129
130 for( int axis = 0; axis < 3; ++axis )
131 {
132 if( std::abs( aLeft.extents[axis] - aRight.extents[axis] )
133 > std::max( 0.05 * std::max( aLeft.extents[axis], aRight.extents[axis] ), 1e-3 * aLength ) )
134 return false;
135 }
136
137 return true;
138 }
139
140 V2 padLocal( const PAD& aPad, const V2& aPoint )
141 {
142 return Eigen::Rotation2Dd( -aPad.rotation * PI / 180 ) * ( aPoint - vec( aPad.position ) );
143 }
144
145 bool overDrill( const PAD& aPad, const V2& aPoint, double aGrow = 0 )
146 {
147 if( aPad.drill.x <= 0 || aPad.drill.y <= 0 )
148 return false;
149
150 V2 local = padLocal( aPad, aPoint );
151 int major = aPad.drill.x >= aPad.drill.y ? 0 : 1;
152 double radius = std::min( aPad.drill.x, aPad.drill.y ) / 2 + aGrow;
153 double halfSegment = std::abs( aPad.drill.x - aPad.drill.y ) / 2;
154 local[major] -= std::clamp( local[major], -halfSegment, halfSegment );
155 return local.squaredNorm() <= radius * radius;
156 }
157
158 bool padContains( const PAD& aPad, const V2& aPoint, double aGrow )
159 {
161 return overDrill( aPad, aPoint, aGrow );
162
163 V2 local = padLocal( aPad, aPoint );
164 return std::abs( local.x() ) <= aPad.size.x / 2 + aGrow && std::abs( local.y() ) <= aPad.size.y / 2 + aGrow;
165 }
166
167 std::vector<int> match( const std::vector<V2>& aPoints, const std::vector<PAD>& aPads, bool aContainment,
168 double aGrow )
169 {
170 std::vector<int> assignment( aPoints.size(), -1 );
171 std::vector<bool> used( aPads.size(), false );
172
173 for( size_t i = 0; i < aPoints.size(); ++i )
174 {
175 int best = -1;
176 double distance = INF;
177
178 for( size_t j = 0; j < aPads.size(); ++j )
179 {
180 double trial = ( aPoints[i] - vec( aPads[j].position ) ).squaredNorm();
181
182 if( trial < distance )
183 {
184 best = static_cast<int>( j );
185 distance = trial;
186 }
187 }
188
189 if( best >= 0 && !used[best] && ( !aContainment || padContains( aPads[best], aPoints[i], aGrow ) ) )
190 {
191 assignment[i] = best;
192 used[best] = true;
193 }
194 }
195
196 return assignment;
197 }
198
199 size_t countMatches( const std::vector<int>& aAssignment )
200 {
201 return std::count_if( aAssignment.begin(), aAssignment.end(),
202 []( int aIndex )
203 {
204 return aIndex >= 0;
205 } );
206 }
207
208 BOX2 bodyBox( const std::vector<REGION>& aRegions, const M3& aRotation, const V3& aOffset )
209 {
210 BOX2 box;
211
212 for( const REGION& region : aRegions )
213 {
214 for( const auto& vertex : region.vertices )
215 box.extend( V3( aRotation * vec( vertex ) + aOffset ).head<2>() );
216 }
217
218 return box;
219 }
220
221 bool geometryGate( ALIGN_CANDIDATE& aCandidate, const std::vector<REGION>& aRegions, const std::vector<PAD>& aPads,
222 const std::vector<PAD>& aAllPads, const std::vector<CONTACT>& aContacts, double aLevel,
223 const V3& aDown, double aEpsilon, double aGrow )
224 {
225 bool throughHole = aPads.front().attribute == PAD_ATTRIBUTE::THROUGH_HOLE;
226 double bottom = INF;
227
228 auto hole = [&]( const V3& aPoint, double aMargin )
229 {
230 return std::any_of( aAllPads.begin(), aAllPads.end(),
231 [&]( const PAD& aPad )
232 {
233 return overDrill( aPad, aPoint.head<2>(), aMargin );
234 } );
235 };
236
237 if( throughHole )
238 {
239 for( const REGION& region : aRegions )
240 {
241 for( const auto& vertex : region.vertices )
242 {
243 V3 point = aCandidate.rotation * vec( vertex ) + aCandidate.offset;
244
245 if( !hole( point, 0 ) )
246 bottom = std::min( bottom, point.z() );
247 }
248 }
249 }
250 else
251 {
252 bottom = ( aCandidate.rotation * ( aDown * aLevel ) ).z();
253 }
254
255 if( !std::isfinite( bottom ) )
256 return false;
257
258 aCandidate.offset.z() -= bottom;
259 double below = 0;
260 double total = std::accumulate( aRegions.begin(), aRegions.end(), 0.0,
261 []( double aTotal, const REGION& aRegion )
262 {
263 return aTotal + aRegion.area;
264 } );
265 double bodyZ = 0;
266
267 for( const REGION& region : aRegions )
268 {
269 V3 point = aCandidate.rotation * vec( region.centroid ) + aCandidate.offset;
270 bodyZ += region.area * point.z();
271
272 if( throughHole )
273 continue;
274
275 for( const auto& triangle : region.triangles )
276 {
277 std::array<V3, 3> vertices;
278
279 for( int corner = 0; corner < 3; ++corner )
280 vertices[corner] =
281 aCandidate.rotation * vec( region.vertices[triangle[corner]] ) + aCandidate.offset;
282
283 std::array<V3, 4> clipped;
284 size_t clippedCount = 0;
285
286 for( int edge = 0; edge < 3; ++edge )
287 {
288 const V3& a = vertices[edge];
289 const V3& b = vertices[( edge + 1 ) % 3];
290 bool aBelow = a.z() < -aEpsilon;
291 bool bBelow = b.z() < -aEpsilon;
292
293 if( aBelow )
294 clipped[clippedCount++] = a;
295
296 if( aBelow != bBelow )
297 clipped[clippedCount++] = a + ( b - a ) * ( ( -aEpsilon - a.z() ) / ( b.z() - a.z() ) );
298 }
299
300 for( size_t corner = 1; corner + 1 < clippedCount; ++corner )
301 {
302 V3 centre = ( clipped[0] + clipped[corner] + clipped[corner + 1] ) / 3;
303
304 if( !hole( centre, aEpsilon ) )
305 below += ( clipped[corner] - clipped[0] ).cross( clipped[corner + 1] - clipped[0] ).norm() / 2;
306
307 if( below > 0.02 * total )
308 return false;
309 }
310 }
311 }
312
313 if( total <= 0 || ( !throughHole && below > 0.02 * total ) || ( throughHole && bodyZ < 0 ) )
314 return false;
315
316 std::vector<V2> finalPoints;
317
318 for( const CONTACT& contact : aContacts )
319 {
320 V3 point = aCandidate.rotation * contact.position + aCandidate.offset;
321 finalPoints.push_back( point.head<2>() );
322
323 if( throughHole && point.z() > -aEpsilon )
324 return false;
325 }
326
327 aCandidate.matched = countMatches( match( finalPoints, aPads, true, aGrow ) );
328 return aCandidate.kind == SOLUTION_KIND::PARTIAL ? aCandidate.matched >= std::ceil( 0.9 * aContacts.size() )
329 : aCandidate.matched == aContacts.size();
330 }
331
332 void centreSlack( ALIGN_CANDIDATE& aCandidate, const std::vector<REGION>& aRegions,
333 const std::vector<CONTACT>& aContacts, const std::vector<PAD>& aPads, const V2& aPadsCentre,
334 const std::vector<int>& aAssignment )
335 {
336 V2 low = V2::Constant( -INF );
337 V2 high = V2::Constant( INF );
338
339 for( size_t i = 0; i < aContacts.size(); ++i )
340 {
341 const PAD& pad = aPads[aAssignment[i]];
342 V3 point = aCandidate.rotation * aContacts[i].position + aCandidate.offset;
343 double angle = std::remainder( pad.rotation, 90.0 );
344
345 // Axis-aligned translation boxes are exact for the normal footprint pad orientations.
346 if( std::abs( angle ) > 1e-6 )
347 return;
348
349 bool swap = std::abs( std::remainder( pad.rotation, 180.0 ) ) > 45;
350 V2 half( ( swap ? pad.size.y : pad.size.x ) / 2, ( swap ? pad.size.x : pad.size.y ) / 2 );
351
352 if( pad.attribute == PAD_ATTRIBUTE::THROUGH_HOLE )
353 half.setZero();
354
355 low = low.cwiseMax( vec( pad.position ) - half - point.head<2>() );
356 high = high.cwiseMin( vec( pad.position ) + half - point.head<2>() );
357 }
358
359 if( ( low.array() <= high.array() ).all() )
360 {
361 V2 modelCentre = bodyBox( aRegions, aCandidate.rotation, aCandidate.offset ).center();
362 aCandidate.offset.head<2>() += ( aPadsCentre - modelCentre ).cwiseMax( low ).cwiseMin( high );
363 }
364 }
365} // namespace
366
367std::vector<REGION> BuildRegions( const S3DMODEL& aModel, const glm::dvec3& aScale )
368{
369 std::vector<REGION> result;
370
371 if( !aModel.m_Meshes || !std::isfinite( aScale.x ) || !std::isfinite( aScale.y ) || !std::isfinite( aScale.z )
372 || aScale.x == 0 || aScale.y == 0 || aScale.z == 0 )
373 return result;
374
375 using POINT_KEY = std::array<double, 3>;
376 using TRIANGLE_KEY = std::array<POINT_KEY, 3>;
377 std::map<std::pair<unsigned int, std::vector<TRIANGLE_KEY>>, size_t> known;
378 auto makeSignature = []( const REGION& aRegion, bool aOriented, bool aReverse )
379 {
380 std::vector<TRIANGLE_KEY> signature;
381 signature.reserve( aRegion.triangles.size() );
382
383 for( const auto& triangle : aRegion.triangles )
384 {
385 TRIANGLE_KEY key;
386
387 for( unsigned int corner = 0; corner < 3; ++corner )
388 {
389 const auto& point = aRegion.vertices[triangle[corner]];
390 key[corner] = { point.x, point.y, point.z };
391 }
392
393 if( aReverse )
394 std::swap( key[1], key[2] );
395
396 if( aOriented )
397 std::rotate( key.begin(), std::min_element( key.begin(), key.end() ), key.end() );
398 else
399 std::sort( key.begin(), key.end() );
400
401 signature.push_back( key );
402 }
403
404 std::sort( signature.begin(), signature.end() );
405 return signature;
406 };
407
408 for( unsigned int meshIndex = 0; meshIndex < aModel.m_MeshesSize; ++meshIndex )
409 {
410 const SMESH& mesh = aModel.m_Meshes[meshIndex];
411
412 if( !mesh.m_Positions || !mesh.m_FaceIdx )
413 continue;
414
415 KI_UNION_FIND connected( mesh.m_VertexSize );
416 std::vector<unsigned int> valid;
417
418 for( unsigned int offset = 0; offset + 2 < mesh.m_FaceIdxSize; offset += 3 )
419 {
420 if( !IsTriangleInRange( mesh.m_FaceIdx, offset, mesh.m_VertexSize ) )
421 continue;
422
423 bool finite = true;
424
425 for( unsigned int corner = 0; corner < 3; ++corner )
426 {
427 const auto& point = mesh.m_Positions[mesh.m_FaceIdx[offset + corner]];
428 finite &= std::isfinite( point.x * aScale.x ) && std::isfinite( point.y * aScale.y )
429 && std::isfinite( point.z * aScale.z );
430 }
431
432 if( !finite )
433 continue;
434
435 valid.push_back( offset );
436 connected.Unite( mesh.m_FaceIdx[offset], mesh.m_FaceIdx[offset + 1] );
437 connected.Unite( mesh.m_FaceIdx[offset], mesh.m_FaceIdx[offset + 2] );
438 }
439
440 std::map<size_t, std::vector<unsigned int>> components;
441
442 for( unsigned int offset : valid )
443 components[connected.FindCompress( mesh.m_FaceIdx[offset] )].push_back( offset );
444
445 for( const auto& [component, offsets] : components )
446 {
447 REGION region;
448 region.material = mesh.m_MaterialIdx;
449 std::map<unsigned int, unsigned int> indices;
450
451 for( unsigned int offset : offsets )
452 {
453 std::array<unsigned int, 3> triangle;
454
455 for( unsigned int corner = 0; corner < 3; ++corner )
456 {
457 unsigned int source = mesh.m_FaceIdx[offset + corner];
458 auto [entry, inserted] = indices.emplace( source, indices.size() );
459
460 if( inserted )
461 {
462 const auto& point = mesh.m_Positions[source];
463 region.vertices.push_back( glm::dvec3( point.x, point.y, point.z ) * aScale );
464 }
465
466 triangle[corner] = entry->second;
467 }
468
469 if( aScale.x * aScale.y * aScale.z < 0 )
470 std::swap( triangle[1], triangle[2] );
471
472 region.triangles.push_back( triangle );
473 region.sourceTriangles.push_back( { meshIndex, offset / 3 } );
474 }
475
476 describe( region );
477
478 if( region.area <= 0 )
479 continue;
480
481 auto [previous, inserted] =
482 known.emplace( std::make_pair( region.material, makeSignature( region, false, false ) ),
483 result.size() );
484
485 if( !inserted )
486 {
487 REGION& original = result[previous->second];
488 original.twoSided |= makeSignature( original, true, true ) == makeSignature( region, true, false );
489 original.sourceTriangles.insert( original.sourceTriangles.end(), region.sourceTriangles.begin(),
490 region.sourceTriangles.end() );
491 continue;
492 }
493
494 result.push_back( std::move( region ) );
495 }
496 }
497
498 return result;
499}
500
501std::vector<PAD> BuildPadGroup( const std::vector<PAD>& aPads, size_t aClickedPad )
502{
503 std::vector<PAD> result;
504
505 if( aClickedPad >= aPads.size() || aPads[aClickedPad].attribute == PAD_ATTRIBUTE::OTHER )
506 return result;
507
508 const PAD& seed = aPads[aClickedPad];
509 result.push_back( seed );
510 auto dimensions = []( const PAD& aPad )
511 {
512 glm::dvec2 size = aPad.attribute == PAD_ATTRIBUTE::THROUGH_HOLE ? aPad.drill : aPad.size;
513 return V2( std::max( size.x, size.y ), std::min( size.x, size.y ) );
514 };
515
516 for( const PAD& pad : aPads )
517 {
518 if( pad.attribute != seed.attribute || ( dimensions( pad ) - dimensions( seed ) ).norm() > 1e-6 )
519 continue;
520
521 bool duplicate = std::any_of( result.begin(), result.end(),
522 [&]( const PAD& aPrevious )
523 {
524 return pad.number == aPrevious.number
525 && glm::length( pad.position - aPrevious.position ) < 1e-6;
526 } );
527
528 if( !duplicate )
529 result.push_back( pad );
530 }
531
532 return result;
533}
534
535std::vector<ALIGN_SOLUTION> SolveAlignment( const std::vector<REGION>& aRegions, size_t aSeed,
536 const std::vector<PAD>& aPadGroup, const std::vector<PAD>& aAllPads,
537 const glm::dvec3& aCurrentRotation )
538{
539 std::vector<ALIGN_SOLUTION> result;
540
541 if( aSeed >= aRegions.size() || aPadGroup.empty() || aAllPads.empty() )
542 return result;
543
544 const REGION& seed = aRegions[aSeed];
545 BOX3 modelBox;
546 BOX2 allPadBox;
547
548 for( const REGION& region : aRegions )
549 {
550 for( const auto& vertex : region.vertices )
551 modelBox.extend( vec( vertex ) );
552 }
553
554 for( const PAD& pad : aAllPads )
555 allPadBox.extend( vec( pad.position ) );
556
557 double length = modelBox.sizes().norm();
558
559 if( !std::isfinite( length ) )
560 return result;
561
562 double epsilon = std::max( 1e-3 * length, 0.01 );
563 double grow = seed.extents.y / 2;
564 V2 padCentre = V2::Zero();
565 BOX2 padBox;
566
567 for( const PAD& pad : aPadGroup )
568 {
569 padCentre += vec( pad.position );
570 padBox.extend( vec( pad.position ) );
571 }
572
573 padCentre /= aPadGroup.size();
574 std::vector<V3> directions{ V3::UnitX(), -V3::UnitX(), V3::UnitY(), -V3::UnitY(), V3::UnitZ(), -V3::UnitZ() };
575 V3 normal = vec( seed.normal );
576
577 if( normal.norm() > 0 && normal.cwiseAbs().maxCoeff() < std::cos( 0.5 * PI / 180 ) )
578 {
579 directions.push_back( normal );
580
581 if( seed.twoSided )
582 directions.push_back( -normal );
583 }
584
585 std::vector<ALIGN_CANDIDATE> full;
586 std::vector<ALIGN_CANDIDATE> partial;
587 std::vector<ALIGN_CANDIDATE> single;
588 double minimumPitch = -1;
589
590 for( const V3& down : directions )
591 {
592 double facing = normal.dot( down );
593
594 if( seed.twoSided )
595 facing = std::abs( facing );
596
597 if( facing < 0.25 && ( seed.planar || seed.normalMagnitude > 0.3 ) )
598 continue;
599
600 std::vector<double> levels;
601
602 for( const REGION& region : aRegions )
603 {
604 double level = -INF;
605
606 for( const auto& vertex : region.vertices )
607 level = std::max( level, vec( vertex ).dot( down ) );
608
609 levels.push_back( level );
610 }
611
612 double level = levels[aSeed];
613 std::vector<CONTACT> contacts;
614 V3 seedContact = vec( seed.centroid );
615
616 for( size_t i = 0; i < aRegions.size(); ++i )
617 {
618 const REGION& region = aRegions[i];
619
620 double peerFacing = vec( region.normal ).dot( down );
621
622 if( region.twoSided )
623 peerFacing = std::abs( peerFacing );
624
625 if( std::abs( levels[i] - level ) > epsilon || !congruent( seed, region, length )
626 || ( region.planar && peerFacing < 0.25 ) )
627 continue;
628
629 V3 accumulator = V3::Zero();
630 double weight = 0;
631
632 for( const auto& triangle : region.triangles )
633 {
634 V3 a = vec( region.vertices[triangle[0]] );
635 V3 b = vec( region.vertices[triangle[1]] );
636 V3 c = vec( region.vertices[triangle[2]] );
637
638 if( std::min( { a.dot( down ), b.dot( down ), c.dot( down ) } ) < level - epsilon )
639 continue;
640
641 double area = ( b - a ).cross( c - a ).norm() / 2;
642 accumulator += area * ( a + b + c ) / 3;
643 weight += area;
644 }
645
646 if( weight <= 0 )
647 {
648 for( const auto& vertex : region.vertices )
649 {
650 if( vec( vertex ).dot( down ) >= level - epsilon )
651 {
652 accumulator += vec( vertex );
653 weight += 1;
654 }
655 }
656 }
657
658 if( weight <= 0 )
659 continue;
660
661 CONTACT contact{ accumulator / weight, region.area };
662
663 if( i == aSeed )
664 seedContact = contact.position;
665
666 auto peer = std::find_if( contacts.begin(), contacts.end(),
667 [&]( const CONTACT& aContact )
668 {
669 return ( aContact.position - contact.position ).norm() < seed.extents.x / 2;
670 } );
671
672 if( peer == contacts.end() )
673 {
674 contacts.push_back( contact );
675 }
676 else
677 {
678 peer->position = ( peer->position * peer->weight + contact.position * contact.weight )
679 / ( peer->weight + contact.weight );
680 peer->weight += contact.weight;
681 }
682 }
683
684 if( contacts.empty() )
685 continue;
686
687 M3 seat = down.z() > 1 - 1e-12
688 ? Eigen::AngleAxisd( PI, V3::UnitX() ).toRotationMatrix()
689 : Eigen::Quaterniond::FromTwoVectors( down, V3( 0, 0, -1 ) ).toRotationMatrix();
690 BOX2 contactBox;
691 V2 centre = V2::Zero();
692 std::vector<V2> points;
693
694 for( const CONTACT& contact : contacts )
695 {
696 V3 point = seat * contact.position;
697 points.push_back( point.head<2>() );
698 contactBox.extend( points.back() );
699 centre += points.back();
700 }
701
702 centre /= points.size();
703 V2 contactExtents = contactBox.sizes();
704 V2 padExtents = padBox.sizes();
705 std::sort( contactExtents.data(), contactExtents.data() + 2 );
706 std::sort( padExtents.data(), padExtents.data() + 2 );
707 bool subset = contacts.size() < aPadGroup.size() && contacts.size() >= 3
708 && ( contactExtents - padExtents ).cwiseAbs().maxCoeff() <= seed.extents.x / 2;
709
710 if( ( contacts.size() != aPadGroup.size() && !subset ) || aPadGroup.size() == 1 )
711 {
712 double bestDistance = INF;
713 ALIGN_CANDIDATE best;
714 bool found = false;
715
716 for( double angle : { 0.0, PI / 2, PI, -PI / 2 } )
717 {
718 M3 rotation = Eigen::AngleAxisd( angle, V3::UnitZ() ).toRotationMatrix() * seat;
719 V3 offset = -rotation * seedContact;
720 offset.head<2>() += vec( aPadGroup.front().position );
721 offset.z() = 0;
722 ALIGN_CANDIDATE candidate{ rotation, offset, 1, 1, SOLUTION_KIND::SINGLE_PAIR };
723
724 if( !geometryGate( candidate, aRegions, aPadGroup, aAllPads, { { seedContact, 1 } }, level, down,
725 epsilon, grow ) )
726 continue;
727
728 V2 centre = bodyBox( aRegions, rotation, candidate.offset ).center();
729 double distance = ( centre - allPadBox.center() ).squaredNorm();
730
731 if( distance < bestDistance )
732 {
733 bestDistance = distance;
734 best = candidate;
735 best.turn = std::abs( angle );
736 found = true;
737 }
738 }
739
740 if( found )
741 single.push_back( best );
742
743 continue;
744 }
745
746 if( subset )
747 centre = contactBox.center();
748
749 V2 target = subset ? padBox.center() : padCentre;
750
751 for( V2& point : points )
752 point -= centre;
753
754 std::vector<double> angles;
755
756 if( down.cwiseAbs().maxCoeff() > std::cos( 0.5 * PI / 180 ) )
757 {
758 angles = { 0, PI / 2, PI, -PI / 2 };
759 }
760 else
761 {
762 auto farthest = std::max_element( points.begin(), points.end(),
763 []( const V2& a, const V2& b )
764 {
765 return a.squaredNorm() < b.squaredNorm();
766 } );
767 double maxPad = 0;
768
769 for( const PAD& pad : aPadGroup )
770 maxPad = std::max( { maxPad, pad.size.x, pad.size.y } );
771
772 for( const PAD& pad : aPadGroup )
773 {
774 V2 delta = vec( pad.position ) - target;
775
776 if( std::abs( delta.norm() - farthest->norm() ) > maxPad / 2 )
777 continue;
778
779 double angle = std::atan2( delta.y(), delta.x() ) - std::atan2( farthest->y(), farthest->x() );
780
781 if( std::none_of( angles.begin(), angles.end(),
782 [&]( double aOther )
783 {
784 return std::abs( std::remainder( angle - aOther, 2 * PI ) ) < PI / 180;
785 } ) )
786 angles.push_back( angle );
787 }
788 }
789
790 std::vector<const REGION*> copperRegions;
791
792 for( size_t i = 0; i < aRegions.size(); ++i )
793 {
794 if( std::abs( levels[i] - level ) <= epsilon && !congruent( seed, aRegions[i], length ) )
795 copperRegions.push_back( &aRegions[i] );
796 }
797
798 for( bool isPartial : { false, true } )
799 {
800 if( isPartial && !full.empty() )
801 break;
802
803 for( double angle : angles )
804 {
805 std::vector<V2> transformed;
806
807 for( const V2& point : points )
808 transformed.push_back( Eigen::Rotation2Dd( angle ) * point + target );
809
810 std::vector<int> assignment = match( transformed, aPadGroup, false, grow );
811
812 if( !isPartial && countMatches( assignment ) < points.size() )
813 continue;
814
815 V2 shift = V2::Zero();
816
817 if( isPartial )
818 {
819 for( int iteration = 0; iteration < 4; ++iteration )
820 {
821 std::vector<V2> shifted;
822
823 for( const V2& point : transformed )
824 shifted.push_back( point + shift );
825
826 assignment = match( shifted, aPadGroup, true, grow );
827 V2 accumulator = V2::Zero();
828 size_t count = countMatches( assignment );
829
830 for( size_t i = 0; i < shifted.size(); ++i )
831 {
832 if( assignment[i] >= 0 )
833 accumulator += vec( aPadGroup[assignment[i]].position ) - shifted[i];
834 }
835
836 if( count )
837 shift += accumulator / count;
838 }
839
840 if( minimumPitch < 0 )
841 {
842 minimumPitch = INF;
843
844 for( size_t i = 0; i < aPadGroup.size(); ++i )
845 {
846 for( size_t j = i + 1; j < aPadGroup.size(); ++j )
847 {
848 double distance = glm::length( aPadGroup[i].position - aPadGroup[j].position );
849
850 if( distance > 1e-6 )
851 minimumPitch = std::min( minimumPitch, distance );
852 }
853 }
854 }
855
856 if( countMatches( assignment ) < std::ceil( 0.9 * points.size() )
857 || shift.norm() >= minimumPitch / 2 )
858 continue;
859 }
860 else
861 {
862 double cross = 0;
863 double dot = 0;
864
865 for( size_t i = 0; i < points.size(); ++i )
866 {
867 V2 delta = vec( aPadGroup[assignment[i]].position ) - target;
868 cross += points[i].x() * delta.y() - points[i].y() * delta.x();
869 dot += points[i].dot( delta );
870 }
871
872 angle = std::atan2( cross, dot );
873 double snapped = std::round( angle / ( PI / 2 ) ) * ( PI / 2 );
874
875 if( std::abs( angle - snapped ) < 0.5 * PI / 180 )
876 angle = snapped;
877 }
878
879 M3 inPlane = Eigen::AngleAxisd( angle, V3::UnitZ() ).toRotationMatrix();
880 ALIGN_CANDIDATE candidate{ inPlane * seat,
881 V3( target.x() + shift.x(), target.y() + shift.y(), 0 )
882 - inPlane * V3( centre.x(), centre.y(), 0 ),
883 0, static_cast<unsigned int>( contacts.size() ),
884 isPartial ? SOLUTION_KIND::PARTIAL
885 : subset ? SOLUTION_KIND::SUBSET
887 candidate.turn = std::abs( std::remainder( angle, 2 * PI ) );
888
889 if( !isPartial )
890 centreSlack( candidate, aRegions, contacts, aPadGroup, allPadBox.center(), assignment );
891
892 if( !geometryGate( candidate, aRegions, aPadGroup, aAllPads, contacts, level, down, epsilon, grow ) )
893 continue;
894
895 for( const REGION* region : copperRegions )
896 {
897 V3 point = candidate.rotation * vec( region->centroid ) + candidate.offset;
898
899 if( std::any_of( aAllPads.begin(), aAllPads.end(),
900 [&]( const PAD& aPad )
901 {
902 return padContains( aPad, point.head<2>(), 0 );
903 } ) )
904 candidate.copper += region->area;
905 }
906
907 ( isPartial ? partial : full ).push_back( candidate );
908 }
909 }
910 }
911
912 std::vector<ALIGN_CANDIDATE>& candidates = !full.empty() ? full : !partial.empty() ? partial : single;
913 std::vector<ALIGN_CANDIDATE> unique;
914
915 for( const ALIGN_CANDIDATE& candidate : candidates )
916 {
917 if( !candidate.rotation.allFinite() || !candidate.offset.allFinite() )
918 continue;
919
920 bool duplicate = std::any_of( unique.begin(), unique.end(),
921 [&]( const ALIGN_CANDIDATE& aPrevious )
922 {
923 return ( candidate.rotation - aPrevious.rotation ).norm() < 1e-6
924 && ( candidate.offset - aPrevious.offset ).norm() < 1e-3;
925 } );
926
927 if( !duplicate )
928 unique.push_back( candidate );
929 }
930
931 std::stable_sort( unique.begin(), unique.end(),
932 []( const ALIGN_CANDIDATE& a, const ALIGN_CANDIDATE& b )
933 {
934 return a.copper > b.copper;
935 } );
936
937 // Fixed groups avoid the non-transitive pairwise 10% comparator.
938 for( auto first = unique.begin(); first != unique.end(); )
939 {
940 auto last = std::find_if( first, unique.end(),
941 [&]( const ALIGN_CANDIDATE& aCandidate )
942 {
943 return aCandidate.copper < 0.9 * first->copper;
944 } );
945 std::stable_sort( first, last,
946 []( const ALIGN_CANDIDATE& a, const ALIGN_CANDIDATE& b )
947 {
948 return a.turn < b.turn;
949 } );
950 first = last;
951 }
952
953 for( const ALIGN_CANDIDATE& candidate : unique )
954 {
955 // Eigen::eulerAngles() confines the first angle to [0, pi], which gives unfamiliar UI angles.
956 const M3& rotation = candidate.rotation;
957 glm::dvec3 angles;
958 angles.z = std::atan2( rotation( 1, 0 ), rotation( 0, 0 ) );
959 angles.y = std::atan2( -rotation( 2, 0 ), std::hypot( rotation( 2, 1 ), rotation( 2, 2 ) ) );
960 double sine = std::sin( angles.z );
961 double cosine = std::cos( angles.z );
962 angles.x = std::atan2( sine * rotation( 0, 2 ) - cosine * rotation( 1, 2 ),
963 cosine * rotation( 1, 1 ) - sine * rotation( 0, 1 ) );
964 angles *= 180 / PI;
965
966 for( int axis = 0; axis < 3; ++axis )
967 {
968 double snapped = std::round( angles[axis] / 90 ) * 90;
969
970 if( std::abs( angles[axis] - snapped ) < 1e-6 )
971 angles[axis] = snapped;
972
973 angles[axis] += 360 * std::round( ( aCurrentRotation[axis] - angles[axis] ) / 360 );
974 }
975
976 result.push_back( { angles, vec( candidate.offset ), candidate.matched, candidate.total, candidate.kind } );
977 }
978
979 return result;
980}
981} // namespace MODEL_ALIGN
define an internal structure to be used by the 3D renders
bool IsTriangleInRange(const unsigned int *aFaceIdx, unsigned int aTriangleIdx, unsigned int aVertexCount)
Test whether the three face indices of a triangle all reference valid vertices.
Definition c3dmodel.h:100
A 2D bounding box built on top of an origin point and size vector.
Definition box2.h:41
Lock-free disjoint-set over a dense range of indices.
Definition union_find.h:48
size_t FindCompress(size_t aX)
Shorten the path from aX to its root so that later queries walk less of it.
Definition union_find.h:150
bool Unite(size_t aA, size_t aB)
Merge the components that hold aA and aB.
Definition union_find.h:82
std::vector< REGION > BuildRegions(const S3DMODEL &aModel, const glm::dvec3 &aScale)
std::vector< PAD > BuildPadGroup(const std::vector< PAD > &aPads, size_t aClickedPad)
Return the clicked pad first, followed by its congruent, deduplicated peers.
std::vector< ALIGN_SOLUTION > SolveAlignment(const std::vector< REGION > &aRegions, size_t aSeed, const std::vector< PAD > &aPadGroup, const std::vector< PAD > &aAllPads, const glm::dvec3 &aCurrentRotation)
The first element of aPadGroup must be the clicked pad; all coordinates are millimetres,...
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
static float distance(const SFVEC2UI &a, const SFVEC2UI &b)
const double epsilon
PAD_ATTRIBUTE attribute
glm::dvec2 position
Footprint-local millimetres, Y up.
double rotation
Degrees in the Y-up frame.
unsigned int material
std::vector< std::array< unsigned int, 3 > > triangles
glm::dvec3 extents
Sorted PCA extents, largest first.
std::vector< std::array< unsigned int, 2 > > sourceTriangles
Mesh/triangle pairs, including reversed duplicates.
std::vector< glm::dvec3 > vertices
Scaled model coordinates, millimetres.
Store the a model based on meshes and materials.
Definition c3dmodel.h:111
unsigned int m_MeshesSize
Number of meshes in the array.
Definition c3dmodel.h:112
SMESH * m_Meshes
The meshes list of this model.
Definition c3dmodel.h:113
Per-vertex normal/color/texcoors structure.
Definition c3dmodel.h:77
unsigned int * m_FaceIdx
Triangle Face Indexes.
Definition c3dmodel.h:84
unsigned int m_MaterialIdx
Material Index to be used in this mesh (must be < m_MaterialsSize )
Definition c3dmodel.h:85
unsigned int m_VertexSize
Number of vertex in the arrays.
Definition c3dmodel.h:78
unsigned int m_FaceIdxSize
Number of elements of the m_FaceIdx array.
Definition c3dmodel.h:83
SFVEC3F * m_Positions
Vertex position array.
Definition c3dmodel.h:79
int radius
wxString result
Test unit parsing edge cases and error handling.
int delta