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