KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_diptrace_benchmarks.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 The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
29
31#include "wx_log_utils.h"
32
33#include <wx_log_utils.h>
34
35
36namespace
37{
38static constexpr int CONNECT_TOL_NM = 1000; // 1 um
39
40
41bool PointsNear( const VECTOR2I& aA, const VECTOR2I& aB )
42{
43 return ( aA - aB ).EuclideanNorm() <= CONNECT_TOL_NM;
44}
45
46
47bool IsHeuristicParserWarning( const wxString& aMessage )
48{
49 return aMessage.Contains( wxS( "design rule set" ) )
50 || aMessage.Contains( wxS( "inter-ruleset transition marker" ) )
51 || aMessage.Contains( wxS( "no validated component boundaries found" ) )
52 || aMessage.Contains( wxS( "parse error" ) )
53 || aMessage.Contains( wxS( "parsing failed" ) )
54 || aMessage.Contains( wxS( "outline traversal aborted" ) );
55}
56
57
58class DIPTRACE_WARNING_CAPTURE : public wxLog
59{
60public:
61 std::vector<wxString> m_warnings;
62
63protected:
64 void DoLogRecord( wxLogLevel aLevel, const wxString& aMessage,
65 const wxLogRecordInfo& ) override
66 {
67 if( aLevel == wxLOG_Warning )
68 m_warnings.push_back( aMessage );
69 }
70};
71
72
73int CountDisconnectedEdgeCutsEndpoints( const BOARD& aBoard, int& aTotalEndpoints )
74{
75 std::vector<VECTOR2I> endpoints;
76
77 for( const BOARD_ITEM* item : aBoard.Drawings() )
78 {
79 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != Edge_Cuts )
80 continue;
81
82 const PCB_SHAPE* shape = static_cast<const PCB_SHAPE*>( item );
83
84 if( shape->GetShape() != SHAPE_T::SEGMENT && shape->GetShape() != SHAPE_T::ARC )
85 continue;
86
87 endpoints.push_back( shape->GetStart() );
88 endpoints.push_back( shape->GetEnd() );
89 }
90
91 aTotalEndpoints = static_cast<int>( endpoints.size() );
92
93 int disconnected = 0;
94
95 for( size_t i = 0; i < endpoints.size(); i++ )
96 {
97 bool connected = false;
98
99 for( size_t j = 0; j < endpoints.size(); j++ )
100 {
101 if( i == j )
102 continue;
103
104 if( PointsNear( endpoints[i], endpoints[j] ) )
105 {
106 connected = true;
107 break;
108 }
109 }
110
111 if( !connected )
112 disconnected++;
113 }
114
115 return disconnected;
116}
117
118
119int CountDisconnectedTraceEndpoints( const BOARD& aBoard, int& aTotalEndpoints )
120{
121 std::map<int, std::vector<VECTOR2I>> netAnchors;
122 std::unordered_map<int, std::unordered_multimap<int64_t, VECTOR2I>> netAnchorBuckets;
123
124 auto cellCoord = []( int aValue ) -> int
125 {
126 if( aValue >= 0 )
127 return aValue / CONNECT_TOL_NM;
128
129 return -( ( -aValue + CONNECT_TOL_NM - 1 ) / CONNECT_TOL_NM );
130 };
131
132 auto cellKey = []( int aCellX, int aCellY ) -> int64_t
133 {
134 return ( static_cast<int64_t>( aCellX ) << 32 ) ^ static_cast<uint32_t>( aCellY );
135 };
136
137 auto addAnchor = [&]( int aNetCode, const VECTOR2I& aPos )
138 {
139 netAnchors[aNetCode].push_back( aPos );
140
141 int cx = cellCoord( aPos.x );
142 int cy = cellCoord( aPos.y );
143 netAnchorBuckets[aNetCode].emplace( cellKey( cx, cy ), aPos );
144 };
145
146 for( const PCB_TRACK* track : aBoard.Tracks() )
147 {
148 int netCode = track->GetNetCode();
149
150 if( netCode <= 0 )
151 continue;
152
153 if( track->Type() == PCB_TRACE_T || track->Type() == PCB_ARC_T )
154 {
155 addAnchor( netCode, track->GetStart() );
156 addAnchor( netCode, track->GetEnd() );
157 }
158 else if( track->Type() == PCB_VIA_T )
159 {
160 addAnchor( netCode, track->GetPosition() );
161 }
162 }
163
164 for( const FOOTPRINT* fp : aBoard.Footprints() )
165 {
166 for( const PAD* pad : fp->Pads() )
167 {
168 int netCode = pad->GetNetCode();
169
170 if( netCode > 0 )
171 addAnchor( netCode, pad->GetPosition() );
172 }
173 }
174
175 aTotalEndpoints = 0;
176 int disconnected = 0;
177
178 for( const PCB_TRACK* track : aBoard.Tracks() )
179 {
180 if( track->Type() != PCB_TRACE_T && track->Type() != PCB_ARC_T )
181 continue;
182
183 int netCode = track->GetNetCode();
184
185 if( netCode <= 0 )
186 continue;
187
188 auto bucketIt = netAnchorBuckets.find( netCode );
189
190 if( bucketIt == netAnchorBuckets.end() )
191 continue;
192
193 const auto& buckets = bucketIt->second;
194 const VECTOR2I endpoints[2] = { track->GetStart(), track->GetEnd() };
195
196 for( const VECTOR2I& endpoint : endpoints )
197 {
198 aTotalEndpoints++;
199
200 int nearbyCount = 0;
201 int cx = cellCoord( endpoint.x );
202 int cy = cellCoord( endpoint.y );
203
204 for( int dx = -1; dx <= 1 && nearbyCount < 2; dx++ )
205 {
206 for( int dy = -1; dy <= 1 && nearbyCount < 2; dy++ )
207 {
208 auto range = buckets.equal_range( cellKey( cx + dx, cy + dy ) );
209
210 for( auto it = range.first; it != range.second; ++it )
211 {
212 if( PointsNear( endpoint, it->second ) )
213 {
214 nearbyCount++;
215
216 if( nearbyCount >= 2 )
217 break;
218 }
219 }
220 }
221 }
222
223 if( nearbyCount < 2 )
224 disconnected++;
225 }
226 }
227
228 return disconnected;
229}
230
231
232int CountViaNetShorts( const BOARD& aBoard, int& aCheckedVias, std::vector<std::string>* aReports = nullptr )
233{
234 struct NET_ANCHOR
235 {
236 VECTOR2I pos;
237 int netCode = 0;
238 LSET layers;
239 };
240
241 std::vector<NET_ANCHOR> anchors;
242
243 for( const PCB_TRACK* track : aBoard.Tracks() )
244 {
245 int netCode = track->GetNetCode();
246
247 if( netCode <= 0 )
248 continue;
249
250 LSET copperLayers = track->GetLayerSet() & LSET::AllCuMask();
251
252 if( copperLayers.none() )
253 continue;
254
255 if( track->Type() == PCB_TRACE_T || track->Type() == PCB_ARC_T )
256 {
257 anchors.push_back( { track->GetStart(), netCode, copperLayers } );
258 anchors.push_back( { track->GetEnd(), netCode, copperLayers } );
259 }
260 else if( track->Type() == PCB_VIA_T )
261 {
262 anchors.push_back( { track->GetPosition(), netCode, copperLayers } );
263 }
264 }
265
266 for( const FOOTPRINT* fp : aBoard.Footprints() )
267 {
268 for( const PAD* pad : fp->Pads() )
269 {
270 int netCode = pad->GetNetCode();
271 LSET padLayers = pad->GetLayerSet() & LSET::AllCuMask();
272
273 if( netCode > 0 && !padLayers.none() )
274 anchors.push_back( { pad->GetPosition(), netCode, padLayers } );
275 }
276 }
277
278 int shortedVias = 0;
279 aCheckedVias = 0;
280
281 for( const PCB_TRACK* track : aBoard.Tracks() )
282 {
283 if( track->Type() != PCB_VIA_T )
284 continue;
285
286 const PCB_VIA* via = static_cast<const PCB_VIA*>( track );
287 int viaNet = via->GetNetCode();
288 LSET viaLayers = via->GetLayerSet() & LSET::AllCuMask();
289
290 if( viaNet <= 0 || viaLayers.none() )
291 continue;
292
293 aCheckedVias++;
294 bool shortFound = false;
295
296 for( const NET_ANCHOR& anchor : anchors )
297 {
298 if( anchor.netCode == viaNet )
299 continue;
300
301 if( ( viaLayers & anchor.layers ).none() )
302 continue;
303
304 if( PointsNear( via->GetPosition(), anchor.pos ) )
305 {
306 shortFound = true;
307
308 if( aReports && aReports->size() < 20 )
309 {
310 const NETINFO_ITEM* viaNetInfo = aBoard.FindNet( viaNet );
311 const NETINFO_ITEM* otherNetInfo = aBoard.FindNet( anchor.netCode );
312 std::string viaName =
313 viaNetInfo ? std::string( viaNetInfo->GetNetname().utf8_str() ) : std::to_string( viaNet );
314 std::string otherName = otherNetInfo ? std::string( otherNetInfo->GetNetname().utf8_str() )
315 : std::to_string( anchor.netCode );
316
317 aReports->push_back( "via(" + viaName + ") at (" + std::to_string( via->GetPosition().x ) + ","
318 + std::to_string( via->GetPosition().y ) + ") overlaps net " + otherName );
319 }
320
321 break;
322 }
323 }
324
325 if( shortFound )
326 shortedVias++;
327 }
328
329 return shortedVias;
330}
331
332
333int CountPadsOutsideBoardOutline( BOARD& aBoard, int& aTotalPads, bool& aHasOutline )
334{
335 SHAPE_POLY_SET boardOutline;
336 aHasOutline = aBoard.GetBoardPolygonOutlines( boardOutline, true ) && boardOutline.OutlineCount() > 0;
337
338 aTotalPads = 0;
339
340 if( !aHasOutline )
341 return 0;
342
343 int outsidePads = 0;
344
345 for( const FOOTPRINT* fp : aBoard.Footprints() )
346 {
347 for( const PAD* pad : fp->Pads() )
348 {
349 aTotalPads++;
350
351 if( !boardOutline.Contains( pad->GetPosition(), -1, CONNECT_TOL_NM ) )
352 outsidePads++;
353 }
354 }
355
356 return outsidePads;
357}
358
359
360bool IsRectLikeSmdPadShape( PAD_SHAPE aShape )
361{
362 return aShape == PAD_SHAPE::RECTANGLE || aShape == PAD_SHAPE::ROUNDRECT || aShape == PAD_SHAPE::OVAL;
363}
364
365
366bool HasDipExtension( const std::filesystem::path& aPath )
367{
368 std::string ext = aPath.extension().string();
369 std::transform( ext.begin(), ext.end(), ext.begin(),
370 []( unsigned char c )
371 {
372 return static_cast<char>( std::tolower( c ) );
373 } );
374 return ext == ".dip";
375}
376
377
378const FOOTPRINT* FindFootprintByRef( const BOARD& aBoard, const wxString& aRef )
379{
380 for( const FOOTPRINT* fp : aBoard.Footprints() )
381 {
382 if( fp->GetReference() == aRef )
383 return fp;
384 }
385
386 return nullptr;
387}
388
389
390int CardinalDeg( double aDegrees )
391{
392 int deg = static_cast<int>( std::lround( aDegrees ) );
393 deg = ( ( deg % 360 ) + 360 ) % 360;
394
395 int cardinal = static_cast<int>( std::lround( deg / 90.0 ) ) * 90;
396 return ( ( cardinal % 360 ) + 360 ) % 360;
397}
398
399
400int NormalizeDeg( double aDegrees )
401{
402 int deg = static_cast<int>( std::lround( aDegrees ) );
403 return ( ( deg % 360 ) + 360 ) % 360;
404}
405
406
407int DipXmlSpokeMode( std::string aSpoke )
408{
409 aSpoke.erase( std::remove_if( aSpoke.begin(), aSpoke.end(),
410 []( unsigned char c ) { return std::isspace( c ) != 0; } ),
411 aSpoke.end() );
412 std::transform( aSpoke.begin(), aSpoke.end(), aSpoke.begin(),
413 []( unsigned char c ) { return static_cast<char>( std::tolower( c ) ); } );
414
415 if( aSpoke == "direct" )
416 return 0;
417
418 if( aSpoke == "2spoke90" )
419 return 1;
420
421 if( aSpoke == "2spoke" )
422 return 2;
423
424 if( aSpoke == "4spoke45" )
425 return 3;
426
427 if( aSpoke == "4spoke" )
428 return 4;
429
430 return -1;
431}
432
433
434ISLAND_REMOVAL_MODE DipXmlIslandModeToKiCad( bool aIslandRegion, bool aIslandInternal,
435 bool aIslandConnection )
436{
437 if( aIslandInternal || aIslandConnection )
439
440 if( aIslandRegion )
442
444}
445
446
447long long DipXmlMinimumAreaToKiCadIu2( double aMinimumAreaMm )
448{
449 // Match importer conversion path:
450 // DipXML MinimumArea (mm scalar) -> DipTrace units -> KiCad IU -> IU^2.
451 long long dipUnits = static_cast<long long>( std::llround( aMinimumAreaMm * 30000.0 ) );
452 long long linearIu = dipUnits * 100 / 3;
453 return linearIu * linearIu;
454}
455
456
457struct DIPXML_PAD_STYLE
458{
459 bool isSurface = false;
460 bool isThrough = false;
461 bool isRoundHole = false;
462 double holeMm = 0.0;
463};
464
465
466struct DIPXML_PATTERN_PAD
467{
468 std::string styleName;
469 int angleCardinalDeg = 0;
470};
471
472
473struct DIPXML_BOARD_MODEL
474{
475 std::unordered_map<std::string, DIPXML_PAD_STYLE> styles;
476 std::unordered_map<std::string, std::unordered_map<std::string, DIPXML_PATTERN_PAD>> patterns;
477 std::vector<std::tuple<std::string, std::string, int>> components;
478 std::unordered_map<int, std::string> netNames;
479
480 struct DIPXML_COPPER_POUR
481 {
482 int netId = -1;
483 int layer = -1;
484 int priority = 0;
485 double clearanceMm = 0.0;
486 double lineWidthMm = 0.0;
487 double minimumAreaMm = 0.0;
488 std::string spoke;
489 double spokeWidthMm = 0.0;
490 bool islandRegion = false;
491 bool islandInternal = false;
492 bool islandConnection = false;
493 };
494
495 std::vector<DIPXML_COPPER_POUR> copperPours;
496 int traceViaPointsRaw = 0;
497 int traceViaPointsStyleZeroRaw = 0;
498 int traceViaPointsUniqueNetPos = 0;
499 int viaComponentCount = 0;
500};
501
502
503std::string ToUtf8( const wxString& aText )
504{
505 return std::string( aText.utf8_str() );
506}
507
508
509wxString ChildTextByName( const wxXmlNode* aParent, const wxString& aName )
510{
511 if( !aParent )
512 return wxString();
513
514 for( const wxXmlNode* child = aParent->GetChildren(); child; child = child->GetNext() )
515 {
516 if( child->GetType() == wxXML_ELEMENT_NODE && child->GetName() == aName )
517 {
518 wxString out;
519
520 for( const wxXmlNode* text = child->GetChildren(); text; text = text->GetNext() )
521 {
522 if( text->GetType() == wxXML_TEXT_NODE || text->GetType() == wxXML_CDATA_SECTION_NODE )
523 out += text->GetContent();
524 }
525
526 out.Trim( true );
527 out.Trim( false );
528 return out;
529 }
530 }
531
532 return wxString();
533}
534
535
536bool ParseDoubleAttr( const wxString& aRaw, double& aOut )
537{
538 wxString tmp = aRaw;
539 tmp.Trim( true );
540 tmp.Trim( false );
541
542 return !tmp.IsEmpty() && tmp.ToDouble( &aOut );
543}
544
545
546int CardinalDegFromRadians( const wxString& aRadiansRaw )
547{
548 double radians = 0.0;
549
550 if( !ParseDoubleAttr( aRadiansRaw, radians ) )
551 return 0;
552
553 return CardinalDeg( radians * 180.0 / M_PI );
554}
555
556
557int DipLayerIndexFromKiCadLayer( const BOARD& aBoard, PCB_LAYER_ID aLayer )
558{
559 int copperCount = static_cast<int>( aBoard.GetCopperLayerCount() );
560
561 if( copperCount < 2 )
562 return -1;
563
564 if( aLayer == F_Cu )
565 return 0;
566
567 if( aLayer == B_Cu )
568 return copperCount - 1;
569
570 if( aLayer >= In1_Cu && aLayer <= In30_Cu )
571 {
572 int innerDelta = static_cast<int>( aLayer - In1_Cu );
573
574 if( innerDelta % 2 != 0 )
575 return -1;
576
577 int idx = 1 + innerDelta / 2;
578 int maxInnerIdx = copperCount - 2;
579
580 if( idx >= 1 && idx <= maxInnerIdx )
581 return idx;
582 }
583
584 return -1;
585}
586
587
588bool LoadDipXmlModel( const std::string& aPath, DIPXML_BOARD_MODEL& aOut )
589{
590 wxXmlDocument doc;
591
592 if( !doc.Load( wxString::FromUTF8( aPath ) ) )
593 return false;
594
595 wxXmlNode* root = doc.GetRoot();
596
597 if( !root )
598 return false;
599
600 std::set<std::string> traceViaNetPointKeys;
601
602 std::function<void( wxXmlNode* )> walk = [&]( wxXmlNode* node )
603 {
604 for( ; node; node = node->GetNext() )
605 {
606 if( node->GetType() == wxXML_ELEMENT_NODE )
607 {
608 if( node->GetName() == wxT( "PadStyle" ) )
609 {
610 std::string styleName = ToUtf8( node->GetAttribute( wxT( "Name" ), wxString() ) );
611
612 if( !styleName.empty() )
613 {
614 DIPXML_PAD_STYLE style;
615 wxString type = node->GetAttribute( wxT( "Type" ), wxString() );
616 wxString holeType = node->GetAttribute( wxT( "HoleType" ), wxString() );
617 style.isSurface = ( type.CmpNoCase( wxT( "Surface" ) ) == 0 );
618 style.isThrough = ( type.CmpNoCase( wxT( "Through" ) ) == 0 );
619 style.isRoundHole = ( holeType.CmpNoCase( wxT( "Round" ) ) == 0 );
620 ParseDoubleAttr( node->GetAttribute( wxT( "Hole" ), wxT( "0" ) ), style.holeMm );
621 aOut.styles[styleName] = style;
622 }
623 }
624 else if( node->GetName() == wxT( "Pattern" ) )
625 {
626 std::string patternStyle = ToUtf8( node->GetAttribute( wxT( "PatternStyle" ), wxString() ) );
627
628 if( !patternStyle.empty() )
629 {
630 auto& padMap = aOut.patterns[patternStyle];
631 wxXmlNode* padsNode = nullptr;
632
633 for( wxXmlNode* child = node->GetChildren(); child; child = child->GetNext() )
634 {
635 if( child->GetType() == wxXML_ELEMENT_NODE && child->GetName() == wxT( "Pads" ) )
636 {
637 padsNode = child;
638 break;
639 }
640 }
641
642 if( padsNode )
643 {
644 for( wxXmlNode* padNode = padsNode->GetChildren(); padNode; padNode = padNode->GetNext() )
645 {
646 if( padNode->GetType() != wxXML_ELEMENT_NODE || padNode->GetName() != wxT( "Pad" ) )
647 {
648 continue;
649 }
650
651 wxString padKey = ChildTextByName( padNode, wxT( "Number" ) );
652
653 if( padKey.IsEmpty() )
654 padKey = padNode->GetAttribute( wxT( "Id" ), wxString() );
655
656 std::string key = ToUtf8( padKey );
657
658 if( key.empty() )
659 continue;
660
661 DIPXML_PATTERN_PAD pad;
662 pad.styleName = ToUtf8( padNode->GetAttribute( wxT( "Style" ), wxString() ) );
663 pad.angleCardinalDeg =
664 CardinalDegFromRadians( padNode->GetAttribute( wxT( "Angle" ), wxT( "0" ) ) );
665 padMap[key] = pad;
666 }
667 }
668 }
669 }
670 else if( node->GetName() == wxT( "Component" ) )
671 {
672 if( node->GetAttribute( wxT( "Type" ), wxString() ).CmpNoCase( wxT( "Via" ) ) == 0 )
673 aOut.viaComponentCount++;
674
675 wxString ref = ChildTextByName( node, wxT( "RefDes" ) );
676
677 if( !ref.IsEmpty() )
678 {
679 std::string refUtf8 = ToUtf8( ref );
680 std::string patternStyle = ToUtf8( node->GetAttribute( wxT( "PatternStyle" ), wxString() ) );
681 int angleCardinal = CardinalDegFromRadians( node->GetAttribute( wxT( "Angle" ), wxT( "0" ) ) );
682 aOut.components.emplace_back( std::move( refUtf8 ), std::move( patternStyle ), angleCardinal );
683 }
684 }
685 else if( node->GetName() == wxT( "Net" ) )
686 {
687 long netId = -1;
688
689 if( node->GetAttribute( wxT( "Id" ), wxString() ).ToLong( &netId ) )
690 {
691 wxString netName = ChildTextByName( node, wxT( "Name" ) );
692 aOut.netNames[static_cast<int>( netId )] = ToUtf8( netName );
693
694 for( wxXmlNode* child = node->GetChildren(); child; child = child->GetNext() )
695 {
696 if( child->GetType() != wxXML_ELEMENT_NODE || child->GetName() != wxT( "Traces" ) )
697 continue;
698
699 for( wxXmlNode* traceNode = child->GetChildren(); traceNode;
700 traceNode = traceNode->GetNext() )
701 {
702 if( traceNode->GetType() != wxXML_ELEMENT_NODE
703 || traceNode->GetName() != wxT( "Trace" ) )
704 {
705 continue;
706 }
707
708 for( wxXmlNode* traceChild = traceNode->GetChildren(); traceChild;
709 traceChild = traceChild->GetNext() )
710 {
711 if( traceChild->GetType() != wxXML_ELEMENT_NODE
712 || traceChild->GetName() != wxT( "Points" ) )
713 {
714 continue;
715 }
716
717 for( wxXmlNode* pointNode = traceChild->GetChildren(); pointNode;
718 pointNode = pointNode->GetNext() )
719 {
720 if( pointNode->GetType() != wxXML_ELEMENT_NODE
721 || pointNode->GetName() != wxT( "Point" ) )
722 {
723 continue;
724 }
725
726 wxString viaStyle = pointNode->GetAttribute( wxT( "ViaStyle" ), wxString() );
727
728 if( viaStyle.IsEmpty() )
729 continue;
730
731 aOut.traceViaPointsRaw++;
732
733 long viaStyleId = -1;
734
735 if( viaStyle.ToLong( &viaStyleId ) && viaStyleId == 0 )
736 aOut.traceViaPointsStyleZeroRaw++;
737
738 std::string key = std::to_string( netId ) + "|"
739 + ToUtf8( pointNode->GetAttribute( wxT( "X" ), wxString() ) )
740 + "|"
741 + ToUtf8( pointNode->GetAttribute( wxT( "Y" ), wxString() ) );
742 traceViaNetPointKeys.insert( std::move( key ) );
743 }
744 }
745 }
746 }
747 }
748 }
749 else if( node->GetName() == wxT( "CopperPour" ) )
750 {
751 long netId = -1;
752 long lay = -1;
753 double clearance = 0.0;
754 double lineWidth = 0.0;
755 double minimumArea = 0.0;
756 double spokeWidth = 0.0;
757
758 if( node->GetAttribute( wxT( "NetId" ), wxString() ).ToLong( &netId )
759 && node->GetAttribute( wxT( "Lay" ), wxString() ).ToLong( &lay ) )
760 {
761 ParseDoubleAttr( node->GetAttribute( wxT( "Clearance" ), wxT( "0" ) ), clearance );
762 ParseDoubleAttr( node->GetAttribute( wxT( "LineWidth" ), wxT( "0" ) ), lineWidth );
763 ParseDoubleAttr( node->GetAttribute( wxT( "MinimumArea" ), wxT( "0" ) ), minimumArea );
764 ParseDoubleAttr( node->GetAttribute( wxT( "SpokeWidth" ), wxT( "0" ) ), spokeWidth );
765
766 long priority = 0;
767 node->GetAttribute( wxT( "Priority" ), wxT( "0" ) ).ToLong( &priority );
768
769 DIPXML_BOARD_MODEL::DIPXML_COPPER_POUR pour;
770 pour.netId = static_cast<int>( netId );
771 pour.layer = static_cast<int>( lay );
772 pour.priority = static_cast<int>( priority );
773 pour.clearanceMm = clearance;
774 pour.lineWidthMm = lineWidth;
775 pour.minimumAreaMm = minimumArea;
776 pour.spoke = ToUtf8( node->GetAttribute( wxT( "Spoke" ), wxString() ) );
777 pour.spokeWidthMm = spokeWidth;
778 pour.islandRegion =
779 node->GetAttribute( wxT( "IslandRegion" ), wxT( "N" ) ).CmpNoCase( wxT( "Y" ) ) == 0;
780 pour.islandInternal =
781 node->GetAttribute( wxT( "IslandInternal" ), wxT( "N" ) ).CmpNoCase( wxT( "Y" ) ) == 0;
782 pour.islandConnection = node->GetAttribute( wxT( "IslandConnection" ), wxT( "N" ) )
783 .CmpNoCase( wxT( "Y" ) )
784 == 0;
785 aOut.copperPours.push_back( pour );
786 }
787 }
788 }
789
790 if( node->GetChildren() )
791 walk( node->GetChildren() );
792 }
793 };
794
795 walk( root );
796 aOut.traceViaPointsUniqueNetPos = static_cast<int>( traceViaNetPointKeys.size() );
797 return true;
798}
799} // namespace
800
801
802BOOST_FIXTURE_TEST_SUITE( DipTraceBenchmarks, DIPTRACE_BENCHMARK_FIXTURE )
803
804
805
811BOOST_AUTO_TEST_CASE( TotalPadCount )
812{
813 auto board = LoadBoard( "z80_board.dip" );
814 BOOST_REQUIRE( board );
815
816 int totalPads = 0;
817
818 for( const FOOTPRINT* fp : board->Footprints() )
819 totalPads += static_cast<int>( fp->Pads().size() );
820
821 BOOST_CHECK_MESSAGE( totalPads > 400, "Z80 board should have >400 total pads, got " + std::to_string( totalPads ) );
822}
823
824
830BOOST_AUTO_TEST_CASE( MultiPinFootprints )
831{
832 auto board = LoadBoard( "z80_board.dip" );
833 BOOST_REQUIRE( board );
834
835 int maxPads = 0;
836 int icCount = 0;
837
838 for( const FOOTPRINT* fp : board->Footprints() )
839 {
840 int padCount = static_cast<int>( fp->Pads().size() );
841
842 if( padCount > maxPads )
843 maxPads = padCount;
844
845 if( padCount >= 14 )
846 icCount++;
847 }
848
849 BOOST_CHECK_MESSAGE( maxPads >= 28, "Z80 board should have a footprint with >=28 pads (Z80 DIP-40), "
850 "max found: "
851 + std::to_string( maxPads ) );
852
853 BOOST_CHECK_MESSAGE( icCount >= 5, "Z80 board should have >=5 footprints with >=14 pads (ICs), "
854 "found: "
855 + std::to_string( icCount ) );
856}
857
858
865BOOST_AUTO_TEST_CASE( PadDimensionsReasonable )
866{
867 auto board = LoadBoard( "z80_board.dip" );
868 BOOST_REQUIRE( board );
869
870 int totalPads = 0;
871 int reasonablePads = 0;
872 int tinyPads = 0;
873
874 for( const FOOTPRINT* fp : board->Footprints() )
875 {
876 for( const PAD* pad : fp->Pads() )
877 {
878 totalPads++;
879
880 VECTOR2I size = pad->GetSize( PADSTACK::ALL_LAYERS );
881 double widthMm = pcbIUScale.IUTomm( size.x );
882 double heightMm = pcbIUScale.IUTomm( size.y );
883 double maxDim = std::max( widthMm, heightMm );
884
885 if( maxDim >= 0.8 && maxDim <= 5.0 )
886 reasonablePads++;
887
888 if( maxDim < 0.5 )
889 tinyPads++;
890 }
891 }
892
893 BOOST_REQUIRE_GT( totalPads, 0 );
894
895 double reasonablePercent = 100.0 * reasonablePads / totalPads;
896 double tinyPercent = 100.0 * tinyPads / totalPads;
897
898 BOOST_CHECK_MESSAGE( reasonablePercent > 50.0,
899 "At least 50% of pads should be 0.8-5.0mm, got " + std::to_string( reasonablePercent ) + "% ("
900 + std::to_string( reasonablePads ) + "/" + std::to_string( totalPads ) + ")" );
901
902 BOOST_CHECK_MESSAGE( tinyPercent < 20.0,
903 "Less than 20% of pads should be <0.5mm, got " + std::to_string( tinyPercent ) + "% ("
904 + std::to_string( tinyPads ) + "/" + std::to_string( totalPads ) + ")" );
905}
906
907
913BOOST_AUTO_TEST_CASE( DipPinSpacing )
914{
915 auto board = LoadBoard( "z80_board.dip" );
916 BOOST_REQUIRE( board );
917
918 bool foundGoodSpacing = false;
919 double toleranceMm = 0.3;
920
921 for( const FOOTPRINT* fp : board->Footprints() )
922 {
923 if( fp->Pads().size() < 14 )
924 continue;
925
926 // Collect pad local positions (relative to footprint origin)
927 std::vector<VECTOR2I> positions;
928
929 for( const PAD* pad : fp->Pads() )
930 {
931 VECTOR2I local = pad->GetPosition() - fp->GetPosition();
932 positions.push_back( local );
933 }
934
935 // Sort by Y then X to get column ordering
936 std::sort( positions.begin(), positions.end(),
937 []( const VECTOR2I& a, const VECTOR2I& b )
938 {
939 if( std::abs( a.x - b.x ) < 100000 )
940 return a.y < b.y;
941
942 return a.x < b.x;
943 } );
944
945 // Check adjacent spacing within the same column
946 for( size_t i = 1; i < positions.size(); i++ )
947 {
948 if( std::abs( positions[i].x - positions[i - 1].x ) > 100000 )
949 continue;
950
951 double spacingMm = pcbIUScale.IUTomm( std::abs( positions[i].y - positions[i - 1].y ) );
952
953 if( std::abs( spacingMm - 2.54 ) < toleranceMm )
954 {
955 foundGoodSpacing = true;
956 break;
957 }
958 }
959
960 if( foundGoodSpacing )
961 break;
962 }
963
964 BOOST_CHECK_MESSAGE( foundGoodSpacing, "At least one multi-pin IC should have ~2.54mm DIP pin spacing" );
965}
966
967
972BOOST_AUTO_TEST_CASE( PadNetAssignment )
973{
974 auto board = LoadBoard( "z80_board.dip" );
975 BOOST_REQUIRE( board );
976
977 int totalPads = 0;
978 int padsWithNets = 0;
979
980 for( const FOOTPRINT* fp : board->Footprints() )
981 {
982 for( const PAD* pad : fp->Pads() )
983 {
984 totalPads++;
985
986 if( pad->GetNetCode() > 0 )
987 padsWithNets++;
988 }
989 }
990
991 BOOST_REQUIRE_GT( totalPads, 0 );
992
993 double netPercent = 100.0 * padsWithNets / totalPads;
994
995 BOOST_CHECK_MESSAGE( padsWithNets > 0, "At least some pads should have net assignments, got 0 out of "
996 + std::to_string( totalPads ) );
997
998 BOOST_CHECK_MESSAGE( netPercent > 30.0,
999 "At least 30% of pads should have nets, got " + std::to_string( netPercent ) + "%" );
1000}
1001
1002
1007BOOST_AUTO_TEST_CASE( KnownNetsOnPads )
1008{
1009 auto board = LoadBoard( "z80_board.dip" );
1010 BOOST_REQUIRE( board );
1011
1012 std::set<wxString> padNetNames;
1013
1014 for( const FOOTPRINT* fp : board->Footprints() )
1015 {
1016 for( const PAD* pad : fp->Pads() )
1017 {
1018 if( pad->GetNetCode() > 0 )
1019 padNetNames.insert( pad->GetNet()->GetNetname() );
1020 }
1021 }
1022
1023 BOOST_CHECK_MESSAGE( padNetNames.count( wxT( "GND" ) ) > 0, "GND should appear on at least one pad" );
1024
1025 BOOST_CHECK_MESSAGE( padNetNames.count( wxT( "A0" ) ) > 0, "A0 should appear on at least one pad" );
1026
1027 BOOST_CHECK_MESSAGE( padNetNames.count( wxT( "D0" ) ) > 0, "D0 should appear on at least one pad" );
1028}
1029
1030
1035BOOST_AUTO_TEST_CASE( CrossVersionPadConsistency )
1036{
1037 struct TestCase
1038 {
1039 std::string file;
1040 int minPads;
1041 int componentCount;
1042 };
1043
1044 std::vector<TestCase> cases = {
1045 { "project4.dip", 30, 27 }, { "z80_board.dip", 200, 104 }, { "logic_probe.dip", 100, 113 },
1046 { "keyboard.dip", 200, 123 }, { "156bus_narrow.dip", 20, 17 },
1047 };
1048
1049 for( const TestCase& tc : cases )
1050 {
1051 auto board = LoadBoard( tc.file );
1052 BOOST_REQUIRE_MESSAGE( board, "Failed to load " + tc.file );
1053
1054 int totalPads = 0;
1055
1056 for( const FOOTPRINT* fp : board->Footprints() )
1057 totalPads += static_cast<int>( fp->Pads().size() );
1058
1059 BOOST_CHECK_MESSAGE( totalPads >= tc.minPads, tc.file + ": expected >=" + std::to_string( tc.minPads )
1060 + " pads, got " + std::to_string( totalPads ) );
1061 }
1062}
1063
1064
1070BOOST_AUTO_TEST_CASE( TrackSegmentCounts )
1071{
1072 struct TestCase
1073 {
1074 std::string file;
1075 int minTracks;
1076 };
1077
1078 std::vector<TestCase> cases = {
1079 { "project4.dip", 100 }, { "156bus_narrow.dip", 30 }, { "z80_board.dip", 1500 },
1080 { "logic_probe.dip", 400 }, { "keyboard.dip", 400 },
1081 };
1082
1083 for( const TestCase& tc : cases )
1084 {
1085 auto board = LoadBoard( tc.file );
1086 BOOST_REQUIRE_MESSAGE( board, "Failed to load " + tc.file );
1087
1088 int trackCount = 0;
1089
1090 for( const PCB_TRACK* trk : board->Tracks() )
1091 {
1092 if( trk->Type() == PCB_TRACE_T )
1093 trackCount++;
1094 }
1095
1096 BOOST_CHECK_MESSAGE( trackCount >= tc.minTracks, tc.file + ": expected >=" + std::to_string( tc.minTracks )
1097 + " tracks, got " + std::to_string( trackCount ) );
1098 }
1099}
1100
1101
1108{
1109 struct TestCase
1110 {
1111 std::string file;
1112 int minVias;
1113 int maxVias;
1114 };
1115
1116 // After the standalone-via classification fix, single-pad Pad/Fiducial components become
1117 // footprints instead of bare vias, and Static Via components remain vias. Observed counts
1118 // stay inside these ranges (project4=63, z80=463, logic_probe=90, 156bus=0, keyboard=20).
1119 std::vector<TestCase> cases = {
1120 { "project4.dip", 50, 200 }, { "z80_board.dip", 400, 1000 }, { "logic_probe.dip", 10, 100 },
1121 { "156bus_narrow.dip", 0, 5 }, { "keyboard.dip", 10, 40 },
1122 };
1123
1124 for( const TestCase& tc : cases )
1125 {
1126 auto board = LoadBoard( tc.file );
1127 BOOST_REQUIRE_MESSAGE( board, "Failed to load " + tc.file );
1128
1129 int viaCount = 0;
1130
1131 for( const PCB_TRACK* trk : board->Tracks() )
1132 {
1133 if( trk->Type() == PCB_VIA_T )
1134 viaCount++;
1135 }
1136
1137 BOOST_CHECK_MESSAGE( viaCount >= tc.minVias, tc.file + ": expected >=" + std::to_string( tc.minVias )
1138 + " vias, got " + std::to_string( viaCount ) );
1139
1140 BOOST_CHECK_MESSAGE( viaCount <= tc.maxVias, tc.file + ": expected <=" + std::to_string( tc.maxVias )
1141 + " vias, got " + std::to_string( viaCount ) );
1142 }
1143}
1144
1145
1151BOOST_AUTO_TEST_CASE( TrackAndViaDimensionsReasonable )
1152{
1153 auto board = LoadBoard( "z80_board.dip" );
1154 BOOST_REQUIRE( board );
1155
1156 int totalTracks = 0;
1157 int reasonableTracks = 0;
1158 int totalVias = 0;
1159 int reasonableVias = 0;
1160
1161 for( const PCB_TRACK* trk : board->Tracks() )
1162 {
1163 if( trk->Type() == PCB_TRACE_T )
1164 {
1165 totalTracks++;
1166 double widthMm = pcbIUScale.IUTomm( trk->GetWidth() );
1167
1168 if( widthMm >= 0.1 && widthMm <= 3.0 )
1169 reasonableTracks++;
1170 }
1171 else if( trk->Type() == PCB_VIA_T )
1172 {
1173 totalVias++;
1174 const PCB_VIA* via = static_cast<const PCB_VIA*>( trk );
1175 double diamMm = pcbIUScale.IUTomm( via->GetWidth( F_Cu ) );
1176
1177 if( diamMm >= 0.3 && diamMm <= 2.0 )
1178 reasonableVias++;
1179 }
1180 }
1181
1182 if( totalTracks > 0 )
1183 {
1184 double pct = 100.0 * reasonableTracks / totalTracks;
1185
1186 BOOST_CHECK_MESSAGE( pct > 90.0, "At least 90% of tracks should have reasonable widths (0.1-3.0mm), got "
1187 + std::to_string( pct ) + "%" );
1188 }
1189
1190 if( totalVias > 0 )
1191 {
1192 double pct = 100.0 * reasonableVias / totalVias;
1193
1194 BOOST_CHECK_MESSAGE( pct > 90.0, "At least 90% of vias should have reasonable diameters (0.3-2.0mm), got "
1195 + std::to_string( pct ) + "%" );
1196 }
1197}
1198
1199
1204BOOST_AUTO_TEST_CASE( TrackNetAssignment )
1205{
1206 auto board = LoadBoard( "z80_board.dip" );
1207 BOOST_REQUIRE( board );
1208
1209 int totalTracks = 0;
1210 int tracksWithNets = 0;
1211 std::set<wxString> trackNetNames;
1212
1213 for( const PCB_TRACK* trk : board->Tracks() )
1214 {
1215 if( trk->Type() == PCB_TRACE_T )
1216 {
1217 totalTracks++;
1218
1219 if( trk->GetNetCode() > 0 )
1220 {
1221 tracksWithNets++;
1222 trackNetNames.insert( trk->GetNet()->GetNetname() );
1223 }
1224 }
1225 }
1226
1227 BOOST_REQUIRE_GT( totalTracks, 0 );
1228
1229 double netPct = 100.0 * tracksWithNets / totalTracks;
1230
1231 BOOST_CHECK_MESSAGE( netPct > 90.0,
1232 "At least 90% of tracks should have net assignments, got " + std::to_string( netPct ) + "%" );
1233
1234 BOOST_CHECK_MESSAGE( trackNetNames.count( wxT( "GND" ) ) > 0, "GND net should appear on tracks" );
1235
1236 BOOST_CHECK_MESSAGE( trackNetNames.count( wxT( "A0" ) ) > 0, "A0 net should appear on tracks" );
1237}
1238
1239
1244{
1245 struct TestCase
1246 {
1247 std::string file;
1248 int expected;
1249 };
1250
1251 std::vector<TestCase> cases = {
1252 { "project4.dip", 0 }, { "156bus_narrow.dip", 1 }, { "z80_board.dip", 2 },
1253 { "logic_probe.dip", 2 }, { "keyboard.dip", 1 },
1254 };
1255
1256 for( const TestCase& tc : cases )
1257 {
1258 auto board = LoadBoard( tc.file );
1259 BOOST_REQUIRE_MESSAGE( board, "Failed to load " + tc.file );
1260
1261 int zoneCount = static_cast<int>( board->Zones().size() );
1262
1263 BOOST_CHECK_MESSAGE( zoneCount == tc.expected, tc.file + ": expected " + std::to_string( tc.expected )
1264 + " zones, got " + std::to_string( zoneCount ) );
1265 }
1266}
1267
1268
1273BOOST_AUTO_TEST_CASE( ZoneNetAssignment )
1274{
1275 auto board = LoadBoard( "z80_board.dip" );
1276 BOOST_REQUIRE( board );
1277
1278 int zonesWithNets = 0;
1279 std::set<wxString> zoneNetNames;
1280
1281 for( const ZONE* zone : board->Zones() )
1282 {
1283 if( zone->GetNetCode() > 0 )
1284 {
1285 zonesWithNets++;
1286 zoneNetNames.insert( zone->GetNet()->GetNetname() );
1287 }
1288 }
1289
1290 BOOST_CHECK_MESSAGE( zonesWithNets == static_cast<int>( board->Zones().size() ),
1291 "All zones should have net assignments, got " + std::to_string( zonesWithNets ) + "/"
1292 + std::to_string( board->Zones().size() ) );
1293
1294 BOOST_CHECK_MESSAGE( zoneNetNames.size() >= 1, "At least 1 distinct net should appear on zones" );
1295}
1296
1297
1303BOOST_AUTO_TEST_CASE( ZoneOutlineDimensions )
1304{
1305 auto board = LoadBoard( "z80_board.dip" );
1306 BOOST_REQUIRE( board );
1307 BOOST_REQUIRE_GT( board->Zones().size(), 0u );
1308
1309 for( const ZONE* zone : board->Zones() )
1310 {
1311 const SHAPE_POLY_SET* outline = zone->Outline();
1312 BOOST_REQUIRE( outline );
1313 BOOST_REQUIRE_GT( outline->OutlineCount(), 0 );
1314
1315 int vertexCount = outline->COutline( 0 ).PointCount();
1316
1317 BOOST_CHECK_MESSAGE( vertexCount >= 3,
1318 "Zone outline should have at least 3 vertices, got " + std::to_string( vertexCount ) );
1319
1320 BOX2I bbox = outline->BBox();
1321 double widthMm = pcbIUScale.IUTomm( bbox.GetWidth() );
1322 double heightMm = pcbIUScale.IUTomm( bbox.GetHeight() );
1323
1324 BOOST_CHECK_MESSAGE( widthMm > 5.0, "Zone width should be >5mm, got " + std::to_string( widthMm ) + "mm" );
1325
1326 BOOST_CHECK_MESSAGE( heightMm > 5.0, "Zone height should be >5mm, got " + std::to_string( heightMm ) + "mm" );
1327 }
1328}
1329
1330
1334BOOST_AUTO_TEST_CASE( ZoneLayerAssignment )
1335{
1336 auto board = LoadBoard( "z80_board.dip" );
1337 BOOST_REQUIRE( board );
1338
1339 for( const ZONE* zone : board->Zones() )
1340 {
1341 PCB_LAYER_ID layer = zone->GetFirstLayer();
1342
1343 BOOST_CHECK_MESSAGE( IsCopperLayer( layer ), "Zone should be on a copper layer, got layer "
1344 + std::to_string( static_cast<int>( layer ) ) );
1345 }
1346}
1347
1348
1353BOOST_AUTO_TEST_CASE( FootprintGraphics )
1354{
1355 auto board = LoadBoard( "z80_board.dip" );
1356 BOOST_REQUIRE( board );
1357
1358 int footprintsWithGraphics = 0;
1359 int totalGraphics = 0;
1360
1361 for( const FOOTPRINT* fp : board->Footprints() )
1362 {
1363 int graphicCount = 0;
1364
1365 for( const BOARD_ITEM* item : fp->GraphicalItems() )
1366 {
1367 if( item->Type() == PCB_SHAPE_T )
1368 graphicCount++;
1369 }
1370
1371 if( graphicCount > 0 )
1372 {
1373 footprintsWithGraphics++;
1374 totalGraphics += graphicCount;
1375 }
1376 }
1377
1378 BOOST_CHECK_MESSAGE( footprintsWithGraphics > 0, "At least some footprints should have outline graphics, got 0" );
1379
1380 BOOST_CHECK_MESSAGE( totalGraphics > 10,
1381 "Board should have >10 total footprint graphics, got " + std::to_string( totalGraphics ) );
1382}
1383
1384
1390BOOST_AUTO_TEST_CASE( FootprintGraphicsDimensions )
1391{
1392 auto board = LoadBoard( "z80_board.dip" );
1393 BOOST_REQUIRE( board );
1394
1395 int reasonableCount = 0;
1396 int tinyCount = 0;
1397
1398 for( const FOOTPRINT* fp : board->Footprints() )
1399 {
1400 bool hasGraphics = false;
1401
1402 for( const BOARD_ITEM* item : fp->GraphicalItems() )
1403 {
1404 if( item->Type() == PCB_SHAPE_T )
1405 {
1406 hasGraphics = true;
1407 break;
1408 }
1409 }
1410
1411 if( !hasGraphics )
1412 continue;
1413
1414 BOX2I gfxBbox;
1415 bool first = true;
1416
1417 for( const BOARD_ITEM* item : fp->GraphicalItems() )
1418 {
1419 if( item->Type() == PCB_SHAPE_T )
1420 {
1421 const PCB_SHAPE* shape = static_cast<const PCB_SHAPE*>( item );
1422
1423 if( first )
1424 {
1425 gfxBbox = shape->GetBoundingBox();
1426 first = false;
1427 }
1428 else
1429 {
1430 gfxBbox.Merge( shape->GetBoundingBox() );
1431 }
1432 }
1433 }
1434
1435 double widthMm = pcbIUScale.IUTomm( gfxBbox.GetWidth() );
1436 double heightMm = pcbIUScale.IUTomm( gfxBbox.GetHeight() );
1437 double maxDim = std::max( widthMm, heightMm );
1438
1439 if( maxDim >= 2.0 && maxDim <= 80.0 )
1440 reasonableCount++;
1441 else if( maxDim < 0.5 )
1442 tinyCount++;
1443 }
1444
1445 BOOST_CHECK_MESSAGE( reasonableCount > 0, "At least some footprints should have reasonably-sized "
1446 "outline graphics (2-80mm)" );
1447
1448 BOOST_CHECK_MESSAGE( tinyCount == 0,
1449 "No footprint graphics should be tiny (<0.5mm), got " + std::to_string( tinyCount ) );
1450}
1451
1452
1457BOOST_AUTO_TEST_CASE( EdgeCutsOutlineConnectivity )
1458{
1459 const std::vector<std::string> files = {
1460 "project4.dip", "156bus_narrow.dip", "z80_board.dip", "logic_probe.dip", "keyboard.dip",
1461 };
1462
1463 for( const std::string& file : files )
1464 {
1465 auto board = LoadBoard( file );
1466 BOOST_REQUIRE_MESSAGE( board, "Failed to load " + file );
1467
1468 int totalEndpoints = 0;
1469 int disconnected = CountDisconnectedEdgeCutsEndpoints( *board, totalEndpoints );
1470
1471 BOOST_CHECK_MESSAGE( totalEndpoints > 0, file + ": expected Edge.Cuts endpoints, got 0" );
1472
1473 BOOST_CHECK_MESSAGE( disconnected == 0,
1474 file + ": expected contiguous outline; found " + std::to_string( disconnected )
1475 + " disconnected endpoints out of " + std::to_string( totalEndpoints ) );
1476 }
1477}
1478
1479
1484BOOST_AUTO_TEST_CASE( TraceEndpointConnectivity )
1485{
1486 const std::vector<std::string> files = {
1487 "z80_board.dip",
1488 "logic_probe.dip",
1489 "keyboard.dip",
1490 };
1491
1492 for( const std::string& file : files )
1493 {
1494 auto board = LoadBoard( file );
1495 BOOST_REQUIRE_MESSAGE( board, "Failed to load " + file );
1496
1497 int totalEndpoints = 0;
1498 int disconnected = CountDisconnectedTraceEndpoints( *board, totalEndpoints );
1499 BOOST_REQUIRE_MESSAGE( totalEndpoints > 0, file + ": expected routed trace endpoints, got 0" );
1500
1501 double disconnectedPct = 100.0 * disconnected / totalEndpoints;
1502
1503 BOOST_CHECK_MESSAGE( disconnectedPct <= 15.0,
1504 file + ": disconnected trace endpoints = " + std::to_string( disconnected ) + "/"
1505 + std::to_string( totalEndpoints ) + " (" + std::to_string( disconnectedPct )
1506 + "%)" );
1507 }
1508}
1509
1510
1515BOOST_AUTO_TEST_CASE( Smd0805PadSanity )
1516{
1517 const std::vector<std::string> files = {
1518 "logic_probe.dip",
1519 "156bus_narrow.dip",
1520 };
1521
1522 int footprints0805 = 0;
1523 int valid0805 = 0;
1524
1525 for( const std::string& file : files )
1526 {
1527 auto board = LoadBoard( file );
1528 BOOST_REQUIRE_MESSAGE( board, "Failed to load " + file );
1529
1530 for( const FOOTPRINT* fp : board->Footprints() )
1531 {
1532 wxString fpName = wxString::FromUTF8( fp->GetFPID().GetLibItemName() ).Upper();
1533
1534 if( !fpName.Contains( "0805" ) )
1535 continue;
1536
1537 footprints0805++;
1538
1539 if( fp->Pads().size() != 2 )
1540 continue;
1541
1542 bool padsValid = true;
1543 std::vector<VECTOR2I> padPos;
1544
1545 for( const PAD* pad : fp->Pads() )
1546 {
1547 if( pad->GetAttribute() != PAD_ATTRIB::SMD )
1548 {
1549 padsValid = false;
1550 break;
1551 }
1552
1553 PAD_SHAPE shape = pad->GetShape( PADSTACK::ALL_LAYERS );
1554
1555 if( !IsRectLikeSmdPadShape( shape ) )
1556 {
1557 padsValid = false;
1558 break;
1559 }
1560
1561 VECTOR2I size = pad->GetSize( PADSTACK::ALL_LAYERS );
1562 double widthMm = pcbIUScale.IUTomm( size.x );
1563 double heightMm = pcbIUScale.IUTomm( size.y );
1564
1565 if( widthMm < 0.2 || widthMm > 2.5 || heightMm < 0.2 || heightMm > 2.5 )
1566 {
1567 padsValid = false;
1568 break;
1569 }
1570
1571 padPos.push_back( pad->GetPosition() );
1572 }
1573
1574 if( padsValid )
1575 {
1576 double pitchMm = pcbIUScale.IUTomm( ( padPos[0] - padPos[1] ).EuclideanNorm() );
1577
1578 if( pitchMm < 0.5 || pitchMm > 3.5 )
1579 padsValid = false;
1580 }
1581
1582 if( padsValid )
1583 valid0805++;
1584 }
1585 }
1586
1587 BOOST_CHECK_MESSAGE( footprints0805 >= 3,
1588 "Expected at least 3 imported 0805 footprints, got " + std::to_string( footprints0805 ) );
1589
1590 BOOST_CHECK_MESSAGE( valid0805 == footprints0805,
1591 "All imported 0805 footprints should satisfy SMD/pad-shape/pitch sanity; "
1592 "valid=" + std::to_string( valid0805 )
1593 + ", total=" + std::to_string( footprints0805 ) );
1594}
1595
1596
1597BOOST_AUTO_TEST_CASE( ImportedViasDoNotShortDifferentNets )
1598{
1599 const std::vector<std::string> files = {
1600 "project4.dip",
1601 "z80_board.dip",
1602 "logic_probe.dip",
1603 "keyboard.dip",
1604 };
1605
1606 for( const std::string& file : files )
1607 {
1608 auto board = LoadBoard( file );
1609 BOOST_REQUIRE_MESSAGE( board, "Failed to load " + file );
1610
1611 int checkedVias = 0;
1612 std::vector<std::string> shortReports;
1613 int shortedVias = CountViaNetShorts( *board, checkedVias, &shortReports );
1614
1615 for( const std::string& report : shortReports )
1616 BOOST_TEST_MESSAGE( file + ": " + report );
1617
1618 BOOST_CHECK_MESSAGE( shortedVias == 0, file + ": vias shorting distinct nets = " + std::to_string( shortedVias )
1619 + " out of " + std::to_string( checkedVias ) + " vias" );
1620 }
1621}
1622
1623
1624BOOST_AUTO_TEST_CASE( FootprintPadsInsideBoardOutline )
1625{
1626 const std::vector<std::string> files = {
1627 "project4.dip",
1628 "z80_board.dip",
1629 "logic_probe.dip",
1630 "keyboard.dip",
1631 };
1632
1633 for( const std::string& file : files )
1634 {
1635 auto board = LoadBoard( file );
1636 BOOST_REQUIRE_MESSAGE( board, "Failed to load " + file );
1637
1638 int totalPads = 0;
1639 bool hasOutline = false;
1640 int outsidePads = CountPadsOutsideBoardOutline( *board, totalPads, hasOutline );
1641
1642 BOOST_REQUIRE_MESSAGE( hasOutline, file + ": board outline not available" );
1643 BOOST_REQUIRE_MESSAGE( totalPads > 0, file + ": no pads found" );
1644
1645 if( outsidePads > 0 )
1646 {
1647 SHAPE_POLY_SET outline;
1648 board->GetBoardPolygonOutlines( outline, true );
1649 int reported = 0;
1650
1651 for( const FOOTPRINT* fp : board->Footprints() )
1652 {
1653 for( const PAD* pad : fp->Pads() )
1654 {
1655 if( outline.Contains( pad->GetPosition(), -1, CONNECT_TOL_NM ) )
1656 continue;
1657
1658 BOOST_TEST_MESSAGE( file + ": outside pad " + std::string( fp->GetReference().utf8_str() ) + ":"
1659 + std::string( pad->GetNumber().utf8_str() ) + " at ("
1660 + std::to_string( pad->GetPosition().x ) + ","
1661 + std::to_string( pad->GetPosition().y ) + ")" );
1662
1663 if( ++reported >= 20 )
1664 break;
1665 }
1666
1667 if( reported >= 20 )
1668 break;
1669 }
1670 }
1671
1672 BOOST_CHECK_MESSAGE( outsidePads == 0, file + ": pads outside board outline = " + std::to_string( outsidePads )
1673 + "/" + std::to_string( totalPads ) );
1674 }
1675}
1676
1677
1678BOOST_AUTO_TEST_CASE( ViewerExamplesOptional )
1679{
1680 const char* examplesEnv = std::getenv( "DIPTRACE_VIEWER_EXAMPLES_DIR" );
1681 std::string examplesDir =
1682 examplesEnv && *examplesEnv ? examplesEnv : "/home/seth/Downloads/DipTrace Viewer/Examples";
1683
1684 if( !std::filesystem::exists( examplesDir ) )
1685 {
1686 BOOST_TEST_MESSAGE( "Viewer examples path not found; skipping ViewerExamplesOptional" );
1687 return;
1688 }
1689
1690 auto pcb2 = LoadBoardFromPath( examplesDir + "/PCB_2.dip" );
1691 BOOST_REQUIRE( pcb2 );
1692 BOOST_CHECK_EQUAL( pcb2->GetCopperLayerCount(), 2 );
1693
1694 int pcb2Tracks = 0;
1695 int pcb2Vias = 0;
1696
1697 for( const PCB_TRACK* trk : pcb2->Tracks() )
1698 {
1699 if( trk->Type() == PCB_TRACE_T || trk->Type() == PCB_ARC_T )
1700 pcb2Tracks++;
1701 else if( trk->Type() == PCB_VIA_T )
1702 pcb2Vias++;
1703 }
1704
1705 BOOST_CHECK_MESSAGE( pcb2Vias <= pcb2Tracks,
1706 "PCB_2: via count should not exceed segment count; tracks=" + std::to_string( pcb2Tracks )
1707 + ", vias=" + std::to_string( pcb2Vias ) );
1708
1709 int viasWithDrill = 0;
1710 int viasAt191Mil = 0;
1711
1712 for( const PCB_TRACK* trk : pcb2->Tracks() )
1713 {
1714 if( trk->Type() != PCB_VIA_T )
1715 continue;
1716
1717 const PCB_VIA* via = static_cast<const PCB_VIA*>( trk );
1718 int drillIU = via->GetDrillValue();
1719
1720 if( drillIU <= 0 )
1721 continue;
1722
1723 viasWithDrill++;
1724
1725 double drillMm = pcbIUScale.IUTomm( drillIU );
1726 double targetMm = 19.1 * 0.0254;
1727
1728 if( std::abs( drillMm - targetMm ) <= 0.02 ) // ~0.8 mil tolerance
1729 viasAt191Mil++;
1730 }
1731
1732 BOOST_REQUIRE_MESSAGE( viasWithDrill > 0, "PCB_2: expected vias with non-zero drill" );
1733 BOOST_CHECK_MESSAGE( viasAt191Mil == viasWithDrill,
1734 "PCB_2: vias with 19.1mil drill = " + std::to_string( viasAt191Mil ) + "/"
1735 + std::to_string( viasWithDrill ) );
1736
1737 int pcb2ShortVias = 0;
1738 int pcb2CheckedVias = 0;
1739 pcb2ShortVias = CountViaNetShorts( *pcb2, pcb2CheckedVias );
1740 BOOST_CHECK_MESSAGE( pcb2ShortVias == 0,
1741 "PCB_2: vias shorting distinct nets = " + std::to_string( pcb2ShortVias ) );
1742
1743 const ZONE* pcb2BottomZone = nullptr;
1744
1745 for( const ZONE* zone : pcb2->Zones() )
1746 {
1747 if( zone->GetLayer() == B_Cu )
1748 {
1749 pcb2BottomZone = zone;
1750 break;
1751 }
1752 }
1753
1754 BOOST_REQUIRE_MESSAGE( pcb2BottomZone, "PCB_2: expected a B.Cu copper zone" );
1755
1756 wxString pcb2ZoneNet = pcb2BottomZone->GetNet() ? pcb2BottomZone->GetNet()->GetNetname() : wxString();
1757
1758 BOOST_CHECK_MESSAGE( pcb2ZoneNet == wxString( "Net 7" ), "PCB_2: B.Cu zone net should be 'Net 7', got '"
1759 + std::string( pcb2ZoneNet.utf8_str() ) + "'" );
1760 BOOST_CHECK( pcb2BottomZone->GetPadConnection() == ZONE_CONNECTION::THERMAL );
1761 BOOST_CHECK_SMALL( std::abs( pcbIUScale.IUTomm( pcb2BottomZone->GetThermalReliefSpokeWidth() ) - 0.3303 ), 0.03 );
1762
1763 int pcb2Net7PthPads = 0;
1764 int pcb2Net7At90 = 0;
1765
1766 for( const FOOTPRINT* fp : pcb2->Footprints() )
1767 {
1768 for( const PAD* pad : fp->Pads() )
1769 {
1770 if( pad->GetAttribute() == PAD_ATTRIB::SMD )
1771 continue;
1772
1773 if( !pad->GetNet() || pad->GetNet()->GetNetname() != wxString( "Net 7" ) )
1774 continue;
1775
1776 pcb2Net7PthPads++;
1777
1778 if( ( NormalizeDeg( pad->GetThermalSpokeAngle().AsDegrees() ) % 180 ) == 90 )
1779 pcb2Net7At90++;
1780 }
1781 }
1782
1783 BOOST_REQUIRE_GT( pcb2Net7PthPads, 0 );
1784 BOOST_CHECK_EQUAL( pcb2Net7At90, pcb2Net7PthPads );
1785
1786 int pcb2Pads = 0;
1787 bool pcb2HasOutline = false;
1788 int pcb2OutsidePads = CountPadsOutsideBoardOutline( *pcb2, pcb2Pads, pcb2HasOutline );
1789 BOOST_REQUIRE( pcb2HasOutline );
1790
1791 if( pcb2OutsidePads > 0 )
1792 {
1793 SHAPE_POLY_SET outline;
1794 pcb2->GetBoardPolygonOutlines( outline, true );
1795 int reported = 0;
1796
1797 for( const FOOTPRINT* fp : pcb2->Footprints() )
1798 {
1799 for( const PAD* pad : fp->Pads() )
1800 {
1801 if( !outline.Contains( pad->GetPosition(), -1, CONNECT_TOL_NM ) )
1802 {
1803 BOOST_TEST_MESSAGE( "PCB_2 outside pad: " + std::string( fp->GetReference().utf8_str() ) + ":"
1804 + std::string( pad->GetNumber().utf8_str() )
1805 + " fpLayer=" + std::string( pcb2->GetLayerName( fp->GetLayer() ).utf8_str() )
1806 + " fpOrient=" + std::to_string( fp->GetOrientation().AsDegrees() ) + " pad=("
1807 + std::to_string( pad->GetPosition().x ) + ","
1808 + std::to_string( pad->GetPosition().y ) + ")" );
1809 reported++;
1810
1811 if( reported >= 20 )
1812 break;
1813 }
1814 }
1815
1816 if( reported >= 20 )
1817 break;
1818 }
1819 }
1820
1821 BOOST_CHECK_MESSAGE( pcb2OutsidePads == 0, "PCB_2: pads outside outline = " + std::to_string( pcb2OutsidePads ) );
1822
1823 const FOOTPRINT* j1 = FindFootprintByRef( *pcb2, wxT( "J1" ) );
1824 const FOOTPRINT* j4 = FindFootprintByRef( *pcb2, wxT( "J4" ) );
1825 const FOOTPRINT* j7 = FindFootprintByRef( *pcb2, wxT( "J7" ) );
1826 const FOOTPRINT* j8 = FindFootprintByRef( *pcb2, wxT( "J8" ) );
1827 const FOOTPRINT* j10 = FindFootprintByRef( *pcb2, wxT( "J10" ) );
1828 const FOOTPRINT* u2 = FindFootprintByRef( *pcb2, wxT( "U2" ) );
1829 const FOOTPRINT* u3 = FindFootprintByRef( *pcb2, wxT( "U3" ) );
1830
1831 BOOST_REQUIRE_MESSAGE( j1, "PCB_2: footprint J1 not found" );
1832 BOOST_REQUIRE_MESSAGE( j4, "PCB_2: footprint J4 not found" );
1833 BOOST_REQUIRE_MESSAGE( j7, "PCB_2: footprint J7 not found" );
1834 BOOST_REQUIRE_MESSAGE( j8, "PCB_2: footprint J8 not found" );
1835 BOOST_REQUIRE_MESSAGE( j10, "PCB_2: footprint J10 not found" );
1836 BOOST_REQUIRE_MESSAGE( u2, "PCB_2: footprint U2 not found" );
1837 BOOST_REQUIRE_MESSAGE( u3, "PCB_2: footprint U3 not found" );
1838
1839 BOOST_CHECK_EQUAL( CardinalDeg( j4->GetOrientation().AsDegrees() ), 90 );
1840 BOOST_CHECK_EQUAL( CardinalDeg( j7->GetOrientation().AsDegrees() ), 90 );
1841 BOOST_CHECK_EQUAL( CardinalDeg( j10->GetOrientation().AsDegrees() ), 90 );
1842 BOOST_CHECK_EQUAL( CardinalDeg( u2->GetOrientation().AsDegrees() ), 0 );
1843 BOOST_CHECK_EQUAL( CardinalDeg( u3->GetOrientation().AsDegrees() ), 180 );
1844
1845 int j4SilkSegments = 0;
1846
1847 for( const BOARD_ITEM* item : j4->GraphicalItems() )
1848 {
1849 if( item->Type() != PCB_SHAPE_T )
1850 continue;
1851
1852 const PCB_SHAPE* shape = static_cast<const PCB_SHAPE*>( item );
1853
1854 if( ( shape->GetLayer() == F_SilkS || shape->GetLayer() == B_SilkS ) && shape->GetShape() == SHAPE_T::SEGMENT )
1855 {
1856 j4SilkSegments++;
1857 }
1858 }
1859
1860 BOOST_CHECK_MESSAGE( j4SilkSegments >= 4, "PCB_2: J4 should import as a silkscreen box; silk segments="
1861 + std::to_string( j4SilkSegments ) );
1862
1863 int j1Pads = 0;
1864
1865 for( const PAD* pad : j1->Pads() )
1866 {
1867 j1Pads++;
1868 BOOST_CHECK_MESSAGE( pad->GetAttribute() == PAD_ATTRIB::SMD,
1869 "PCB_2: J1 pad should be SMD, got attribute="
1870 + std::to_string( static_cast<int>( pad->GetAttribute() ) ) );
1871 }
1872
1873 BOOST_REQUIRE_GT( j1Pads, 0 );
1874
1875 int j8Pads = 0;
1876
1877 for( const PAD* pad : j8->Pads() )
1878 {
1879 j8Pads++;
1880 VECTOR2I drill = pad->GetDrillSize();
1881
1882 BOOST_CHECK_MESSAGE( pad->GetAttribute() == PAD_ATTRIB::PTH, "PCB_2: J8 pad should be through-hole" );
1883 BOOST_CHECK_MESSAGE( pad->GetDrillShape() == PAD_DRILL_SHAPE::CIRCLE,
1884 "PCB_2: J8 pad drill should be circular" );
1885 BOOST_CHECK_EQUAL( drill.x, drill.y );
1886 }
1887
1888 BOOST_REQUIRE_GT( j8Pads, 0 );
1889
1890 int u2PadsExpectedRotated = 0;
1891
1892 for( const PAD* pad : u2->Pads() )
1893 {
1894 long padNumLong = 0;
1895
1896 if( !pad->GetNumber().ToLong( &padNumLong ) )
1897 continue;
1898
1899 int padNum = static_cast<int>( padNumLong );
1900
1901 if( ( padNum >= 12 && padNum <= 22 ) || ( padNum >= 34 && padNum <= 44 ) )
1902 {
1903 u2PadsExpectedRotated++;
1904
1905 int padDeg = CardinalDeg( pad->GetOrientation().AsDegrees() );
1906 BOOST_CHECK_MESSAGE( ( padDeg % 180 ) == 90, "PCB_2: U2 pad " + std::to_string( padNum )
1907 + " should be rotated by 90 degrees (got "
1908 + std::to_string( padDeg ) + ")" );
1909 }
1910 }
1911
1912 BOOST_CHECK_EQUAL( u2PadsExpectedRotated, 22 );
1913
1914 auto pcb4 = LoadBoardFromPath( examplesDir + "/PCB_4.dip" );
1915 BOOST_REQUIRE( pcb4 );
1916 BOOST_CHECK_GT( pcb4->Footprints().size(), 0u );
1917 BOOST_CHECK_EQUAL( pcb4->GetCopperLayerCount(), 2 );
1918
1919 auto pcb6 = LoadBoardFromPath( examplesDir + "/PCB_6.dip" );
1920 BOOST_REQUIRE( pcb6 );
1921 BOOST_CHECK_EQUAL( pcb6->GetCopperLayerCount(), 4 );
1922 BOOST_CHECK_EQUAL( pcb6->GetLayerName( F_Cu ), wxString( "Top" ) );
1923 BOOST_CHECK_EQUAL( pcb6->GetLayerName( In1_Cu ), wxString( "Gnd" ) );
1924 BOOST_CHECK_EQUAL( pcb6->GetLayerName( In2_Cu ), wxString( "Pwr" ) );
1925 BOOST_CHECK_EQUAL( pcb6->GetLayerName( B_Cu ), wxString( "Bottom" ) );
1926
1927 // 5 stored CopperPours plus 2 synthesized fills for the Gnd (Lay1) and Pwr (Lay2) plane
1928 // layers, which DipTrace describes at the layer level rather than as pours.
1929 BOOST_CHECK_EQUAL( pcb6->Zones().size(), 7u );
1930
1931 int pwrZones = 0;
1932 int pwr3V3 = 0;
1933 int pwr5V = 0;
1934 int pwrVCC = 0;
1935 int gndInnerZones = 0;
1936
1937 for( const ZONE* zone : pcb6->Zones() )
1938 {
1939 wxString netName = zone->GetNet() ? zone->GetNet()->GetNetname() : wxString();
1940
1941 if( zone->GetLayer() == In2_Cu )
1942 {
1943 pwrZones++;
1944
1945 if( netName == wxString( "3V3" ) )
1946 pwr3V3++;
1947 else if( netName == wxString( "5V" ) )
1948 pwr5V++;
1949 else if( netName == wxString( "VCC" ) )
1950 pwrVCC++;
1951 }
1952 else if( zone->GetLayer() == In1_Cu && netName == wxString( "GND" ) )
1953 {
1954 gndInnerZones++;
1955 }
1956 }
1957
1958 // In2_Cu (Pwr plane) carries 4 stored pours plus the synthesized Pwr plane fill (net 3V3);
1959 // In1_Cu (Gnd plane) carries 1 stored GND pour plus the synthesized GND plane fill.
1960 BOOST_CHECK_EQUAL( pwrZones, 5 );
1961 BOOST_CHECK_EQUAL( pwr3V3, 2 );
1962 BOOST_CHECK_EQUAL( pwr5V, 2 );
1963 BOOST_CHECK_EQUAL( pwrVCC, 1 );
1964 BOOST_CHECK_EQUAL( gndInnerZones, 2 );
1965
1966 int pcb6ThermalZones = 0;
1967 int pcb6SpokeWidthMatches = 0;
1968
1969 for( const ZONE* zone : pcb6->Zones() )
1970 {
1971 if( zone->GetPadConnection() == ZONE_CONNECTION::THERMAL )
1972 pcb6ThermalZones++;
1973
1974 if( std::abs( pcbIUScale.IUTomm( zone->GetThermalReliefSpokeWidth() ) - 0.33 ) <= 0.03 )
1975 pcb6SpokeWidthMatches++;
1976 }
1977
1978 // 5 stored pours + 2 synthesized plane fills, all defaulting to thermal pad connection.
1979 BOOST_CHECK_EQUAL( pcb6ThermalZones, 7 );
1980 BOOST_CHECK_EQUAL( pcb6SpokeWidthMatches, 5 );
1981
1982 int pcb6PowerNetPthPads = 0;
1983 int pcb6PowerNetPadsAt45 = 0;
1984
1985 for( const FOOTPRINT* fp : pcb6->Footprints() )
1986 {
1987 for( const PAD* pad : fp->Pads() )
1988 {
1989 if( pad->GetAttribute() == PAD_ATTRIB::SMD || !pad->GetNet() )
1990 continue;
1991
1992 wxString netName = pad->GetNet()->GetNetname();
1993
1994 if( netName != wxString( "GND" ) && netName != wxString( "3V3" )
1995 && netName != wxString( "5V" ) && netName != wxString( "VCC" ) )
1996 {
1997 continue;
1998 }
1999
2000 pcb6PowerNetPthPads++;
2001
2002 if( ( NormalizeDeg( pad->GetThermalSpokeAngle().AsDegrees() ) % 180 ) == 45 )
2003 pcb6PowerNetPadsAt45++;
2004 }
2005 }
2006
2007 BOOST_REQUIRE_GT( pcb6PowerNetPthPads, 0 );
2008 BOOST_CHECK_EQUAL( pcb6PowerNetPadsAt45, pcb6PowerNetPthPads );
2009
2010 const FOOTPRINT* ic4 = FindFootprintByRef( *pcb6, wxT( "IC4" ) );
2011 BOOST_REQUIRE_MESSAGE( ic4, "PCB_6: footprint IC4 not found" );
2012 BOOST_CHECK_EQUAL( CardinalDeg( ic4->GetOrientation().AsDegrees() ), 90 );
2013
2014 const FOOTPRINT* q4 = FindFootprintByRef( *pcb6, wxT( "Q4" ) );
2015 BOOST_REQUIRE_MESSAGE( q4, "PCB_6: footprint Q4 not found" );
2016 BOOST_CHECK_EQUAL( q4->Pads().size(), 3u );
2017
2018 int q4SilkSegments = 0;
2019 int q4SilkArcs = 0;
2020
2021 for( const BOARD_ITEM* item : q4->GraphicalItems() )
2022 {
2023 if( item->Type() != PCB_SHAPE_T )
2024 continue;
2025
2026 const PCB_SHAPE* shape = static_cast<const PCB_SHAPE*>( item );
2027
2028 if( shape->GetLayer() != F_SilkS && shape->GetLayer() != B_SilkS )
2029 continue;
2030
2031 if( shape->GetShape() == SHAPE_T::SEGMENT )
2032 q4SilkSegments++;
2033 else if( shape->GetShape() == SHAPE_T::ARC )
2034 q4SilkArcs++;
2035 }
2036
2037 BOOST_CHECK_MESSAGE( q4SilkSegments == 4,
2038 "PCB_6: Q4 should have 4 silkscreen segments, got " + std::to_string( q4SilkSegments ) );
2039 BOOST_CHECK_MESSAGE( q4SilkArcs == 1,
2040 "PCB_6: Q4 should have 1 silkscreen arc, got " + std::to_string( q4SilkArcs ) );
2041
2042 const FOOTPRINT* u10 = FindFootprintByRef( *pcb6, wxT( "U10" ) );
2043 BOOST_REQUIRE_MESSAGE( u10, "PCB_6: footprint U10 not found" );
2044
2045 int u10NpthPads = 0;
2046 int u10Drill508 = 0;
2047 int u10Outer5747 = 0;
2048 std::vector<VECTOR2I> u10NpthLocals;
2049
2050 for( const PAD* pad : u10->Pads() )
2051 {
2052 if( pad->GetAttribute() != PAD_ATTRIB::NPTH )
2053 continue;
2054
2055 u10NpthPads++;
2056 u10NpthLocals.push_back( pad->GetPosition() - u10->GetPosition() );
2057
2058 VECTOR2I drill = pad->GetDrillSize();
2059 VECTOR2I size = pad->GetSize( PADSTACK::ALL_LAYERS );
2060
2061 BOOST_CHECK_MESSAGE( pad->GetDrillShape() == PAD_DRILL_SHAPE::CIRCLE,
2062 "PCB_6: U10 NPTH drill should be circular" );
2063 BOOST_CHECK_EQUAL( drill.x, drill.y );
2064 BOOST_CHECK_EQUAL( size.x, size.y );
2065 BOOST_CHECK_MESSAGE( pad->GetNetCode() <= 0, "PCB_6: U10 NPTH should not have a net assignment" );
2066
2067 double drillMm = pcbIUScale.IUTomm( drill.x );
2068 double outerMm = pcbIUScale.IUTomm( size.x );
2069
2070 if( std::abs( drillMm - 5.08 ) <= 0.02 )
2071 u10Drill508++;
2072
2073 if( std::abs( outerMm - 5.7467 ) <= 0.03 )
2074 u10Outer5747++;
2075 }
2076
2077 BOOST_CHECK_EQUAL( u10NpthPads, 2 );
2078 BOOST_CHECK_EQUAL( u10Drill508, 2 );
2079 BOOST_CHECK_EQUAL( u10Outer5747, 2 );
2080 BOOST_REQUIRE_EQUAL( u10NpthLocals.size(), 2u );
2081
2082 VECTOR2I sym = u10NpthLocals[0] + u10NpthLocals[1];
2083 int symTol = pcbIUScale.mmToIU( 0.05 );
2084
2085 BOOST_CHECK_SMALL( std::abs( sym.x ), symTol );
2086 BOOST_CHECK_SMALL( std::abs( sym.y ), symTol );
2087
2088 double holeSpanMm = pcbIUScale.IUTomm( ( u10NpthLocals[0] - u10NpthLocals[1] ).EuclideanNorm() );
2089 BOOST_CHECK_SMALL( std::abs( holeSpanMm - 24.9934 ), 0.05 );
2090
2091 auto cnc = LoadBoardFromPath( examplesDir + "/CNC_controller.dip" );
2092 BOOST_REQUIRE( cnc );
2093 BOOST_CHECK_EQUAL( cnc->GetCopperLayerCount(), 2 );
2094 BOOST_CHECK_GT( cnc->Footprints().size(), 120u );
2095 BOOST_CHECK_LT( cnc->Footprints().size(), 200u );
2096 BOOST_CHECK_EQUAL( cnc->Zones().size(), 5u );
2097
2098 int cncViaCount = 0;
2099
2100 for( const PCB_TRACK* trk : cnc->Tracks() )
2101 {
2102 if( trk->Type() == PCB_VIA_T )
2103 cncViaCount++;
2104 }
2105
2106 BOOST_CHECK_GT( cncViaCount, 300 );
2107
2108 int cncFull = 0;
2109 int cncThtThermal = 0;
2110
2111 for( const ZONE* zone : cnc->Zones() )
2112 {
2113 if( zone->GetPadConnection() == ZONE_CONNECTION::FULL )
2114 cncFull++;
2115 else if( zone->GetPadConnection() == ZONE_CONNECTION::THT_THERMAL )
2116 cncThtThermal++;
2117 }
2118
2119 BOOST_CHECK_EQUAL( cncFull, 3 );
2120 BOOST_CHECK_GE( cncThtThermal, 1 );
2121}
2122
2123
2124BOOST_AUTO_TEST_CASE( ViewerExamplesDipXmlParityOptional )
2125{
2126 const char* examplesEnv = std::getenv( "DIPTRACE_VIEWER_EXAMPLES_DIR" );
2127 std::string examplesDir =
2128 examplesEnv && *examplesEnv ? examplesEnv : "/home/seth/Downloads/DipTrace Viewer/Examples";
2129
2130 if( !std::filesystem::exists( examplesDir ) )
2131 {
2132 BOOST_TEST_MESSAGE( "Viewer examples path not found; skipping ViewerExamplesDipXmlParityOptional" );
2133 return;
2134 }
2135
2136 struct SAMPLE
2137 {
2138 const char* dip;
2139 const char* dipxml;
2140 int minComparedFootprints;
2141 int minComparedPads;
2142 };
2143
2144 static const std::array<SAMPLE, 3> samples = { {
2145 { "PCB_2.dip", "PCB_2.dipxml", 60, 200 },
2146 { "PCB_4.dip", "PCB_4.dipxml", 60, 200 },
2147 { "PCB_6.dip", "PCB_6.dipxml", 100, 700 },
2148 } };
2149
2150 for( const SAMPLE& sample : samples )
2151 {
2152 std::string dipPath = examplesDir + "/" + sample.dip;
2153 std::string xmlPath = examplesDir + "/" + sample.dipxml;
2154
2155 if( !std::filesystem::exists( dipPath ) || !std::filesystem::exists( xmlPath ) )
2156 {
2157 BOOST_TEST_MESSAGE( "Skipping " + std::string( sample.dip ) + " parity check; missing .dip or .dipxml" );
2158 continue;
2159 }
2160
2161 DIPXML_BOARD_MODEL model;
2162 BOOST_REQUIRE_MESSAGE( LoadDipXmlModel( xmlPath, model ), "Failed to load DipXML model: " + xmlPath );
2163
2164 auto board = LoadBoardFromPath( dipPath );
2165 BOOST_REQUIRE( board );
2166
2167 int importedViaCount = 0;
2168
2169 for( const PCB_TRACK* trk : board->Tracks() )
2170 {
2171 if( trk->Type() == PCB_VIA_T )
2172 importedViaCount++;
2173 }
2174
2175 int expectedViaCount = model.traceViaPointsUniqueNetPos + model.viaComponentCount;
2176
2177 BOOST_CHECK_MESSAGE( importedViaCount == expectedViaCount,
2178 std::string( sample.dip ) + ": via parity mismatch; imported="
2179 + std::to_string( importedViaCount )
2180 + " expected(trace-via unique net+xy="
2181 + std::to_string( model.traceViaPointsUniqueNetPos )
2182 + ", standalone via components=" + std::to_string( model.viaComponentCount )
2183 + ", trace-via raw points=" + std::to_string( model.traceViaPointsRaw ) + ")" );
2184
2185 int comparedFootprints = 0;
2186 int footprintAngleMismatches = 0;
2187 int comparedPads = 0;
2188 int padAngleMismatches = 0;
2189 int padTypeMismatches = 0;
2190 int padDrillMismatches = 0;
2191 int missingPatternPads = 0;
2192 int zoneKeyMismatches = 0;
2193 int zoneClearanceMismatches = 0;
2194 int zoneMinWidthMismatches = 0;
2195 int zoneConnectionMismatches = 0;
2196 int zoneSpokeWidthMismatches = 0;
2197 int zoneIslandModeMismatches = 0;
2198 int zoneMinAreaMismatches = 0;
2199 int zonePriorityMismatches = 0;
2200 std::vector<std::string> missingPadReports;
2201 std::vector<std::string> zoneMismatchReports;
2202
2203 for( const auto& [ref, patternStyle, componentAngleCardinal] : model.components )
2204 {
2205 const FOOTPRINT* fp = FindFootprintByRef( *board, wxString::FromUTF8( ref ) );
2206
2207 if( !fp )
2208 continue;
2209
2210 comparedFootprints++;
2211
2212 if( CardinalDeg( fp->GetOrientation().AsDegrees() ) != componentAngleCardinal )
2213 footprintAngleMismatches++;
2214
2215 auto patternIt = model.patterns.find( patternStyle );
2216
2217 if( patternIt == model.patterns.end() )
2218 continue;
2219
2220 const auto& patternPads = patternIt->second;
2221
2222 for( const auto& [padKey, padExpected] : patternPads )
2223 {
2224 const PAD* foundPad = nullptr;
2225
2226 for( const PAD* pad : fp->Pads() )
2227 {
2228 if( ToUtf8( pad->GetNumber() ) == padKey )
2229 {
2230 foundPad = pad;
2231 break;
2232 }
2233 }
2234
2235 if( !foundPad )
2236 {
2237 missingPatternPads++;
2238
2239 if( missingPadReports.size() < 20 )
2240 {
2241 missingPadReports.push_back( ref + ":" + padKey + " style=" + padExpected.styleName
2242 + " patt=" + patternStyle );
2243 }
2244
2245 continue;
2246 }
2247
2248 comparedPads++;
2249
2250 auto styleIt = model.styles.find( padExpected.styleName );
2251
2252 if( styleIt != model.styles.end() )
2253 {
2254 const DIPXML_PAD_STYLE& style = styleIt->second;
2255
2256 if( style.isSurface )
2257 {
2258 if( foundPad->GetAttribute() != PAD_ATTRIB::SMD )
2259 padTypeMismatches++;
2260 }
2261 else if( style.isThrough )
2262 {
2263 if( foundPad->GetAttribute() == PAD_ATTRIB::SMD )
2264 padTypeMismatches++;
2265
2266 if( style.holeMm > 0.0 )
2267 {
2268 VECTOR2I drill = foundPad->GetDrillSize();
2269
2270 if( drill.x <= 0 || drill.y <= 0 )
2271 {
2272 padDrillMismatches++;
2273 }
2274 else if( style.isRoundHole
2275 && ( foundPad->GetDrillShape() != PAD_DRILL_SHAPE::CIRCLE || drill.x != drill.y ) )
2276 {
2277 padDrillMismatches++;
2278 }
2279 }
2280 }
2281 }
2282
2283 VECTOR2I padSize = foundPad->GetSize( PADSTACK::ALL_LAYERS );
2284
2285 if( padSize.x != padSize.y )
2286 {
2287 int gotParity = CardinalDeg( foundPad->GetFPRelativeOrientation().AsDegrees() ) % 180;
2288 int expectedParity = padExpected.angleCardinalDeg % 180;
2289
2290 if( gotParity != expectedParity )
2291 padAngleMismatches++;
2292 }
2293 }
2294 }
2295
2296 BOOST_CHECK_MESSAGE( comparedFootprints >= sample.minComparedFootprints,
2297 std::string( sample.dip )
2298 + ": compared footprints=" + std::to_string( comparedFootprints ) );
2299 BOOST_CHECK_MESSAGE( comparedPads >= sample.minComparedPads,
2300 std::string( sample.dip ) + ": compared pads=" + std::to_string( comparedPads ) );
2301 BOOST_CHECK_EQUAL( footprintAngleMismatches, 0 );
2302 BOOST_CHECK_EQUAL( padAngleMismatches, 0 );
2303 BOOST_CHECK_EQUAL( padTypeMismatches, 0 );
2304 BOOST_CHECK_EQUAL( padDrillMismatches, 0 );
2305
2306 if( !model.copperPours.empty() )
2307 {
2308 std::map<std::string, std::vector<int>> expectedZonesByKey;
2309 std::map<std::string, std::vector<int>> expectedZoneMinWidthsByKey;
2310 std::map<std::string, std::vector<int>> expectedZoneConnectionsByKey;
2311 std::map<std::string, std::vector<int>> expectedZoneSpokeWidthsByKey;
2312 std::map<std::string, std::vector<int>> expectedZoneIslandModesByKey;
2313 std::map<std::string, std::vector<long long>> expectedZoneMinAreaByKey;
2314 std::map<std::string, std::vector<int>> expectedZonePrioritiesByKey;
2315 std::map<std::string, std::vector<int>> importedZonePrioritiesByKey;
2316 std::map<std::string, std::vector<int>> importedZonesByKey;
2317 std::map<std::string, std::vector<int>> importedZoneMinWidthsByKey;
2318 std::map<std::string, std::vector<int>> importedZoneConnectionsByKey;
2319 std::map<std::string, std::vector<int>> importedZoneSpokeWidthsByKey;
2320 std::map<std::string, std::vector<int>> importedZoneIslandModesByKey;
2321 std::map<std::string, std::vector<long long>> importedZoneMinAreaByKey;
2322
2323 for( const auto& pour : model.copperPours )
2324 {
2325 std::string netName;
2326 auto netIt = model.netNames.find( pour.netId );
2327
2328 if( netIt != model.netNames.end() )
2329 netName = netIt->second;
2330
2331 std::string key = std::to_string( pour.layer ) + "|" + netName;
2332 expectedZonePrioritiesByKey[key].push_back( pour.priority );
2333 expectedZonesByKey[key].push_back( static_cast<int>( std::lround( pour.clearanceMm * 1000.0 ) ) );
2334 expectedZoneMinWidthsByKey[key].push_back(
2335 static_cast<int>( std::lround( pour.lineWidthMm * 1000.0 ) ) );
2336 expectedZoneSpokeWidthsByKey[key].push_back(
2337 static_cast<int>( std::lround( pour.spokeWidthMm * 1000.0 ) ) );
2338
2339 int spokeMode = DipXmlSpokeMode( pour.spoke );
2340 int connection = ( spokeMode == 0 ) ? 0 : 1;
2341 expectedZoneConnectionsByKey[key].push_back( connection );
2342
2343 ISLAND_REMOVAL_MODE islandMode = DipXmlIslandModeToKiCad(
2344 pour.islandRegion, pour.islandInternal, pour.islandConnection );
2345 expectedZoneIslandModesByKey[key].push_back( static_cast<int>( islandMode ) );
2346
2347 if( islandMode == ISLAND_REMOVAL_MODE::AREA )
2348 {
2349 expectedZoneMinAreaByKey[key].push_back(
2350 DipXmlMinimumAreaToKiCadIu2( pour.minimumAreaMm ) );
2351 }
2352 }
2353
2354 for( const ZONE* zone : board->Zones() )
2355 {
2356 // Synthesized plane fills are layer-level constructs, not stored CopperPours, so
2357 // they have no XML pour to compare against; skip them in this parity check.
2358 if( zone->GetZoneName() == wxString( "DipTrace Plane" ) )
2359 continue;
2360
2361 int dipLayer = DipLayerIndexFromKiCadLayer( *board, zone->GetLayer() );
2362
2363 if( dipLayer < 0 )
2364 continue;
2365
2366 std::string netName = zone->GetNet() ? ToUtf8( zone->GetNet()->GetNetname() ) : std::string();
2367 std::string key = std::to_string( dipLayer ) + "|" + netName;
2368 int clearanceUm = static_cast<int>(
2369 std::lround( pcbIUScale.IUTomm( zone->GetLocalClearance().value_or( 0 ) ) * 1000.0 ) );
2370 int minWidthUm = static_cast<int>( std::lround( pcbIUScale.IUTomm( zone->GetMinThickness() ) * 1000.0 ) );
2371 int spokeWidthUm = static_cast<int>(
2372 std::lround( pcbIUScale.IUTomm( zone->GetThermalReliefSpokeWidth() ) * 1000.0 ) );
2373 int connection = zone->GetPadConnection() == ZONE_CONNECTION::FULL ? 0 : 1;
2374 int islandMode = static_cast<int>( zone->GetIslandRemovalMode() );
2375 importedZonePrioritiesByKey[key].push_back( zone->GetAssignedPriority() );
2376 importedZonesByKey[key].push_back( clearanceUm );
2377 importedZoneMinWidthsByKey[key].push_back( minWidthUm );
2378 importedZoneSpokeWidthsByKey[key].push_back( spokeWidthUm );
2379 importedZoneConnectionsByKey[key].push_back( connection );
2380 importedZoneIslandModesByKey[key].push_back( islandMode );
2381
2382 if( zone->GetIslandRemovalMode() == ISLAND_REMOVAL_MODE::AREA )
2383 importedZoneMinAreaByKey[key].push_back( zone->GetMinIslandArea() );
2384 }
2385
2386 std::set<std::string> allKeys;
2387
2388 for( const auto& [key, _] : expectedZonesByKey )
2389 allKeys.insert( key );
2390
2391 for( const auto& [key, _] : importedZonesByKey )
2392 allKeys.insert( key );
2393
2394 for( const std::string& key : allKeys )
2395 {
2396 auto expIt = expectedZonesByKey.find( key );
2397 auto gotIt = importedZonesByKey.find( key );
2398
2399 if( expIt == expectedZonesByKey.end() || gotIt == importedZonesByKey.end() )
2400 {
2401 zoneKeyMismatches++;
2402
2403 if( zoneMismatchReports.size() < 20 )
2404 {
2405 zoneMismatchReports.push_back( std::string( "key-missing " ) + key + " exp="
2406 + std::to_string( expIt != expectedZonesByKey.end() ) + " got="
2407 + std::to_string( gotIt != importedZonesByKey.end() ) );
2408 }
2409
2410 continue;
2411 }
2412
2413 auto expVals = expIt->second;
2414 auto gotVals = gotIt->second;
2415 auto expWidthVals = expectedZoneMinWidthsByKey[key];
2416 auto gotWidthVals = importedZoneMinWidthsByKey[key];
2417 auto expConnVals = expectedZoneConnectionsByKey[key];
2418 auto gotConnVals = importedZoneConnectionsByKey[key];
2419 auto expSpokeWidthVals = expectedZoneSpokeWidthsByKey[key];
2420 auto gotSpokeWidthVals = importedZoneSpokeWidthsByKey[key];
2421 auto expIslandVals = expectedZoneIslandModesByKey[key];
2422 auto gotIslandVals = importedZoneIslandModesByKey[key];
2423 auto expMinAreaVals = expectedZoneMinAreaByKey[key];
2424 auto gotMinAreaVals = importedZoneMinAreaByKey[key];
2425 auto expPriorityVals = expectedZonePrioritiesByKey[key];
2426 auto gotPriorityVals = importedZonePrioritiesByKey[key];
2427 std::sort( expPriorityVals.begin(), expPriorityVals.end() );
2428 std::sort( gotPriorityVals.begin(), gotPriorityVals.end() );
2429 std::sort( expVals.begin(), expVals.end() );
2430 std::sort( gotVals.begin(), gotVals.end() );
2431 std::sort( expWidthVals.begin(), expWidthVals.end() );
2432 std::sort( gotWidthVals.begin(), gotWidthVals.end() );
2433 std::sort( expConnVals.begin(), expConnVals.end() );
2434 std::sort( gotConnVals.begin(), gotConnVals.end() );
2435 std::sort( expSpokeWidthVals.begin(), expSpokeWidthVals.end() );
2436 std::sort( gotSpokeWidthVals.begin(), gotSpokeWidthVals.end() );
2437 std::sort( expIslandVals.begin(), expIslandVals.end() );
2438 std::sort( gotIslandVals.begin(), gotIslandVals.end() );
2439 std::sort( expMinAreaVals.begin(), expMinAreaVals.end() );
2440 std::sort( gotMinAreaVals.begin(), gotMinAreaVals.end() );
2441
2442 if( expVals.size() != gotVals.size()
2443 || expWidthVals.size() != gotWidthVals.size()
2444 || expConnVals.size() != gotConnVals.size()
2445 || expSpokeWidthVals.size() != gotSpokeWidthVals.size()
2446 || expIslandVals.size() != gotIslandVals.size()
2447 || expMinAreaVals.size() != gotMinAreaVals.size()
2448 || expPriorityVals.size() != gotPriorityVals.size() )
2449 {
2450 zoneKeyMismatches++;
2451
2452 if( zoneMismatchReports.size() < 20 )
2453 {
2454 zoneMismatchReports.push_back( std::string( "key-count " ) + key
2455 + " expN=" + std::to_string( expVals.size() )
2456 + " gotN=" + std::to_string( gotVals.size() ) );
2457 }
2458
2459 continue;
2460 }
2461
2462 for( size_t i = 0; i < expVals.size(); i++ )
2463 {
2464 // 20 um tolerance handles import scale quantization.
2465 if( std::abs( expVals[i] - gotVals[i] ) > 20 )
2466 {
2467 zoneClearanceMismatches++;
2468
2469 if( zoneMismatchReports.size() < 20 )
2470 {
2471 zoneMismatchReports.push_back( std::string( "clearance " ) + key
2472 + " expUm=" + std::to_string( expVals[i] )
2473 + " gotUm=" + std::to_string( gotVals[i] ) );
2474 }
2475 }
2476
2477 if( std::abs( expWidthVals[i] - gotWidthVals[i] ) > 20 )
2478 {
2479 zoneMinWidthMismatches++;
2480
2481 if( zoneMismatchReports.size() < 20 )
2482 {
2483 zoneMismatchReports.push_back( std::string( "min-width " ) + key
2484 + " expUm=" + std::to_string( expWidthVals[i] )
2485 + " gotUm=" + std::to_string( gotWidthVals[i] ) );
2486 }
2487 }
2488
2489 if( expPriorityVals[i] != gotPriorityVals[i] )
2490 {
2491 zonePriorityMismatches++;
2492
2493 if( zoneMismatchReports.size() < 20 )
2494 {
2495 zoneMismatchReports.push_back( std::string( "priority " ) + key
2496 + " exp=" + std::to_string( expPriorityVals[i] )
2497 + " got=" + std::to_string( gotPriorityVals[i] ) );
2498 }
2499 }
2500
2501 if( expConnVals[i] != gotConnVals[i] )
2502 {
2503 zoneConnectionMismatches++;
2504
2505 if( zoneMismatchReports.size() < 20 )
2506 {
2507 zoneMismatchReports.push_back( std::string( "connection " ) + key
2508 + " exp=" + std::to_string( expConnVals[i] )
2509 + " got=" + std::to_string( gotConnVals[i] ) );
2510 }
2511 }
2512
2513 if( std::abs( expSpokeWidthVals[i] - gotSpokeWidthVals[i] ) > 20 )
2514 {
2515 zoneSpokeWidthMismatches++;
2516
2517 if( zoneMismatchReports.size() < 20 )
2518 {
2519 zoneMismatchReports.push_back( std::string( "spoke-width " ) + key
2520 + " expUm=" + std::to_string( expSpokeWidthVals[i] )
2521 + " gotUm=" + std::to_string( gotSpokeWidthVals[i] ) );
2522 }
2523 }
2524
2525 if( expIslandVals[i] != gotIslandVals[i] )
2526 {
2527 zoneIslandModeMismatches++;
2528
2529 if( zoneMismatchReports.size() < 20 )
2530 {
2531 zoneMismatchReports.push_back( std::string( "island-mode " ) + key
2532 + " exp=" + std::to_string( expIslandVals[i] )
2533 + " got=" + std::to_string( gotIslandVals[i] ) );
2534 }
2535 }
2536 }
2537
2538 for( size_t i = 0; i < expMinAreaVals.size(); i++ )
2539 {
2540 long long diff = std::llabs( expMinAreaVals[i] - gotMinAreaVals[i] );
2541 long long tol = std::max<long long>( 1'000'000'000LL, expMinAreaVals[i] / 100 );
2542
2543 if( diff > tol )
2544 {
2545 zoneMinAreaMismatches++;
2546
2547 if( zoneMismatchReports.size() < 20 )
2548 {
2549 zoneMismatchReports.push_back( std::string( "island-area " ) + key
2550 + " exp=" + std::to_string( expMinAreaVals[i] )
2551 + " got=" + std::to_string( gotMinAreaVals[i] ) );
2552 }
2553 }
2554 }
2555 }
2556 }
2557
2558 BOOST_CHECK_EQUAL( zoneKeyMismatches, 0 );
2559 BOOST_CHECK_EQUAL( zoneClearanceMismatches, 0 );
2560 BOOST_CHECK_EQUAL( zoneMinWidthMismatches, 0 );
2561 BOOST_CHECK_EQUAL( zoneConnectionMismatches, 0 );
2562 BOOST_CHECK_EQUAL( zoneSpokeWidthMismatches, 0 );
2563 BOOST_CHECK_EQUAL( zoneIslandModeMismatches, 0 );
2564 BOOST_CHECK_EQUAL( zoneMinAreaMismatches, 0 );
2565 BOOST_CHECK_EQUAL( zonePriorityMismatches, 0 );
2566
2567 BOOST_TEST_MESSAGE( std::string( sample.dip )
2568 + ": dipxml parity footprintComparisons=" + std::to_string( comparedFootprints )
2569 + ", padComparisons=" + std::to_string( comparedPads )
2570 + ", missingPatternPads=" + std::to_string( missingPatternPads ) );
2571
2572 if( !missingPadReports.empty() )
2573 {
2574 for( const std::string& rep : missingPadReports )
2575 BOOST_TEST_MESSAGE( std::string( sample.dip ) + ": missing pad " + rep );
2576 }
2577
2578 if( !zoneMismatchReports.empty() )
2579 {
2580 for( const std::string& rep : zoneMismatchReports )
2581 BOOST_TEST_MESSAGE( std::string( sample.dip ) + ": zone mismatch " + rep );
2582 }
2583 }
2584}
2585
2586
2587BOOST_AUTO_TEST_CASE( ViewerExamplesViaParityOptional )
2588{
2589 const char* examplesEnv = std::getenv( "DIPTRACE_VIEWER_EXAMPLES_DIR" );
2590 std::string examplesDir =
2591 examplesEnv && *examplesEnv ? examplesEnv : "/home/seth/Downloads/DipTrace Viewer/Examples";
2592
2593 if( !std::filesystem::exists( examplesDir ) )
2594 {
2595 BOOST_TEST_MESSAGE( "Viewer examples path not found; skipping ViewerExamplesViaParityOptional" );
2596 return;
2597 }
2598
2599 struct SAMPLE
2600 {
2601 const char* dip;
2602 const char* dipxml;
2603 bool expectAllTraceViaStyleZero = false;
2604 };
2605
2606 static const std::array<SAMPLE, 4> samples = { {
2607 { "PCB_2.dip", "PCB_2.dipxml", false },
2608 { "PCB_4.dip", "PCB_4.dipxml", false },
2609 { "PCB_6.dip", "PCB_6.dipxml", false },
2610 { "CNC_controller.dip", "CNC_controller.dipxml", true },
2611 } };
2612
2613 for( const SAMPLE& sample : samples )
2614 {
2615 std::string dipPath = examplesDir + "/" + sample.dip;
2616 std::string xmlPath = examplesDir + "/" + sample.dipxml;
2617
2618 if( !std::filesystem::exists( dipPath ) || !std::filesystem::exists( xmlPath ) )
2619 {
2620 BOOST_TEST_MESSAGE( "Skipping " + std::string( sample.dip ) + " via parity check; missing .dip or .dipxml" );
2621 continue;
2622 }
2623
2624 DIPXML_BOARD_MODEL model;
2625 BOOST_REQUIRE_MESSAGE( LoadDipXmlModel( xmlPath, model ), "Failed to load DipXML model: " + xmlPath );
2626
2627 auto board = LoadBoardFromPath( dipPath );
2628 BOOST_REQUIRE( board );
2629
2630 int importedViaCount = 0;
2631
2632 for( const PCB_TRACK* trk : board->Tracks() )
2633 {
2634 if( trk->Type() == PCB_VIA_T )
2635 importedViaCount++;
2636 }
2637
2638 int expectedViaCount = model.traceViaPointsUniqueNetPos + model.viaComponentCount;
2639
2640 BOOST_CHECK_MESSAGE( importedViaCount == expectedViaCount,
2641 std::string( sample.dip ) + ": via parity mismatch; imported="
2642 + std::to_string( importedViaCount )
2643 + " expected(trace-via unique net+xy="
2644 + std::to_string( model.traceViaPointsUniqueNetPos )
2645 + ", standalone via components=" + std::to_string( model.viaComponentCount )
2646 + ", trace-via raw points=" + std::to_string( model.traceViaPointsRaw )
2647 + ", trace-via style0 raw="
2648 + std::to_string( model.traceViaPointsStyleZeroRaw ) + ")" );
2649
2650 if( sample.expectAllTraceViaStyleZero )
2651 {
2652 BOOST_REQUIRE_MESSAGE( model.traceViaPointsRaw > 0,
2653 std::string( sample.dip ) + ": expected routed via points in DipXML" );
2654 BOOST_CHECK_EQUAL( model.traceViaPointsStyleZeroRaw, model.traceViaPointsRaw );
2655 }
2656 }
2657}
2658
2659
2664BOOST_AUTO_TEST_CASE( ExternalCorpusImportOptional )
2665{
2666 const char* corpusEnv = std::getenv( "DIPTRACE_EXTERNAL_CORPUS_DIR" );
2667
2668 if( !corpusEnv || !*corpusEnv )
2669 {
2670 BOOST_TEST_MESSAGE( "DIPTRACE_EXTERNAL_CORPUS_DIR not set; skipping external corpus sweep" );
2671 return;
2672 }
2673
2674 std::filesystem::path corpusRoot( corpusEnv );
2675
2676 if( !std::filesystem::exists( corpusRoot ) )
2677 {
2678 BOOST_TEST_MESSAGE( "External corpus path does not exist; skipping external corpus sweep" );
2679 return;
2680 }
2681
2682 std::vector<std::filesystem::path> dipFiles;
2683
2684 for( const auto& entry : std::filesystem::recursive_directory_iterator( corpusRoot ) )
2685 {
2686 if( entry.is_regular_file() && HasDipExtension( entry.path() ) )
2687 dipFiles.push_back( entry.path() );
2688 }
2689
2690 std::sort( dipFiles.begin(), dipFiles.end() );
2691
2692 BOOST_REQUIRE_MESSAGE( !dipFiles.empty(), "No .dip files found under: " + corpusRoot.string() );
2693
2694 int loaded = 0;
2695 int skippedUnreadable = 0;
2696
2697 for( const std::filesystem::path& path : dipFiles )
2698 {
2699 if( !m_plugin.CanReadBoard( path.string() ) )
2700 {
2701 skippedUnreadable++;
2702 continue;
2703 }
2704
2705 std::unique_ptr<BOARD> board;
2706
2707 DIPTRACE_WARNING_CAPTURE capture;
2708 SCOPED_WXLOG_TARGET logOverride( &capture );
2709
2710 try
2711 {
2712 board = LoadBoardFromPath( path.string() );
2713 }
2714 catch( const IO_ERROR& e )
2715 {
2716 BOOST_ERROR( path.string() + ": IO_ERROR: " + std::string( e.What().utf8_str() ) );
2717 continue;
2718 }
2719 catch( const std::exception& e )
2720 {
2721 BOOST_ERROR( path.string() + ": exception: " + std::string( e.what() ) );
2722 continue;
2723 }
2724
2725 BOOST_REQUIRE_MESSAGE( board, "Failed to load: " + path.string() );
2726 loaded++;
2727
2728 for( const wxString& warning : capture.m_warnings )
2729 {
2730 BOOST_CHECK_MESSAGE( !IsHeuristicParserWarning( warning ),
2731 path.string() + ": unexpected heuristic parser warning: "
2732 + std::string( warning.utf8_str() ) );
2733 }
2734
2735 int outlineEndpoints = 0;
2736 int outlineDisconnected = CountDisconnectedEdgeCutsEndpoints( *board, outlineEndpoints );
2737
2738 if( outlineEndpoints > 0 )
2739 {
2740 BOOST_CHECK_MESSAGE( outlineDisconnected == 0, path.string() + ": disconnected Edge.Cuts endpoints = "
2741 + std::to_string( outlineDisconnected ) + "/"
2742 + std::to_string( outlineEndpoints ) );
2743 }
2744 }
2745
2746 BOOST_CHECK_MESSAGE( loaded > 0, "External corpus sweep loaded zero boards" );
2747
2748 if( skippedUnreadable > 0 )
2749 BOOST_TEST_MESSAGE( "Skipped " << skippedUnreadable << " .dip files without DTBOARD magic" );
2750}
2751
2752
2758BOOST_AUTO_TEST_CASE( ZoneDesignRulesValid )
2759{
2760 static const std::array<const char*, 5> boards = {
2761 "z80_board.dip", "keyboard.dip", "logic_probe.dip", "project4.dip", "156bus_narrow.dip"
2762 };
2763
2764 int boardsWithRules = 0;
2765
2766 for( const char* name : boards )
2767 {
2768 BOARD board;
2769 DIPTRACE::PCB_PARSER parser( wxString::FromUTF8( GetTestDataDir() + name ), &board );
2770 parser.Parse();
2771
2772 wxString dru = parser.GenerateDesignRules();
2773
2774 if( dru.IsEmpty() )
2775 continue;
2776
2777 boardsWithRules++;
2778
2779 std::vector<std::shared_ptr<DRC_RULE>> rules;
2781 DRC_RULES_PARSER rulesParser( dru, wxString::FromUTF8( name ) );
2782
2783 BOOST_REQUIRE_NO_THROW( rulesParser.Parse( rules, &reporter ) );
2784 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR | RPT_SEVERITY_WARNING ),
2785 std::string( name ) + " rules should parse without error:\n"
2786 + reporter.GetMessages().ToStdString() + "\n--- rules ---\n"
2787 + dru.ToStdString() );
2788 BOOST_CHECK_GT( rules.size(), 0u );
2789 }
2790
2791 BOOST_CHECK_MESSAGE( boardsWithRules > 0,
2792 "Expected at least one committed board to generate zone DRC rules" );
2793}
2794
2795
2802BOOST_AUTO_TEST_CASE( ZoneDesignRulesParityOptional )
2803{
2804 const char* examplesEnv = std::getenv( "DIPTRACE_VIEWER_EXAMPLES_DIR" );
2805 std::string examplesDir =
2806 examplesEnv && *examplesEnv ? examplesEnv : "/home/seth/Downloads/DipTrace Viewer/Examples";
2807 std::string cncPath = examplesDir + "/CNC_controller.dip";
2808
2809 if( !std::filesystem::exists( cncPath ) )
2810 {
2811 BOOST_TEST_MESSAGE( "Viewer examples path not found; skipping ZoneDesignRulesParityOptional" );
2812 return;
2813 }
2814
2815 BOARD board;
2816 DIPTRACE::PCB_PARSER parser( wxString::FromUTF8( cncPath ), &board );
2817 parser.Parse();
2818
2819 wxString dru = parser.GenerateDesignRules();
2820
2821 std::vector<std::shared_ptr<DRC_RULE>> rules;
2823 DRC_RULES_PARSER rulesParser( dru, wxT( "DipTrace CNC rules" ) );
2824
2825 BOOST_REQUIRE_NO_THROW( rulesParser.Parse( rules, &reporter ) );
2826 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR | RPT_SEVERITY_WARNING ),
2827 "CNC rules should parse without error:\n" + reporter.GetMessages().ToStdString() );
2828
2829 int edgeClearanceIU = 0;
2830 int solidViaRules = 0;
2831
2832 for( const std::shared_ptr<DRC_RULE>& rule : rules )
2833 {
2834 for( const DRC_CONSTRAINT& constraint : rule->m_Constraints )
2835 {
2836 if( constraint.m_Type == EDGE_CLEARANCE_CONSTRAINT )
2837 edgeClearanceIU = constraint.GetValue().Min();
2838 else if( constraint.m_Type == ZONE_CONNECTION_CONSTRAINT
2839 && constraint.m_ZoneConnection == ZONE_CONNECTION::FULL )
2840 solidViaRules++;
2841 }
2842 }
2843
2844 BOOST_CHECK_SMALL( std::abs( pcbIUScale.IUTomm( edgeClearanceIU ) - 0.66 ), 0.01 );
2845 BOOST_CHECK_GT( solidViaRules, 0 );
2846}
2847
2848
2856BOOST_AUTO_TEST_CASE( PlacementRotationAngles )
2857{
2858 struct CASE
2859 {
2860 std::string file;
2861 std::map<std::string, double> expected; // refdes -> degrees
2862 };
2863
2864 const std::vector<CASE> cases = {
2865 { "rotate.dip", { { "C1", 0.0 }, { "C2", 45.0 }, { "C3", 90.0 }, { "C4", 309.33 } } },
2866 { "rotate4.dip", { { "C1", 0.0 }, { "C2", 90.0 }, { "C3", 45.0 }, { "C4", 327.96 } } },
2867 };
2868
2869 for( const CASE& tc : cases )
2870 {
2871 auto board = LoadBoard( tc.file );
2872 BOOST_REQUIRE( board );
2873
2874 std::map<std::string, double> seen;
2875
2876 for( FOOTPRINT* fp : board->Footprints() )
2877 seen[fp->GetReference().ToStdString()] = fp->GetOrientation().Normalize().AsDegrees();
2878
2879 for( const auto& [ref, deg] : tc.expected )
2880 {
2881 BOOST_REQUIRE_MESSAGE( seen.count( ref ), tc.file + ": missing " + ref );
2882
2883 double got = seen[ref];
2884 double gap = std::abs( got - deg );
2885 gap = std::min( gap, 360.0 - gap );
2886
2887 BOOST_CHECK_MESSAGE( gap < 0.1, tc.file + ": " + ref + " orientation " + std::to_string( got )
2888 + " deg, expected " + std::to_string( deg ) );
2889 }
2890 }
2891}
2892
2893
const char * name
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
BOX2< VECTOR2I > BOX2I
Definition box2.h:927
NETINFO_ITEM * GetNet() const
Return #NET_INFO object for a given item.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:409
NETINFO_ITEM * FindNet(int aNetcode) const
Search for a net with the given netcode.
Definition board.cpp:2980
int GetCopperLayerCount() const
Definition board.cpp:1131
const FOOTPRINTS & Footprints() const
Definition board.h:463
const TRACKS & Tracks() const
Definition board.h:461
bool GetBoardPolygonOutlines(SHAPE_POLY_SET &aOutlines, bool aInferOutlineIfNecessary, OUTLINE_ERROR_HANDLER *aErrorHandler=nullptr, bool aAllowUseArcsInPolygons=false, bool aIncludeNPTHAsOutlines=false)
Extract the board outlines and build a closed polygon from lines, arcs and circle items on edge cut l...
Definition board.cpp:3784
const DRAWINGS & Drawings() const
Definition board.h:465
constexpr size_type GetWidth() const
Definition box2.h:211
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:653
constexpr size_type GetHeight() const
Definition box2.h:212
Parses a DipTrace .dip binary board file and populates a KiCad BOARD.
wxString GenerateDesignRules() const
Build a KiCad custom design-rule (.kicad_dru) document for the per-zone DipTrace properties that have...
void Parse()
Parse the file and populate the board. Throws IO_ERROR on failure.
const MINOPTMAX< int > & GetValue() const
Definition drc_rule.h:200
ZONE_CONNECTION m_ZoneConnection
Definition drc_rule.h:246
DRC_CONSTRAINT_T m_Type
Definition drc_rule.h:243
void Parse(std::vector< std::shared_ptr< DRC_RULE > > &aRules, REPORTER *aReporter)
double AsDegrees() const
Definition eda_angle.h:116
SHAPE_T GetShape() const
Definition eda_shape.h:175
const VECTOR2I & GetEnd() const
Return the ending point of the graphic.
Definition eda_shape.h:325
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:275
EDA_ANGLE GetOrientation() const
Definition footprint.h:438
std::deque< PAD * > & Pads()
Definition footprint.h:404
VECTOR2I GetPosition() const override
Definition footprint.h:435
DRAWINGS & GraphicalItems()
Definition footprint.h:407
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString What() const
A composite of Problem() and Where()
virtual const char * what() const override
std::exception interface, returned as UTF-8
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
T Min() const
Definition minoptmax.h:29
Handle the data for a net.
Definition netinfo.h:50
const wxString & GetNetname() const
Definition netinfo.h:110
static constexpr PCB_LAYER_ID ALL_LAYERS
! The layer identifier to use for the single defintion on normal padstacks
Definition padstack.h:179
Definition pad.h:61
PAD_ATTRIB GetAttribute() const
Definition pad.h:558
VECTOR2I GetDrillSize() const
Definition pad.h:318
VECTOR2I GetSize(PCB_LAYER_ID aLayer) const
Definition pad.cpp:288
PAD_DRILL_SHAPE GetDrillShape() const
Definition pad.h:432
EDA_ANGLE GetFPRelativeOrientation() const
Definition pad.cpp:1756
const BOX2I GetBoundingBox() const override
Return the orthogonal bounding box of this object for display purposes.
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition pcb_shape.h:68
A scoped application of a wxLog target.
int PointCount() const
Return the number of points (vertices) in this line chain.
Represent a set of closed polygons.
SHAPE_LINE_CHAIN & Outline(int aIndex)
Return the reference to aIndex-th outline in the set.
int OutlineCount() const
Return the number of outlines in the set.
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.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
A wrapper for reporting to a wxString object.
Definition reporter.h:242
Handle a list of polygons defining a copper zone.
Definition zone.h:70
ZONE_CONNECTION GetPadConnection() const
Definition zone.h:312
int GetThermalReliefSpokeWidth() const
Definition zone.h:259
@ ZONE_CONNECTION_CONSTRAINT
Definition drc_rule.h:64
@ EDGE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:55
#define _(s)
@ SEGMENT
Definition eda_shape.h:56
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:703
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ In30_Cu
Definition layer_ids.h:91
@ Edge_Cuts
Definition layer_ids.h:108
@ B_Cu
Definition layer_ids.h:61
@ In2_Cu
Definition layer_ids.h:63
@ F_SilkS
Definition layer_ids.h:96
@ In1_Cu
Definition layer_ids.h:62
@ B_SilkS
Definition layer_ids.h:97
@ F_Cu
Definition layer_ids.h:60
STL namespace.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
@ NPTH
like PAD_PTH, but not plated mechanical use only, no connection allowed
Definition padstack.h:102
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:98
@ PTH
Plated through hole pad.
Definition padstack.h:97
PAD_SHAPE
The set of pad shapes, used with PAD::{Set,Get}Shape()
Definition padstack.h:51
@ ROUNDRECT
Definition padstack.h:56
@ RECTANGLE
Definition padstack.h:53
@ RPT_SEVERITY_WARNING
@ RPT_SEVERITY_ERROR
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_CASE(TotalPadCount)
Verify that the Z80 board produces a substantial pad count.
Shared fixture and includes for the DipTrace PCB benchmark test suite.
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
IbisParser parser & reporter
KIBIS_MODEL * model
VECTOR3I expected(15, 30, 45)
int clearance
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")
#define M_PI
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:80
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:89
@ PCB_ARC_T
class PCB_ARC, an arc track segment on a copper layer
Definition typeinfo.h:90
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:88
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
ISLAND_REMOVAL_MODE
Whether or not to remove isolated islands from a zone.
@ THERMAL
Use thermal relief for pads.
Definition zones.h:46
@ THT_THERMAL
Thermal relief only for THT pads.
Definition zones.h:48
@ FULL
pads are covered by copper
Definition zones.h:47