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