KiCad PCB EDA Suite
Loading...
Searching...
No Matches
pads_binary_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) 2026 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_binary_parser.h"
21
22#include <algorithm>
23#include <cmath>
24#include <cstring>
25#include <fstream>
26#include <set>
27
28#include <ki_exception.h>
29#include <wx/log.h>
30
31namespace PADS_IO
32{
33
34// Expected footer GUID
35static const uint8_t FOOTER_GUID[] = "{2FE18320-6448-11d1-A412-000000000000}";
36
37// Pad shape codes from binary format
38static const std::map<uint8_t, std::string> PAD_SHAPE_NAMES = {
39 { 0x01, "RF" },
40 { 0x02, "R" },
41 { 0x03, "S" },
42 { 0x04, "OF" },
43};
44
45
47
48
50
51
52bool BINARY_PARSER::IsBinaryPadsFile( const wxString& aFileName )
53{
54 std::ifstream file( aFileName.fn_str(), std::ios::binary );
55
56 if( !file.is_open() )
57 return false;
58
59 uint8_t header[4];
60 file.read( reinterpret_cast<char*>( header ), 4 );
61
62 if( file.gcount() < 4 )
63 return false;
64
65 // Magic bytes: 0x00 0xFF
66 if( header[0] != 0x00 || header[1] != 0xFF )
67 return false;
68
69 uint16_t version = static_cast<uint16_t>( header[2] ) | ( static_cast<uint16_t>( header[3] ) << 8 );
70
71 return version == 0x2021 || version == 0x2025 || version == 0x2026 || version == 0x2027;
72}
73
74
75void BINARY_PARSER::Parse( const wxString& aFileName )
76{
77 std::ifstream file( aFileName.fn_str(), std::ios::binary | std::ios::ate );
78
79 if( !file.is_open() )
80 THROW_IO_ERROR( "Cannot open file" );
81
82 std::streamsize fileSize = file.tellg();
83 file.seekg( 0, std::ios::beg );
84
85 m_data.resize( static_cast<size_t>( fileSize ) );
86 file.read( reinterpret_cast<char*>( m_data.data() ), fileSize );
87
88 if( file.gcount() != fileSize )
89 THROW_IO_ERROR( "Failed to read entire file" );
90
108
109 // Filter out parts with empty ref des
110 m_parts.erase( std::remove_if( m_parts.begin(), m_parts.end(),
111 []( const PART& p ) { return p.name.empty(); } ),
112 m_parts.end() );
113}
114
115
117{
118 switch( m_version )
119 {
120 case 0x2021: return 73;
121 case 0x2025:
122 case 0x2026:
123 case 0x2027: return 74;
124 default: return 0;
125 }
126}
127
128
129uint8_t BINARY_PARSER::readU8( size_t aOffset ) const
130{
131 if( aOffset >= m_data.size() )
132 {
133 THROW_IO_ERROR( wxString::Format( "PADS binary read out of bounds at offset %zu (file size %zu)",
134 aOffset, m_data.size() ) );
135 }
136
137 return m_data[aOffset];
138}
139
140
141uint16_t BINARY_PARSER::readU16( size_t aOffset ) const
142{
143 if( aOffset + 2 > m_data.size() )
144 {
145 THROW_IO_ERROR( wxString::Format( "PADS binary read out of bounds at offset %zu (file size %zu)",
146 aOffset, m_data.size() ) );
147 }
148
149 return static_cast<uint16_t>( m_data[aOffset] )
150 | ( static_cast<uint16_t>( m_data[aOffset + 1] ) << 8 );
151}
152
153
154uint32_t BINARY_PARSER::readU32( size_t aOffset ) const
155{
156 if( aOffset + 4 > m_data.size() )
157 {
158 THROW_IO_ERROR( wxString::Format( "PADS binary read out of bounds at offset %zu (file size %zu)",
159 aOffset, m_data.size() ) );
160 }
161
162 return static_cast<uint32_t>( m_data[aOffset] )
163 | ( static_cast<uint32_t>( m_data[aOffset + 1] ) << 8 )
164 | ( static_cast<uint32_t>( m_data[aOffset + 2] ) << 16 )
165 | ( static_cast<uint32_t>( m_data[aOffset + 3] ) << 24 );
166}
167
168
169int32_t BINARY_PARSER::readI32( size_t aOffset ) const
170{
171 return static_cast<int32_t>( readU32( aOffset ) );
172}
173
174
175std::string BINARY_PARSER::readFixedString( size_t aOffset, size_t aMaxLen ) const
176{
177 if( aOffset >= m_data.size() )
178 return {};
179
180 size_t available = std::min( aMaxLen, m_data.size() - aOffset );
181 const uint8_t* start = &m_data[aOffset];
182 const uint8_t* end = start + available;
183
184 // Find null terminator
185 const uint8_t* null_pos = std::find( start, end, 0 );
186 size_t len = static_cast<size_t>( null_pos - start );
187
188 // Validate printable ASCII
189 for( size_t i = 0; i < len; ++i )
190 {
191 if( start[i] < 0x20 || start[i] >= 0x7F )
192 return {};
193 }
194
195 std::string result( reinterpret_cast<const char*>( start ), len );
196
197 // Trim trailing whitespace
198 while( !result.empty() && result.back() == ' ' )
199 result.pop_back();
200
201 return result;
202}
203
204
206{
207 if( m_data.size() < static_cast<size_t>( HEADER_SIZE + FOOTER_SIZE ) )
208 THROW_IO_ERROR( "File too small for PADS binary format" );
209
210 if( m_data[0] != 0x00 || m_data[1] != 0xFF )
211 THROW_IO_ERROR( "Invalid magic bytes" );
212
213 m_version = readU16( 2 );
214
215 if( m_version != 0x2021 && m_version != 0x2025 && m_version != 0x2026 && m_version != 0x2027 )
216 THROW_IO_ERROR( "Unsupported PADS binary version" );
217
219}
220
221
223{
224 size_t footerStart = m_data.size() - FOOTER_SIZE;
225
226 // Verify GUID at footer offset + 4
227 if( std::memcmp( &m_data[footerStart + 4], FOOTER_GUID, 38 ) != 0 )
228 THROW_IO_ERROR( "Invalid footer GUID" );
229
230 uint32_t sizeCheck = readU32( footerStart + 42 );
231 uint32_t expected = static_cast<uint32_t>( m_data.size() - FOOTER_SIZE );
232
233 if( sizeCheck != expected )
234 {
235 wxLogWarning( "PADS binary footer size mismatch: stored=%u, expected=%u",
236 sizeCheck, expected );
237 }
238}
239
240
242{
243 size_t dirStart = HEADER_SIZE;
244 size_t dirSize = static_cast<size_t>( m_numDirEntries ) * DIR_ENTRY_SIZE;
245
246 if( dirStart + dirSize > m_data.size() )
247 THROW_IO_ERROR( "File too small for section directory" );
248
249 uint32_t dataOffset = static_cast<uint32_t>( dirStart + dirSize );
250
251 m_dirEntries.clear();
252 m_dirEntries.reserve( m_numDirEntries );
253
254 for( int i = 0; i < m_numDirEntries; ++i )
255 {
256 size_t off = dirStart + static_cast<size_t>( i ) * DIR_ENTRY_SIZE;
257
258 DirEntry entry;
259 entry.index = i;
260 entry.count = readU32( off );
261 entry.totalBytes = readU32( off + 4 );
262 entry.dataOffset = 0;
263 entry.perItem = 0;
264
265 if( i > 0 )
266 {
267 entry.dataOffset = dataOffset;
268
269 if( entry.count > 0 && entry.totalBytes > 0 )
270 entry.perItem = entry.totalBytes / entry.count;
271
272 dataOffset += entry.totalBytes;
273 }
274
275 m_dirEntries.push_back( entry );
276 }
277}
278
279
281{
282 if( aIndex >= 0 && aIndex < static_cast<int>( m_dirEntries.size() ) )
283 return &m_dirEntries[aIndex];
284
285 return nullptr;
286}
287
288
289const uint8_t* BINARY_PARSER::sectionData( int aIndex ) const
290{
291 const DirEntry* entry = getSection( aIndex );
292
293 if( !entry || entry->totalBytes == 0 )
294 return nullptr;
295
296 if( entry->dataOffset + entry->totalBytes > m_data.size() )
297 return nullptr;
298
299 return &m_data[entry->dataOffset];
300}
301
302
303uint32_t BINARY_PARSER::sectionSize( int aIndex ) const
304{
305 const DirEntry* entry = getSection( aIndex );
306
307 if( !entry )
308 return 0;
309
310 return entry->totalBytes;
311}
312
313
314double BINARY_PARSER::toBasicCoordX( int32_t aRawValue ) const
315{
316 return static_cast<double>( aRawValue - ( m_originFound ? m_originX : 0 ) );
317}
318
319
320double BINARY_PARSER::toBasicCoordY( int32_t aRawValue ) const
321{
322 return static_cast<double>( aRawValue - ( m_originFound ? m_originY : 0 ) );
323}
324
325
326double BINARY_PARSER::toBasicAngle( int32_t aRawAngle ) const
327{
328 if( aRawAngle == 0 )
329 return 0.0;
330
331 return static_cast<double>( aRawAngle ) / static_cast<double>( ANGLE_SCALE );
332}
333
334
336{
337 const uint8_t* data = sectionData( 1 );
338 uint32_t size = sectionSize( 1 );
339
340 if( !data || size < 160 )
341 return;
342
343 // Board setup section contains u32 parameters at known offsets.
344 // Index 4 holds the maximum layer count.
345 uint32_t maxLayer = readU32( m_dirEntries[1].dataOffset + 4 * 4 );
346
347 if( maxLayer >= 1 && maxLayer <= 64 )
348 m_parameters.layer_count = static_cast<int>( maxLayer );
349 else
350 m_parameters.layer_count = 2;
351
352 // Section 1 stores the coordinate origin at offset +60/+64 as i32 LE pair.
353 // This is the same value as DFT_CONFIGURATION POLAR_GRID X/Y but is always present.
354 size_t secBase = m_dirEntries[1].dataOffset;
355
356 if( size >= 68 )
357 {
358 m_originX = readI32( secBase + 60 );
359 m_originY = readI32( secBase + 64 );
360 m_originFound = true;
361
362 m_parameters.origin.x = static_cast<double>( m_originX );
363 m_parameters.origin.y = static_cast<double>( m_originY );
364 }
365
366 // Binary coordinates are in BASIC units (1 BASIC = 1/38100 mil).
367 // Set MILS for the display unit; actual coordinate handling uses BASIC mode
368 // in the wrapper via SetBasicUnitsMode(true).
370}
371
372
374{
375 const uint8_t* data = sectionData( 57 );
376 uint32_t size = sectionSize( 57 );
377
378 if( !data || size == 0 )
379 return;
380
381 m_stringPoolBytes.assign( data, data + size );
382}
383
384
386{
387 const DirEntry* entry = getSection( 22 );
388
389 if( !entry || entry->count == 0 || entry->perItem == 0 )
390 return;
391
392 const uint8_t* data = sectionData( 22 );
393
394 if( !data )
395 return;
396
397 bool isOld = isOldFormat();
398 uint32_t recSize = entry->perItem;
399
400 // Field offsets differ between versions
401 int nameOff = 0, xOff = 0, yOff = 0, angleOff = 0;
402
403 if( isOld )
404 {
405 nameOff = 76;
406 xOff = 92;
407 yOff = -1;
408 angleOff = 4;
409 }
410 else
411 {
412 nameOff = 44;
413 xOff = 60;
414 yOff = 64;
415 angleOff = 68;
416 }
417
418 for( uint32_t i = 0; i < entry->count; ++i )
419 {
420 size_t off = static_cast<size_t>( i ) * recSize;
421
422 if( off + recSize > entry->totalBytes )
423 break;
424
425 size_t base = entry->dataOffset + off;
426 std::string refDes = readFixedString( base + nameOff, 16 );
427
428 if( refDes.empty() || !std::isalnum( static_cast<unsigned char>( refDes[0] ) ) )
429 continue;
430
431 int32_t x = readI32( base + xOff );
432
433 // v0x2021 Y coordinate encoding is not yet solved. Use 0 as placeholder.
434 int32_t y = ( yOff >= 0 ) ? readI32( base + yOff ) : 0;
435 int32_t angleRaw = readI32( base + angleOff );
436
437 PART part;
438 part.name = refDes;
439 part.location.x = toBasicCoordX( x );
440 part.location.y = toBasicCoordY( y );
441 part.rotation = toBasicAngle( angleRaw );
442 part.bottom_layer = false;
443 part.units = "M";
444
445 m_parts.push_back( part );
446 }
447}
448
449
451{
452 // Part records can be embedded in sections other than section 22.
453 // Scan sections 19 (design_rules) and 21 (board_outline) for FEFF-delimited part records.
454 bool isOld = isOldFormat();
455 int nameOff = 0, xOff = 0, yOff = 0, angleOff = 0, feffOff = 0;
456
457 if( isOld )
458 {
459 nameOff = 76;
460 xOff = 92;
461 yOff = -1;
462 angleOff = 4;
463 feffOff = 28;
464 }
465 else
466 {
467 nameOff = 44;
468 xOff = 60;
469 yOff = 64;
470 angleOff = 68;
471 feffOff = 92;
472 }
473
474 int recSize = feffOff + 2;
475
476 std::set<std::string> existingRefs;
477
478 for( const auto& p : m_parts )
479 existingRefs.insert( p.name );
480
481 for( int secIdx : { 19, 21 } )
482 {
483 const DirEntry* entry = getSection( secIdx );
484
485 if( !entry || entry->totalBytes == 0 )
486 continue;
487
488 const uint8_t* data = sectionData( secIdx );
489 uint32_t size = sectionSize( secIdx );
490
491 if( !data || size == 0 )
492 continue;
493
494 for( size_t pos = 0; pos + 1 < size; ++pos )
495 {
496 if( data[pos] != 0xFE || data[pos + 1] != 0xFF )
497 continue;
498
499 int recStart = static_cast<int>( pos ) - feffOff;
500
501 if( recStart < 0 || recStart + recSize > static_cast<int>( size ) )
502 continue;
503
504 size_t base = entry->dataOffset + recStart;
505 std::string refDes = readFixedString( base + nameOff, 16 );
506
507 if( refDes.empty() || !std::isalnum( static_cast<unsigned char>( refDes[0] ) ) )
508 continue;
509
510 if( existingRefs.count( refDes ) )
511 continue;
512
513 int32_t x = readI32( base + xOff );
514 int32_t y = ( yOff >= 0 ) ? readI32( base + yOff ) : 0;
515 int32_t angleRaw = readI32( base + angleOff );
516
517 PART part;
518 part.name = refDes;
519 part.location.x = toBasicCoordX( x );
520 part.location.y = toBasicCoordY( y );
521 part.rotation = toBasicAngle( angleRaw );
522 part.bottom_layer = false;
523 part.units = "M";
524
525 m_parts.push_back( part );
526 existingRefs.insert( refDes );
527 }
528 }
529}
530
531
533{
534 const DirEntry* entry = getSection( 4 );
535
536 if( !entry || entry->count == 0 || entry->perItem == 0 )
537 return;
538
539 const uint8_t* data = sectionData( 4 );
540
541 if( !data )
542 return;
543
544 bool isNew = !isOldFormat();
545 uint32_t recSize = entry->perItem;
546
547 // We store pad stacks indexed by their position in section 4.
548 // Part decals reference these by index.
549 for( uint32_t i = 0; i < entry->count; ++i )
550 {
551 size_t off = static_cast<size_t>( i ) * recSize;
552
553 if( off + recSize > entry->totalBytes )
554 break;
555
556 size_t base = entry->dataOffset + off;
557
558 int32_t padWidth = 0, drill = 0, finLength = 0, angleRaw = 0;
559 uint8_t marker = 0, shapeCode = 0;
560 uint16_t layerCount = 0;
561
562 if( isNew )
563 {
564 padWidth = readI32( base + 28 );
565 drill = readI32( base + 32 );
566 finLength = readI32( base + 36 );
567 angleRaw = readI32( base + 48 );
568 marker = readU8( base + 56 );
569 shapeCode = readU8( base + 57 );
570 layerCount = readU16( base + 58 );
571 }
572 else
573 {
574 padWidth = readI32( base + 24 );
575 drill = readI32( base + 28 );
576 finLength = readI32( base + 32 );
577 angleRaw = readI32( base + 40 );
578 marker = readU8( base + 48 );
579 shapeCode = readU8( base + 49 );
580 layerCount = readU16( base + 50 );
581 }
582
583 // Only process valid pad definitions (marker == 0xFE)
584 if( marker != 0xFE )
585 continue;
586
587 std::string shapeName = "R";
588 auto shapeIt = PAD_SHAPE_NAMES.find( shapeCode );
589
590 if( shapeIt != PAD_SHAPE_NAMES.end() )
591 shapeName = shapeIt->second;
592
593 double angle = toBasicAngle( angleRaw );
594
595 // Build a PAD_STACK_LAYER for the default layer (layer 0)
596 PAD_STACK_LAYER psl;
597 psl.layer = 0;
598 psl.shape = shapeName;
599 psl.sizeA = static_cast<double>( padWidth );
600 psl.sizeB = static_cast<double>( padWidth );
601 psl.drill = static_cast<double>( drill );
602 psl.plated = ( drill > 0 );
603 psl.rotation = angle;
604 psl.finger_offset = static_cast<double>( finLength );
605
606 m_padStackCache[static_cast<int>( i )].push_back( psl );
607 }
608}
609
610
612{
613 const DirEntry* entry = getSection( 10 );
614
615 if( !entry || entry->count == 0 || entry->perItem == 0 )
616 return;
617
618 const uint8_t* data = sectionData( 10 );
619
620 if( !data )
621 return;
622
623 bool isNew = !isOldFormat();
624 uint32_t recSize = entry->perItem;
625
626 for( uint32_t i = 0; i < entry->count; ++i )
627 {
628 size_t off = static_cast<size_t>( i ) * recSize;
629
630 if( off + recSize > entry->totalBytes )
631 break;
632
633 size_t base = entry->dataOffset + off;
634
635 std::string name;
636 std::string units = "I";
637
638 if( isNew )
639 {
640 name = readFixedString( base + 44, 32 );
641 uint8_t unitFlag = readU8( base + 76 );
642 units = ( unitFlag == 0x4D ) ? "M" : "I";
643 }
644 else
645 {
646 name = readFixedString( base + 28, 32 );
647 }
648
649 if( name.empty() )
650 continue;
651
652 PART_DECAL decal;
653 decal.name = name;
654 decal.units = units;
655
656 // TODO: Parse terminal positions from the binary format.
657 // For now, create a minimal decal that the converter can reference.
658
659 m_decals[name] = decal;
660 }
661}
662
663
665{
666 // Section 17 links part type names to decal indices via 224-byte records.
667 // name@+156, decal_idx@+112. Old format stores UI data here instead.
668 if( isOldFormat() )
669 return;
670
671 const DirEntry* entry = getSection( 17 );
672
673 if( !entry || entry->count == 0 || entry->perItem < 188 )
674 return;
675
676 const uint8_t* data = sectionData( 17 );
677
678 if( !data )
679 return;
680
681 // Build index of section 10 decal names for resolving decal indices
682 const DirEntry* decalEntry = getSection( 10 );
683 std::map<uint32_t, std::string> decalIndexToName;
684
685 if( decalEntry && decalEntry->count > 0 && decalEntry->perItem > 0 )
686 {
687 for( uint32_t i = 0; i < decalEntry->count; ++i )
688 {
689 size_t dOff = static_cast<size_t>( i ) * decalEntry->perItem;
690
691 if( dOff + decalEntry->perItem > decalEntry->totalBytes )
692 break;
693
694 size_t dBase = decalEntry->dataOffset + dOff;
695 std::string decalName = readFixedString( dBase + 44, 32 );
696
697 if( !decalName.empty() )
698 decalIndexToName[i] = decalName;
699 }
700 }
701
702 uint32_t recSize = entry->perItem;
703
704 // Build a map from footprint type name to decal name
705 std::map<std::string, std::string> fpTypeToDecal;
706
707 for( uint32_t i = 0; i < entry->count; ++i )
708 {
709 size_t off = static_cast<size_t>( i ) * recSize;
710
711 if( off + recSize > entry->totalBytes )
712 break;
713
714 size_t base = entry->dataOffset + off;
715 uint32_t decalIdx = readU32( base + 112 );
716 std::string fpTypeName = readFixedString( base + 156, 32 );
717
718 if( fpTypeName.empty() )
719 continue;
720
721 auto decalIt = decalIndexToName.find( decalIdx );
722
723 if( decalIt != decalIndexToName.end() )
724 fpTypeToDecal[fpTypeName] = decalIt->second;
725 }
726
727 // Store the mapping for use by part placement linking.
728 // The metadata region maps ref-des -> part-type-name, which we
729 // don't parse yet. For now, store as a member for future use.
730 m_fpTypeToDecal = fpTypeToDecal;
731}
732
733
735{
736 const DirEntry* entry = getSection( 12 );
737
738 if( !entry || entry->count == 0 )
739 return;
740
741 const uint8_t* data = sectionData( 12 );
742
743 if( !data )
744 return;
745
746 m_lineVertices.clear();
747 m_lineVertices.reserve( entry->count );
748
749 for( uint32_t i = 0; i < entry->count; ++i )
750 {
751 size_t off = static_cast<size_t>( i ) * 12;
752
753 if( off + 12 > entry->totalBytes )
754 break;
755
756 size_t base = entry->dataOffset + off;
757
758 LineVertex v;
759 v.x = readI32( base );
760 v.y = readI32( base + 4 );
761 v.extra = readU32( base + 8 );
762
763 m_lineVertices.push_back( v );
764 }
765}
766
767
769{
770 // Section 21 format varies by version:
771 // v0x2026: 16-byte records [u32 vertex_count, u32 unk1, u32 unk2, u32 sentinel=0xFFFFFFFF]
772 // v0x2025: Mixed ASCII/binary records with completely different layout
773 // v0x2027: Stores coordinates directly rather than vertex counts
774 // v0x2021: Old format with embedded outline data
775 // Only v0x2026 has a decoded record layout, so restrict parsing to that version.
776 if( m_version != 0x2026 )
777 return;
778
779 if( m_lineVertices.empty() )
780 return;
781
782 const DirEntry* entry = getSection( 21 );
783
784 if( !entry || entry->count == 0 )
785 return;
786
787 const uint8_t* data = sectionData( 21 );
788
789 if( !data )
790 return;
791
792 size_t vertexIdx = 0;
793
794 for( uint32_t i = 0; i < entry->count; ++i )
795 {
796 size_t off = static_cast<size_t>( i ) * 16;
797
798 if( off + 16 > entry->totalBytes )
799 break;
800
801 size_t base = entry->dataOffset + off;
802 uint32_t vertexCount = readU32( base );
803 uint32_t sentinel = readU32( base + 12 );
804
805 if( sentinel != 0xFFFFFFFF )
806 continue;
807
808 if( vertexCount == 0 || vertexCount > 10000
809 || vertexIdx + vertexCount > m_lineVertices.size() )
810 {
811 continue;
812 }
813
814 POLYLINE outline;
815 outline.layer = 1;
816 outline.width = 0.0;
817 outline.closed = true;
818
819 for( uint32_t v = 0; v < vertexCount; ++v )
820 {
821 const LineVertex& lv = m_lineVertices[vertexIdx + v];
822 outline.points.emplace_back( static_cast<double>( lv.x ),
823 static_cast<double>( lv.y ) );
824 }
825
826 vertexIdx += vertexCount;
827
828 if( outline.points.size() >= 3 )
829 m_boardOutlines.push_back( std::move( outline ) );
830 }
831}
832
833
834std::string BINARY_PARSER::extractNetName( const uint8_t* aData, size_t aOffset ) const
835{
836 if( !aData )
837 return {};
838
839 std::string name = readFixedString( aOffset, 48 );
840
841 if( name.empty() )
842 return {};
843
844 return name;
845}
846
847
848bool BINARY_PARSER::isValidNetName( const std::string& aName ) const
849{
850 if( aName.empty() )
851 return false;
852
853 char first = aName[0];
854
855 return std::isalpha( static_cast<unsigned char>( first ) )
856 || std::isdigit( static_cast<unsigned char>( first ) )
857 || first == '+' || first == '~' || first == '_' || first == '/';
858}
859
860
862{
863 std::set<std::string> existing;
864
865 if( !isOldFormat() )
866 {
867 // New format: section 23 has 424-byte records with net index at +112 and name at +116
868 const DirEntry* entry23 = getSection( 23 );
869
870 if( entry23 && entry23->count > 0 && entry23->perItem == 424 )
871 {
872 for( uint32_t i = 0; i < entry23->count; ++i )
873 {
874 size_t off = static_cast<size_t>( i ) * 424;
875
876 if( off + 424 > entry23->totalBytes )
877 break;
878
879 size_t base = entry23->dataOffset + off;
880 std::string name = readFixedString( base + 116, 48 );
881
882 if( !name.empty() && isValidNetName( name ) && !existing.count( name ) )
883 {
884 NET net;
885 net.name = name;
886 m_nets.push_back( net );
887 existing.insert( name );
888 }
889 }
890 }
891
892 // Section 22 fills in power/ground nets from 112-byte records
893 const DirEntry* entry22 = getSection( 22 );
894
895 if( entry22 && entry22->count > 0 && entry22->perItem == 112 )
896 {
897 for( uint32_t i = 0; i < entry22->count; ++i )
898 {
899 size_t off = static_cast<size_t>( i ) * 112;
900
901 if( off + 112 > entry22->totalBytes )
902 break;
903
904 size_t base = entry22->dataOffset + off;
905
906 for( int nameOff : { 28, 52, 76 } )
907 {
908 std::string name = readFixedString( base + nameOff, 24 );
909
910 if( !name.empty() && isValidNetName( name ) && !existing.count( name ) )
911 {
912 uint32_t netIdx = readU32( base + nameOff - 4 );
913
914 if( netIdx < 100000 || netIdx >= 0xFFFF0000 )
915 {
916 NET net;
917 net.name = name;
918 m_nets.push_back( net );
919 existing.insert( name );
920 break;
921 }
922 }
923 }
924 }
925 }
926 }
927 else
928 {
929 // Old format: section 23 has 144-byte records
930 const DirEntry* entry23 = getSection( 23 );
931
932 if( entry23 && entry23->count > 0 && entry23->perItem == 144 )
933 {
934 for( uint32_t i = 0; i < entry23->count; ++i )
935 {
936 size_t off = static_cast<size_t>( i ) * 144;
937
938 if( off + 144 > entry23->totalBytes )
939 break;
940
941 size_t base = entry23->dataOffset + off;
942 uint32_t netIdx = readU32( base + 8 );
943 std::string name = readFixedString( base + 12, 48 );
944
945 if( !name.empty() && isValidNetName( name ) && netIdx < 100000
946 && !existing.count( name ) )
947 {
948 NET net;
949 net.name = name;
950 m_nets.push_back( net );
951 existing.insert( name );
952 }
953 }
954 }
955
956 // Old format: section 22 has 96-byte records
957 const DirEntry* entry22 = getSection( 22 );
958
959 if( entry22 && entry22->count > 0 && entry22->perItem == 96 )
960 {
961 for( uint32_t i = 0; i < entry22->count; ++i )
962 {
963 size_t off = static_cast<size_t>( i ) * 96;
964
965 if( off + 96 > entry22->totalBytes )
966 break;
967
968 size_t base = entry22->dataOffset + off;
969
970 for( int nameOff : { 12, 60 } )
971 {
972 std::string name = readFixedString( base + nameOff, 48 );
973
974 if( !name.empty() && isValidNetName( name ) && !existing.count( name ) )
975 {
976 uint32_t netIdx = readU32( base + nameOff - 4 );
977
978 if( netIdx < 100000 )
979 {
980 NET net;
981 net.name = name;
982 m_nets.push_back( net );
983 existing.insert( name );
984 break;
985 }
986 }
987 }
988 }
989 }
990
991 // Old format: section 19 (design rules) has some nets stored after 0xFFFFFFFF markers
992 const DirEntry* entry19 = getSection( 19 );
993
994 if( entry19 && entry19->count > 0 )
995 {
996 const uint8_t* sec19Data = sectionData( 19 );
997
998 if( sec19Data )
999 {
1000 size_t sec19Size = entry19->totalBytes;
1001
1002 for( size_t pos = 0; pos + 4 < sec19Size; ++pos )
1003 {
1004 uint32_t val = static_cast<uint32_t>( sec19Data[pos] )
1005 | ( static_cast<uint32_t>( sec19Data[pos + 1] ) << 8 )
1006 | ( static_cast<uint32_t>( sec19Data[pos + 2] ) << 16 )
1007 | ( static_cast<uint32_t>( sec19Data[pos + 3] ) << 24 );
1008
1009 if( val == 0xFFFFFFFF )
1010 {
1011 for( size_t scan = pos + 4; scan + 2 < sec19Size && scan < pos + 40; ++scan )
1012 {
1013 if( sec19Data[scan] != 0
1014 && std::isalpha( static_cast<unsigned char>( sec19Data[scan] ) ) )
1015 {
1016 std::string name = readFixedString(
1017 entry19->dataOffset + scan, 48 );
1018
1019 if( !name.empty() && isValidNetName( name )
1020 && !existing.count( name ) )
1021 {
1022 NET net;
1023 net.name = name;
1024 m_nets.push_back( net );
1025 existing.insert( name );
1026 }
1027
1028 break;
1029 }
1030 }
1031
1032 pos += 3;
1033 }
1034 }
1035 }
1036 }
1037 }
1038}
1039
1040
1042{
1043 // Origin is already read from section 1 in parseBoardSetup().
1044 // Only fall back to the DFT_CONFIGURATION scan if that didn't work.
1045 if( m_originFound )
1046 return;
1047
1048 size_t lastDataEnd = HEADER_SIZE + static_cast<size_t>( m_numDirEntries ) * DIR_ENTRY_SIZE;
1049
1050 for( const auto& entry : m_dirEntries )
1051 {
1052 if( entry.index > 0 && entry.totalBytes > 0 )
1053 {
1054 size_t end = entry.dataOffset + entry.totalBytes;
1055
1056 if( end > lastDataEnd )
1057 lastDataEnd = end;
1058 }
1059 }
1060
1061 size_t footerStart = m_data.size() - FOOTER_SIZE;
1062
1063 if( lastDataEnd >= footerStart )
1064 return;
1065
1066 size_t dirEnd = HEADER_SIZE + static_cast<size_t>( m_numDirEntries ) * DIR_ENTRY_SIZE;
1067 parseDftConfig( dirEnd, footerStart );
1068}
1069
1070
1071void BINARY_PARSER::parseDftConfig( size_t aStart, size_t aEnd )
1072{
1073 // Search for "DFT_CONFIGURATION\0" marker
1074 static const char DFT_MARKER[] = "DFT_CONFIGURATION";
1075 size_t markerLen = std::strlen( DFT_MARKER );
1076
1077 for( size_t pos = aStart; pos + markerLen + 1 < aEnd; ++pos )
1078 {
1079 if( std::memcmp( &m_data[pos], DFT_MARKER, markerLen ) == 0
1080 && m_data[pos + markerLen] == 0 )
1081 {
1082 size_t configStart = pos + markerLen + 1;
1083
1084 // Skip PARENT markers and null bytes
1085 while( configStart < aEnd )
1086 {
1087 if( m_data[configStart] == 0 )
1088 {
1089 ++configStart;
1090 continue;
1091 }
1092
1093 if( configStart + 7 <= aEnd
1094 && std::memcmp( &m_data[configStart], "PARENT\0", 7 ) == 0 )
1095 {
1096 configStart += 7;
1097 continue;
1098 }
1099
1100 break;
1101 }
1102
1103 if( configStart >= aEnd )
1104 return;
1105
1106 // Detect format by checking for '.' padding in the first 16 bytes
1107 std::map<std::string, std::string> config;
1108 bool hasDot = false;
1109
1110 if( configStart + 16 <= aEnd )
1111 {
1112 for( size_t i = configStart; i < configStart + 16; ++i )
1113 {
1114 if( m_data[i] == '.' )
1115 {
1116 hasDot = true;
1117 break;
1118 }
1119 }
1120 }
1121
1122 if( hasDot )
1123 config = parseDftDotPadded( configStart, aEnd );
1124 else
1125 config = parseDftNullSeparated( configStart, aEnd );
1126
1127 auto xIt = config.find( "X" );
1128 auto yIt = config.find( "Y" );
1129
1130 if( xIt != config.end() && yIt != config.end() )
1131 {
1132 try
1133 {
1134 m_originX = static_cast<int32_t>( std::stod( xIt->second ) );
1135 m_originY = static_cast<int32_t>( std::stod( yIt->second ) );
1136 m_originFound = true;
1137
1138 m_parameters.origin.x = static_cast<double>( m_originX );
1139 m_parameters.origin.y = static_cast<double>( m_originY );
1140 }
1141 catch( ... )
1142 {
1143 wxLogTrace( "PADS", "Failed to parse DFT origin values" );
1144 }
1145 }
1146
1147 return;
1148 }
1149 }
1150}
1151
1152
1153std::map<std::string, std::string>
1154BINARY_PARSER::parseDftDotPadded( size_t aPos, size_t aEnd ) const
1155{
1156 std::map<std::string, std::string> config;
1157
1158 while( aPos + 16 <= aEnd )
1159 {
1160 // Keys are 16-byte fields padded with ASCII '.' (0x2E)
1161 bool validKey = true;
1162
1163 for( size_t i = aPos; i < aPos + 16; ++i )
1164 {
1165 uint8_t b = m_data[i];
1166
1167 if( !( ( b >= 0x20 && b <= 0x7E ) || b == 0x00 ) )
1168 {
1169 validKey = false;
1170 break;
1171 }
1172 }
1173
1174 if( !validKey )
1175 break;
1176
1177 // Extract key by stripping null bytes and dot padding
1178 std::string key;
1179
1180 for( size_t i = aPos; i < aPos + 16; ++i )
1181 {
1182 if( m_data[i] == 0 || m_data[i] == '.' )
1183 break;
1184
1185 key += static_cast<char>( m_data[i] );
1186 }
1187
1188 if( key.empty() )
1189 break;
1190
1191 aPos += 16;
1192
1193 // Skip optional null separator
1194 if( aPos < aEnd && m_data[aPos] == 0 )
1195 ++aPos;
1196
1197 // Read null-terminated value
1198 size_t valStart = aPos;
1199
1200 while( aPos < aEnd && m_data[aPos] != 0 )
1201 ++aPos;
1202
1203 if( aPos > valStart )
1204 {
1205 std::string value( reinterpret_cast<const char*>( &m_data[valStart] ),
1206 aPos - valStart );
1207 config[key] = value;
1208 }
1209
1210 if( aPos < aEnd )
1211 ++aPos;
1212
1213 // Skip PARENT markers
1214 if( aPos + 7 <= aEnd
1215 && std::memcmp( &m_data[aPos], "PARENT\0", 7 ) == 0 )
1216 {
1217 aPos += 7;
1218 }
1219 }
1220
1221 return config;
1222}
1223
1224
1225std::map<std::string, std::string>
1226BINARY_PARSER::parseDftNullSeparated( size_t aPos, size_t aEnd ) const
1227{
1228 std::map<std::string, std::string> config;
1229
1230 while( aPos < aEnd )
1231 {
1232 // Find null-terminated key
1233 size_t keyStart = aPos;
1234
1235 while( aPos < aEnd && m_data[aPos] != 0 )
1236 ++aPos;
1237
1238 if( aPos == keyStart )
1239 break;
1240
1241 // Validate key is printable ASCII
1242 bool validKey = true;
1243
1244 for( size_t i = keyStart; i < aPos; ++i )
1245 {
1246 if( m_data[i] < 0x20 || m_data[i] > 0x7E )
1247 {
1248 validKey = false;
1249 break;
1250 }
1251 }
1252
1253 if( !validKey )
1254 break;
1255
1256 std::string key( reinterpret_cast<const char*>( &m_data[keyStart] ), aPos - keyStart );
1257
1258 // Skip null terminator
1259 if( aPos < aEnd )
1260 ++aPos;
1261
1262 if( key == "PARENT" )
1263 continue;
1264
1265 // Read null-terminated value
1266 size_t valStart = aPos;
1267
1268 while( aPos < aEnd && m_data[aPos] != 0 )
1269 ++aPos;
1270
1271 if( aPos <= valStart )
1272 break;
1273
1274 std::string value( reinterpret_cast<const char*>( &m_data[valStart] ), aPos - valStart );
1275 config[key] = value;
1276
1277 if( aPos < aEnd )
1278 ++aPos;
1279 }
1280
1281 return config;
1282}
1283
1284
1285std::string BINARY_PARSER::resolveString( uint32_t aByteOffset ) const
1286{
1287 if( m_stringPoolBytes.empty() || aByteOffset >= m_stringPoolBytes.size() )
1288 return {};
1289
1290 const uint8_t* start = &m_stringPoolBytes[aByteOffset];
1291 const uint8_t* end = m_stringPoolBytes.data() + m_stringPoolBytes.size();
1292 const uint8_t* null_pos = std::find( start, end, 0 );
1293 size_t len = static_cast<size_t>( null_pos - start );
1294
1295 for( size_t i = 0; i < len; ++i )
1296 {
1297 if( start[i] < 0x20 || start[i] >= 0x7F )
1298 return {};
1299 }
1300
1301 return std::string( reinterpret_cast<const char*>( start ), len );
1302}
1303
1304
1306{
1307 // Section 8 contains text records (72 bytes each, all versions)
1308 // Field offsets (from binary-ASC cross-reference):
1309 // [0..3] u32 string pool byte offset (into section 57)
1310 // [28..31] i32 text height (confirmed via ASC height matching)
1311 // [32..35] i32 linewidth
1312 // [44..47] i32 X coordinate (confirmed via ASC coordinate matching)
1313 // [48..51] i32 Y coordinate
1314 // [52..55] i32 rotation angle (degrees * ANGLE_SCALE)
1315 // [56] u8 layer number
1316 // [68..69] u16 terminator (0xFEFF)
1317 const DirEntry* entry = getSection( 8 );
1318
1319 if( !entry || entry->count == 0 || entry->perItem < 72 )
1320 return;
1321
1322 const uint8_t* data = sectionData( 8 );
1323
1324 if( !data )
1325 return;
1326
1327 for( uint32_t i = 0; i < entry->count; ++i )
1328 {
1329 size_t off = static_cast<size_t>( i ) * 72;
1330
1331 if( off + 72 > entry->totalBytes )
1332 break;
1333
1334 size_t base = entry->dataOffset + off;
1335
1336 uint32_t strOffset = readU32( base );
1337 int32_t height = readI32( base + 28 );
1338 int32_t linewidth = readI32( base + 32 );
1339 int32_t x = readI32( base + 44 );
1340 int32_t y = readI32( base + 48 );
1341 int32_t angleRaw = readI32( base + 52 );
1342 uint8_t layer = readU8( base + 56 );
1343
1344 std::string content = resolveString( strOffset );
1345
1346 if( content.empty() )
1347 continue;
1348
1349 TEXT text;
1350 text.content = content;
1351 text.location.x = toBasicCoordX( x );
1352 text.location.y = toBasicCoordY( y );
1353 text.height = static_cast<double>( height );
1354 text.width = static_cast<double>( linewidth );
1355 text.layer = static_cast<int>( layer );
1356 text.rotation = toBasicAngle( angleRaw );
1357
1358 m_texts.push_back( text );
1359 }
1360}
1361
1362
1364{
1365 // Route data structure (new format only, v0x2025+):
1366 // Section 24 (68 bytes/record): connection records. Records with sentinel
1367 // 0xFE000000 at u32@20 are track segments. u32@8 and u32@12 are indices
1368 // into the section 60 vertex pool (start/end of each segment).
1369 // Section 59 (32 bytes/record): via/pin connection endpoints with XY at
1370 // the auto-detected marker offset (same layout as section 60 sub-records).
1371 // Section 60 (64 bytes/record): route vertex pool. Each record contains
1372 // two 32-byte sub-records with a 0x80 marker byte preceding XY data.
1373 // The marker position varies between files and is auto-detected.
1374 if( isOldFormat() )
1375 return;
1376
1377 m_routeSegments.clear();
1378 m_viaLocations.clear();
1379
1380 // Section 60 vertex pool (primary route vertices).
1381 // Each 64-byte record contains two 32-byte sub-records. Within each sub-record,
1382 // a 0x80 marker byte precedes the XY coordinate pair (two i32 values). The marker
1383 // position varies between files even of the same version, so we auto-detect it by
1384 // scanning the first few records for the most common 0x80 position.
1385 const DirEntry* entry60 = getSection( 60 );
1386
1387 if( !entry60 || entry60->count == 0 || entry60->perItem == 0 || !sectionData( 60 ) )
1388 return;
1389
1390 uint32_t n60 = entry60->count;
1391 uint32_t r60 = entry60->perItem;
1392
1393 // Auto-detect the 0x80 marker position by scanning the first 32 bytes of up to
1394 // 100 records and picking the position with the highest hit count.
1395 int markerOffset = -1;
1396 {
1397 uint32_t sampleCount = std::min( n60, static_cast<uint32_t>( 100 ) );
1398 int bestPos = -1;
1399 int bestCount = 0;
1400
1401 for( int candidate = 8; candidate < 28 && candidate + 8 < static_cast<int>( r60 ); ++candidate )
1402 {
1403 int hits = 0;
1404
1405 for( uint32_t s = 0; s < sampleCount; ++s )
1406 {
1407 size_t recOff = static_cast<size_t>( s ) * r60;
1408
1409 if( recOff + r60 > entry60->totalBytes )
1410 break;
1411
1412 if( readU8( entry60->dataOffset + recOff + candidate ) == 0x80 )
1413 hits++;
1414 }
1415
1416 if( hits > bestCount )
1417 {
1418 bestCount = hits;
1419 bestPos = candidate;
1420 }
1421 }
1422
1423 if( bestCount < static_cast<int>( sampleCount ) / 2 )
1424 return;
1425
1426 markerOffset = bestPos;
1427 }
1428
1429 int xyOffset = markerOffset + 1;
1430
1431 // Helper to read XY from a section 60 record
1432 auto readSec60XY = [&]( uint32_t aRecIdx, int32_t& aX, int32_t& aY ) -> bool
1433 {
1434 if( aRecIdx >= n60 )
1435 return false;
1436
1437 size_t off = static_cast<size_t>( aRecIdx ) * r60;
1438
1439 if( off + r60 > entry60->totalBytes )
1440 return false;
1441
1442 size_t base = entry60->dataOffset + off;
1443
1444 if( readU8( base + markerOffset ) != 0x80 )
1445 return false;
1446
1447 aX = readI32( base + xyOffset );
1448 aY = readI32( base + xyOffset + 4 );
1449 return true;
1450 };
1451
1452 // Section 24 connection records: build route segments by following the linking
1453 static constexpr uint32_t SEC24_SENTINEL = 0xFE000000;
1454 static constexpr int SEC24_REC_SIZE = 68;
1455
1456 const DirEntry* entry24 = getSection( 24 );
1457
1458 if( entry24 && entry24->count > 0 && entry24->perItem == SEC24_REC_SIZE && sectionData( 24 ) )
1459 {
1460 uint32_t n24 = entry24->count;
1461
1462 for( uint32_t i = 0; i < n24; ++i )
1463 {
1464 size_t off = static_cast<size_t>( i ) * SEC24_REC_SIZE;
1465
1466 if( off + SEC24_REC_SIZE > entry24->totalBytes )
1467 break;
1468
1469 size_t base = entry24->dataOffset + off;
1470 uint32_t sentinel = readU32( base + 20 );
1471
1472 if( sentinel != SEC24_SENTINEL )
1473 continue;
1474
1475 int32_t sec60Start = readI32( base + 8 );
1476 int32_t sec60End = readI32( base + 12 );
1477
1478 if( sec60Start < 0 || sec60End < 0 )
1479 continue;
1480
1481 int32_t x1 = 0, y1 = 0, x2 = 0, y2 = 0;
1482
1483 if( !readSec60XY( static_cast<uint32_t>( sec60Start ), x1, y1 ) )
1484 continue;
1485
1486 if( !readSec60XY( static_cast<uint32_t>( sec60End ), x2, y2 ) )
1487 continue;
1488
1489 // Width is at u32@24 in section 24 for v0x2025.
1490 // For other versions it's been observed as 0, so we leave it unset.
1491 int32_t width = readI32( base + 24 );
1492
1493 RouteSegment seg;
1494 seg.x1 = x1;
1495 seg.y1 = y1;
1496 seg.x2 = x2;
1497 seg.y2 = y2;
1498 seg.width = width;
1499 m_routeSegments.push_back( seg );
1500 }
1501 }
1502
1503 // Section 59: via/pin connection endpoints
1504 const DirEntry* entry59 = getSection( 59 );
1505
1506 if( entry59 && entry59->count > 0 && entry59->perItem > 0 && sectionData( 59 ) )
1507 {
1508 uint32_t recSize = entry59->perItem;
1509
1510 for( uint32_t i = 0; i < entry59->count; ++i )
1511 {
1512 size_t off = static_cast<size_t>( i ) * recSize;
1513
1514 if( off + recSize > entry59->totalBytes )
1515 break;
1516
1517 size_t base = entry59->dataOffset + off;
1518
1519 if( readU8( base + markerOffset ) != 0x80 )
1520 continue;
1521
1523 via.x = readI32( base + xyOffset );
1524 via.y = readI32( base + xyOffset + 4 );
1525 m_viaLocations.push_back( via );
1526 }
1527 }
1528
1529 // Build ROUTE objects from the extracted segments.
1530 // Without layer/net/width fully decoded, we emit a single anonymous route with
1531 // all segments. The loadTracksAndVias() converter assigns default layer/width.
1532 if( m_routeSegments.empty() && m_viaLocations.empty() )
1533 return;
1534
1535 ROUTE route;
1536 route.net_name = "";
1537
1538 for( const auto& seg : m_routeSegments )
1539 {
1540 TRACK track;
1541 track.layer = 0;
1542 track.width = static_cast<double>( seg.width );
1543 track.points.emplace_back( static_cast<double>( seg.x1 ), static_cast<double>( seg.y1 ) );
1544 track.points.emplace_back( static_cast<double>( seg.x2 ), static_cast<double>( seg.y2 ) );
1545 route.tracks.push_back( std::move( track ) );
1546 }
1547
1548 for( const auto& via : m_viaLocations )
1549 {
1550 VIA viaDef;
1551 viaDef.location.x = static_cast<double>( via.x );
1552 viaDef.location.y = static_cast<double>( via.y );
1553 route.vias.push_back( std::move( viaDef ) );
1554 }
1555
1556 m_routes.push_back( std::move( route ) );
1557}
1558
1559
1561{
1562 // Section 14 was originally labeled "copper_pours" but analysis revealed it
1563 // stores footprint pad position data (36-byte stride entries with XY pairs
1564 // relative to the footprint origin). Copper pour geometry is stored in the
1565 // metadata region, not in a numbered section.
1566 //
1567 // The pad position data supplements section 10 (partdecals) but is not yet
1568 // needed since the converter creates placeholder footprints without pads.
1569}
1570
1571
1572std::vector<LAYER_INFO> BINARY_PARSER::GetLayerInfos() const
1573{
1574 std::vector<LAYER_INFO> infos;
1575 int layerCount = m_parameters.layer_count;
1576
1577 for( int i = 1; i <= layerCount; ++i )
1578 {
1580 info.number = i;
1581 info.name = "Layer " + std::to_string( i );
1582 info.is_copper = true;
1583 info.required = true;
1584
1586
1587 infos.push_back( info );
1588 }
1589
1590 return infos;
1591}
1592
1593} // namespace PADS_IO
const char * name
std::map< std::string, std::string > parseDftNullSeparated(size_t aPos, size_t aEnd) const
uint8_t readU8(size_t aOffset) const
double toBasicCoordY(int32_t aRawValue) const
std::vector< uint8_t > m_stringPoolBytes
std::vector< NET > m_nets
std::string readFixedString(size_t aOffset, size_t aMaxLen) const
bool isValidNetName(const std::string &aName) const
static constexpr int FOOTER_SIZE
std::vector< PART > m_parts
uint32_t readU32(size_t aOffset) const
std::map< std::string, std::string > parseDftDotPadded(size_t aPos, size_t aEnd) const
std::map< int, std::vector< PAD_STACK_LAYER > > m_padStackCache
const DirEntry * getSection(int aIndex) const
static constexpr int32_t ANGLE_SCALE
static bool IsBinaryPadsFile(const wxString &aFileName)
Check if a file appears to be a PADS binary PCB file.
std::map< std::string, PART_DECAL > m_decals
static constexpr int HEADER_SIZE
std::vector< uint8_t > m_data
std::vector< POLYLINE > m_boardOutlines
std::string resolveString(uint32_t aByteOffset) const
uint16_t readU16(size_t aOffset) const
uint32_t sectionSize(int aIndex) const
void parseDftConfig(size_t aStart, size_t aEnd)
std::string extractNetName(const uint8_t *aData, size_t aOffset) const
void Parse(const wxString &aFileName)
double toBasicCoordX(int32_t aRawValue) const
const uint8_t * sectionData(int aIndex) const
static constexpr int DIR_ENTRY_SIZE
std::vector< RouteSegment > m_routeSegments
std::map< std::string, std::string > m_fpTypeToDecal
std::vector< ViaLocation > m_viaLocations
std::vector< DirEntry > m_dirEntries
std::vector< ROUTE > m_routes
std::vector< LineVertex > m_lineVertices
std::vector< TEXT > m_texts
int32_t readI32(size_t aOffset) const
double toBasicAngle(int32_t aRawAngle) const
std::vector< LAYER_INFO > GetLayerInfos() const
#define THROW_IO_ERROR(msg)
macro which captures the "call site" values of FILE_, __FUNCTION & LINE
static const uint8_t FOOTER_GUID[]
static const std::map< uint8_t, std::string > PAD_SHAPE_NAMES
@ ROUTING
Copper routing layer.
std::string name
double drill
Drill hole diameter (0 for SMD)
std::string shape
Shape code: R, S, A, O, OF, RF, RT, ST, RA, SA, RC, OC.
bool plated
True if drill is plated (PTH vs NPTH)
double rotation
Pad rotation angle in degrees.
double finger_offset
Finger pad offset along orientation axis.
double sizeB
Secondary size (height for rectangles/ovals)
double sizeA
Primary size (diameter or width)
std::string name
std::string units
A polyline that may contain arc segments.
bool closed
True if polyline forms a closed shape.
std::vector< ARC_POINT > points
Polyline vertices, may include arcs.
std::vector< VIA > vias
std::vector< TRACK > tracks
std::string net_name
std::vector< ARC_POINT > points
Track points, may include arc segments.
VECTOR3I expected(15, 30, 45)
std::vector< std::string > header
table push_back({ "Source", "Layer", "Vertices", "Strategy", "Build(us)", "ns/query", "Mquery/s", "Inside" })
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.