KiCad PCB EDA Suite
Loading...
Searching...
No Matches
pads_parser.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2025 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
20#include "pads_parser.h"
21#include <io/pads/pads_common.h>
22#include <fstream>
23#include <sstream>
24#include <iostream>
25#include <algorithm>
26#include <climits>
27#include <cmath>
28#include <cstdlib>
29#include <limits>
30#include <wx/log.h>
31#include <trace_helpers.h>
32
33namespace PADS_IO
34{
35
37{
38 return aLayer.shape == "RT" || aLayer.shape == "ST";
39}
40
41
42bool IsAntiPadRow( const PAD_STACK_LAYER& aLayer )
43{
44 return aLayer.shape == "RA" || aLayer.shape == "SA";
45}
46
47
48bool IsCopperPadRow( const PAD_STACK_LAYER& aLayer )
49{
50 return !IsThermalReliefPadRow( aLayer ) && !IsAntiPadRow( aLayer );
51}
52
53
63static std::vector<std::string> expandShortcutPattern( const std::string& aPattern )
64{
65 std::vector<std::string> result;
66
67 size_t braceStart = aPattern.find( '{' );
68 size_t braceEnd = aPattern.find( '}' );
69
70 if( braceStart == std::string::npos || braceEnd == std::string::npos || braceEnd <= braceStart )
71 {
72 result.push_back( aPattern );
73 return result;
74 }
75
76 std::string prefix = aPattern.substr( 0, braceStart );
77 std::string suffix = ( braceEnd + 1 < aPattern.length() ) ? aPattern.substr( braceEnd + 1 ) : "";
78 std::string range = aPattern.substr( braceStart + 1, braceEnd - braceStart - 1 );
79
80 size_t dashPos = range.find( '-' );
81
82 if( dashPos == std::string::npos )
83 {
84 result.push_back( aPattern );
85 return result;
86 }
87
88 int start = PADS_COMMON::ParseInt( range.substr( 0, dashPos ), INT_MIN, "shortcut range" );
89 int end = PADS_COMMON::ParseInt( range.substr( dashPos + 1 ), INT_MIN, "shortcut range" );
90
91 if( start == INT_MIN || end == INT_MIN )
92 {
93 result.push_back( aPattern );
94 return result;
95 }
96
97 static constexpr int MAX_EXPANSION = 10000;
98
99 if( std::abs( end - start ) > MAX_EXPANSION )
100 {
101 wxLogTrace( tracePadsIo, wxT( "PADS Import: shortcut range {%d-%d} exceeds limit, skipped" ),
102 start, end );
103 result.push_back( aPattern );
104 return result;
105 }
106
107 for( int i = start; i <= end; ++i )
108 {
109 result.push_back( prefix + std::to_string( i ) + suffix );
110 }
111
112 return result;
113}
114
115
116static std::string normalizeNetName( const std::string& aNetName )
117{
118 // PADS uses numeric sentinels in optional owner/signame fields for unassigned nets.
119 if( aNetName == "-1" || aNetName == "0" )
120 return {};
121
122 return aNetName;
123}
124
125
127{
128}
129
131{
132}
133
134void PARSER::Parse( const wxString& aFileName )
135{
136 std::ifstream file( aFileName.ToStdString() );
137 if( !file.is_open() )
138 {
139 throw std::runtime_error( "Could not open file " + aFileName.ToStdString() );
140 }
141
142 std::string line;
143
144 // Read header
145 if( !readLine( file, line ) )
146 {
147 throw std::runtime_error( "Empty file" );
148 }
149
150 // Parse header line format:
151 // PCB files: !PADS-product-version-units[-mode][-encoding]!
152 // Example: !PADS-POWERPCB-V9.4-MILS!
153 // Library files: *PADS-LIBRARY-type-Vversion*
154 // Example: *PADS-LIBRARY-PCB-DECALS-V9*
155 m_is_basic_units = false;
157
158 // Check for library file format (uses * delimiters)
159 if( line.size() > 2 && line[0] == '*' && line.back() == '*' )
160 {
161 std::string header = line.substr( 1, line.size() - 2 );
162 m_file_header.product = header;
163
164 // Detect library type from header
165 if( header.find( "LIBRARY-LINE-ITEMS" ) != std::string::npos ||
166 header.find( "LIBRARY-LINE" ) != std::string::npos )
167 {
169 }
170 else if( header.find( "LIBRARY-SCH-DECALS" ) != std::string::npos )
171 {
173 }
174 else if( header.find( "LIBRARY-PCB-DECALS" ) != std::string::npos ||
175 header.find( "LIBRARY-DECALS" ) != std::string::npos )
176 {
178 }
179 else if( header.find( "LIBRARY-PART-TYPES" ) != std::string::npos )
180 {
182 }
183
184 // Extract version from library header (e.g., V9 from *PADS-LIBRARY-PCB-DECALS-V9*)
185 size_t v_pos = header.rfind( "-V" );
186
187 if( v_pos != std::string::npos )
188 {
189 m_file_header.version = header.substr( v_pos + 1 );
190 }
191
192 // Library files default to mils
194 }
195 else if( line.size() > 2 && line[0] == '!' )
196 {
197 // PCB file format: !PADS-product-version-units[-mode][-encoding]! [description...]
198 // Find the closing '!' to extract just the header marker, ignoring any trailing text
199 size_t close_pos = line.find( '!', 1 );
200
201 if( close_pos == std::string::npos )
202 close_pos = line.size();
203
204 std::string header = line.substr( 1, close_pos - 1 );
205
206 // Split by '-'
207 std::vector<std::string> parts;
208 size_t start = 0;
209 size_t pos = 0;
210
211 while( ( pos = header.find( '-', start ) ) != std::string::npos )
212 {
213 parts.push_back( header.substr( start, pos - start ) );
214 start = pos + 1;
215 }
216
217 parts.push_back( header.substr( start ) );
218
219 // Parse parts: PADS, product, version, units, [mode], [encoding]
220 if( parts.size() >= 4 )
221 {
222 // First part should be "PADS"
223 m_file_header.product = parts[1];
224 m_file_header.version = parts[2];
225 m_file_header.units = parts[3];
226
227 if( parts.size() >= 5 )
228 m_file_header.mode = parts[4];
229
230 if( parts.size() >= 6 )
231 m_file_header.encoding = parts[5];
232 }
233 else if( parts.size() >= 2 )
234 {
235 // Simpler format
236 m_file_header.product = parts[0];
237
238 if( parts.size() >= 2 )
239 m_file_header.version = parts[1];
240
241 if( parts.size() >= 3 )
242 m_file_header.units = parts[2];
243 }
244
245 // Set units based on parsed header
246 if( m_file_header.units == "BASIC" )
247 {
248 m_is_basic_units = true;
249 }
250 else if( m_file_header.units == "MILS" || m_file_header.units == "MIL" )
251 {
253 }
254 else if( m_file_header.units == "MM" || m_file_header.units == "METRIC" )
255 {
257 }
258 else if( m_file_header.units == "INCH" || m_file_header.units == "INCHES" )
259 {
261 }
262 }
263 else if( line.find( "BASIC" ) != std::string::npos )
264 {
265 m_is_basic_units = true;
266 }
267
268 // PADS V3-V5 use 2-line text/label entries (no font line). The version
269 // sequence then jumps to the year releases V2003/V2005/V2007 and to V9+,
270 // all of which insert a font style line between the attribute and name
271 // lines. An unrecognized header defaults to the modern 3-line format.
272 int majorVer = parseMajorVersion();
273 m_has_font_lines = ( majorVer == 0 || majorVer >= 9 );
274
275 while( readLine( file, line ) )
276 {
277 if( line.empty() ) continue;
278
279 if( line.rfind( "*PCB*", 0 ) == 0 )
280 {
281 parseSectionPCB( file );
282 }
283 else if( line.rfind( "*PART*", 0 ) == 0 )
284 {
285 parseSectionPARTS( file );
286 }
287 else if( line.rfind( "*NET*", 0 ) == 0 )
288 {
289 parseSectionNETS( file );
290 }
291 else if( line.rfind( "*ROUTE*", 0 ) == 0 )
292 {
293 parseSectionROUTES( file );
294 }
295 else if( line.rfind( "*TEXT*", 0 ) == 0 )
296 {
297 parseSectionTEXT( file );
298 }
299 else if( line.rfind( "*BOARD*", 0 ) == 0 )
300 {
301 parseSectionBOARD( file );
302 }
303 else if( line.rfind( "*LINES*", 0 ) == 0 )
304 {
305 parseSectionLINES( file );
306 }
307 else if( line.rfind( "*VIA*", 0 ) == 0 )
308 {
309 parseSectionVIA( file );
310 }
311 else if( line.rfind( "*POUR*", 0 ) == 0 )
312 {
313 parseSectionPOUR( file );
314 }
315 else if( line.rfind( "*PARTDECAL*", 0 ) == 0 )
316 {
317 parseSectionPARTDECAL( file );
318 }
319 else if( line.rfind( "*PARTTYPE*", 0 ) == 0 )
320 {
321 parseSectionPARTTYPE( file );
322 }
323 else if( line.rfind( "*REUSE*", 0 ) == 0 )
324 {
325 parseSectionREUSE( file );
326 }
327 else if( line.rfind( "*CLUSTER*", 0 ) == 0 )
328 {
329 parseSectionCLUSTER( file );
330 }
331 else if( line.rfind( "*JUMPER*", 0 ) == 0 )
332 {
333 parseSectionJUMPER( file );
334 }
335 else if( line.rfind( "*TESTPOINT*", 0 ) == 0 )
336 {
337 parseSectionTESTPOINT( file );
338 }
339 else if( line.rfind( "*NETCLASS*", 0 ) == 0 || line.rfind( "*NETDEF*", 0 ) == 0 )
340 {
341 parseSectionNETCLASS( file );
342 }
343 else if( line.rfind( "*DIFFPAIR*", 0 ) == 0 || line.rfind( "*DIFFPAIRS*", 0 ) == 0 )
344 {
345 parseSectionDIFFPAIR( file );
346 }
347 else if( line.rfind( "LAYER MILS", 0 ) == 0 || line.rfind( "LAYER METRIC", 0 ) == 0 )
348 {
349 parseSectionLAYERDEFS( file );
350 }
351 else if( line.rfind( "*MISC*", 0 ) == 0 )
352 {
353 parseSectionMISC( file );
354 }
355 }
356}
357
358bool PARSER::readLine( std::ifstream& aStream, std::string& aLine )
359{
360 if( m_pushed_line )
361 {
362 aLine = *m_pushed_line;
363 m_pushed_line.reset();
364 return true;
365 }
366
367 while( std::getline( aStream, aLine ) )
368 {
369 // Trim whitespace
370 aLine.erase( 0, aLine.find_first_not_of( " \t\r\n" ) );
371 aLine.erase( aLine.find_last_not_of( " \t\r\n" ) + 1 );
372
373 if( aLine.empty() ) continue;
374 if( aLine.rfind( "*REMARK*", 0 ) == 0 ) continue;
375 return true;
376 }
377 return false;
378}
379
380void PARSER::pushBackLine( const std::string& aLine )
381{
382 m_pushed_line = aLine;
383}
384
385
387{
388 const std::string& ver = m_file_header.version;
389
390 // Version strings look like "V5.0", "V9.4", "V2005.0", etc.
391 size_t start = 0;
392
393 if( !ver.empty() && ( ver[0] == 'V' || ver[0] == 'v' ) )
394 start = 1;
395
396 size_t dot = ver.find( '.', start );
397 std::string major_str = ( dot != std::string::npos ) ? ver.substr( start, dot - start )
398 : ver.substr( start );
399
400 try
401 {
402 return std::stoi( major_str );
403 }
404 catch( const std::exception& )
405 {
406 return 0;
407 }
408}
409
410void PARSER::parseSectionPCB( std::ifstream& aStream )
411{
412 std::string line;
413 while( readLine( aStream, line ) )
414 {
415 if( line[0] == '*' )
416 {
417 pushBackLine( line );
418 break;
419 }
420
421 std::istringstream iss( line );
422 std::string token;
423 iss >> token;
424
425 if( token == "UNITS" )
426 {
427 std::string val;
428 iss >> val;
429
430 if( val == "0" ) m_parameters.units = UNIT_TYPE::MILS;
431 else if( val == "1" ) m_parameters.units = UNIT_TYPE::METRIC;
432 else if( val == "2" ) m_parameters.units = UNIT_TYPE::INCHES;
433 }
434 else if( token == "USERGRID" )
435 {
436 iss >> m_parameters.user_grid;
437 }
438 else if( token == "MAXIMUMLAYER" )
439 {
440 iss >> m_parameters.layer_count;
441 }
442 else if( token == "ORIGIN" )
443 {
444 iss >> m_parameters.origin.x >> m_parameters.origin.y;
445 }
446 else if( token == "THERLINEWID" )
447 {
448 iss >> m_parameters.thermal_line_width;
449 }
450 else if( token == "THERSMDWID" )
451 {
452 iss >> m_parameters.thermal_smd_width;
453 }
454 else if( token == "THERFLAGS" )
455 {
456 std::string flags_str;
457 iss >> flags_str;
458
459 try
460 {
461 m_parameters.thermal_flags = std::stoi( flags_str, nullptr, 0 );
462 }
463 catch( const std::exception& )
464 {
465 m_parameters.thermal_flags = 0;
466 }
467 }
468 else if( token == "DRLOVERSIZE" )
469 {
470 iss >> m_parameters.drill_oversize;
471 }
472 else if( token == "VIAPSHVIA" )
473 {
474 iss >> m_parameters.default_signal_via;
475 }
476 else if( token == "STMINCLEAR" )
477 {
478 iss >> m_parameters.thermal_min_clearance;
479 }
480 else if( token == "STMINSPOKES" )
481 {
482 iss >> m_parameters.thermal_min_spokes;
483 }
484 else if( token == "MINCLEAR" )
485 {
486 iss >> m_design_rules.min_clearance;
487 }
488 else if( token == "DEFAULTCLEAR" )
489 {
490 iss >> m_design_rules.default_clearance;
491 }
492 else if( token == "MINTRACKWID" )
493 {
494 iss >> m_design_rules.min_track_width;
495 }
496 else if( token == "DEFAULTTRACKWID" )
497 {
498 iss >> m_design_rules.default_track_width;
499 }
500 else if( token == "MINVIASIZE" )
501 {
502 iss >> m_design_rules.min_via_size;
503 }
504 else if( token == "DEFAULTVIASIZE" )
505 {
506 iss >> m_design_rules.default_via_size;
507 }
508 else if( token == "MINVIADRILL" )
509 {
510 iss >> m_design_rules.min_via_drill;
511 }
512 else if( token == "DEFAULTVIADRILL" )
513 {
514 iss >> m_design_rules.default_via_drill;
515 }
516 else if( token == "HOLEHOLE" )
517 {
518 iss >> m_design_rules.hole_to_hole;
519 }
520 else if( token == "SILKCLEAR" )
521 {
522 iss >> m_design_rules.silk_clearance;
523 }
524 else if( token == "MASKCLEAR" )
525 {
526 iss >> m_design_rules.mask_clearance;
527 }
528 }
529}
530
531void PARSER::parseSectionPARTS( std::ifstream& aStream )
532{
533 std::string line;
534 while( readLine( aStream, line ) )
535 {
536 if( line.find( "*REMARK*" ) == 0 )
537 continue;
538
539 if( line[0] == '*' )
540 {
541 pushBackLine( line );
542 break;
543 }
544
545 // Skip attribute lines and other non-part lines
546 if( line.rfind( "}", 0 ) == 0 ||
547 line.rfind( "{", 0 ) == 0 )
548 {
549 continue;
550 }
551
552 std::istringstream iss( line );
553 PART part;
554 part.location.x = 0.0;
555 part.location.y = 0.0;
556
557 std::string name_token, parttype_string;
558 iss >> name_token >> parttype_string >> part.location.x >> part.location.y >> part.rotation;
559
560 if( iss.fail() )
561 {
562 continue;
563 }
564
565 // Check for shortcut format: PRE{n1-n2}
566 // Example: C{2-20} with same attributes creates C2 through C20
567 std::vector<std::string> expanded_names = expandShortcutPattern( name_token );
568 bool is_shortcut = ( expanded_names.size() > 1 );
569 part.name = expanded_names[0];
570
571 // Check for explicit decal override using @ syntax
572 // Format: PARTTYPE@DECAL_NAME means use DECAL_NAME instead of looking up from PARTTYPE
573 size_t at_pos = parttype_string.find( '@' );
574
575 if( at_pos != std::string::npos )
576 {
577 // Explicit decal specified after @
578 part.part_type = parttype_string.substr( 0, at_pos );
579 part.decal = parttype_string.substr( at_pos + 1 );
580 part.explicit_decal = true;
581 }
582 else
583 {
584 // No @ - could be a direct decal name or a part type name
585 // Store as decal for now, resolution happens in pcb_io_pads.cpp
586 // Split on ':' to get primary and alternates (for direct decal lists)
587 size_t pos = 0;
588 size_t colon_pos = 0;
589 bool first = true;
590
591 while( ( colon_pos = parttype_string.find( ':', pos ) ) != std::string::npos )
592 {
593 std::string decal_name = parttype_string.substr( pos, colon_pos - pos );
594
595 if( first )
596 {
597 part.decal = decal_name;
598 first = false;
599 }
600 else
601 {
602 part.alternate_decals.push_back( decal_name );
603 }
604
605 pos = colon_pos + 1;
606 }
607
608 // Handle the last (or only) decal name
609 std::string last_decal = parttype_string.substr( pos );
610
611 if( first )
612 {
613 part.decal = last_decal;
614 }
615 else
616 {
617 part.alternate_decals.push_back( last_decal );
618 }
619 }
620
621 // Read all remaining tokens
622 std::vector<std::string> tokens;
623 std::string token;
624 while( iss >> token )
625 {
626 tokens.push_back( token );
627 }
628
629 int labels = 0;
630
631 // Process tokens for flags and label count
632 // Format per REMARK: GLUE MIRROR ALT CLSTID CLSTATTR BROTHERID LABELS
633 // GLUE: U (unglued) or G (glued)
634 // MIRROR: N (normal/top) or M (mirrored/bottom)
635 // ALT: Alternate decal index (0-based, -1 or missing = use primary)
636 for( size_t i = 0; i < tokens.size(); ++i )
637 {
638 const std::string& t = tokens[i];
639
640 if( t == "G" )
641 part.glued = true;
642 else if( t == "M" )
643 part.bottom_layer = true;
644
645 // U = unglued (default), N = normal/not-mirrored (default)
646 // These are defaults so we don't need to explicitly handle them
647
648 // Parse ALT field (token index 2 after GLUE and MIRROR)
649 // ALT field is 0-indexed in PADS format
650 if( i == 2 )
651 {
652 int alt = PADS_COMMON::ParseInt( t, -1, "PART ALT" );
653
654 if( alt >= 0 )
655 part.alt_decal_index = alt;
656 }
657
658 // The last token is the label count
659 if( i == tokens.size() - 1 )
660 {
661 try
662 {
663 size_t pos = 0;
664 labels = std::stoi( t, &pos );
665
666 if( pos != t.length() )
667 labels = 0;
668 }
669 catch( const std::exception& )
670 {
671 labels = 0;
672 }
673 }
674 }
675
676 // Check for optional .REUSE. line following part header
677 // Format: .REUSE. instance part
678 if( readLine( aStream, line ) )
679 {
680 if( line.find( ".REUSE." ) == 0 )
681 {
682 std::istringstream riss( line );
683 std::string reuse_keyword;
684 riss >> reuse_keyword >> part.reuse_instance >> part.reuse_part;
685 }
686 else
687 {
688 pushBackLine( line );
689 }
690 }
691
692 for( int i = 0; i < labels; ++i )
693 {
694 ATTRIBUTE attr;
695 if( !readLine( aStream, line ) ) break;
696
697 std::stringstream iss_attr( line );
698 std::string visible_str;
699 std::string mirrored_str;
700 std::string right_reading_str;
701
702 // VISIBLE XLOC YLOC ORI LEVEL HEIGHT WIDTH MIRRORED HJUST VJUST [RIGHTREADING]
703 if( iss_attr >> visible_str >> attr.x >> attr.y >> attr.orientation >> attr.level
704 >> attr.height >> attr.width >> mirrored_str >> attr.hjust >> attr.vjust )
705 {
706 attr.visible = ( visible_str == "VALUE" || visible_str == "FULL_NAME"
707 || visible_str == "NAME" || visible_str == "FULL_BOTH"
708 || visible_str == "BOTH" );
709 attr.mirrored = ( mirrored_str == "M" );
710 iss_attr >> right_reading_str;
711 attr.right_reading = ( right_reading_str == "Y" || right_reading_str == "ORTHO" );
712 }
713
714 if( m_has_font_lines )
715 {
716 if( !readLine( aStream, line ) ) break;
717 attr.font_info = line;
718 }
719
720 if( !readLine( aStream, line ) ) break;
721 attr.name = line;
722
723 part.attributes.push_back( attr );
724 }
725
726 // Add the first part (or only part if not a shortcut)
727 m_parts.push_back( part );
728
729 // If this was a shortcut pattern, create additional parts with same attributes
730 // but different reference designators
731 if( is_shortcut )
732 {
733 for( size_t i = 1; i < expanded_names.size(); ++i )
734 {
735 PART additional_part = part;
736 additional_part.name = expanded_names[i];
737 m_parts.push_back( additional_part );
738 }
739 }
740 }
741}
742
743void PARSER::parseSectionNETS( std::ifstream& aStream )
744{
745 // Implementation for NETS
746 // Format: *NET* NETNAME
747 // REF.PIN REF.PIN ... [.REUSE. instance rsignal]
748 // Supports shortcut format: PRE{n1-n2}.{pin1-pin2} expands to multiple pins
749 std::string line;
750 NET* current_net = nullptr;
751
752 // Helper lambda to parse a pin token that may have .REUSE. suffix
753 auto parsePinToken = []( const std::string& token, NET_PIN& pin ) -> bool
754 {
755 size_t dot_pos = token.find( '.' );
756
757 if( dot_pos == std::string::npos )
758 return false;
759
760 pin.ref_des = token.substr( 0, dot_pos );
761 pin.pin_name = token.substr( dot_pos + 1 );
762 return true;
763 };
764
765 // Helper lambda to expand shortcut format tokens like U{4-8}.{7-8}
766 // Returns a vector of expanded pins
767 auto expandShortcutPin = []( const std::string& token ) -> std::vector<std::string>
768 {
769 std::vector<std::string> results;
770
771 // Check if this contains any {n-m} range patterns
772 if( token.find( '{' ) == std::string::npos )
773 {
774 results.push_back( token );
775 return results;
776 }
777
778 // Parse the token to find all range patterns
779 // Format: PREFIX{start-end}MIDDLE{start-end}SUFFIX...
780 struct RangePart
781 {
782 std::string prefix;
783 int start = 0;
784 int end = 0;
785 bool is_range = false;
786 };
787
788 std::vector<RangePart> parts;
789 size_t pos = 0;
790 std::string current_prefix;
791
792 while( pos < token.size() )
793 {
794 if( token[pos] == '{' )
795 {
796 size_t close_pos = token.find( '}', pos );
797
798 if( close_pos == std::string::npos )
799 {
800 // Malformed, return as-is
801 results.push_back( token );
802 return results;
803 }
804
805 std::string range_str = token.substr( pos + 1, close_pos - pos - 1 );
806 size_t dash_pos = range_str.find( '-' );
807
808 if( dash_pos != std::string::npos )
809 {
810 RangePart part;
811 part.prefix = current_prefix;
812 part.is_range = true;
813
814 part.start = PADS_COMMON::ParseInt( range_str.substr( 0, dash_pos ),
815 INT_MIN, "net range" );
816 part.end = PADS_COMMON::ParseInt( range_str.substr( dash_pos + 1 ),
817 INT_MIN, "net range" );
818
819 if( part.start == INT_MIN || part.end == INT_MIN )
820 {
821 results.push_back( token );
822 return results;
823 }
824
825 parts.push_back( part );
826 current_prefix.clear();
827 }
828 else
829 {
830 // Single value in braces, treat as literal
831 current_prefix += range_str;
832 }
833
834 pos = close_pos + 1;
835 }
836 else
837 {
838 current_prefix += token[pos];
839 pos++;
840 }
841 }
842
843 // Add any trailing text as a final non-range part
844 if( !current_prefix.empty() || parts.empty() )
845 {
846 RangePart final_part;
847 final_part.prefix = current_prefix;
848 final_part.is_range = false;
849 final_part.start = 0;
850 final_part.end = 0;
851 parts.push_back( final_part );
852 }
853
854 // Generate all combinations
855 // Start with empty string
856 results.push_back( "" );
857
858 for( const auto& part : parts )
859 {
860 std::vector<std::string> new_results;
861
862 if( part.is_range )
863 {
864 for( const auto& base : results )
865 {
866 int step = ( part.start <= part.end ) ? 1 : -1;
867
868 for( int i = part.start; step > 0 ? i <= part.end : i >= part.end; i += step )
869 {
870 new_results.push_back( base + part.prefix + std::to_string( i ) );
871 }
872 }
873 }
874 else
875 {
876 for( const auto& base : results )
877 {
878 new_results.push_back( base + part.prefix );
879 }
880 }
881
882 results = std::move( new_results );
883 }
884
885 return results;
886 };
887
888 while( readLine( aStream, line ) )
889 {
890 if( line[0] == '*' )
891 {
892 pushBackLine( line );
893 break;
894 }
895
896 std::istringstream iss( line );
897 std::string token;
898 iss >> token;
899
900 if( token == "SIGNAL" )
901 {
902 NET net;
903 iss >> net.name;
904 m_nets.push_back( net );
905 current_net = &m_nets.back();
906
907 // Parse remaining tokens on this line
908 std::string pin_token;
909
910 while( iss >> pin_token )
911 {
912 // Check for .REUSE. suffix
913 if( pin_token == ".REUSE." )
914 {
915 // Read instance and signal for the previous pin
916 std::string instance, rsignal;
917
918 if( ( iss >> instance >> rsignal ) && !current_net->pins.empty() )
919 {
920 current_net->pins.back().reuse_instance = instance;
921 current_net->pins.back().reuse_signal = rsignal;
922 }
923
924 continue;
925 }
926
927 // Expand shortcut format and add all resulting pins
928 for( const auto& expanded : expandShortcutPin( pin_token ) )
929 {
930 NET_PIN pin;
931
932 if( parsePinToken( expanded, pin ) )
933 current_net->pins.push_back( pin );
934 }
935 }
936 }
937 else
938 {
939 // Continuation of pins for current net
940 if( current_net )
941 {
942 do
943 {
944 // Check for .REUSE. suffix
945 if( token == ".REUSE." )
946 {
947 std::string instance, rsignal;
948
949 if( ( iss >> instance >> rsignal ) && !current_net->pins.empty() )
950 {
951 current_net->pins.back().reuse_instance = instance;
952 current_net->pins.back().reuse_signal = rsignal;
953 }
954
955 continue;
956 }
957
958 // Expand shortcut format and add all resulting pins
959 for( const auto& expanded : expandShortcutPin( token ) )
960 {
961 NET_PIN pin;
962
963 if( parsePinToken( expanded, pin ) )
964 current_net->pins.push_back( pin );
965 }
966
967 } while( iss >> token );
968 }
969 }
970 }
971}
972
973void PARSER::parseSectionVIA( std::ifstream& aStream )
974{
975 std::string line;
976
977 while( readLine( aStream, line ) )
978 {
979 if( line[0] == '*' )
980 {
981 pushBackLine( line );
982 return;
983 }
984
985 std::stringstream iss( line );
986 std::string name;
987 double drill = 0.0;
988 int stacklines = 0;
989
990 if( !( iss >> name >> drill >> stacklines ) )
991 continue;
992
993 VIA_DEF def;
994 def.name = name;
995 def.drill = drill;
996
997 // Parse optional drill_start and drill_end for blind/buried vias
998 int drill_start_val = 0;
999 int drill_end_val = 0;
1000
1001 if( iss >> drill_start_val >> drill_end_val )
1002 {
1003 def.drill_start = drill_start_val;
1004 def.drill_end = drill_end_val;
1005 }
1006
1007 int min_layer = INT_MAX;
1008 int max_layer = INT_MIN;
1009
1010 for( int i = 0; i < stacklines; ++i )
1011 {
1012 if( !readLine( aStream, line ) )
1013 break;
1014
1015 std::stringstream iss2( line );
1016 int level = 0;
1017 double size = 0.0;
1018 std::string shape;
1019
1020 if( !( iss2 >> level >> size >> shape ) )
1021 continue;
1022
1023 PAD_STACK_LAYER layer_data;
1024 layer_data.layer = level;
1025 layer_data.shape = shape;
1026 layer_data.sizeA = size;
1027 layer_data.plated = true;
1028
1029 // Parse shape-specific parameters per PADS spec
1030 if( shape == "R" || shape == "S" )
1031 {
1032 // Round or Square pad: level size shape [corner]
1033 // Negative corner = chamfered, positive = rounded, zero = square
1034 double corner = 0;
1035
1036 if( shape == "S" && ( iss2 >> corner ) )
1037 {
1038 if( corner < 0 )
1039 {
1040 layer_data.corner_radius = -corner;
1041 layer_data.chamfered = true;
1042 }
1043 else
1044 {
1045 layer_data.corner_radius = corner;
1046 }
1047 }
1048 }
1049 else if( shape == "RA" || shape == "SA" )
1050 {
1051 // Anti-pad shapes: level size shape (no additional params)
1052 // These define clearance shapes in planes
1053 }
1054 else if( shape == "A" )
1055 {
1056 // Annular pad: level size shape inner_diameter
1057 double intd = 0;
1058
1059 if( iss2 >> intd )
1060 layer_data.inner_diameter = intd;
1061 }
1062 else if( shape == "OF" )
1063 {
1064 // Oval finger: level size shape orientation length offset
1065 double ori = 0, length = 0, offset = 0;
1066
1067 if( iss2 >> ori >> length >> offset )
1068 {
1069 layer_data.rotation = ori;
1070 layer_data.sizeB = length;
1071 layer_data.finger_offset = offset;
1072 }
1073 }
1074 else if( shape == "RF" )
1075 {
1076 // Rectangular finger: level size shape orientation length offset
1077 // Per reference parser: rotation is first, then length (becomes sizeB), then offset
1078 double ori = 0, length = 0, offset = 0;
1079
1080 if( iss2 >> ori >> length >> offset )
1081 {
1082 layer_data.rotation = ori;
1083 layer_data.sizeB = length;
1084 layer_data.finger_offset = offset;
1085 }
1086 }
1087 else if( shape == "RT" || shape == "ST" )
1088 {
1089 // Thermal pads: level size shape orientation inner_diam spoke_width spoke_count
1090 double ori = 0, intd = 0, spkwid = 0;
1091 int spknum = 4;
1092
1093 if( iss2 >> ori >> intd >> spkwid >> spknum )
1094 {
1095 layer_data.thermal_spoke_orientation = ori;
1096 layer_data.thermal_outer_diameter = intd;
1097 layer_data.thermal_spoke_width = spkwid;
1098 layer_data.thermal_spoke_count = spknum;
1099 }
1100 }
1101 else if( shape == "O" || shape == "OC" )
1102 {
1103 // Odd shape (O) or Odd Circle (OC): level size shape
1104 // These use custom pad shapes defined elsewhere
1105 // No additional parameters, just store the shape type
1106 }
1107 else if( shape == "RC" )
1108 {
1109 // Rectangular with Corner: level size RC orientation length offset [corner]
1110 // Similar to RF but with optional corner radius
1111 double ori = 0, length = 0, offset = 0, corner = 0;
1112
1113 if( iss2 >> ori >> length >> offset )
1114 {
1115 layer_data.rotation = ori;
1116 layer_data.sizeB = length;
1117 layer_data.finger_offset = offset;
1118
1119 if( iss2 >> corner )
1120 {
1121 if( corner < 0 )
1122 {
1123 layer_data.corner_radius = -corner;
1124 layer_data.chamfered = true;
1125 }
1126 else
1127 {
1128 layer_data.corner_radius = corner;
1129 }
1130 }
1131 }
1132 }
1133
1134 def.stack.push_back( layer_data );
1135
1136 // Map special layer numbers to copper layer indices.
1137 // Non-copper layers (soldermask, silkscreen, etc.) must not
1138 // affect via type classification or pad size.
1139 int effective_layer = level;
1140
1141 if( level == -2 )
1142 effective_layer = 1;
1143 else if( level == -1 )
1144 effective_layer = m_parameters.layer_count;
1145
1146 bool is_copper = ( effective_layer >= 1
1147 && effective_layer <= m_parameters.layer_count );
1148
1149 if( is_copper )
1150 {
1151 if( size > def.size )
1152 def.size = size;
1153
1154 if( effective_layer < min_layer )
1155 min_layer = effective_layer;
1156
1157 if( effective_layer > max_layer )
1158 max_layer = effective_layer;
1159 }
1160
1161 // PADS layer 25 = top soldermask, 28 = bottom soldermask
1162 if( level == 25 )
1163 def.has_mask_front = true;
1164 else if( level == 28 )
1165 def.has_mask_back = true;
1166 }
1167
1168 if( def.drill_start > 0 && def.drill_end > 0 )
1169 {
1170 min_layer = std::min( def.drill_start, def.drill_end );
1171 max_layer = std::max( def.drill_start, def.drill_end );
1172 }
1173
1174 // Determine layer span and via type
1175 if( min_layer <= max_layer )
1176 {
1177 def.start_layer = min_layer;
1178 def.end_layer = max_layer;
1179
1180 int layer_count = m_parameters.layer_count;
1181 bool starts_at_surface = ( min_layer == 1 || max_layer == layer_count );
1182 bool ends_at_surface = ( max_layer == layer_count || min_layer == 1 );
1183 bool is_full_span = ( min_layer == 1 && max_layer == layer_count );
1184 int span = max_layer - min_layer;
1185
1186 if( is_full_span )
1187 {
1189 }
1190 else if( span == 1 && ( min_layer == 1 || max_layer == layer_count ) )
1191 {
1193 }
1194 else if( starts_at_surface || ends_at_surface )
1195 {
1197 }
1198 else
1199 {
1201 }
1202 }
1203
1204 m_via_defs[name] = def;
1205 }
1206
1207 // If no signal via was specified in the header, fall back to the first definition
1208 if( m_parameters.default_signal_via.empty() && !m_via_defs.empty() )
1209 m_parameters.default_signal_via = m_via_defs.begin()->first;
1210}
1211
1212void PARSER::parseSectionPOUR( std::ifstream& aStream )
1213{
1214 std::string line;
1215
1216 while( readLine( aStream, line ) )
1217 {
1218 if( line[0] == '*' )
1219 {
1220 pushBackLine( line );
1221 return;
1222 }
1223
1224 // Parse Header
1225 // NAME TYPE XLOC YLOC PIECES FLAGS [OWNERNAME SIGNAME [HATCHGRID HATCHRAD [PRIORITY]]]
1226 std::stringstream iss( line );
1227 std::string name, type;
1228 double x = 0.0, y = 0.0;
1229 int pieces = 0, flags = 0;
1230
1231 if( !( iss >> name >> type >> x >> y >> pieces >> flags ) )
1232 continue;
1233
1234 std::string owner, signame;
1235 double hatchgrid = 0.0, hatchrad = 0.0;
1236 int priority = 0;
1237
1238 if( iss >> owner >> signame )
1239 {
1240 iss >> hatchgrid >> hatchrad >> priority;
1241 }
1242
1243 for( int i = 0; i < pieces; ++i )
1244 {
1245 if( !readLine( aStream, line ) )
1246 break;
1247
1248 // PIECETYPE CORNERS ARCS WIDTH LEVEL [THERMALS]
1249 // PIECETYPE: POLY, SEG, CIRCLE, CUTOUT, CIRCUT, POCUT
1250 std::stringstream iss2( line );
1251 std::string poly_type;
1252 int corners = 0, arcs = 0;
1253 double width = 0.0;
1254 int level = 0;
1255
1256 if( !( iss2 >> poly_type >> corners >> arcs >> width >> level ) )
1257 continue;
1258
1259 POUR pour;
1260 pour.name = name;
1261 pour.net_name = normalizeNetName( signame );
1262 pour.layer = level;
1263 pour.priority = priority;
1264 pour.width = width;
1265 pour.is_cutout = ( poly_type == "POCUT" || poly_type == "CUTOUT"
1266 || poly_type == "CIRCUT" );
1267 pour.owner_pour = owner;
1268 pour.hatch_grid = hatchgrid;
1269 pour.hatch_width = hatchrad;
1270
1271 // The header TYPE field (POUROUT, HATOUT, VOIDOUT, PADTHERM, VIATHERM)
1272 // determines the record's role. The piece-level poly_type (POLY, SEG, etc.)
1273 // only describes the geometry shape.
1274 if( type == "HATOUT" )
1275 {
1277 }
1278 else if( type == "VOIDOUT" )
1279 {
1281 pour.is_cutout = true;
1282 }
1283 else if( type == "PADTHERM" )
1284 {
1286 }
1287 else if( type == "VIATHERM" )
1288 {
1290 }
1291
1292 // Handle different piece types
1293 if( poly_type == "CIRCLE" || poly_type == "CIRCUT" )
1294 {
1295 // Circle piece: `corners` lines (always 2 in practice), each a diametrically
1296 // opposite endpoint -- not a single "xloc yloc radius" line.
1297 std::vector<std::pair<double, double>> endpoints;
1298
1299 for( int j = 0; j < corners; ++j )
1300 {
1301 if( !readLine( aStream, line ) )
1302 break;
1303
1304 std::stringstream iss3( line );
1305 double px = 0.0, py = 0.0;
1306
1307 if( iss3 >> px >> py )
1308 endpoints.emplace_back( px, py );
1309 }
1310
1311 if( endpoints.size() == 2 )
1312 {
1313 double cx = ( endpoints[0].first + endpoints[1].first ) / 2.0;
1314 double cy = ( endpoints[0].second + endpoints[1].second ) / 2.0;
1315 double radius = std::hypot( endpoints[1].first - endpoints[0].first,
1316 endpoints[1].second - endpoints[0].second ) / 2.0;
1317
1318 // Create arc representing full circle
1319 ARC arc{};
1320 arc.cx = x + cx;
1321 arc.cy = y + cy;
1322 arc.radius = radius;
1323 arc.start_angle = 0.0;
1324 arc.delta_angle = 360.0;
1325 pour.points.emplace_back( x + cx + radius, y + cy, arc );
1326 }
1327 }
1328 else if( poly_type == "SEG" )
1329 {
1330 // Segment piece: pairs of points defining line segments
1331 for( int j = 0; j < corners; ++j )
1332 {
1333 if( !readLine( aStream, line ) )
1334 break;
1335
1336 std::stringstream iss3( line );
1337 double px = 0.0, py = 0.0;
1338
1339 if( iss3 >> px >> py )
1340 {
1341 pour.points.emplace_back( x + px, y + py );
1342 }
1343 }
1344 }
1345 else
1346 {
1347 // Polygon piece types: POLY, POCUT, HATOUT, POUROUT, VOIDOUT,
1348 // PADTHERM, VIATHERM.
1349 //
1350 // Total data lines = corners + arcs. Lines with 4 values
1351 // (cx cy beginAngle sweepAngle) define an arc center and
1352 // angles. The following line gives the arc endpoint.
1353 int totalLines = corners + arcs;
1354 bool nextIsArcEndpoint = false;
1355 ARC pendingArc{};
1356
1357 for( int j = 0; j < totalLines; ++j )
1358 {
1359 if( !readLine( aStream, line ) )
1360 break;
1361
1362 std::stringstream iss3( line );
1363 double px = 0.0, py = 0.0;
1364
1365 if( !( iss3 >> px >> py ) )
1366 continue;
1367
1368 int angle1 = 0, angle2 = 0;
1369
1370 if( iss3 >> angle1 >> angle2 )
1371 {
1372 // Arc center line. The two angles are begin angle
1373 // (direction from center to the previous vertex) and
1374 // sweep angle, both in tenths of degrees.
1375 pendingArc = ARC{};
1376 pendingArc.cx = x + px;
1377 pendingArc.cy = y + py;
1378 pendingArc.start_angle = angle1 / 10.0;
1379 pendingArc.delta_angle = angle2 / 10.0;
1380
1381 if( !pour.points.empty() )
1382 {
1383 double dx = pour.points.back().x - pendingArc.cx;
1384 double dy = pour.points.back().y - pendingArc.cy;
1385 pendingArc.radius = std::sqrt( dx * dx + dy * dy );
1386 }
1387
1388 nextIsArcEndpoint = true;
1389 }
1390 else if( nextIsArcEndpoint )
1391 {
1392 if( pendingArc.radius == 0.0 )
1393 {
1394 double dx = ( x + px ) - pendingArc.cx;
1395 double dy = ( y + py ) - pendingArc.cy;
1396 pendingArc.radius = std::sqrt( dx * dx + dy * dy );
1397 }
1398
1399 pour.points.emplace_back( x + px, y + py, pendingArc );
1400 nextIsArcEndpoint = false;
1401 }
1402 else
1403 {
1404 pour.points.emplace_back( x + px, y + py );
1405 }
1406 }
1407 }
1408
1409 m_pours.push_back( pour );
1410 }
1411 }
1412}
1413
1414void PARSER::parseSectionPARTDECAL( std::ifstream& aStream )
1415{
1416 std::string line;
1417 while( readLine( aStream, line ) )
1418 {
1419 if( line[0] == '*' )
1420 {
1421 pushBackLine( line );
1422 return;
1423 }
1424
1425 // Header: NAME UNITS ORIX ORIY PIECES TERMINALS STACKS TEXT LABELS
1426 std::stringstream iss( line );
1427 std::string name, units;
1428 double orix = 0.0, oriy = 0.0;
1429 int pieces = 0, terminals = 0, stacks = 0, text_cnt = 0, labels = 0;
1430
1431 if( !( iss >> name >> units >> orix >> oriy >> pieces >> terminals >> stacks >> text_cnt >> labels ) )
1432 continue;
1433
1434 PART_DECAL decal;
1435 decal.name = name;
1436 decal.units = units;
1437
1438 // Parse Pieces (Graphics)
1439 for( int i = 0; i < pieces; ++i )
1440 {
1441 if( !readLine( aStream, line ) ) break;
1442
1443 // PIECETYPE CORNERS WIDTHHGHT LINESTYLE LEVEL [RESTRICTIONS]
1444 std::stringstream iss2( line );
1445 std::string type;
1446 int corners = 0;
1447 double width = 0;
1448 int level = 0;
1449
1450 if( !( iss2 >> type >> corners >> width ) )
1451 {
1452 // Should not happen if line is valid
1453 continue;
1454 }
1455
1456 // Try to read optional fields
1457 // Some formats have LINESTYLE LEVEL, others just LEVEL
1458 int val1 = 0;
1459 if( iss2 >> val1 )
1460 {
1461 int val2 = 0;
1462 if( iss2 >> val2 )
1463 {
1464 level = val2;
1465 }
1466 else
1467 {
1468 level = val1;
1469 }
1470 }
1471
1472 DECAL_ITEM item;
1473 item.type = type;
1474 item.width = width;
1475 item.layer = level;
1476
1477 // Handle TAG piece type (no coordinates, used for grouping copper/cutouts)
1478 if( type == "TAG" )
1479 {
1480 // Level is used as open/close flag: 1=open group, 0=close group
1481 item.is_tag_open = ( level == 1 );
1482 item.is_tag_close = ( level == 0 );
1483 decal.items.push_back( item );
1484 continue;
1485 }
1486
1487 // Parse pinnum for copper pieces (COPCLS, COPOPN, COPCIR, COPCUT, COPCCO)
1488 // Format includes [pinnum] at the end for copper associated with a pin
1489 if( type.find( "COP" ) == 0 )
1490 {
1491 std::string remaining;
1492 std::getline( iss2, remaining );
1493
1494 // Check for pinnum in remaining tokens
1495 std::istringstream rem_ss( remaining );
1496 int pinnum_val = -1;
1497
1498 if( rem_ss >> pinnum_val )
1499 item.pinnum = pinnum_val;
1500 }
1501
1502 // Parse restrictions for keepout pieces (KPTCLS, KPTCIR)
1503 if( type.find( "KPT" ) == 0 )
1504 {
1505 std::string restrictions;
1506
1507 if( iss2 >> restrictions )
1508 item.restrictions = restrictions;
1509 }
1510
1511 for( int j = 0; j < corners; ++j )
1512 {
1513 if( !readLine( aStream, line ) )
1514 break;
1515
1516 std::stringstream iss3( line );
1517 double px = 0.0, py = 0.0;
1518
1519 if( !( iss3 >> px >> py ) )
1520 continue;
1521
1522 // Per PADS spec, arc format is: x1 y1 ab aa ax1 ay1 ax2 ay2
1523 // where x1,y1 = arc start point, ab = begin angle (tenths of deg),
1524 // aa = sweep angle (tenths of deg), ax1,ay1/ax2,ay2 = bounding box
1525 int startAngleTenths = 0, deltaAngleTenths = 0;
1526 double bboxMinX = 0.0, bboxMinY = 0.0, bboxMaxX = 0.0, bboxMaxY = 0.0;
1527
1528 if( iss3 >> startAngleTenths >> deltaAngleTenths
1529 >> bboxMinX >> bboxMinY >> bboxMaxX >> bboxMaxY )
1530 {
1531 double cx = ( bboxMinX + bboxMaxX ) / 2.0;
1532 double cy = ( bboxMinY + bboxMaxY ) / 2.0;
1533 double radius = ( bboxMaxX - bboxMinX ) / 2.0;
1534 double startAngle = startAngleTenths / 10.0;
1535 double deltaAngle = deltaAngleTenths / 10.0;
1536
1537 // Calculate arc start point (center + radius at start angle)
1538 double startAngleRad = startAngle * M_PI / 180.0;
1539 double startX = cx + radius * std::cos( startAngleRad );
1540 double startY = cy + radius * std::sin( startAngleRad );
1541
1542 // Calculate arc endpoint (center + radius at end angle)
1543 double endAngleRad = ( startAngle + deltaAngle ) * M_PI / 180.0;
1544 double endX = cx + radius * std::cos( endAngleRad );
1545 double endY = cy + radius * std::sin( endAngleRad );
1546
1547 // Add arc start as a regular point (connects from previous point)
1548 item.points.emplace_back( startX, startY );
1549
1550 ARC arc{};
1551 arc.cx = cx;
1552 arc.cy = cy;
1553 arc.radius = radius;
1554 arc.start_angle = startAngle;
1555 arc.delta_angle = deltaAngle;
1556
1557 // Add arc end with arc data (draws the arc from start to end)
1558 item.points.emplace_back( endX, endY, arc );
1559 }
1560 else
1561 {
1562 item.points.emplace_back( px, py );
1563 }
1564 }
1565
1566 decal.items.push_back( item );
1567 }
1568
1569 // Parse Text/Labels
1570 // V9+ format: 3 lines per entry (VALUE line, font line, name line)
1571 // V5.x format: 2 lines per entry (VALUE line, name line, no font)
1572
1573 for( int i = 0; i < text_cnt + labels; ++i )
1574 {
1575 std::string attrLine, fontLine, nameLine;
1576
1577 if( !readLine( aStream, attrLine ) )
1578 break;
1579
1580 if( m_has_font_lines )
1581 {
1582 if( !readLine( aStream, fontLine ) )
1583 break;
1584 }
1585
1586 if( !readLine( aStream, nameLine ) )
1587 break;
1588
1589 ATTRIBUTE attr;
1590 std::stringstream ss( attrLine );
1591 std::string type_token;
1592 ss >> type_token;
1593
1594 std::string mirrored_str, right_reading_str;
1595
1596 if( ss >> attr.x >> attr.y >> attr.orientation >> attr.level
1597 >> attr.height >> attr.width >> mirrored_str >> attr.hjust >> attr.vjust )
1598 {
1599 attr.visible = ( type_token == "VALUE" || type_token == "FULL_NAME"
1600 || type_token == "NAME" || type_token == "FULL_BOTH"
1601 || type_token == "BOTH" );
1602 attr.mirrored = ( mirrored_str == "M" );
1603 ss >> right_reading_str;
1604 attr.right_reading = ( right_reading_str == "Y" || right_reading_str == "ORTHO" );
1605 }
1606
1607 attr.font_info = fontLine;
1608 attr.name = nameLine;
1609
1610 decal.attributes.push_back( attr );
1611 }
1612
1613 // Parse Terminals (T lines)
1614 // T-150 -110 -150 -110 1
1615 // Format: T X Y NMX NMY [PINNUM]
1616 // The T prefix is concatenated with the X coordinate (e.g. "T-150").
1617 // V5.x omits the pin number; V9+ includes it.
1618
1619 for( int i = 0; i < terminals; ++i )
1620 {
1621 if( !readLine( aStream, line ) ) break;
1622
1623 size_t t_pos = line.find( 'T' );
1624
1625 if( t_pos != std::string::npos )
1626 line[t_pos] = ' ';
1627
1628 std::stringstream iss_t( line );
1629 TERMINAL term;
1630 double nmx = 0.0, nmy = 0.0;
1631
1632 if( iss_t >> term.x >> term.y >> nmx >> nmy )
1633 {
1634 iss_t >> term.name;
1635
1636 if( term.name.empty() )
1637 term.name = std::to_string( i + 1 );
1638
1639 decal.terminals.push_back( term );
1640 }
1641 }
1642
1643 // Parse Stacks (PAD definitions)
1644 // PAD <PIN_INDEX> <STACK_LINES>
1645 // Then <STACK_LINES> lines of data.
1646
1647 for( int i = 0; i < stacks; ++i )
1648 {
1649 if( !readLine( aStream, line ) )
1650 break;
1651
1652 std::stringstream iss_pad( line );
1653 std::string token;
1654 int pin_idx = 0;
1655 int stack_lines = 0;
1656 iss_pad >> token >> pin_idx >> stack_lines;
1657
1658 if( token != "PAD" )
1659 continue;
1660
1661 // Parse optional P (plated) or N (non-plated) after stack_lines
1662 std::string plated_token;
1663 bool default_plated = true;
1664 double header_drill = 0.0;
1665
1666 if( iss_pad >> plated_token )
1667 {
1668 if( plated_token == "P" )
1669 default_plated = true;
1670 else if( plated_token == "N" )
1671 default_plated = false;
1672 else
1673 {
1674 header_drill = PADS_COMMON::ParseDouble( plated_token, 0.0, "pad drill" );
1675 }
1676 }
1677
1678 // Parse optional slotted drill parameters from header
1679 double header_slot_ori = 0.0;
1680 double header_slot_len = 0.0;
1681 double header_slot_off = 0.0;
1682
1683 if( iss_pad >> header_slot_ori >> header_slot_len >> header_slot_off )
1684 {
1685 // Got slotted drill from header
1686 }
1687
1688 std::vector<PAD_STACK_LAYER> stack;
1689
1690 for( int j = 0; j < stack_lines; ++j )
1691 {
1692 if( !readLine( aStream, line ) )
1693 break;
1694
1695 std::stringstream line_ss( line );
1696
1697 int layer = 0;
1698 double size = 0.0;
1699 std::string shape;
1700
1701 if( !( line_ss >> layer >> size >> shape ) )
1702 continue;
1703
1704 PAD_STACK_LAYER layer_data;
1705 layer_data.layer = layer;
1706 layer_data.sizeA = size;
1707 layer_data.sizeB = size;
1708 layer_data.shape = shape;
1709 layer_data.plated = default_plated;
1710 layer_data.drill = header_drill;
1711 layer_data.slot_orientation = header_slot_ori;
1712 layer_data.slot_length = header_slot_len;
1713 layer_data.slot_offset = header_slot_off;
1714
1715 // Parse shape-specific parameters per PADS specification
1716 if( shape == "R" )
1717 {
1718 // Round pad: level size R
1719 // No additional shape params, may have drill after
1720 }
1721 else if( shape == "S" )
1722 {
1723 // Square pad: level size S [corner]
1724 // Negative corner = chamfered, positive = rounded, zero = square
1725 double corner = 0.0;
1726
1727 if( line_ss >> corner )
1728 {
1729 if( corner < 0 )
1730 {
1731 layer_data.corner_radius = -corner;
1732 layer_data.chamfered = true;
1733 }
1734 else
1735 {
1736 layer_data.corner_radius = corner;
1737 }
1738 }
1739 }
1740 else if( shape == "RA" || shape == "SA" )
1741 {
1742 // Anti-pad shapes: level size RA/SA (no additional params)
1743 // These define clearance shapes in plane layers
1744 }
1745 else if( shape == "A" )
1746 {
1747 // Annular pad: level size A inner_diameter
1748 double intd = 0.0;
1749
1750 if( line_ss >> intd )
1751 layer_data.inner_diameter = intd;
1752 }
1753 else if( shape == "OF" )
1754 {
1755 // Oval finger: level size OF orientation length offset
1756 double ori = 0.0, length = 0.0, offset = 0.0;
1757
1758 if( line_ss >> ori >> length >> offset )
1759 {
1760 layer_data.rotation = ori;
1761 layer_data.sizeB = length;
1762 layer_data.finger_offset = offset;
1763 }
1764 }
1765 else if( shape == "RF" )
1766 {
1767 // Rectangular finger: level size RF orientation length offset [corner]
1768 // Per PADS spec, corner radius exists for square and rectangular finger shapes.
1769 double ori = 0.0, length = 0.0, offset = 0.0;
1770
1771 if( line_ss >> ori >> length >> offset )
1772 {
1773 layer_data.rotation = ori;
1774 layer_data.sizeB = length;
1775 layer_data.finger_offset = offset;
1776
1777 double corner = 0.0;
1778
1779 if( line_ss >> corner )
1780 {
1781 if( corner < 0 )
1782 {
1783 layer_data.corner_radius = -corner;
1784 layer_data.chamfered = true;
1785 }
1786 else
1787 {
1788 layer_data.corner_radius = corner;
1789 }
1790 }
1791 }
1792 }
1793 else if( shape == "RT" || shape == "ST" )
1794 {
1795 // Thermal pads: level size RT/ST orientation inner_diam spoke_width spoke_count
1796 double ori = 0.0, outsize = 0.0, spkwid = 0.0;
1797 int spknum = 4;
1798
1799 if( line_ss >> ori >> outsize >> spkwid >> spknum )
1800 {
1801 layer_data.thermal_spoke_orientation = ori;
1802 layer_data.thermal_outer_diameter = outsize;
1803 layer_data.thermal_spoke_width = spkwid;
1804 layer_data.thermal_spoke_count = spknum;
1805 }
1806 }
1807 else if( shape == "O" || shape == "OC" )
1808 {
1809 // Odd shape (O) or Odd Circle (OC): level size shape
1810 // These use custom pad shapes defined elsewhere
1811 // No additional parameters, just store the shape type
1812 }
1813 else if( shape == "RC" )
1814 {
1815 // Rectangular with Corner: level size RC orientation length offset [corner]
1816 // Similar to RF but with optional corner radius
1817 double ori = 0.0, length = 0.0, offset = 0.0, corner = 0.0;
1818
1819 if( line_ss >> ori >> length >> offset )
1820 {
1821 layer_data.rotation = ori;
1822 layer_data.sizeB = length;
1823 layer_data.finger_offset = offset;
1824
1825 if( line_ss >> corner )
1826 {
1827 if( corner < 0 )
1828 {
1829 layer_data.corner_radius = -corner;
1830 layer_data.chamfered = true;
1831 }
1832 else
1833 {
1834 layer_data.corner_radius = corner;
1835 }
1836 }
1837 }
1838 }
1839
1840 // For some shapes, additional tokens may be drill and plated
1841 // Read remaining tokens
1842 std::vector<std::string> remaining;
1843 std::string token_rem;
1844
1845 while( line_ss >> token_rem )
1846 remaining.push_back( token_rem );
1847
1848 // Parse remaining tokens for drill, plated, and slotted drill
1849 if( !remaining.empty() )
1850 {
1851 size_t idx = 0;
1852
1853 // Check for drill value (numeric)
1854 double drill_val = PADS_COMMON::ParseDouble( remaining[idx],
1855 -1.0, "pad layer drill" );
1856
1857 if( drill_val >= 0.0 )
1858 {
1859 layer_data.drill = drill_val;
1860 idx++;
1861 }
1862
1863 // Check for plated flag
1864 if( idx < remaining.size() )
1865 {
1866 if( remaining[idx] == "P" || remaining[idx] == "Y" )
1867 {
1868 layer_data.plated = true;
1869 idx++;
1870 }
1871 else if( remaining[idx] == "N" )
1872 {
1873 layer_data.plated = false;
1874 idx++;
1875 }
1876 }
1877
1878 // Check for slotted drill parameters
1879 if( idx + 2 < remaining.size() )
1880 {
1881 layer_data.slot_orientation =
1882 PADS_COMMON::ParseDouble( remaining[idx], 0.0, "slot params" );
1883 layer_data.slot_length =
1884 PADS_COMMON::ParseDouble( remaining[idx + 1], 0.0, "slot params" );
1885 layer_data.slot_offset =
1886 PADS_COMMON::ParseDouble( remaining[idx + 2], 0.0, "slot params" );
1887 }
1888 }
1889
1890 stack.push_back( layer_data );
1891 }
1892
1893 decal.pad_stacks[pin_idx] = stack;
1894 }
1895
1896 m_decals[name] = decal;
1897 }
1898}
1899
1900void PARSER::parseSectionROUTES( std::ifstream& aStream )
1901{
1902 std::string line;
1903 ROUTE* current_route = nullptr;
1904 TRACK current_track;
1905 bool in_track = false;
1906 bool prev_is_plane_connection = false;
1907 ARC_POINT last_plane_connection_pt;
1908 int last_plane_connection_layer = 0;
1909 double last_plane_connection_width = 0;
1910 bool last_plane_on_copper = false;
1911 std::string default_via_name;
1912 bool has_pending_arc_center = false;
1913 ARC_POINT pending_arc_center;
1914 std::string pending_arc_dir;
1915
1916 while( readLine( aStream, line ) )
1917 {
1918 if( line[0] == '*' )
1919 {
1920 if( line.rfind( "*SIGNAL*", 0 ) == 0 )
1921 {
1922 if( in_track && current_route )
1923 {
1924 current_route->tracks.push_back( current_track );
1925 current_track.points.clear();
1926 in_track = false;
1927 }
1928
1929 prev_is_plane_connection = false;
1930 has_pending_arc_center = false;
1931
1932 std::istringstream iss( line );
1933 std::string token;
1934 iss >> token; // *SIGNAL*
1935
1936 std::string net_name;
1937 iss >> net_name;
1938
1939 // Parse optional flags and default via
1940 default_via_name.clear();
1941
1942 while( iss >> token )
1943 {
1944 if( !token.empty() && token.back() == ';' )
1945 token.pop_back();
1946
1947 if( m_via_defs.count( token ) )
1948 default_via_name = token;
1949 }
1950
1951 m_routes.push_back( ROUTE() );
1952 current_route = &m_routes.back();
1953 current_route->net_name = net_name;
1954 continue;
1955 }
1956
1957 pushBackLine( line );
1958 break;
1959 }
1960
1961 // Parse pin pair lines (start with non-digit/non-sign)
1962 // Format: "REF.PIN REF.PIN"
1963 // These indicate which pins are connected by the following route segments
1964 if( !isdigit( line[0] ) && line[0] != '-' && line[0] != '+' )
1965 {
1966 if( in_track && current_route )
1967 {
1968 current_route->tracks.push_back( current_track );
1969 current_track.points.clear();
1970 in_track = false;
1971 }
1972
1973 prev_is_plane_connection = false;
1974
1975 // Parse pin pairs from this line and add to current route
1976 if( current_route )
1977 {
1978 std::istringstream pin_iss( line );
1979 std::string pin_token;
1980
1981 while( pin_iss >> pin_token )
1982 {
1983 size_t dot_pos = pin_token.find( '.' );
1984
1985 if( dot_pos != std::string::npos )
1986 {
1987 NET_PIN pin;
1988 pin.ref_des = pin_token.substr( 0, dot_pos );
1989 pin.pin_name = pin_token.substr( dot_pos + 1 );
1990
1991 // Check if pin already exists (avoid duplicates)
1992 bool found = false;
1993
1994 for( const auto& existing : current_route->pins )
1995 {
1996 if( existing.ref_des == pin.ref_des &&
1997 existing.pin_name == pin.pin_name )
1998 {
1999 found = true;
2000 break;
2001 }
2002 }
2003
2004 if( !found )
2005 current_route->pins.push_back( pin );
2006 }
2007 }
2008 }
2009
2010 continue;
2011 }
2012
2013 std::istringstream iss( line );
2014 ARC_POINT pt;
2015 int layer = 0;
2016 double width = 0.0;
2017 int flags = 0;
2018 iss >> pt.x >> pt.y >> layer >> width >> flags;
2019
2020 if( iss.fail() )
2021 continue;
2022
2023 // SEGMENTWIDTH is already in mils (not 1/256 mil units as previously thought)
2024
2025 // Parse FLAGS and optional arc direction / via name
2026 // Format: FLAGS [ARCDIR/VIANAME] [POWER] [TEARDROP ...] [JUMPER ...]
2027 // ARCDIR can be CW (clockwise) or CCW (counter-clockwise)
2028 // POWER indicates a connection through a power/ground plane (not a discrete track)
2029 std::string token;
2030 std::string via_name;
2031 std::string arc_dir;
2032
2033 // Per PADS spec, layer 0 means "unrouted portion" (virtual connection through
2034 // a plane or ratline). Only layer 0 makes a segment non-physical. Flag 0x100
2035 // ("plane thermal") and the THERMAL keyword describe pad/via thermal relief
2036 // style and do not affect whether the track segment exists.
2037 bool is_unrouted = ( layer == 0 );
2038 bool is_plane_connection = is_unrouted;
2039 TEARDROP teardrop;
2040 JUMPER_MARKER jumper;
2041 bool has_teardrop = false;
2042 bool has_jumper = false;
2043 bool has_power = false;
2044
2045 while( iss >> token )
2046 {
2047 // Check for arc direction
2048 if( token == "CW" || token == "CCW" )
2049 {
2050 arc_dir = token;
2051 continue;
2052 }
2053
2054 // POWER indicates a connection through a power/ground plane.
2055 // In PADS files, "POWER" often doubles as a via definition name.
2056 // THERMAL describes pad/via thermal relief style.
2057 if( token == "POWER" )
2058 {
2059 has_power = true;
2060
2061 if( m_via_defs.count( token ) )
2062 via_name = token;
2063
2064 continue;
2065 }
2066
2067 if( token == "THERMAL" )
2068 continue;
2069
2070 // Check for via name
2071 if( m_via_defs.count( token ) )
2072 {
2073 via_name = token;
2074 continue;
2075 }
2076
2077 // Parse TEARDROP: TEARDROP [P width length [flags]] [N width length [flags]]
2078 if( token == "TEARDROP" )
2079 {
2080 has_teardrop = true;
2081 std::string td_token;
2082
2083 while( iss >> td_token )
2084 {
2085 if( td_token == "P" )
2086 {
2087 teardrop.has_pad_teardrop = true;
2088 iss >> teardrop.pad_width >> teardrop.pad_length;
2089
2090 // Check for optional flags (numeric)
2091 std::streampos pos = iss.tellg();
2092 int td_flags = 0;
2093
2094 if( iss >> td_flags )
2095 {
2096 teardrop.pad_flags = td_flags;
2097 }
2098 else
2099 {
2100 iss.clear();
2101 iss.seekg( pos );
2102 }
2103 }
2104 else if( td_token == "N" )
2105 {
2106 teardrop.has_net_teardrop = true;
2107 iss >> teardrop.net_width >> teardrop.net_length;
2108
2109 std::streampos pos = iss.tellg();
2110 int td_flags = 0;
2111
2112 if( iss >> td_flags )
2113 {
2114 teardrop.net_flags = td_flags;
2115 }
2116 else
2117 {
2118 iss.clear();
2119 iss.seekg( pos );
2120 }
2121 }
2122 else
2123 {
2124 // Not a teardrop param, push back for further parsing
2125 // Since we can't push back easily, break and handle below
2126 if( td_token == "CW" || td_token == "CCW" )
2127 arc_dir = td_token;
2128 else if( td_token == "POWER" )
2129 {
2130 has_power = true;
2131
2132 if( m_via_defs.count( td_token ) )
2133 via_name = td_token;
2134 }
2135 else if( td_token == "THERMAL" )
2136 ;
2137 else if( m_via_defs.count( td_token ) )
2138 via_name = td_token;
2139
2140 break;
2141 }
2142 }
2143
2144 continue;
2145 }
2146
2147 // Parse JUMPER: jumper_name S|E
2148 // Jumper names are typically followed by S (start) or E (end)
2149 std::streampos pos = iss.tellg();
2150 std::string jumper_flag;
2151
2152 if( iss >> jumper_flag )
2153 {
2154 if( jumper_flag == "S" || jumper_flag == "E" )
2155 {
2156 has_jumper = true;
2157 jumper.name = token;
2158 jumper.is_start = ( jumper_flag == "S" );
2159 jumper.x = pt.x;
2160 jumper.y = pt.y;
2161 continue;
2162 }
2163 else
2164 {
2165 // Not a jumper, restore stream position
2166 iss.clear();
2167 iss.seekg( pos );
2168 }
2169 }
2170 else
2171 {
2172 iss.clear();
2173 iss.seekg( pos );
2174 }
2175
2176 // Skip REUSE tokens
2177 if( token == "REUSE" || token == ".REUSE." )
2178 {
2179 // Skip the instance name that follows
2180 std::string instance;
2181 iss >> instance;
2182 continue;
2183 }
2184 }
2185
2186 // A corner carrying an arc direction is the arc's center, not a track vertex.
2187 // Per the PADS ASCII spec the arc begins on the preceding corner and ends on
2188 // the following one, curving around this corner. Defer it and attach explicit
2189 // geometry to the next corner.
2190 if( !arc_dir.empty() )
2191 {
2192 pending_arc_center = pt;
2193 pending_arc_dir = arc_dir;
2194 has_pending_arc_center = true;
2195 continue;
2196 }
2197
2198 if( has_pending_arc_center )
2199 {
2200 has_pending_arc_center = false;
2201
2202 if( in_track && !current_track.points.empty() )
2203 {
2204 const ARC_POINT& arc_start = current_track.points.back();
2205 double dx0 = arc_start.x - pending_arc_center.x;
2206 double dy0 = arc_start.y - pending_arc_center.y;
2207 double start_angle = std::atan2( dy0, dx0 );
2208 double end_angle =
2209 std::atan2( pt.y - pending_arc_center.y, pt.x - pending_arc_center.x );
2210 double sweep = end_angle - start_angle;
2211
2212 // atan2 differences land in (-2pi, 2pi); pull the sweep onto the arc's
2213 // side so the winding matches the recorded direction.
2214 if( pending_arc_dir == "CCW" )
2215 {
2216 while( sweep <= 0.0 )
2217 sweep += 2.0 * M_PI;
2218 }
2219 else
2220 {
2221 while( sweep >= 0.0 )
2222 sweep -= 2.0 * M_PI;
2223 }
2224
2225 pt.is_arc = true;
2226 pt.arc.cx = pending_arc_center.x;
2227 pt.arc.cy = pending_arc_center.y;
2228 pt.arc.radius = std::sqrt( dx0 * dx0 + dy0 * dy0 );
2229 pt.arc.start_angle = start_angle * 180.0 / M_PI;
2230 pt.arc.delta_angle = sweep * 180.0 / M_PI;
2231 }
2232 }
2233
2234 // Per PADS spec: Layer 0 means "unrouted portion" - these are NOT physical tracks.
2235 // Layer 65 indicates the end of route/connection at a component pin.
2236 // Vias are only created when an explicit via token (STANDARDVIA, etc.) is present.
2237
2238 int effective_layer = layer;
2239 bool is_pad_connection = ( layer == 65 );
2240
2241 // Layer 0 means "unrouted" - this segment goes through a plane or is a ratline.
2242 // We still need an effective layer for via purposes, so use current track layer if available.
2243 if( is_unrouted && in_track )
2244 effective_layer = current_track.layer;
2245
2246 // Create via at this point if a via token was present.
2247 // This must happen before plane connection handling since plane connection points
2248 // with vias (STANDARDVIA + THERMAL) would otherwise skip via creation.
2249 if( !via_name.empty() && current_route )
2250 {
2251 VIA via;
2252 via.name = via_name;
2253 via.location = { pt.x, pt.y };
2254 current_route->vias.push_back( via );
2255 }
2256
2257 // POWER on a real copper layer means a via to the inner power/ground plane.
2258 // Routes often stub out to a POWER point and backtrack, with the via providing
2259 // the connection to the plane. Normally the POWER token itself names a via
2260 // definition (handled above), but if not, create an implicit via with the
2261 // route's default via type.
2262 if( has_power && via_name.empty() && !is_unrouted && !is_pad_connection && current_route )
2263 {
2264 VIA implicit_via;
2265
2266 if( !default_via_name.empty() )
2267 implicit_via.name = default_via_name;
2268 else if( !m_parameters.default_signal_via.empty() )
2269 implicit_via.name = m_parameters.default_signal_via;
2270
2271 implicit_via.location = { pt.x, pt.y };
2272 current_route->vias.push_back( implicit_via );
2273 }
2274
2275 // Store teardrop if present
2276 if( has_teardrop && current_route )
2277 {
2278 current_route->teardrops.push_back( teardrop );
2279 }
2280
2281 // Store jumper marker if present
2282 if( has_jumper && current_route )
2283 {
2284 current_route->jumpers.push_back( jumper );
2285 }
2286
2287 // Handle plane connections (POWER or THERMAL markers)
2288 // Segments between consecutive plane connection points are virtual connections through
2289 // copper pours and should not be created as discrete tracks.
2290 if( is_plane_connection && prev_is_plane_connection )
2291 {
2292 // Both current and previous points are plane connections - skip this segment.
2293 // The connection is made through the copper pour, not a discrete track.
2294 // Save this point as a potential track start/end if it's on a real copper layer
2295 // (not layer 0 / unrouted). Copper-layer plane points are where signals transition
2296 // between physical tracks and the pour.
2297 if( !is_unrouted )
2298 {
2299 last_plane_connection_pt = pt;
2300 last_plane_connection_layer = effective_layer;
2301 last_plane_connection_width = width;
2302 last_plane_on_copper = true;
2303 }
2304
2305 prev_is_plane_connection = true;
2306 continue;
2307 }
2308
2309 if( is_plane_connection && !prev_is_plane_connection )
2310 {
2311 // Transitioning from track to plane - add this point to complete the track
2312 // then end the track (no further segments until we exit the plane)
2313 if( in_track )
2314 {
2315 current_track.points.push_back( pt );
2316
2317 if( current_route && current_track.points.size() > 1 )
2318 current_route->tracks.push_back( current_track );
2319
2320 current_track.points.clear();
2321 in_track = false;
2322 }
2323
2324 // Save this plane connection point as a potential track start for the next
2325 // non-plane segment, if it's on a real copper layer (not layer 0 / unrouted).
2326 // Copper-layer plane points mark where signals transition between tracks and pours.
2327 last_plane_on_copper = !is_unrouted;
2328
2329 if( last_plane_on_copper )
2330 {
2331 last_plane_connection_pt = pt;
2332 last_plane_connection_layer = effective_layer;
2333 last_plane_connection_width = width;
2334 }
2335
2336 prev_is_plane_connection = true;
2337 continue;
2338 }
2339
2340 if( !is_plane_connection && prev_is_plane_connection )
2341 {
2342 // Transitioning from plane to track. Start a new track from the last copper-layer
2343 // plane point if it was on the same layer as the current point.
2344 if( in_track && current_route && current_track.points.size() > 1 )
2345 current_route->tracks.push_back( current_track );
2346
2347 prev_is_plane_connection = false;
2348
2349 if( is_pad_connection )
2350 {
2351 in_track = false;
2352 continue;
2353 }
2354
2355 current_track.points.clear();
2356
2357 if( last_plane_on_copper && last_plane_connection_layer == effective_layer )
2358 {
2359 current_track.layer = effective_layer;
2360 current_track.width = std::max( width, last_plane_connection_width );
2361 current_track.points.push_back( last_plane_connection_pt );
2362 current_track.points.push_back( pt );
2363 }
2364 else
2365 {
2366 // Layers differ. Create an implicit via if same location.
2367 // POWER vias are already handled by the central POWER handler above.
2368 if( !has_power && via_name.empty() && current_route && last_plane_on_copper &&
2369 std::abs( pt.x - last_plane_connection_pt.x ) < 0.001 &&
2370 std::abs( pt.y - last_plane_connection_pt.y ) < 0.001 )
2371 {
2372 VIA implicit_via;
2373
2374 if( !default_via_name.empty() )
2375 implicit_via.name = default_via_name;
2376 else if( !m_parameters.default_signal_via.empty() )
2377 implicit_via.name = m_parameters.default_signal_via;
2378
2379 implicit_via.location = { pt.x, pt.y };
2380 current_route->vias.push_back( implicit_via );
2381 }
2382
2383 current_track.layer = effective_layer;
2384 current_track.width = width;
2385 current_track.points.push_back( pt );
2386 }
2387
2388 last_plane_on_copper = false;
2389 in_track = true;
2390 continue;
2391 }
2392
2393 // Layer 65 is a special pad connection marker. Add the final point to terminate the
2394 // track at the pad, then stop building this track segment.
2395 if( is_pad_connection )
2396 {
2397 if( in_track && !current_track.points.empty() )
2398 {
2399 current_track.points.push_back( pt );
2400
2401 if( current_route && current_track.points.size() > 1 )
2402 current_route->tracks.push_back( current_track );
2403
2404 current_track.points.clear();
2405 in_track = false;
2406 }
2407
2408 continue;
2409 }
2410
2411 // Normal track building (neither current nor previous is plane connection)
2412 prev_is_plane_connection = false;
2413
2414 if( !in_track )
2415 {
2416 current_track.layer = effective_layer;
2417 current_track.width = width;
2418 current_track.points.clear();
2419 current_track.points.push_back( pt );
2420 in_track = true;
2421 }
2422 else
2423 {
2424 bool layer_changed = ( effective_layer != current_track.layer );
2425 bool width_changed = ( std::abs( width - current_track.width ) > 0.001 );
2426
2427 if( layer_changed || width_changed )
2428 {
2429 // Check if we should connect to this point
2430 bool connect = true;
2431
2432 if( layer_changed && via_name.empty() )
2433 {
2434 bool same_location =
2435 ( pt.x == current_track.points.back().x &&
2436 pt.y == current_track.points.back().y );
2437
2438 if( same_location )
2439 {
2440 // Same location layer change implies an implicit via.
2441 // POWER vias are already created in the central POWER handler.
2442 if( !has_power && current_route )
2443 {
2444 VIA implicit_via;
2445 implicit_via.name =
2446 default_via_name.empty() ? "STANDARDVIA" : default_via_name;
2447 implicit_via.location = { pt.x, pt.y };
2448 current_route->vias.push_back( implicit_via );
2449 }
2450 }
2451 else if( !has_power )
2452 {
2453 // Different location without POWER, treat as jump/ratline
2454 connect = false;
2455 }
2456 // POWER at different location: via already created, keep connected
2457 }
2458
2459 if( connect )
2460 {
2461 current_track.points.push_back( pt );
2462 }
2463
2464 if( current_route )
2465 current_route->tracks.push_back( current_track );
2466
2467 // Start new track from current point
2468 ARC_POINT prev_pt = pt;
2469
2470 current_track.layer = effective_layer;
2471 current_track.width = width;
2472 current_track.points.clear();
2473 current_track.points.push_back( prev_pt );
2474 }
2475 else
2476 {
2477 current_track.points.push_back( pt );
2478 }
2479 }
2480 }
2481
2482 if( in_track && current_route )
2483 {
2484 current_route->tracks.push_back( current_track );
2485 }
2486}
2487
2488void PARSER::parseSectionTEXT( std::ifstream& aStream )
2489{
2490 std::string line;
2491
2492 while( readLine( aStream, line ) )
2493 {
2494 if( line[0] == '*' )
2495 {
2496 pushBackLine( line );
2497 break;
2498 }
2499
2500 // Format: X Y ORI LEVEL HEIGHT WIDTH M HJUST VJUST [NDIM] [.REUSE. instance]
2501 // HJUST: LEFT, CENTER, RIGHT
2502 // VJUST: UP, CENTER, DOWN
2503 std::istringstream iss( line );
2504 TEXT text;
2505
2506 iss >> text.location.x >> text.location.y >> text.rotation >> text.layer
2507 >> text.height >> text.width;
2508
2509 if( iss.fail() )
2510 continue;
2511
2512 std::string mirrored;
2513 iss >> mirrored;
2514 text.mirrored = ( mirrored == "M" );
2515
2516 // Parse optional hjust and vjust
2517 iss >> text.hjust >> text.vjust;
2518
2519 // Parse optional ndim and .REUSE. instance
2520 std::string token;
2521
2522 if( iss >> token )
2523 {
2524 if( token == ".REUSE." )
2525 {
2526 iss >> text.reuse_instance;
2527 }
2528 else
2529 {
2530 text.ndim = PADS_COMMON::ParseInt( token, 0, "text ndim" );
2531
2532 if( iss >> token && token == ".REUSE." )
2533 {
2534 iss >> text.reuse_instance;
2535 }
2536 }
2537 }
2538
2539 if( m_has_font_lines )
2540 {
2541 // Read Font line
2542 // Format: fontstyle[:fontheight:fontdescent] fontface
2543 if( readLine( aStream, line ) )
2544 {
2545 std::istringstream fiss( line );
2546 std::string font_style_part;
2547
2548 fiss >> font_style_part;
2549
2550 size_t colon_pos = font_style_part.find( ':' );
2551
2552 if( colon_pos != std::string::npos )
2553 {
2554 text.font_style = font_style_part.substr( 0, colon_pos );
2555 std::string remaining = font_style_part.substr( colon_pos + 1 );
2556
2557 size_t second_colon = remaining.find( ':' );
2558
2559 if( second_colon != std::string::npos )
2560 {
2561 text.font_height = PADS_COMMON::ParseDouble(
2562 remaining.substr( 0, second_colon ), 0.0, "font height" );
2563 text.font_descent = PADS_COMMON::ParseDouble(
2564 remaining.substr( second_colon + 1 ), 0.0, "font descent" );
2565 }
2566 else
2567 {
2568 text.font_height =
2569 PADS_COMMON::ParseDouble( remaining, 0.0, "font height" );
2570 }
2571 }
2572 else
2573 {
2574 text.font_style = font_style_part;
2575 }
2576
2577 size_t bracket_start = line.find( '<' );
2578 size_t bracket_end = line.find( '>' );
2579
2580 if( bracket_start != std::string::npos && bracket_end != std::string::npos )
2581 {
2582 text.font_face =
2583 line.substr( bracket_start + 1, bracket_end - bracket_start - 1 );
2584 }
2585 else
2586 {
2587 std::string rest;
2588 std::getline( fiss, rest );
2589
2590 if( !rest.empty() && rest[0] == ' ' )
2591 rest = rest.substr( 1 );
2592
2593 text.font_face = rest;
2594 }
2595 }
2596 }
2597
2598 // Read Content line
2599 if( readLine( aStream, line ) )
2600 {
2601 // Standard PADS format uses literal backslash-n for line breaks
2602 size_t pos = 0;
2603
2604 while( ( pos = line.find( "\\n", pos ) ) != std::string::npos )
2605 {
2606 line.replace( pos, 2, "\n" );
2607 pos += 1;
2608 }
2609
2610 // EasyEDA exports (mode "250L") encode newlines as underscores
2611 if( m_file_header.mode == "250L" )
2612 std::replace( line.begin(), line.end(), '_', '\n' );
2613
2614 text.content = line;
2615 m_texts.push_back( text );
2616 }
2617 }
2618}
2619
2620void PARSER::parseSectionBOARD( std::ifstream& aStream )
2621{
2622 // The *BOARD* section uses the same format as LINES section with linetype=BOARD
2623 // Format: name BOARD xloc yloc pieces flags [text]
2624 std::string line;
2625
2626 while( readLine( aStream, line ) )
2627 {
2628 if( line[0] == '*' )
2629 {
2630 pushBackLine( line );
2631 break;
2632 }
2633
2634 std::istringstream iss( line );
2635 std::string name, type;
2636 double xloc = 0.0, yloc = 0.0;
2637 int pieces = 0;
2638 iss >> name >> type >> xloc >> yloc >> pieces;
2639
2640 // Parse all pieces for this board outline entry
2641 for( int i = 0; i < pieces; ++i )
2642 {
2643 if( !readLine( aStream, line ) )
2644 break;
2645
2646 if( line[0] == '*' )
2647 {
2648 pushBackLine( line );
2649 return;
2650 }
2651
2652 std::istringstream piss( line );
2653 std::string shape_type;
2654 int corners = 0;
2655 double width = 0.0;
2656 int linestyle = 0, level = 0;
2657 piss >> shape_type >> corners >> width >> linestyle >> level;
2658
2659 // Handle CLOSED, OPEN, CIRCLE, BRDCLS (board cutout), BRDCIR (circular cutout)
2660 if( shape_type == "CLOSED" || shape_type == "OPEN" || shape_type == "BRDCLS" )
2661 {
2662 POLYLINE polyline;
2663 polyline.layer = 0;
2664 polyline.width = width;
2665 polyline.closed = ( shape_type == "CLOSED" || shape_type == "BRDCLS" );
2666
2667 for( int j = 0; j < corners; ++j )
2668 {
2669 if( !readLine( aStream, line ) )
2670 break;
2671
2672 if( line[0] == '*' )
2673 {
2674 pushBackLine( line );
2675 return;
2676 }
2677
2678 std::istringstream ciss( line );
2679 double dx = 0.0, dy = 0.0;
2680 ciss >> dx >> dy;
2681
2682 // Per PADS spec, arc format is: x1 y1 ab aa ax1 ay1 ax2 ay2
2683 // where x1,y1 = arc start point; center = bounding box midpoint
2684 int startAngleTenths = 0, deltaAngleTenths = 0;
2685 double bboxMinX = 0.0, bboxMinY = 0.0, bboxMaxX = 0.0, bboxMaxY = 0.0;
2686
2687 if( ciss >> startAngleTenths >> deltaAngleTenths
2688 >> bboxMinX >> bboxMinY >> bboxMaxX >> bboxMaxY )
2689 {
2690 double cx = ( bboxMinX + bboxMaxX ) / 2.0;
2691 double cy = ( bboxMinY + bboxMaxY ) / 2.0;
2692 double radius = ( bboxMaxX - bboxMinX ) / 2.0;
2693 double startAngle = startAngleTenths / 10.0;
2694 double deltaAngle = deltaAngleTenths / 10.0;
2695
2696 double startAngleRad = startAngle * M_PI / 180.0;
2697 double startX = cx + radius * std::cos( startAngleRad );
2698 double startY = cy + radius * std::sin( startAngleRad );
2699
2700 double endAngleRad = ( startAngle + deltaAngle ) * M_PI / 180.0;
2701 double endX = cx + radius * std::cos( endAngleRad );
2702 double endY = cy + radius * std::sin( endAngleRad );
2703
2704 polyline.points.emplace_back( xloc + startX, yloc + startY );
2705
2706 ARC arc{};
2707 arc.cx = xloc + cx;
2708 arc.cy = yloc + cy;
2709 arc.radius = radius;
2710 arc.start_angle = startAngle;
2711 arc.delta_angle = deltaAngle;
2712
2713 polyline.points.emplace_back( xloc + endX, yloc + endY, arc );
2714 }
2715 else
2716 {
2717 polyline.points.emplace_back( xloc + dx, yloc + dy );
2718 }
2719 }
2720
2721 if( !polyline.points.empty() )
2722 m_board_outlines.push_back( polyline );
2723 }
2724 else if( shape_type == "CIRCLE" || shape_type == "BRDCIR" )
2725 {
2726 // Circle format: 2 coordinates define opposite ends of diameter
2727 POLYLINE polyline;
2728 polyline.layer = 0;
2729 polyline.width = width;
2730 polyline.closed = true;
2731
2732 double x1 = 0.0, y1 = 0.0, x2 = 0.0, y2 = 0.0;
2733
2734 if( readLine( aStream, line ) )
2735 {
2736 std::istringstream c1( line );
2737 c1 >> x1 >> y1;
2738 }
2739
2740 if( corners >= 2 && readLine( aStream, line ) )
2741 {
2742 std::istringstream c2( line );
2743 c2 >> x2 >> y2;
2744 }
2745
2746 // Calculate center and radius from diameter endpoints
2747 double cx = xloc + ( x1 + x2 ) / 2.0;
2748 double cy = yloc + ( y1 + y2 ) / 2.0;
2749 double radius = std::sqrt( ( x2 - x1 ) * ( x2 - x1 ) + ( y2 - y1 ) * ( y2 - y1 ) ) / 2.0;
2750
2751 // Create full circle arc
2752 ARC arc{};
2753 arc.cx = cx;
2754 arc.cy = cy;
2755 arc.radius = radius;
2756 arc.start_angle = 0.0;
2757 arc.delta_angle = 360.0;
2758
2759 polyline.points.emplace_back( cx + radius, cy, arc );
2760
2761 if( !polyline.points.empty() )
2762 m_board_outlines.push_back( polyline );
2763 }
2764 else
2765 {
2766 // Unknown shape type, skip corners
2767 for( int j = 0; j < corners; ++j )
2768 {
2769 if( !readLine( aStream, line ) )
2770 break;
2771
2772 if( line[0] == '*' )
2773 {
2774 pushBackLine( line );
2775 return;
2776 }
2777 }
2778 }
2779 }
2780 }
2781}
2782
2783void PARSER::parseSectionLINES( std::ifstream& aStream )
2784{
2785 std::string line;
2786
2787 while( readLine( aStream, line ) )
2788 {
2789 if( line[0] == '*' )
2790 {
2791 pushBackLine( line );
2792 break;
2793 }
2794
2795 // Header format: name type xloc yloc pieces flags [text] [signame]
2796 // While the docs claim `... pieces flags [text [signame]]`, and the `*REMARK*` lines our
2797 // sample files claim `... pieces text signame`, the files themselves clearly contain many
2798 // instances of `pieces flags signame`.
2799 std::istringstream iss( line );
2800 std::string name, type;
2801 double xloc = 0.0, yloc = 0.0;
2802 int pieces = 0, flags = 0, textCount = 0;
2803 std::string seventhToken, eighthToken;
2804 std::string signame;
2805
2806 iss >> name >> type >> xloc >> yloc >> pieces >> flags >> seventhToken >> eighthToken;
2807
2808 if( !eighthToken.empty() )
2809 {
2810 // If we have the full eight tokens, the last two must be text-count and signame.
2811
2812 signame = eighthToken;
2813
2814 std::istringstream tiss( seventhToken );
2815 tiss >> textCount;
2816 }
2817 else if( !seventhToken.empty() )
2818 {
2819 // Here's where it gets dicey: with seven tokens we don't know whether the last
2820 // is a text-count or a signame. We can't compare with known signal names, as
2821 // they're all later in the file. The best we can do is assume that anything that
2822 // is all digits is a text-count, and that any signame will contain at least one
2823 // symbol or letter.
2824
2825 if( std::all_of( seventhToken.begin(), seventhToken.end(),
2826 [](unsigned char c)
2827 {
2828 return std::isdigit( c );
2829 } ) )
2830 {
2831 std::istringstream tiss( seventhToken );
2832 tiss >> textCount;
2833 }
2834 else
2835 {
2836 signame = seventhToken;
2837 }
2838 }
2839
2840 // Check for optional .REUSE. line after header
2841 std::string reuse_instance, reuse_signal;
2842
2843 if( readLine( aStream, line ) )
2844 {
2845 if( line.find( ".REUSE." ) != std::string::npos )
2846 {
2847 std::istringstream riss( line );
2848 std::string reuse_keyword;
2849 riss >> reuse_keyword >> reuse_instance >> reuse_signal;
2850 }
2851 else
2852 {
2853 pushBackLine( line );
2854 }
2855 }
2856
2857 if( type == "BOARD" )
2858 {
2859 for( int i=0; i<pieces; ++i )
2860 {
2861 if( !readLine( aStream, line ) ) break;
2862 if( line[0] == '*' ) { pushBackLine( line ); return; }
2863
2864 std::istringstream piss( line );
2865 std::string shape_type;
2866 int corners = 0;
2867 double width = 0.0;
2868 int piece_flags = 0;
2869 int level = 0;
2870 piss >> shape_type >> corners >> width >> piece_flags >> level;
2871
2872 if( shape_type == "CLOSED" || shape_type == "OPEN" || shape_type == "BRDCLS" )
2873 {
2874 POLYLINE polyline;
2875 polyline.layer = 0; // Board outline is layer-agnostic
2876 polyline.width = width;
2877 polyline.closed = ( shape_type == "CLOSED" || shape_type == "BRDCLS" );
2878
2879 for( int j = 0; j < corners; ++j )
2880 {
2881 if( !readLine( aStream, line ) )
2882 break;
2883
2884 if( line[0] == '*' )
2885 {
2886 pushBackLine( line );
2887 return;
2888 }
2889
2890 std::istringstream ciss( line );
2891 double dx = 0.0, dy = 0.0;
2892 ciss >> dx >> dy;
2893
2894 // Per PADS spec, arc format is: x1 y1 ab aa ax1 ay1 ax2 ay2
2895 // where x1,y1 = arc start point; center = bounding box midpoint
2896 int startAngleTenths = 0, deltaAngleTenths = 0;
2897 double bboxMinX = 0.0, bboxMinY = 0.0, bboxMaxX = 0.0, bboxMaxY = 0.0;
2898
2899 if( ciss >> startAngleTenths >> deltaAngleTenths
2900 >> bboxMinX >> bboxMinY >> bboxMaxX >> bboxMaxY )
2901 {
2902 double cx = ( bboxMinX + bboxMaxX ) / 2.0;
2903 double cy = ( bboxMinY + bboxMaxY ) / 2.0;
2904 double radius = ( bboxMaxX - bboxMinX ) / 2.0;
2905 double startAngle = startAngleTenths / 10.0;
2906 double deltaAngle = deltaAngleTenths / 10.0;
2907
2908 double startAngleRad = startAngle * M_PI / 180.0;
2909 double startX = cx + radius * std::cos( startAngleRad );
2910 double startY = cy + radius * std::sin( startAngleRad );
2911
2912 double endAngleRad = ( startAngle + deltaAngle ) * M_PI / 180.0;
2913 double endX = cx + radius * std::cos( endAngleRad );
2914 double endY = cy + radius * std::sin( endAngleRad );
2915
2916 polyline.points.emplace_back( xloc + startX, yloc + startY );
2917
2918 ARC arc{};
2919 arc.cx = xloc + cx;
2920 arc.cy = yloc + cy;
2921 arc.radius = radius;
2922 arc.start_angle = startAngle;
2923 arc.delta_angle = deltaAngle;
2924
2925 polyline.points.emplace_back( xloc + endX, yloc + endY, arc );
2926 }
2927 else
2928 {
2929 polyline.points.emplace_back( xloc + dx, yloc + dy );
2930 }
2931 }
2932
2933 if( !polyline.points.empty() )
2934 m_board_outlines.push_back( polyline );
2935 }
2936 else if( shape_type == "CIRCLE" || shape_type == "BRDCIR" )
2937 {
2938 // Circle: 2 coordinates define opposite ends of diameter
2939 double x1 = 0.0, y1 = 0.0, x2 = 0.0, y2 = 0.0;
2940
2941 if( readLine( aStream, line ) )
2942 {
2943 std::istringstream c1( line );
2944 c1 >> x1 >> y1;
2945 }
2946
2947 if( corners >= 2 && readLine( aStream, line ) )
2948 {
2949 std::istringstream c2( line );
2950 c2 >> x2 >> y2;
2951 }
2952
2953 double cx = xloc + ( x1 + x2 ) / 2.0;
2954 double cy = yloc + ( y1 + y2 ) / 2.0;
2955 double radius = std::sqrt( ( x2 - x1 ) * ( x2 - x1 ) +
2956 ( y2 - y1 ) * ( y2 - y1 ) ) / 2.0;
2957
2958 POLYLINE polyline;
2959 polyline.layer = 0;
2960 polyline.width = width;
2961 polyline.closed = true;
2962
2963 ARC arc{};
2964 arc.cx = cx;
2965 arc.cy = cy;
2966 arc.radius = radius;
2967 arc.start_angle = 0.0;
2968 arc.delta_angle = 360.0;
2969
2970 polyline.points.emplace_back( cx + radius, cy, arc );
2971
2972 if( !polyline.points.empty() )
2973 m_board_outlines.push_back( polyline );
2974 }
2975 else
2976 {
2977 for( int j=0; j<corners; ++j )
2978 {
2979 if( !readLine( aStream, line ) ) break;
2980 if( line[0] == '*' ) { pushBackLine( line ); return; }
2981 }
2982 }
2983 }
2984 }
2985 else if( name.rfind( "DIM", 0 ) == 0 && type == "LINES" )
2986 {
2987 // Dimension annotation with BASPNT (base points), ARWLN/ARWHD (arrows),
2988 // and EXTLN (extension lines).
2989 //
2990 // BASPNT pairs define the measurement endpoints. There are typically two
2991 // BASPNT shapes: the first defines the start point (usually at origin),
2992 // and the second defines the end point as an offset from the dimension origin.
2993 //
2994 // ARWLN defines the crossbar position (Y for horizontal, X for vertical).
2995 //
2996 // For a proper linear dimension, we extract:
2997 // - Start point from first BASPNT (first coordinate of the pair)
2998 // - End point from second BASPNT (first coordinate of the pair)
2999 // - Crossbar position from ARWLN (used to compute height)
3000 DIMENSION dim;
3001 dim.name = name;
3002 dim.x = xloc;
3003 dim.y = yloc;
3004
3005 double baspnt1_x = 0, baspnt1_y = 0;
3006 double baspnt2_x = 0, baspnt2_y = 0;
3007 double arwln_x = 0, arwln_y = 0;
3008 int baspnt_count = 0;
3009 bool hasArwln = false;
3010
3011 for( int i = 0; i < pieces; ++i )
3012 {
3013 if( !readLine( aStream, line ) )
3014 break;
3015
3016 if( line[0] == '*' )
3017 {
3018 pushBackLine( line );
3019 break;
3020 }
3021
3022 std::istringstream piss( line );
3023 std::string shape_type;
3024 int corners = 0;
3025 double width = 0;
3026 int piece_flags = 0;
3027 int level = 0;
3028 piss >> shape_type >> corners >> width >> piece_flags >> level;
3029
3030 dim.layer = level;
3031
3032 for( int j = 0; j < corners; ++j )
3033 {
3034 if( !readLine( aStream, line ) )
3035 break;
3036
3037 if( line[0] == '*' )
3038 {
3039 pushBackLine( line );
3040 break;
3041 }
3042
3043 std::istringstream ciss( line );
3044 double dx = 0.0, dy = 0.0;
3045 ciss >> dx >> dy;
3046
3047 // BASPNT defines measurement endpoints. First BASPNT is start,
3048 // second BASPNT is end. Only capture the first point of each pair.
3049 if( shape_type == "BASPNT" && j == 0 )
3050 {
3051 if( baspnt_count == 0 )
3052 {
3053 baspnt1_x = xloc + dx;
3054 baspnt1_y = yloc + dy;
3055 }
3056 else if( baspnt_count == 1 )
3057 {
3058 baspnt2_x = xloc + dx;
3059 baspnt2_y = yloc + dy;
3060 }
3061
3062 baspnt_count++;
3063 }
3064
3065 // ARWLN1 first point: crossbar position
3066 if( shape_type == "ARWLN1" && j == 0 )
3067 {
3068 arwln_x = xloc + dx;
3069 arwln_y = yloc + dy;
3070 hasArwln = true;
3071 }
3072 }
3073 }
3074
3075 // Build measurement points from BASPNT positions.
3076 if( baspnt_count >= 2 )
3077 {
3078 // Determine if this is a horizontal or vertical dimension based on
3079 // which axis has the larger offset between the two BASPNT points.
3080 double dx = std::abs( baspnt2_x - baspnt1_x );
3081 double dy = std::abs( baspnt2_y - baspnt1_y );
3082 bool isHorizontal = dx > dy;
3083
3084 dim.is_horizontal = isHorizontal;
3085
3086 POINT pt1{}, pt2{};
3087 pt1.x = baspnt1_x;
3088 pt1.y = baspnt1_y;
3089 pt2.x = baspnt2_x;
3090 pt2.y = baspnt2_y;
3091
3092 // Store crossbar position for height calculation
3093 if( hasArwln )
3094 {
3095 if( isHorizontal )
3096 dim.crossbar_pos = arwln_y;
3097 else
3098 dim.crossbar_pos = arwln_x;
3099 }
3100
3101 dim.points.push_back( pt1 );
3102 dim.points.push_back( pt2 );
3103 }
3104
3105 // Parse text items for this dimension.
3106 // The first text is used as the dimension value label.
3107 for( int t = 0; t < textCount; ++t )
3108 {
3109 if( !readLine( aStream, line ) )
3110 break;
3111
3112 if( line[0] == '*' )
3113 {
3114 pushBackLine( line );
3115 break;
3116 }
3117
3118 std::istringstream tiss( line );
3119 double tx = 0.0, ty = 0.0;
3120 tiss >> tx >> ty;
3121
3122 if( tiss.fail() )
3123 {
3124 int skipLines = m_has_font_lines ? 2 : 1;
3125
3126 for( int s = 0; s < skipLines; ++s )
3127 readLine( aStream, line );
3128
3129 continue;
3130 }
3131
3132 double trot = 0.0;
3133 int tlayer = 0;
3134 double theight = 0.0, twidth = 0.0;
3135 tiss >> trot >> tlayer >> theight >> twidth;
3136
3137 if( m_has_font_lines )
3138 {
3139 if( !readLine( aStream, line ) )
3140 break;
3141 }
3142
3143 // Content line
3144 if( !readLine( aStream, line ) )
3145 break;
3146
3147 if( t == 0 )
3148 {
3149 dim.text = line;
3150 dim.text_height = theight;
3151 dim.text_width = twidth;
3152 dim.rotation = trot;
3153 }
3154 }
3155
3156 textCount = 0;
3157
3158 if( !dim.points.empty() )
3159 m_dimensions.push_back( dim );
3160 }
3161 else if( type == "KEEPOUT" || type == "RESTRICTVIA" || type == "RESTRICTROUTE"
3162 || type == "RESTRICTAREA" || type == "PLACEMENT_KEEPOUT" )
3163 {
3164 // Parse keepout area definition
3165 KEEPOUT keepout;
3166
3167 // Set defaults based on type name (fallback if no restriction codes in piece)
3168 if( type == "KEEPOUT" || type == "RESTRICTAREA" )
3169 {
3170 keepout.type = KEEPOUT_TYPE::ALL;
3171 keepout.no_traces = true;
3172 keepout.no_vias = true;
3173 keepout.no_copper = true;
3174 }
3175 else if( type == "RESTRICTVIA" )
3176 {
3177 keepout.type = KEEPOUT_TYPE::VIA;
3178 keepout.no_traces = false;
3179 keepout.no_vias = true;
3180 keepout.no_copper = false;
3181 }
3182 else if( type == "RESTRICTROUTE" )
3183 {
3184 keepout.type = KEEPOUT_TYPE::ROUTE;
3185 keepout.no_traces = true;
3186 keepout.no_vias = false;
3187 keepout.no_copper = false;
3188 }
3189 else if( type == "PLACEMENT_KEEPOUT" )
3190 {
3191 keepout.type = KEEPOUT_TYPE::PLACEMENT;
3192 keepout.no_traces = false;
3193 keepout.no_vias = false;
3194 keepout.no_copper = false;
3195 keepout.no_components = true;
3196 }
3197
3198 for( int i = 0; i < pieces; ++i )
3199 {
3200 if( !readLine( aStream, line ) )
3201 break;
3202
3203 if( line[0] == '*' )
3204 {
3205 pushBackLine( line );
3206 break;
3207 }
3208
3209 // Piece format: PIECETYPE CORNERS WIDTH FLAGS LEVEL [RESTRICTIONS]
3210 // RESTRICTIONS is a string containing: P H R C V T A
3211 std::istringstream piss( line );
3212 std::string shape_type;
3213 int corners = 0;
3214 double width = 0;
3215 int piece_flags = 0;
3216 int level = 0;
3217 std::string restrictions;
3218 piss >> shape_type >> corners >> width >> piece_flags >> level >> restrictions;
3219
3220 if( level > 0 )
3221 keepout.layers.push_back( level );
3222
3223 // Parse restriction codes if present
3224 // Per PADS spec: P=Placement, H=Height, R=Trace/copper, C=Copper pour,
3225 // V=Via/jumper, T=Test point, A=Accordion
3226 // Only override defaults from type name if explicit restrictions are specified
3227 if( !restrictions.empty() )
3228 {
3229 // Check if this looks like a restriction code string (contains letters)
3230 bool has_restriction_codes = false;
3231
3232 for( char c : restrictions )
3233 {
3234 if( std::isalpha( c ) )
3235 {
3236 has_restriction_codes = true;
3237 break;
3238 }
3239 }
3240
3241 if( has_restriction_codes )
3242 {
3243 // Clear all defaults and set based on explicit restriction codes
3244 keepout.no_traces = false;
3245 keepout.no_vias = false;
3246 keepout.no_copper = false;
3247 keepout.no_components = false;
3248 keepout.height_restriction = false;
3249 keepout.no_test_points = false;
3250 keepout.no_accordion = false;
3251
3252 for( char c : restrictions )
3253 {
3254 switch( c )
3255 {
3256 case 'P':
3257 keepout.no_components = true;
3258 break;
3259
3260 case 'H':
3261 keepout.height_restriction = true;
3262 keepout.max_height = width;
3263 break;
3264
3265 case 'R':
3266 keepout.no_traces = true;
3267 break;
3268
3269 case 'C':
3270 keepout.no_copper = true;
3271 break;
3272
3273 case 'V':
3274 keepout.no_vias = true;
3275 break;
3276
3277 case 'T':
3278 keepout.no_test_points = true;
3279 break;
3280
3281 case 'A':
3282 keepout.no_accordion = true;
3283 break;
3284
3285 default:
3286 break;
3287 }
3288 }
3289 }
3290 }
3291
3292 // Handle KPTCIR (circle keepout) differently from KPTCLS (polygon keepout)
3293 if( shape_type == "KPTCIR" )
3294 {
3295 // Circle format: 2 coordinates define opposite ends of diameter
3296 double x1 = 0.0, y1 = 0.0, x2 = 0.0, y2 = 0.0;
3297
3298 if( readLine( aStream, line ) )
3299 {
3300 std::istringstream c1( line );
3301 c1 >> x1 >> y1;
3302 }
3303
3304 if( corners >= 2 && readLine( aStream, line ) )
3305 {
3306 std::istringstream c2( line );
3307 c2 >> x2 >> y2;
3308 }
3309
3310 // Calculate center and radius from diameter endpoints
3311 double cx = xloc + ( x1 + x2 ) / 2.0;
3312 double cy = yloc + ( y1 + y2 ) / 2.0;
3313 double radius = std::sqrt( ( x2 - x1 ) * ( x2 - x1 ) +
3314 ( y2 - y1 ) * ( y2 - y1 ) ) / 2.0;
3315
3316 // Create full circle arc for keepout outline
3317 ARC arc{};
3318 arc.cx = cx;
3319 arc.cy = cy;
3320 arc.radius = radius;
3321 arc.start_angle = 0.0;
3322 arc.delta_angle = 360.0;
3323
3324 keepout.outline.emplace_back( cx + radius, cy, arc );
3325 }
3326 else
3327 {
3328 // KPTCLS or other polygon piece types
3329 for( int j = 0; j < corners; ++j )
3330 {
3331 if( !readLine( aStream, line ) )
3332 break;
3333
3334 if( line[0] == '*' )
3335 {
3336 pushBackLine( line );
3337 break;
3338 }
3339
3340 std::istringstream ciss( line );
3341 double dx = 0.0, dy = 0.0;
3342 ciss >> dx >> dy;
3343
3344 // Per PADS spec, arc format is: x1 y1 ab aa ax1 ay1 ax2 ay2
3345 // where x1,y1 = arc start point; center = bounding box midpoint
3346 int startAngleTenths = 0, deltaAngleTenths = 0;
3347 double bboxMinX = 0.0, bboxMinY = 0.0, bboxMaxX = 0.0, bboxMaxY = 0.0;
3348
3349 if( ciss >> startAngleTenths >> deltaAngleTenths
3350 >> bboxMinX >> bboxMinY >> bboxMaxX >> bboxMaxY )
3351 {
3352 double cx = ( bboxMinX + bboxMaxX ) / 2.0;
3353 double cy = ( bboxMinY + bboxMaxY ) / 2.0;
3354 double radius = ( bboxMaxX - bboxMinX ) / 2.0;
3355 double startAngle = startAngleTenths / 10.0;
3356 double deltaAngle = deltaAngleTenths / 10.0;
3357
3358 double startAngleRad = startAngle * M_PI / 180.0;
3359 double startX = cx + radius * std::cos( startAngleRad );
3360 double startY = cy + radius * std::sin( startAngleRad );
3361
3362 double endAngleRad = ( startAngle + deltaAngle ) * M_PI / 180.0;
3363 double endX = cx + radius * std::cos( endAngleRad );
3364 double endY = cy + radius * std::sin( endAngleRad );
3365
3366 keepout.outline.emplace_back( xloc + startX, yloc + startY );
3367
3368 ARC arc{};
3369 arc.cx = xloc + cx;
3370 arc.cy = yloc + cy;
3371 arc.radius = radius;
3372 arc.start_angle = startAngle;
3373 arc.delta_angle = deltaAngle;
3374
3375 keepout.outline.emplace_back( xloc + endX, yloc + endY, arc );
3376 }
3377 else
3378 {
3379 keepout.outline.emplace_back( xloc + dx, yloc + dy );
3380 }
3381 }
3382 }
3383 }
3384
3385 if( !keepout.outline.empty() )
3386 m_keepouts.push_back( keepout );
3387 }
3388 else if( type == "COPPER" || type == "COPCUT" )
3389 {
3390 // Parse copper shape definition
3391 // Header already parsed: name type xloc yloc pieces flags [text [signame]]
3392 // signame was parsed earlier if present
3393
3394 for( int i = 0; i < pieces; ++i )
3395 {
3396 if( !readLine( aStream, line ) )
3397 break;
3398
3399 if( line[0] == '*' )
3400 {
3401 pushBackLine( line );
3402 return;
3403 }
3404
3405 // Piece format: PIECETYPE CORNERS WIDTH FLAGS LEVEL
3406 // PIECETYPE: COPOPN (polyline), COPCLS (filled polygon), COPCIR (filled circle),
3407 // COPCUT (polygon void), COPCCO (circle void), CIRCUR (circle void for COPCUT)
3408 std::istringstream piss( line );
3409 std::string shape_type;
3410 int corners = 0;
3411 double width = 0;
3412 int piece_flags = 0;
3413 int level = 0;
3414 piss >> shape_type >> corners >> width >> piece_flags >> level;
3415
3416 COPPER_SHAPE copper;
3417 copper.name = name;
3418 copper.layer = level;
3419 copper.width = width;
3420 copper.net_name = normalizeNetName( signame );
3421
3422 copper.filled = ( shape_type == "COPCLS" || shape_type == "COPCIR" );
3423 copper.is_cutout = ( shape_type == "COPCUT" || shape_type == "COPCCO" ||
3424 shape_type == "CIRCUR" || type == "COPCUT" );
3425
3426 // Handle circle shapes specially
3427 if( shape_type == "COPCIR" || shape_type == "COPCCO" || shape_type == "CIRCUR" )
3428 {
3429 // Circle: 2 coordinates define opposite ends of diameter
3430 double x1 = 0.0, y1 = 0.0, x2 = 0.0, y2 = 0.0;
3431
3432 if( readLine( aStream, line ) )
3433 {
3434 std::istringstream c1( line );
3435 c1 >> x1 >> y1;
3436 }
3437
3438 if( corners >= 2 && readLine( aStream, line ) )
3439 {
3440 std::istringstream c2( line );
3441 c2 >> x2 >> y2;
3442 }
3443
3444 double cx = xloc + ( x1 + x2 ) / 2.0;
3445 double cy = yloc + ( y1 + y2 ) / 2.0;
3446 double radius = std::sqrt( ( x2 - x1 ) * ( x2 - x1 ) +
3447 ( y2 - y1 ) * ( y2 - y1 ) ) / 2.0;
3448
3449 ARC arc{};
3450 arc.cx = cx;
3451 arc.cy = cy;
3452 arc.radius = radius;
3453 arc.start_angle = 0.0;
3454 arc.delta_angle = 360.0;
3455
3456 copper.outline.emplace_back( cx + radius, cy, arc );
3457 }
3458 else
3459 {
3460 // COPOPN, COPCLS, COPCUT - polygon shapes
3461 for( int j = 0; j < corners; ++j )
3462 {
3463 if( !readLine( aStream, line ) )
3464 break;
3465
3466 if( line[0] == '*' )
3467 {
3468 pushBackLine( line );
3469 break;
3470 }
3471
3472 std::istringstream ciss( line );
3473 double dx = 0.0, dy = 0.0;
3474 ciss >> dx >> dy;
3475
3476 // Per PADS spec, arc format is: x1 y1 ab aa ax1 ay1 ax2 ay2
3477 // where x1,y1 = arc start point; center = bounding box midpoint
3478 int startAngleTenths = 0, deltaAngleTenths = 0;
3479 double bboxMinX = 0.0, bboxMinY = 0.0, bboxMaxX = 0.0, bboxMaxY = 0.0;
3480
3481 if( ciss >> startAngleTenths >> deltaAngleTenths
3482 >> bboxMinX >> bboxMinY >> bboxMaxX >> bboxMaxY )
3483 {
3484 double cx = ( bboxMinX + bboxMaxX ) / 2.0;
3485 double cy = ( bboxMinY + bboxMaxY ) / 2.0;
3486 double radius = ( bboxMaxX - bboxMinX ) / 2.0;
3487 double startAngle = startAngleTenths / 10.0;
3488 double deltaAngle = deltaAngleTenths / 10.0;
3489
3490 double startAngleRad = startAngle * M_PI / 180.0;
3491 double startX = cx + radius * std::cos( startAngleRad );
3492 double startY = cy + radius * std::sin( startAngleRad );
3493
3494 double endAngleRad = ( startAngle + deltaAngle ) * M_PI / 180.0;
3495 double endX = cx + radius * std::cos( endAngleRad );
3496 double endY = cy + radius * std::sin( endAngleRad );
3497
3498 copper.outline.emplace_back( xloc + startX, yloc + startY );
3499
3500 ARC arc{};
3501 arc.cx = xloc + cx;
3502 arc.cy = yloc + cy;
3503 arc.radius = radius;
3504 arc.start_angle = startAngle;
3505 arc.delta_angle = deltaAngle;
3506
3507 copper.outline.emplace_back( xloc + endX, yloc + endY, arc );
3508 }
3509 else
3510 {
3511 copper.outline.emplace_back( xloc + dx, yloc + dy );
3512 }
3513 }
3514 }
3515
3516 if( !copper.outline.empty() )
3517 m_copper_shapes.push_back( copper );
3518 }
3519 }
3520 else if( type == "LINES" )
3521 {
3522 // Generic 2D graphic lines (non-dimension LINES items)
3523 for( int i = 0; i < pieces; ++i )
3524 {
3525 if( !readLine( aStream, line ) )
3526 break;
3527
3528 if( line[0] == '*' )
3529 {
3530 pushBackLine( line );
3531 return;
3532 }
3533
3534 // Piece format: PIECETYPE CORNERS WIDTH FLAGS LEVEL
3535 std::istringstream piss( line );
3536 std::string shape_type;
3537 int corners = 0;
3538 double width = 0;
3539 int piece_flags = 0;
3540 int level = 0;
3541 piss >> shape_type >> corners >> width >> piece_flags >> level;
3542
3543 GRAPHIC_LINE graphic;
3544 graphic.name = name;
3545 graphic.layer = level;
3546 graphic.width = width;
3547 graphic.reuse_instance = reuse_instance;
3548
3549 // Determine if closed based on shape type
3550 graphic.closed = ( shape_type == "CLOSED" || shape_type == "CIRCLE" );
3551
3552 if( shape_type == "CIRCLE" )
3553 {
3554 // Circle: 2 coordinates define opposite ends of diameter
3555 double x1 = 0.0, y1 = 0.0, x2 = 0.0, y2 = 0.0;
3556
3557 if( readLine( aStream, line ) )
3558 {
3559 std::istringstream c1( line );
3560 c1 >> x1 >> y1;
3561 }
3562
3563 if( corners >= 2 && readLine( aStream, line ) )
3564 {
3565 std::istringstream c2( line );
3566 c2 >> x2 >> y2;
3567 }
3568
3569 double cx = xloc + ( x1 + x2 ) / 2.0;
3570 double cy = yloc + ( y1 + y2 ) / 2.0;
3571 double radius = std::sqrt( ( x2 - x1 ) * ( x2 - x1 ) +
3572 ( y2 - y1 ) * ( y2 - y1 ) ) / 2.0;
3573
3574 ARC arc{};
3575 arc.cx = cx;
3576 arc.cy = cy;
3577 arc.radius = radius;
3578 arc.start_angle = 0.0;
3579 arc.delta_angle = 360.0;
3580
3581 graphic.points.emplace_back( cx + radius, cy, arc );
3582 }
3583 else
3584 {
3585 // OPEN or CLOSED polyline
3586 for( int j = 0; j < corners; ++j )
3587 {
3588 if( !readLine( aStream, line ) )
3589 break;
3590
3591 if( line[0] == '*' )
3592 {
3593 pushBackLine( line );
3594 break;
3595 }
3596
3597 std::istringstream ciss( line );
3598 double dx = 0.0, dy = 0.0;
3599 ciss >> dx >> dy;
3600
3601 // Check for arc parameters
3602 int startAngleTenths = 0, deltaAngleTenths = 0;
3603 double bboxMinX = 0.0, bboxMinY = 0.0, bboxMaxX = 0.0, bboxMaxY = 0.0;
3604
3605 if( ciss >> startAngleTenths >> deltaAngleTenths
3606 >> bboxMinX >> bboxMinY >> bboxMaxX >> bboxMaxY )
3607 {
3608 double cx = ( bboxMinX + bboxMaxX ) / 2.0;
3609 double cy = ( bboxMinY + bboxMaxY ) / 2.0;
3610 double radius = ( bboxMaxX - bboxMinX ) / 2.0;
3611 double startAngle = startAngleTenths / 10.0;
3612 double deltaAngle = deltaAngleTenths / 10.0;
3613
3614 double startAngleRad = startAngle * M_PI / 180.0;
3615 double startX = cx + radius * std::cos( startAngleRad );
3616 double startY = cy + radius * std::sin( startAngleRad );
3617
3618 double endAngleRad = ( startAngle + deltaAngle ) * M_PI / 180.0;
3619 double endX = cx + radius * std::cos( endAngleRad );
3620 double endY = cy + radius * std::sin( endAngleRad );
3621
3622 graphic.points.emplace_back( xloc + startX, yloc + startY );
3623
3624 ARC arc{};
3625 arc.cx = xloc + cx;
3626 arc.cy = yloc + cy;
3627 arc.radius = radius;
3628 arc.start_angle = startAngle;
3629 arc.delta_angle = deltaAngle;
3630
3631 graphic.points.emplace_back( xloc + endX, yloc + endY, arc );
3632 }
3633 else
3634 {
3635 graphic.points.emplace_back( xloc + dx, yloc + dy );
3636 }
3637 }
3638 }
3639
3640 if( !graphic.points.empty() )
3641 m_graphic_lines.push_back( graphic );
3642 }
3643 }
3644 else
3645 {
3646 // Skip unknown types
3647 for( int i = 0; i < pieces; ++i )
3648 {
3649 if( !readLine( aStream, line ) )
3650 break;
3651
3652 if( line[0] == '*' )
3653 {
3654 pushBackLine( line );
3655 return;
3656 }
3657
3658 std::istringstream piss( line );
3659 std::string shape_type;
3660 int corners = 0;
3661 piss >> shape_type >> corners;
3662
3663 for( int j = 0; j < corners; ++j )
3664 {
3665 if( !readLine( aStream, line ) )
3666 break;
3667
3668 if( line[0] == '*' )
3669 {
3670 pushBackLine( line );
3671 return;
3672 }
3673 }
3674 }
3675 }
3676
3677 // Parse text items that follow the pieces.
3678 // V9+ format: 3 lines each (properties, font, content)
3679 // V5.x format: 2 lines each (properties, content)
3680 for( int t = 0; t < textCount; ++t )
3681 {
3682 if( !readLine( aStream, line ) )
3683 break;
3684
3685 if( line[0] == '*' )
3686 {
3687 pushBackLine( line );
3688 return;
3689 }
3690
3691 std::istringstream tiss( line );
3692 TEXT text;
3693
3694 tiss >> text.location.x >> text.location.y >> text.rotation >> text.layer
3695 >> text.height >> text.width;
3696
3697 if( tiss.fail() )
3698 {
3699 int skipLines = m_has_font_lines ? 2 : 1;
3700
3701 for( int s = 0; s < skipLines; ++s )
3702 readLine( aStream, line );
3703
3704 continue;
3705 }
3706
3707 text.location.x += xloc;
3708 text.location.y += yloc;
3709
3710 std::string mirrored;
3711 tiss >> mirrored;
3712 text.mirrored = ( mirrored == "M" );
3713 tiss >> text.hjust >> text.vjust;
3714
3715 if( m_has_font_lines )
3716 {
3717 if( !readLine( aStream, line ) )
3718 break;
3719
3720 if( line[0] == '*' )
3721 {
3722 pushBackLine( line );
3723 return;
3724 }
3725
3726 size_t bracket_start = line.find( '<' );
3727 size_t bracket_end = line.find( '>' );
3728
3729 if( bracket_start != std::string::npos && bracket_end != std::string::npos )
3730 {
3731 text.font_face =
3732 line.substr( bracket_start + 1, bracket_end - bracket_start - 1 );
3733 }
3734
3735 std::istringstream fiss( line );
3736 std::string font_style_part;
3737 fiss >> font_style_part;
3738
3739 size_t colon_pos = font_style_part.find( ':' );
3740
3741 if( colon_pos != std::string::npos )
3742 text.font_style = font_style_part.substr( 0, colon_pos );
3743 else
3744 text.font_style = font_style_part;
3745 }
3746
3747 // Content line
3748 if( !readLine( aStream, line ) )
3749 break;
3750
3751 if( line[0] == '*' )
3752 {
3753 pushBackLine( line );
3754 return;
3755 }
3756
3757 text.content = line;
3758 m_texts.push_back( text );
3759 }
3760 }
3761}
3762
3763void PARSER::parseSectionPARTTYPE( std::ifstream& aStream )
3764{
3765 std::string line;
3766 PART_TYPE* currentPartType = nullptr;
3767 GATE_DEF* currentGate = nullptr;
3768
3769 // Records carry no terminator and a part type name can look exactly like pin data, so the
3770 // header counts are the only reliable way to find where a record ends
3771 int pendingGates = 0;
3772 int pendingGatePins = 0;
3773 int pendingSigPins = 0;
3774 int pendingUnusedPins = 0;
3775
3776 // Helper to parse pin electrical type character
3777 auto parsePinElecType = []( char c ) -> PIN_ELEC_TYPE {
3778 switch( c )
3779 {
3780 case 'S': return PIN_ELEC_TYPE::SOURCE;
3781 case 'B': return PIN_ELEC_TYPE::BIDIRECTIONAL;
3782 case 'C': return PIN_ELEC_TYPE::OPEN_COLLECTOR;
3783 case 'T': return PIN_ELEC_TYPE::TRISTATE;
3784 case 'L': return PIN_ELEC_TYPE::LOAD;
3785 case 'Z': return PIN_ELEC_TYPE::TERMINATOR;
3786 case 'P': return PIN_ELEC_TYPE::POWER;
3787 case 'G': return PIN_ELEC_TYPE::GROUND;
3788 default: return PIN_ELEC_TYPE::UNDEFINED;
3789 }
3790 };
3791
3792 auto isInteger = []( const std::string& aToken ) -> bool
3793 {
3794 return !aToken.empty() && aToken.find_first_not_of( "0123456789" ) == std::string::npos;
3795 };
3796
3797 auto tokenize = []( const std::string& aLine ) -> std::vector<std::string>
3798 {
3799 std::istringstream lss( aLine );
3800 std::vector<std::string> tokens;
3801 std::string token;
3802
3803 while( lss >> token )
3804 tokens.push_back( token );
3805
3806 return tokens;
3807 };
3808
3809 while( readLine( aStream, line ) )
3810 {
3811 if( line[0] == '*' )
3812 {
3813 pushBackLine( line );
3814 break;
3815 }
3816
3817 // Attribute block enclosed in braces
3818 if( line[0] == '{' && currentPartType )
3819 {
3820 while( readLine( aStream, line ) )
3821 {
3822 if( line[0] == '}' )
3823 break;
3824
3825 if( line[0] == '*' )
3826 {
3827 pushBackLine( line );
3828 return;
3829 }
3830
3831 std::string attrName, attrValue;
3832
3833 if( line[0] == '"' )
3834 {
3835 size_t endQuote = line.find( '"', 1 );
3836
3837 if( endQuote != std::string::npos )
3838 {
3839 attrName = line.substr( 1, endQuote - 1 );
3840 attrValue = line.substr( endQuote + 1 );
3841 }
3842 }
3843 else
3844 {
3845 std::istringstream attrSS( line );
3846 attrSS >> attrName;
3847 std::getline( attrSS >> std::ws, attrValue );
3848 }
3849
3850 if( !attrValue.empty() && attrValue[0] == ' ' )
3851 attrValue = attrValue.substr( 1 );
3852
3853 if( !attrName.empty() && !attrValue.empty() )
3854 currentPartType->attributes[attrName] = attrValue;
3855 }
3856
3857 continue;
3858 }
3859
3860 if( line[0] == '{' || line[0] == '}' )
3861 continue;
3862
3863 std::vector<std::string> tokens = tokenize( line );
3864
3865 if( tokens.empty() )
3866 continue;
3867
3868 // Pin tokens wrap freely across lines, so the gate's declared count marks the end
3869 if( pendingGatePins > 0 )
3870 {
3871 for( const std::string& token : tokens )
3872 {
3873 if( pendingGatePins == 0 )
3874 break;
3875
3876 pendingGatePins--;
3877
3878 // Parse pin format: PINNAME.SWAPTYPE.PINTYPE[.FUNCNAME] or PINNAME.PADINDEX.TYPE.NET
3879 std::vector<std::string> parts;
3880 size_t start = 0;
3881 size_t pos = 0;
3882
3883 while( ( pos = token.find( '.', start ) ) != std::string::npos )
3884 {
3885 parts.push_back( token.substr( start, pos - start ) );
3886 start = pos + 1;
3887 }
3888
3889 parts.push_back( token.substr( start ) );
3890
3891 if( parts.size() < 3 )
3892 continue;
3893
3894 // Check if this is a gate pin definition or pad stack mapping
3895 // Gate pin: pinnumber.swaptype.pintype[.funcname]
3896 // Pad map: pinname.padindex.type.netname
3897 bool isNumericSecond = isInteger( parts[1] );
3898
3899 if( currentGate && parts[2].size() == 1 && !isNumericSecond )
3900 {
3901 GATE_PIN gpin;
3902 gpin.pin_number = parts[0];
3903 gpin.swap_type = PADS_COMMON::ParseInt( parts[1], 0, "gate pin swap" );
3904
3905 if( !parts[2].empty() )
3906 gpin.elec_type = parsePinElecType( parts[2][0] );
3907
3908 if( parts.size() >= 4 )
3909 gpin.func_name = parts[3];
3910
3911 currentGate->pins.push_back( gpin );
3912 }
3913 else if( isNumericSecond )
3914 {
3915 int padIdx = PADS_COMMON::ParseInt( parts[1], -1, "pad index" );
3916
3917 if( padIdx >= 0 )
3918 currentPartType->pin_pad_map[parts[0]] = padIdx;
3919 }
3920 }
3921
3922 continue;
3923 }
3924
3925 // Unused pin names are discarded, but still have to be counted off so a run of bare
3926 // pin numbers is not read as the next header
3927 if( pendingGates == 0 && pendingSigPins == 0 && pendingUnusedPins > 0 )
3928 {
3929 pendingUnusedPins -= std::min<int>( pendingUnusedPins, tokens.size() );
3930 continue;
3931 }
3932
3933 // Gate line: G gateswap pins
3934 if( currentPartType && tokens.size() >= 3 && tokens[0] == "G" && isInteger( tokens[1] )
3935 && isInteger( tokens[2] ) )
3936 {
3937 GATE_DEF gate;
3938 gate.gate_swap_type = PADS_COMMON::ParseInt( tokens[1], 0, "gate swap" );
3939 currentPartType->gates.push_back( gate );
3940 currentGate = &currentPartType->gates.back();
3941
3942 pendingGatePins = PADS_COMMON::ParseInt( tokens[2], 0, "gate pin count" );
3943
3944 if( pendingGates > 0 )
3945 pendingGates--;
3946
3947 continue;
3948 }
3949
3950 // SIGPIN pinno width signm
3951 if( currentPartType && tokens[0] == "SIGPIN" )
3952 {
3953 std::istringstream sss( line );
3954 std::string keyword;
3955 SIGPIN sigpin;
3956
3957 sss >> keyword >> sigpin.pin_number >> sigpin.width >> sigpin.signal_name;
3958
3959 if( !sigpin.pin_number.empty() )
3960 currentPartType->signal_pins.push_back( sigpin );
3961
3962 if( pendingSigPins > 0 )
3963 pendingSigPins--;
3964
3965 continue;
3966 }
3967
3968 // Header is NAME DECALNM [UNITS] TYPE GATES SIGPINS UNUSEDPINNMS FLAGS ECO, and V5.0
3969 // and V2005.0 insert UNITS, so the counts start at the first numeric column past the decal
3970 PART_TYPE pt;
3971 pt.name = tokens[0];
3972
3973 if( tokens.size() > 1 )
3974 pt.decal_name = tokens[1];
3975
3976 m_part_types[pt.name] = pt;
3977 currentPartType = &m_part_types[pt.name];
3978 currentGate = nullptr;
3979
3980 pendingGates = 0;
3981 pendingGatePins = 0;
3982 pendingSigPins = 0;
3983 pendingUnusedPins = 0;
3984
3985 for( size_t ii = 2; ii < tokens.size(); ++ii )
3986 {
3987 if( !isInteger( tokens[ii] ) )
3988 continue;
3989
3990 pendingGates = PADS_COMMON::ParseInt( tokens[ii], 0, "part type gates" );
3991
3992 if( ii + 1 < tokens.size() && isInteger( tokens[ii + 1] ) )
3993 pendingSigPins = PADS_COMMON::ParseInt( tokens[ii + 1], 0, "part type sigpins" );
3994
3995 if( ii + 2 < tokens.size() && isInteger( tokens[ii + 2] ) )
3996 pendingUnusedPins = PADS_COMMON::ParseInt( tokens[ii + 2], 0, "part type unused pins" );
3997
3998 break;
3999 }
4000 }
4001}
4002
4003
4004void PARSER::parseSectionREUSE( std::ifstream& aStream )
4005{
4006 std::string line;
4007 REUSE_BLOCK* currentBlock = nullptr;
4008
4009 while( readLine( aStream, line ) )
4010 {
4011 if( line[0] == '*' )
4012 {
4013 pushBackLine( line );
4014 break;
4015 }
4016
4017 std::stringstream ss( line );
4018 std::string keyword;
4019 ss >> keyword;
4020
4021 if( keyword == "TYPE" )
4022 {
4023 std::string typename_val;
4024 std::getline( ss, typename_val );
4025
4026 if( !typename_val.empty() && typename_val[0] == ' ' )
4027 typename_val = typename_val.substr( 1 );
4028
4029 REUSE_BLOCK block;
4030 block.name = typename_val;
4031 m_reuse_blocks[typename_val] = block;
4032 currentBlock = &m_reuse_blocks[typename_val];
4033 }
4034 else if( keyword == "TIMESTAMP" && currentBlock )
4035 {
4036 long timestamp = 0;
4037 ss >> timestamp;
4038 currentBlock->timestamp = timestamp;
4039 }
4040 else if( keyword == "PART_NAMING" && currentBlock )
4041 {
4042 std::string naming;
4043 std::getline( ss, naming );
4044
4045 if( !naming.empty() && naming[0] == ' ' )
4046 naming = naming.substr( 1 );
4047
4048 currentBlock->part_naming = naming;
4049 }
4050 else if( keyword == "PART" && currentBlock )
4051 {
4052 std::string partname;
4053 std::getline( ss, partname );
4054
4055 if( !partname.empty() && partname[0] == ' ' )
4056 partname = partname.substr( 1 );
4057
4058 currentBlock->part_names.push_back( partname );
4059 }
4060 else if( keyword == "NET_NAMING" && currentBlock )
4061 {
4062 std::string naming;
4063 std::getline( ss, naming );
4064
4065 if( !naming.empty() && naming[0] == ' ' )
4066 naming = naming.substr( 1 );
4067
4068 currentBlock->net_naming = naming;
4069 }
4070 else if( keyword == "NET" && currentBlock )
4071 {
4072 int merge_flag = 0;
4073 std::string netname;
4074
4075 ss >> merge_flag;
4076 std::getline( ss, netname );
4077
4078 if( !netname.empty() && netname[0] == ' ' )
4079 netname = netname.substr( 1 );
4080
4081 REUSE_NET net;
4082 net.merge = ( merge_flag == 1 );
4083 net.name = netname;
4084 currentBlock->nets.push_back( net );
4085 }
4086 else if( keyword == "REUSE" && currentBlock )
4087 {
4088 REUSE_INSTANCE instance;
4089 ss >> instance.instance_name;
4090
4091 std::string next_token;
4092 ss >> next_token;
4093
4094 if( next_token == "PREFIX" || next_token == "SUFFIX" )
4095 {
4096 std::string param;
4097 ss >> param;
4098 instance.part_naming = next_token + " " + param;
4099 ss >> next_token;
4100 }
4101 else if( next_token == "START" || next_token == "INCREMENT" )
4102 {
4103 std::string num;
4104 ss >> num;
4105 instance.part_naming = next_token + " " + num;
4106 ss >> next_token;
4107 }
4108 else if( next_token == "NEXT" )
4109 {
4110 instance.part_naming = next_token;
4111 ss >> next_token;
4112 }
4113
4114 if( next_token == "PREFIX" || next_token == "SUFFIX" )
4115 {
4116 std::string param;
4117 ss >> param;
4118 instance.net_naming = next_token + " " + param;
4119 }
4120 else if( next_token == "START" || next_token == "INCREMENT" )
4121 {
4122 std::string num;
4123 ss >> num;
4124 instance.net_naming = next_token + " " + num;
4125 }
4126 else if( next_token == "NEXT" )
4127 {
4128 instance.net_naming = next_token;
4129 }
4130
4131 std::string glued_str;
4132 ss >> instance.location.x >> instance.location.y >> instance.rotation >> glued_str;
4133
4134 instance.glued = ( glued_str == "Y" || glued_str == "YES" || glued_str == "1" );
4135 currentBlock->instances.push_back( instance );
4136 }
4137 }
4138}
4139
4140
4141void PARSER::parseSectionCLUSTER( std::ifstream& aStream )
4142{
4143 std::string line;
4144 CLUSTER* currentCluster = nullptr;
4145
4146 while( readLine( aStream, line ) )
4147 {
4148 if( line[0] == '*' )
4149 {
4150 pushBackLine( line );
4151 break;
4152 }
4153
4154 std::stringstream ss( line );
4155 std::string firstToken;
4156 ss >> firstToken;
4157
4158 // Check if this is a new cluster definition or a member line
4159 // Cluster definition format varies but typically starts with name/id
4160 if( firstToken.empty() )
4161 continue;
4162
4163 // Try parsing as cluster ID
4164 bool isNumeric = !firstToken.empty() &&
4165 std::all_of( firstToken.begin(), firstToken.end(), ::isdigit );
4166
4167 if( isNumeric )
4168 {
4169 // This could be a cluster ID starting a new cluster
4170 CLUSTER cluster;
4171 cluster.id = PADS_COMMON::ParseInt( firstToken, 0, "CLUSTER" );
4172
4173 // Read optional cluster name
4174 std::string name;
4175
4176 if( ss >> name )
4177 cluster.name = name;
4178 else
4179 cluster.name = "Cluster_" + firstToken;
4180
4181 m_clusters.push_back( cluster );
4182 currentCluster = &m_clusters.back();
4183 }
4184 else if( currentCluster )
4185 {
4186 // Could be a net name or segment reference belonging to current cluster
4187 // PADS format varies - add to net_names or segment_refs based on content
4188 if( firstToken.find( '.' ) != std::string::npos )
4189 {
4190 // Looks like a segment reference (e.g., "NET.1")
4191 currentCluster->segment_refs.push_back( firstToken );
4192 }
4193 else
4194 {
4195 // Treat as net name
4196 currentCluster->net_names.push_back( firstToken );
4197 }
4198
4199 // Continue reading additional items on the same line
4200 std::string item;
4201
4202 while( ss >> item )
4203 {
4204 if( item.find( '.' ) != std::string::npos )
4205 currentCluster->segment_refs.push_back( item );
4206 else
4207 currentCluster->net_names.push_back( item );
4208 }
4209 }
4210 }
4211}
4212
4213
4214void PARSER::parseSectionJUMPER( std::ifstream& aStream )
4215{
4216 std::string line;
4217
4218 while( readLine( aStream, line ) )
4219 {
4220 if( line[0] == '*' )
4221 {
4222 pushBackLine( line );
4223 break;
4224 }
4225
4226 // Jumper header format: name flags minlen maxlen lenincr lcount padstack [end_padstack]
4227 // flags: V=via enabled, N=no via, W=wirebond, D=display silk, G=glued
4228 std::stringstream ss( line );
4229 std::string name, flags;
4230 double minlen = 0.0, maxlen = 0.0, lenincr = 0.0;
4231 int lcount = 0;
4232 std::string padstack, end_padstack;
4233
4234 if( !( ss >> name >> flags >> minlen >> maxlen >> lenincr >> lcount >> padstack ) )
4235 continue;
4236
4237 ss >> end_padstack;
4238
4239 JUMPER_DEF jumper;
4240 jumper.name = name;
4241 jumper.min_length = minlen;
4242 jumper.max_length = maxlen;
4243 jumper.length_increment = lenincr;
4244 jumper.padstack = padstack;
4245 jumper.end_padstack = end_padstack;
4246
4247 // Parse flags
4248 for( char c : flags )
4249 {
4250 switch( c )
4251 {
4252 case 'V': jumper.via_enabled = true; break;
4253 case 'N': jumper.via_enabled = false; break;
4254 case 'W': jumper.wirebond = true; break;
4255 case 'D': jumper.display_silk = true; break;
4256 case 'G': jumper.glued = true; break;
4257 default: break;
4258 }
4259 }
4260
4261 // Parse label entries (each label is 2 lines)
4262 for( int i = 0; i < lcount; ++i )
4263 {
4264 ATTRIBUTE attr;
4265
4266 // Line 1: VISIBLE X Y ORI LEVEL HEIGHT WIDTH MIRRORED HJUST VJUST [RIGHTREADING]
4267 if( !readLine( aStream, line ) )
4268 break;
4269
4270 std::stringstream ss_attr( line );
4271 std::string visible_str, mirrored_str, right_reading_str;
4272
4273 if( ss_attr >> visible_str >> attr.x >> attr.y >> attr.orientation >> attr.level
4274 >> attr.height >> attr.width >> mirrored_str >> attr.hjust >> attr.vjust )
4275 {
4276 attr.visible = ( visible_str == "VALUE" || visible_str == "FULL_NAME" ||
4277 visible_str == "NAME" || visible_str == "FULL_BOTH" ||
4278 visible_str == "BOTH" );
4279 attr.mirrored = ( mirrored_str == "M" || mirrored_str == "1" );
4280 ss_attr >> right_reading_str;
4281 attr.right_reading = ( right_reading_str == "Y" || right_reading_str == "ORTHO" );
4282 }
4283
4284 if( m_has_font_lines )
4285 {
4286 if( !readLine( aStream, line ) )
4287 break;
4288
4289 attr.font_info = line;
4290 }
4291
4292 jumper.labels.push_back( attr );
4293 }
4294
4295 m_jumper_defs.push_back( jumper );
4296 }
4297}
4298
4299
4300void PARSER::parseSectionTESTPOINT( std::ifstream& aStream )
4301{
4302 std::string line;
4303
4304 while( readLine( aStream, line ) )
4305 {
4306 if( line[0] == '*' )
4307 {
4308 pushBackLine( line );
4309 break;
4310 }
4311
4312 std::stringstream ss( line );
4313 std::string type;
4314 ss >> type;
4315
4316 if( type.empty() )
4317 continue;
4318
4319 // Format: TYPE X Y SIDE NETNAME SYMBOLNAME
4320 // TYPE is VIA or PIN
4321 TEST_POINT tp;
4322 tp.type = type;
4323
4324 ss >> tp.x >> tp.y >> tp.side >> tp.net_name >> tp.symbol_name;
4325
4326 if( !tp.net_name.empty() )
4327 {
4328 m_test_points.push_back( tp );
4329 }
4330 }
4331}
4332
4333
4334void PARSER::parseSectionNETCLASS( std::ifstream& aStream )
4335{
4336 std::string line;
4337 NET_CLASS_DEF currentClass;
4338 bool inClass = false;
4339
4340 while( readLine( aStream, line ) )
4341 {
4342 if( line[0] == '*' )
4343 {
4344 // Save the last class if we were building one
4345 if( inClass && !currentClass.name.empty() )
4346 m_net_classes.push_back( currentClass );
4347
4348 pushBackLine( line );
4349 break;
4350 }
4351
4352 std::stringstream ss( line );
4353 std::string token;
4354 ss >> token;
4355
4356 if( token.empty() )
4357 continue;
4358
4359 // Check for class name definition (typically first token without a keyword)
4360 if( token == "CLASS" || token == "NETCLASS" )
4361 {
4362 // Save previous class if any
4363 if( inClass && !currentClass.name.empty() )
4364 m_net_classes.push_back( currentClass );
4365
4366 // Start new class
4367 currentClass = NET_CLASS_DEF();
4368 ss >> currentClass.name;
4369 inClass = true;
4370 }
4371 else if( token == "CLEARANCE" && inClass )
4372 {
4373 ss >> currentClass.clearance;
4374 }
4375 else if( token == "TRACKWIDTH" && inClass )
4376 {
4377 ss >> currentClass.track_width;
4378 }
4379 else if( token == "VIASIZE" && inClass )
4380 {
4381 ss >> currentClass.via_size;
4382 }
4383 else if( token == "VIADRILL" && inClass )
4384 {
4385 ss >> currentClass.via_drill;
4386 }
4387 else if( token == "DIFFPAIRGAP" && inClass )
4388 {
4389 ss >> currentClass.diff_pair_gap;
4390 }
4391 else if( token == "DIFFPAIRWIDTH" && inClass )
4392 {
4393 ss >> currentClass.diff_pair_width;
4394 }
4395 else if( token == "NET" && inClass )
4396 {
4397 // Net assignment: NET netname
4398 std::string netName;
4399 ss >> netName;
4400
4401 if( !netName.empty() )
4402 currentClass.net_names.push_back( netName );
4403 }
4404 else if( !token.empty() && token[0] != '#' )
4405 {
4406 // Check if this looks like a class name (no keyword prefix) in some formats
4407 if( !inClass || ( inClass && currentClass.name.empty() ) )
4408 {
4409 // Save previous if any
4410 if( inClass && !currentClass.name.empty() )
4411 m_net_classes.push_back( currentClass );
4412
4413 currentClass = NET_CLASS_DEF();
4414 currentClass.name = token;
4415 inClass = true;
4416 }
4417 }
4418 }
4419
4420 // Save final class
4421 if( inClass && !currentClass.name.empty() )
4422 m_net_classes.push_back( currentClass );
4423}
4424
4425
4426void PARSER::parseSectionDIFFPAIR( std::ifstream& aStream )
4427{
4428 std::string line;
4429
4430 while( readLine( aStream, line ) )
4431 {
4432 if( line[0] == '*' )
4433 {
4434 pushBackLine( line );
4435 break;
4436 }
4437
4438 std::stringstream ss( line );
4439 std::string token;
4440 ss >> token;
4441
4442 if( token.empty() )
4443 continue;
4444
4445 // Differential pair format can vary. Common patterns:
4446 // DIFFPAIR name positive_net negative_net gap width
4447 // or keyword-based like:
4448 // PAIR name
4449 // POS positive_net
4450 // NEG negative_net
4451 // GAP value
4452 // WIDTH value
4453
4454 if( token == "DIFFPAIR" || token == "PAIR" )
4455 {
4456 DIFF_PAIR_DEF dp;
4457 ss >> dp.name;
4458
4459 // Try to read the nets inline
4460 std::string posNet, negNet;
4461 ss >> posNet >> negNet;
4462
4463 if( !posNet.empty() )
4464 dp.positive_net = posNet;
4465
4466 if( !negNet.empty() )
4467 dp.negative_net = negNet;
4468
4469 // Try to read gap and width inline
4470 double gap = 0.0, width = 0.0;
4471
4472 if( ss >> gap )
4473 dp.gap = gap;
4474
4475 if( ss >> width )
4476 dp.width = width;
4477
4478 if( !dp.name.empty() )
4479 m_diff_pairs.push_back( dp );
4480 }
4481 else if( token == "POS" && !m_diff_pairs.empty() )
4482 {
4483 ss >> m_diff_pairs.back().positive_net;
4484 }
4485 else if( token == "NEG" && !m_diff_pairs.empty() )
4486 {
4487 ss >> m_diff_pairs.back().negative_net;
4488 }
4489 else if( token == "GAP" && !m_diff_pairs.empty() )
4490 {
4491 ss >> m_diff_pairs.back().gap;
4492 }
4493 else if( ( token == "WIDTH" || token == "TRACKWIDTH" ) && !m_diff_pairs.empty() )
4494 {
4495 ss >> m_diff_pairs.back().width;
4496 }
4497 }
4498}
4499
4500
4501void PARSER::parseSectionLAYERDEFS( std::ifstream& aStream )
4502{
4503 std::string line;
4504 int braceDepth = 0;
4505 int currentLayerNum = -1;
4506 LAYER_INFO currentLayer;
4507 bool inLayerBlock = false;
4508
4509 // Helper to parse LAYER_TYPE string to enum
4510 auto parseLayerType = []( const std::string& typeStr ) -> PADS_LAYER_FUNCTION {
4511 if( typeStr == "ROUTING" )
4513 else if( typeStr == "PLANE" )
4515 else if( typeStr == "MIXED" )
4517 else if( typeStr == "UNASSIGNED" )
4519 else if( typeStr == "SOLDER_MASK" )
4521 else if( typeStr == "PASTE_MASK" )
4523 else if( typeStr == "SILK_SCREEN" )
4525 else if( typeStr == "ASSEMBLY" )
4527 else if( typeStr == "DOCUMENTATION" )
4529 else if( typeStr == "DRILL" )
4532 };
4533
4534 while( readLine( aStream, line ) )
4535 {
4536 if( line.empty() )
4537 continue;
4538
4539 // Stop if we hit a new section marker
4540 if( line[0] == '*' )
4541 {
4542 pushBackLine( line );
4543 break;
4544 }
4545
4546 std::istringstream iss( line );
4547 std::string token;
4548 iss >> token;
4549
4550 if( token == "{" )
4551 {
4552 braceDepth++;
4553 continue;
4554 }
4555
4556 if( token == "}" )
4557 {
4558 braceDepth--;
4559
4560 // Closing a layer block, save if we have valid data
4561 if( inLayerBlock && braceDepth == 1 )
4562 {
4563 if( currentLayerNum >= 0 )
4564 {
4565 currentLayer.number = currentLayerNum;
4566
4567 // Determine if copper based on layer number and type
4568 currentLayer.is_copper = ( currentLayer.layer_type == PADS_LAYER_FUNCTION::ROUTING ||
4569 currentLayer.layer_type == PADS_LAYER_FUNCTION::PLANE ||
4570 currentLayer.layer_type == PADS_LAYER_FUNCTION::MIXED );
4571 currentLayer.required = currentLayer.is_copper;
4572 m_layer_defs[currentLayerNum] = currentLayer;
4573 }
4574
4575 inLayerBlock = false;
4576 currentLayerNum = -1;
4577 }
4578
4579 // Exiting the outer LAYER block
4580 if( braceDepth <= 0 )
4581 break;
4582
4583 continue;
4584 }
4585
4586 if( token == "LAYER" )
4587 {
4588 int layerNum = -1;
4589 iss >> layerNum;
4590
4591 if( !iss.fail() && layerNum >= 0 )
4592 {
4593 // Starting a new layer definition
4594 currentLayerNum = layerNum;
4595 currentLayer = LAYER_INFO();
4596 currentLayer.number = layerNum;
4598 inLayerBlock = true;
4599 }
4600 }
4601 else if( token == "LAYER_NAME" && inLayerBlock )
4602 {
4603 // Read the rest of the line as the layer name
4604 std::string name;
4605 std::getline( iss >> std::ws, name );
4606 currentLayer.name = name;
4607 }
4608 else if( token == "LAYER_TYPE" && inLayerBlock )
4609 {
4610 std::string typeStr;
4611 iss >> typeStr;
4612 currentLayer.layer_type = parseLayerType( typeStr );
4613 }
4614 else if( token == "LAYER_THICKNESS" && inLayerBlock )
4615 {
4616 iss >> currentLayer.layer_thickness;
4617 }
4618 else if( token == "COPPER_THICKNESS" && inLayerBlock )
4619 {
4620 iss >> currentLayer.copper_thickness;
4621 }
4622 else if( token == "DIELECTRIC" && inLayerBlock )
4623 {
4624 iss >> currentLayer.dielectric_constant;
4625 }
4626 }
4627}
4628
4629
4630void PARSER::parseSectionMISC( std::ifstream& aStream )
4631{
4632 // The MISC section contains various optional data:
4633 // - NET_CLASS DATA (net class definitions with member nets)
4634 // - GROUP DATA (pin pair groups)
4635 // - ASSOCIATED NET DATA (associated net pairs)
4636 // - DIF_PAIR definitions with extended parameters
4637 // - DESIGN_RULES / RULE_SET (hierarchical design rules)
4638 // - ATTRIBUTES DICTIONARY (attribute type definitions)
4639 //
4640 // We parse DIF_PAIR and NET_CLASS definitions as they're most relevant for KiCad.
4641
4642 std::string line;
4643 int braceDepth = 0;
4644 bool inDifPair = false;
4645 bool inNetClassData = false;
4646 bool inNetClass = false;
4647 bool inRuleSet = false;
4648 bool inRuleSetFor = false;
4649 bool inClearanceRule = false;
4650 int netClassDataDepth = -1;
4651 int netClassDepth = -1;
4652 int ruleSetDepth = -1;
4653 int clearanceRuleDepth = -1;
4654 bool foundDefaultRules = false;
4655 bool isDefaultRuleSet = false;
4656 std::string ruleSetNetClass;
4657 DIFF_PAIR_DEF currentDiffPair;
4658 NET_CLASS_DEF currentNetClass;
4659
4660 while( readLine( aStream, line ) )
4661 {
4662 if( line.empty() )
4663 continue;
4664
4665 // Stop at next section marker
4666 if( line[0] == '*' && braceDepth == 0 )
4667 {
4668 pushBackLine( line );
4669 break;
4670 }
4671
4672 // Track brace depth for nested structures
4673 for( char c : line )
4674 {
4675 if( c == '{' )
4676 braceDepth++;
4677 else if( c == '}' )
4678 {
4679 braceDepth--;
4680
4681 if( braceDepth == 0 && inDifPair )
4682 {
4683 // End of DIF_PAIR block
4684 if( !currentDiffPair.name.empty() )
4685 m_diff_pairs.push_back( currentDiffPair );
4686
4687 inDifPair = false;
4688 currentDiffPair = DIFF_PAIR_DEF();
4689 }
4690
4691 if( inNetClass && braceDepth <= netClassDepth )
4692 {
4693 if( !currentNetClass.name.empty() )
4694 m_net_classes.push_back( currentNetClass );
4695
4696 inNetClass = false;
4697 currentNetClass = NET_CLASS_DEF();
4698 }
4699
4700 if( inNetClassData && braceDepth <= netClassDataDepth )
4701 {
4702 inNetClassData = false;
4703 }
4704
4705 if( inClearanceRule && braceDepth < clearanceRuleDepth )
4706 {
4707 inClearanceRule = false;
4708
4709 if( isDefaultRuleSet )
4710 {
4711 if( m_design_rules.default_clearance
4712 == std::numeric_limits<double>::max() )
4713 {
4714 m_design_rules.default_clearance =
4715 DESIGN_RULES().default_clearance;
4716 }
4717
4718 m_design_rules.min_clearance = m_design_rules.default_clearance;
4719
4720 if( m_design_rules.copper_edge_clearance
4721 == std::numeric_limits<double>::max() )
4722 {
4723 m_design_rules.copper_edge_clearance =
4724 m_design_rules.default_clearance;
4725 }
4726
4727 foundDefaultRules = true;
4728 }
4729 }
4730
4731 if( inRuleSetFor && braceDepth < ruleSetDepth + 1 )
4732 inRuleSetFor = false;
4733
4734 if( inRuleSet && braceDepth < ruleSetDepth )
4735 {
4736 inRuleSet = false;
4737 isDefaultRuleSet = false;
4738 ruleSetNetClass.clear();
4739 }
4740 }
4741 }
4742
4743 std::istringstream iss( line );
4744 std::string token;
4745 iss >> token;
4746
4747 // LAYER DATA block contains per-layer definitions (name, type, etc.)
4748 // which may appear inside *MISC* instead of as a standalone section.
4749 if( token == "LAYER" )
4750 {
4751 std::string secondToken;
4752 iss >> secondToken;
4753
4754 if( secondToken == "DATA" )
4755 {
4756 parseSectionLAYERDEFS( aStream );
4757 continue;
4758 }
4759 }
4760
4761 if( token == "NET_CLASS" )
4762 {
4763 std::string secondToken;
4764 iss >> secondToken;
4765
4766 if( secondToken == "DATA" )
4767 {
4768 inNetClassData = true;
4769 netClassDataDepth = braceDepth;
4770 }
4771 else if( inNetClassData && !secondToken.empty() )
4772 {
4773 if( inNetClass && !currentNetClass.name.empty() )
4774 m_net_classes.push_back( currentNetClass );
4775
4776 currentNetClass = NET_CLASS_DEF();
4777 currentNetClass.name = secondToken;
4778 inNetClass = true;
4779 netClassDepth = braceDepth;
4780 }
4781 else if( inRuleSetFor && !secondToken.empty() )
4782 {
4783 ruleSetNetClass = secondToken;
4784 }
4785 }
4786 else if( inNetClass && token == "NET" )
4787 {
4788 std::string netName;
4789 iss >> netName;
4790
4791 if( !netName.empty() )
4792 currentNetClass.net_names.push_back( netName );
4793 }
4794 else if( token == "RULE_SET" )
4795 {
4796 std::string ruleNum;
4797 iss >> ruleNum;
4798
4799 inRuleSet = true;
4800 ruleSetDepth = braceDepth;
4801 ruleSetNetClass.clear();
4802 isDefaultRuleSet = ( ruleNum == "(1)" && !foundDefaultRules );
4803 }
4804 else if( inRuleSet && !inClearanceRule && token == "FOR" )
4805 {
4806 inRuleSetFor = true;
4807 }
4808 else if( inRuleSet && token == "CLEARANCE_RULE" )
4809 {
4810 inClearanceRule = true;
4811 clearanceRuleDepth = braceDepth;
4812
4813 if( isDefaultRuleSet )
4814 {
4815 m_design_rules.default_clearance = std::numeric_limits<double>::max();
4816 m_design_rules.copper_edge_clearance = std::numeric_limits<double>::max();
4817 }
4818 }
4819 else if( inClearanceRule )
4820 {
4821 double val = 0.0;
4822 iss >> val;
4823
4824 if( !iss.fail() && val > 0.0 )
4825 {
4826 if( isDefaultRuleSet )
4827 {
4828 if( token == "MIN_TRACK_WIDTH" )
4829 {
4830 m_design_rules.min_track_width = val;
4831 }
4832 else if( token == "REC_TRACK_WIDTH" )
4833 {
4834 m_design_rules.default_track_width = val;
4835 }
4836 else if( token == "DRILL_TO_DRILL" )
4837 {
4838 m_design_rules.hole_to_hole = val;
4839 }
4840 else if( token == "OUTLINE_TO_TRACK" || token == "OUTLINE_TO_VIA"
4841 || token == "OUTLINE_TO_PAD" || token == "OUTLINE_TO_COPPER"
4842 || token == "OUTLINE_TO_SMD" )
4843 {
4844 m_design_rules.copper_edge_clearance =
4845 std::min( m_design_rules.copper_edge_clearance, val );
4846 }
4847 else if( token.rfind( "SAME_NET_", 0 ) == 0 || token == "BODY_TO_BODY"
4848 || token == "MAX_TRACK_WIDTH"
4849 || token.rfind( "TEXT_TO_", 0 ) == 0
4850 || token.rfind( "COPPER_TO_", 0 ) == 0 )
4851 {
4852 // Exclude same-net spacings, physical body clearances, text
4853 // clearances, and copper-pour clearances from the inter-net
4854 // copper clearance.
4855 }
4856 else if( token == "TRACK_TO_TRACK" || token.rfind( "VIA_TO_", 0 ) == 0
4857 || token.rfind( "PAD_TO_", 0 ) == 0
4858 || token.rfind( "SMD_TO_", 0 ) == 0
4859 || token.rfind( "DRILL_TO_", 0 ) == 0 )
4860 {
4861 m_design_rules.default_clearance =
4862 std::min( m_design_rules.default_clearance, val );
4863 }
4864 }
4865 else if( !ruleSetNetClass.empty() )
4866 {
4867 for( auto& nc : m_net_classes )
4868 {
4869 if( nc.name == ruleSetNetClass )
4870 {
4871 if( token == "REC_TRACK_WIDTH" )
4872 nc.track_width = val;
4873 else if( token == "TRACK_TO_TRACK" )
4874 nc.clearance = val;
4875
4876 break;
4877 }
4878 }
4879 }
4880 }
4881 }
4882 else if( token == "DIF_PAIR" )
4883 {
4884 // Save previous diff pair if any
4885 if( inDifPair && !currentDiffPair.name.empty() )
4886 m_diff_pairs.push_back( currentDiffPair );
4887
4888 currentDiffPair = DIFF_PAIR_DEF();
4889 iss >> currentDiffPair.name;
4890 inDifPair = true;
4891 }
4892 else if( inDifPair )
4893 {
4894 if( token == "NET" )
4895 {
4896 std::string netName;
4897 iss >> netName;
4898
4899 // Assign to positive net first, then negative
4900 if( currentDiffPair.positive_net.empty() )
4901 currentDiffPair.positive_net = netName;
4902 else if( currentDiffPair.negative_net.empty() )
4903 currentDiffPair.negative_net = netName;
4904 }
4905 else if( token == "GAP" )
4906 {
4907 iss >> currentDiffPair.gap;
4908 }
4909 else if( token == "WIDTH" )
4910 {
4911 iss >> currentDiffPair.width;
4912 }
4913 else if( token == "CONNECTION" )
4914 {
4915 // CONNECTION format: ref.pin,ref.pin
4916 // This defines a pin pair for the diff pair
4917 // For now just skip - main net assignment is more important
4918 }
4919 else if( token == "ASSOCIATED" )
4920 {
4921 // ASSOCIATED NET netname - for associated net pairs
4922 std::string keyword, netName;
4923 iss >> keyword >> netName;
4924
4925 if( keyword == "NET" )
4926 {
4927 if( currentDiffPair.positive_net.empty() )
4928 currentDiffPair.positive_net = netName;
4929 else if( currentDiffPair.negative_net.empty() )
4930 currentDiffPair.negative_net = netName;
4931 }
4932 }
4933 }
4934
4935 // Parse per-instance attribute blocks: PART <refdes> { key value ... }
4936 // These appear inside ATTRIBUTE VALUES {...} at variable brace depth.
4937 if( token == "PART" && !inDifPair && !inNetClass )
4938 {
4939 std::string partName;
4940 iss >> partName;
4941
4942 if( !partName.empty() )
4943 {
4944 // Save brace depth before consuming the block's own { ... }
4945 int savedDepth = braceDepth;
4946 auto& attrs = m_part_instance_attrs[partName];
4947
4948 while( readLine( aStream, line ) )
4949 {
4950 if( line.empty() )
4951 continue;
4952
4953 if( line[0] == '}' )
4954 break;
4955
4956 if( line[0] == '{' )
4957 continue;
4958
4959 if( line[0] == '*' )
4960 {
4961 pushBackLine( line );
4963 return;
4964 }
4965
4966 std::string attrName, attrValue;
4967
4968 if( line[0] == '"' )
4969 {
4970 size_t endQuote = line.find( '"', 1 );
4971
4972 if( endQuote != std::string::npos )
4973 {
4974 attrName = line.substr( 1, endQuote - 1 );
4975 attrValue = line.substr( endQuote + 1 );
4976 }
4977 }
4978 else
4979 {
4980 std::istringstream attrSS( line );
4981 attrSS >> attrName;
4982 std::getline( attrSS >> std::ws, attrValue );
4983 }
4984
4985 if( !attrValue.empty() && attrValue[0] == ' ' )
4986 attrValue = attrValue.substr( 1 );
4987
4988 if( !attrName.empty() && !attrValue.empty() )
4989 attrs[attrName] = attrValue;
4990 }
4991
4992 // Restore to the depth before the PART block's braces
4993 braceDepth = savedDepth;
4994 }
4995
4996 continue;
4997 }
4998
4999 // Skip other MISC subsections (ATTRIBUTES DICTIONARY, DESIGN_RULES, etc.)
5000 }
5001
5003}
5004
5005
5007{
5008 DESIGN_RULES defaults;
5009
5010 if( m_design_rules.default_clearance == std::numeric_limits<double>::max() )
5011 m_design_rules.default_clearance = defaults.default_clearance;
5012
5013 if( m_design_rules.copper_edge_clearance == std::numeric_limits<double>::max() )
5014 m_design_rules.copper_edge_clearance = defaults.copper_edge_clearance;
5015}
5016
5017
5018std::vector<LAYER_INFO> PARSER::GetLayerInfos() const
5019{
5020 std::vector<LAYER_INFO> layers;
5021
5022 int layerCount = m_parameters.layer_count;
5023
5024 if( layerCount < 1 )
5025 layerCount = 2;
5026
5027 // Helper to check if a layer number is a copper layer
5028 auto isCopperLayer = [&]( int num ) {
5029 return num >= 1 && num <= layerCount;
5030 };
5031
5032 // Helper to get layer type from parsed defs or default
5033 auto getLayerDef = [&]( int num ) -> const LAYER_INFO* {
5034 auto it = m_layer_defs.find( num );
5035 return it != m_layer_defs.end() ? &it->second : nullptr;
5036 };
5037
5038 // Add copper layers with parsed info if available
5039 for( int i = 1; i <= layerCount; ++i )
5040 {
5041 const LAYER_INFO* parsed = getLayerDef( i );
5042
5043 if( parsed )
5044 {
5045 layers.push_back( *parsed );
5046 }
5047 else
5048 {
5049 // Generate default copper layer info
5051 info.number = i;
5053 info.is_copper = true;
5054 info.required = true;
5055
5056 if( i == 1 )
5057 info.name = "Top";
5058 else if( i == layerCount )
5059 info.name = "Bottom";
5060 else
5061 info.name = "Inner " + std::to_string( i - 1 );
5062
5063 layers.push_back( info );
5064 }
5065 }
5066
5067 // Add non-copper layers from parsed definitions
5068 for( const auto& [num, layerDef] : m_layer_defs )
5069 {
5070 if( !isCopperLayer( num ) )
5071 {
5072 layers.push_back( layerDef );
5073 }
5074 }
5075
5076 // If no non-copper layers were parsed, add default fallbacks
5077 if( m_layer_defs.empty() )
5078 {
5079 // Standard non-copper layers (common PADS layer numbers)
5080 layers.push_back( { 21, "Assembly Top", PADS_LAYER_FUNCTION::ASSEMBLY, false, false } );
5081 layers.push_back( { 22, "Assembly Bottom", PADS_LAYER_FUNCTION::ASSEMBLY, false, false } );
5082 layers.push_back( { 25, "Solder Mask Top", PADS_LAYER_FUNCTION::SOLDER_MASK, false, false } );
5083 layers.push_back( { 26, "Silkscreen Top", PADS_LAYER_FUNCTION::SILK_SCREEN, false, false } );
5084 layers.push_back( { 27, "Silkscreen Bottom", PADS_LAYER_FUNCTION::SILK_SCREEN, false, false } );
5085 layers.push_back( { 28, "Solder Mask Bottom", PADS_LAYER_FUNCTION::SOLDER_MASK, false, false } );
5086 layers.push_back( { 29, "Paste Top", PADS_LAYER_FUNCTION::PASTE_MASK, false, false } );
5087 layers.push_back( { 30, "Paste Bottom", PADS_LAYER_FUNCTION::PASTE_MASK, false, false } );
5088 }
5089
5090 return layers;
5091}
5092
5093} // namespace PADS_IO
const char * name
bool readLine(std::ifstream &aStream, std::string &aLine)
std::map< std::string, PART_DECAL > m_decals
std::vector< CLUSTER > m_clusters
std::map< std::string, REUSE_BLOCK > m_reuse_blocks
bool m_has_font_lines
True if text/label entries include a font line.
std::vector< KEEPOUT > m_keepouts
std::vector< POUR > m_pours
std::vector< JUMPER_DEF > m_jumper_defs
std::vector< TEXT > m_texts
void parseSectionPARTTYPE(std::ifstream &aStream)
void parseSectionJUMPER(std::ifstream &aStream)
void parseSectionTESTPOINT(std::ifstream &aStream)
void parseSectionLINES(std::ifstream &aStream)
PARAMETERS m_parameters
std::map< std::string, VIA_DEF > m_via_defs
void parseSectionBOARD(std::ifstream &aStream)
void parseSectionVIA(std::ifstream &aStream)
std::vector< COPPER_SHAPE > m_copper_shapes
std::map< std::string, PART_TYPE > m_part_types
Per-instance attribute overrides from PART <name> {...} blocks.
std::vector< NET > m_nets
void parseSectionPARTS(std::ifstream &aStream)
std::vector< ROUTE > m_routes
std::map< std::string, std::map< std::string, std::string > > m_part_instance_attrs
std::vector< NET_CLASS_DEF > m_net_classes
void parseSectionNETCLASS(std::ifstream &aStream)
void parseSectionMISC(std::ifstream &aStream)
void parseSectionREUSE(std::ifstream &aStream)
void parseSectionCLUSTER(std::ifstream &aStream)
void parseSectionLAYERDEFS(std::ifstream &aStream)
std::vector< LAYER_INFO > GetLayerInfos() const
Get layer information for the layer mapping dialog.
DESIGN_RULES m_design_rules
void parseSectionNETS(std::ifstream &aStream)
void Parse(const wxString &aFileName)
FILE_HEADER m_file_header
std::vector< GRAPHIC_LINE > m_graphic_lines
std::vector< DIMENSION > m_dimensions
std::vector< DIFF_PAIR_DEF > m_diff_pairs
void parseSectionPCB(std::ifstream &aStream)
void clampDesignRuleSentinels()
void parseSectionTEXT(std::ifstream &aStream)
std::vector< POLYLINE > m_board_outlines
std::vector< PART > m_parts
int parseMajorVersion() const
Parse the major version number from the file header version string.
std::map< int, LAYER_INFO > m_layer_defs
Keyed by layer number.
void parseSectionROUTES(std::ifstream &aStream)
std::vector< TEST_POINT > m_test_points
std::optional< std::string > m_pushed_line
void pushBackLine(const std::string &aLine)
void parseSectionDIFFPAIR(std::ifstream &aStream)
void parseSectionPOUR(std::ifstream &aStream)
void parseSectionPARTDECAL(std::ifstream &aStream)
static bool empty(const wxTextEntryBase *aCtrl)
const wxChar *const tracePadsIo
int ParseInt(const std::string &aStr, int aDefault, const std::string &aContext)
Parse integer from string with error context.
double ParseDouble(const std::string &aStr, double aDefault, const std::string &aContext)
Parse double from string with error context.
static std::vector< std::string > expandShortcutPattern(const std::string &aPattern)
Expand a shortcut format string like "PRE{n1-n2}" into individual names.
@ BURIED
Inner layers only.
@ THROUGH
Spans all copper layers.
@ BLIND
Surface to inner layer.
@ MICROVIA
Single-layer blind, typically HDI.
@ OPEN_COLLECTOR
C, open collector or or-tieable source.
static std::string normalizeNetName(const std::string &aNetName)
bool IsAntiPadRow(const PAD_STACK_LAYER &aLayer)
RA and SA rows carry a plane's anti-pad clearance rather than the pad's own copper.
bool IsThermalReliefPadRow(const PAD_STACK_LAYER &aLayer)
RT and ST rows carry a plane's thermal-relief spoke pattern rather than the pad's own copper.
@ LIB_PCB_DECAL
Library footprints.
Definition pads_parser.h:98
@ LIB_LINE
Library line items (drafting)
Definition pads_parser.h:96
bool IsCopperPadRow(const PAD_STACK_LAYER &aLayer)
True when the row describes pad or via copper.
PADS_LAYER_FUNCTION
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
A polyline point that may instead be an arc segment.
Definition pads_parser.h:64
ARC arc
Only valid when is_arc is true.
Definition pads_parser.h:68
Arc as center, radius and angles.
Definition pads_parser.h:50
double radius
Definition pads_parser.h:53
double delta_angle
Definition pads_parser.h:55
double start_angle
Definition pads_parser.h:54
std::string font_info
std::string hjust
std::string vjust
std::vector< std::string > segment_refs
std::string name
std::vector< std::string > net_names
Standalone copper area from the LINES section (type=COPPER), not part of a pour.
std::vector< ARC_POINT > outline
bool is_cutout
COPCUT, COPCCO.
std::string net_name
Empty if unconnected.
bool filled
COPCLS, COPCIR.
double width
For open polylines.
std::string restrictions
KPTCLS/KPTCIR keepout codes R,C,V,T,A.
bool is_tag_close
Closing TAG, level 0.
std::vector< ARC_POINT > points
int pinnum
Copper-piece pin association, -1 = none.
std::string type
CLOSED, OPEN, CIRCLE, COPCLS, TAG, etc.
bool is_tag_open
Opening TAG, level 1.
double copper_edge_clearance
OUTLINE_TO_*.
double default_clearance
DEFAULTCLEAR.
double x
Origin.
double crossbar_pos
Y for horizontal, X for vertical.
std::vector< POINT > points
Measurement endpoints.
int gate_swap_type
0 = not swappable
std::vector< GATE_PIN > pins
std::string func_name
int swap_type
0 = not swappable
std::string pin_number
PIN_ELEC_TYPE elec_type
Non-electrical drawing item from the LINES section (type=LINES).
std::vector< ARC_POINT > points
std::string reuse_instance
std::string padstack
Start pin, or both if end_padstack empty.
bool wirebond
W flag.
std::string end_padstack
End pin, optional.
bool display_silk
D flag.
std::vector< ATTRIBUTE > labels
bool via_enabled
V flag.
bool is_start
S = start, E = end.
std::vector< ARC_POINT > outline
bool no_vias
V restriction.
bool no_components
P restriction.
bool no_copper
C restriction.
bool no_accordion
A restriction.
bool height_restriction
H restriction.
KEEPOUT_TYPE type
bool no_traces
R restriction.
std::vector< int > layers
Empty = all.
bool no_test_points
T restriction.
PADS_LAYER_FUNCTION layer_type
bool required
Layer must be mapped.
int number
PADS layer number.
double layer_thickness
Dielectric, BASIC units.
double dielectric_constant
Er.
double copper_thickness
Copper foil, BASIC units.
double via_drill
VIADRILL.
double clearance
CLEARANCE.
std::vector< std::string > net_names
double track_width
TRACKWIDTH.
double diff_pair_width
DIFFPAIRWIDTH.
double diff_pair_gap
DIFFPAIRGAP.
double via_size
VIASIZE.
std::string name
std::vector< NET_PIN > pins
bool chamfered
Negative corner value in PADS.
double drill
0 for SMD
std::string shape
R, S, A, O, OF, RF, RT, ST, RA, SA, RC, OC.
bool plated
PTH vs NPTH.
double thermal_outer_diameter
Thermal or void in plane.
double slot_orientation
0-179.999 degrees
double thermal_spoke_orientation
First spoke.
double inner_diameter
Annular ring, 0 = solid.
double finger_offset
Along orientation axis.
double sizeB
Height for rectangles/ovals.
double slot_offset
From electrical center.
double corner_radius
Always positive.
double sizeA
Diameter or width.
std::vector< DECAL_ITEM > items
std::vector< TERMINAL > terminals
std::vector< ATTRIBUTE > attributes
std::map< int, std::vector< PAD_STACK_LAYER > > pad_stacks
std::map< std::string, std::string > attributes
From {...} block.
std::map< std::string, int > pin_pad_map
Pin name to pad stack index.
std::vector< GATE_DEF > gates
std::vector< SIGPIN > signal_pins
std::string decal_name
std::string part_type
Used with PARTTYPE@DECAL syntax.
bool explicit_decal
Decal given explicitly with @ syntax.
std::string reuse_instance
std::string decal
Primary decal (first in colon-separated list)
std::string name
int alt_decal_index
ALT placement field, -1 = use primary decal.
std::vector< ATTRIBUTE > attributes
std::string reuse_part
Original part ref des inside the reuse block.
std::vector< std::string > alternate_decals
Remaining decals after ':' splits.
A polyline that may contain arc segments, used for board outlines and graphics.
std::vector< ARC_POINT > points
std::string name
bool is_cutout
POCUT piece.
POUR_STYLE style
std::string net_name
std::string owner_pour
Parent pour, 7th header field.
double hatch_grid
std::vector< ARC_POINT > points
THERMAL_TYPE thermal_type
double hatch_width
A reuse block definition containing parts and routes that can be instantiated.
std::vector< REUSE_NET > nets
std::string net_naming
Default scheme.
std::string part_naming
Default scheme.
std::vector< std::string > part_names
std::vector< REUSE_INSTANCE > instances
Placements of this block.
std::string part_naming
May be multi-word like "PREFIX pref".
std::string net_naming
May be multi-word like "SUFFIX suf".
std::string name
Original net name from reuse definition.
bool merge
Merge nets vs rename.
std::vector< VIA > vias
std::vector< TEARDROP > teardrops
std::vector< TRACK > tracks
std::vector< NET_PIN > pins
From pin pair lines.
std::vector< JUMPER_MARKER > jumpers
std::string net_name
std::string pin_number
double width
Track width for connections.
std::string signal_name
e.g. VCC, GND
std::string name
std::vector< ARC_POINT > points
bool has_mask_front
Stack includes top soldermask, layer 25.
int drill_start
Drill span from file, for blind/buried.
int start_layer
First PADS layer in span.
int end_layer
Last PADS layer in span.
std::vector< PAD_STACK_LAYER > stack
std::string name
bool has_mask_back
Stack includes bottom soldermask, layer 28.
std::string name
KIBIS_PIN * pin
int radius
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.
#define M_PI
static thread_pool * tp
wxLogTrace helper definitions.