altium-toolkit 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +191 -45
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +466 -28
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +58 -0
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +257 -5
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +29 -0
- package/src/styles/altium-renderers.css +19 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionIndex.mjs +9 -4
- package/src/ui/PcbScene3dBuilder.mjs +137 -3
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1187 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -56,6 +56,12 @@ export class PcbViaStackParser {
|
|
|
56
56
|
|
|
57
57
|
static #DRILL_LAYER_PAIR_TYPE_OFFSET = 317
|
|
58
58
|
|
|
59
|
+
static #PROPAGATION_DELAY_OFFSET = 321
|
|
60
|
+
|
|
61
|
+
static #HOLE_TOLERANCE_UNSET = 0x7fffffff
|
|
62
|
+
|
|
63
|
+
static #PICOSECONDS_PER_SECOND = 1_000_000_000_000
|
|
64
|
+
|
|
59
65
|
static #PHYSICAL_LAYER_COUNT = 32
|
|
60
66
|
|
|
61
67
|
/**
|
|
@@ -417,14 +423,21 @@ export class PcbViaStackParser {
|
|
|
417
423
|
PcbViaStackParser.#TAIL_SIGNATURE_OFFSET + offsetShift,
|
|
418
424
|
16
|
|
419
425
|
)
|
|
420
|
-
const positiveTolerance =
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
PcbViaStackParser.#
|
|
427
|
-
|
|
426
|
+
const positiveTolerance =
|
|
427
|
+
PcbViaStackParser.#readHoleToleranceIfAvailable(
|
|
428
|
+
view,
|
|
429
|
+
PcbViaStackParser.#POSITIVE_TOLERANCE_OFFSET + offsetShift
|
|
430
|
+
)
|
|
431
|
+
const negativeTolerance =
|
|
432
|
+
PcbViaStackParser.#readHoleToleranceIfAvailable(
|
|
433
|
+
view,
|
|
434
|
+
PcbViaStackParser.#NEGATIVE_TOLERANCE_OFFSET + offsetShift
|
|
435
|
+
)
|
|
436
|
+
const propagationDelayPs =
|
|
437
|
+
PcbViaStackParser.#readPropagationDelayIfAvailable(
|
|
438
|
+
view,
|
|
439
|
+
PcbViaStackParser.#PROPAGATION_DELAY_OFFSET + offsetShift
|
|
440
|
+
)
|
|
428
441
|
const drillLayerPairType = PcbViaStackParser.#readByteIfAvailable(
|
|
429
442
|
view,
|
|
430
443
|
PcbViaStackParser.#DRILL_LAYER_PAIR_TYPE_OFFSET + offsetShift
|
|
@@ -439,12 +452,20 @@ export class PcbViaStackParser {
|
|
|
439
452
|
if (tailSignature) {
|
|
440
453
|
result.tailSignature = tailSignature
|
|
441
454
|
}
|
|
442
|
-
if (positiveTolerance) {
|
|
455
|
+
if (positiveTolerance !== null) {
|
|
443
456
|
result.positiveTolerance = positiveTolerance
|
|
444
457
|
}
|
|
445
|
-
if (negativeTolerance) {
|
|
458
|
+
if (negativeTolerance !== null) {
|
|
446
459
|
result.negativeTolerance = negativeTolerance
|
|
447
460
|
}
|
|
461
|
+
PcbViaStackParser.#assignHoleTolerance(
|
|
462
|
+
result,
|
|
463
|
+
positiveTolerance,
|
|
464
|
+
negativeTolerance
|
|
465
|
+
)
|
|
466
|
+
if (propagationDelayPs !== null) {
|
|
467
|
+
result.propagationDelayPs = propagationDelayPs
|
|
468
|
+
}
|
|
448
469
|
if (drillLayerPairType) {
|
|
449
470
|
result.drillLayerPairType = drillLayerPairType
|
|
450
471
|
}
|
|
@@ -494,6 +515,73 @@ export class PcbViaStackParser {
|
|
|
494
515
|
return view.getInt32(offset, true) / 10000
|
|
495
516
|
}
|
|
496
517
|
|
|
518
|
+
/**
|
|
519
|
+
* Reads one optional hole tolerance and suppresses unset sentinel values.
|
|
520
|
+
* @param {DataView} view
|
|
521
|
+
* @param {number} offset
|
|
522
|
+
* @returns {number | null}
|
|
523
|
+
*/
|
|
524
|
+
static #readHoleToleranceIfAvailable(view, offset) {
|
|
525
|
+
if (!view || offset + 4 > view.byteLength) {
|
|
526
|
+
return null
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const rawValue = view.getInt32(offset, true)
|
|
530
|
+
if (
|
|
531
|
+
rawValue === 0 ||
|
|
532
|
+
rawValue === PcbViaStackParser.#HOLE_TOLERANCE_UNSET
|
|
533
|
+
) {
|
|
534
|
+
return null
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return rawValue / 10000
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Adds grouped semantic hole tolerance fields when tolerances are present.
|
|
542
|
+
* @param {Record<string, unknown>} result
|
|
543
|
+
* @param {number | null} positiveTolerance
|
|
544
|
+
* @param {number | null} negativeTolerance
|
|
545
|
+
*/
|
|
546
|
+
static #assignHoleTolerance(result, positiveTolerance, negativeTolerance) {
|
|
547
|
+
const holeTolerance = {}
|
|
548
|
+
|
|
549
|
+
if (positiveTolerance !== null) {
|
|
550
|
+
holeTolerance.positive = positiveTolerance
|
|
551
|
+
}
|
|
552
|
+
if (negativeTolerance !== null) {
|
|
553
|
+
holeTolerance.negative = negativeTolerance
|
|
554
|
+
}
|
|
555
|
+
if (Object.keys(holeTolerance).length) {
|
|
556
|
+
result.holeTolerance = holeTolerance
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Reads one optional via propagation delay stored as seconds.
|
|
562
|
+
* @param {DataView} view
|
|
563
|
+
* @param {number} offset
|
|
564
|
+
* @returns {number | null}
|
|
565
|
+
*/
|
|
566
|
+
static #readPropagationDelayIfAvailable(view, offset) {
|
|
567
|
+
if (!view || offset + 4 > view.byteLength) {
|
|
568
|
+
return null
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const seconds = view.getFloat32(offset, true)
|
|
572
|
+
const picoseconds = seconds * PcbViaStackParser.#PICOSECONDS_PER_SECOND
|
|
573
|
+
|
|
574
|
+
if (
|
|
575
|
+
!Number.isFinite(picoseconds) ||
|
|
576
|
+
Math.abs(picoseconds) < 0.001 ||
|
|
577
|
+
Math.abs(picoseconds) > 1_000_000
|
|
578
|
+
) {
|
|
579
|
+
return null
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return Number(picoseconds.toFixed(4))
|
|
583
|
+
}
|
|
584
|
+
|
|
497
585
|
/**
|
|
498
586
|
* Reads one byte when available.
|
|
499
587
|
* @param {DataView} view
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Decodes via-protection sidecar records and links them to via primitives.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbViaStructureParser {
|
|
11
|
+
static #STRUCTURE_STREAM_NAMES = [
|
|
12
|
+
'ViaStructures/Data',
|
|
13
|
+
'ViaStructureManager/Data'
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extracts via-protection structures and primitive links from stream data.
|
|
18
|
+
* @param {Map<string, Uint8Array>} streams
|
|
19
|
+
* @returns {{ structures: object[], links: object[], byPrimitiveIndex: Record<string, object> }}
|
|
20
|
+
*/
|
|
21
|
+
static extractFromStreams(streams) {
|
|
22
|
+
const structures = []
|
|
23
|
+
const links = []
|
|
24
|
+
|
|
25
|
+
for (const streamName of PcbViaStructureParser
|
|
26
|
+
.#STRUCTURE_STREAM_NAMES) {
|
|
27
|
+
const records = PcbViaStructureParser.#parseLengthPrefixedRecords(
|
|
28
|
+
streams.get(streamName),
|
|
29
|
+
streamName
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
for (const record of records) {
|
|
33
|
+
const structure =
|
|
34
|
+
PcbViaStructureParser.#parseStructureRecord(record)
|
|
35
|
+
const link = PcbViaStructureParser.#parseLinkRecord(record)
|
|
36
|
+
|
|
37
|
+
if (structure) {
|
|
38
|
+
structures.push(structure)
|
|
39
|
+
}
|
|
40
|
+
if (link) {
|
|
41
|
+
links.push(link)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return PcbViaStructureParser.#buildLookup(structures, links)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Adds via-protection metadata to parsed via primitives in place.
|
|
51
|
+
* @param {object[]} vias
|
|
52
|
+
* @param {{ byPrimitiveIndex?: Record<string, object> }} viaStructures
|
|
53
|
+
*/
|
|
54
|
+
static attachToVias(vias, viaStructures) {
|
|
55
|
+
if (!Array.isArray(vias) || !viaStructures?.byPrimitiveIndex) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index < vias.length; index += 1) {
|
|
60
|
+
const viaProtection = viaStructures.byPrimitiveIndex[String(index)]
|
|
61
|
+
if (!viaProtection) {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
vias[index].viaStructureIndex = viaProtection.viaStructureIndex
|
|
66
|
+
if (viaProtection.ipc4761Type !== undefined) {
|
|
67
|
+
vias[index].ipc4761Type = viaProtection.ipc4761Type
|
|
68
|
+
}
|
|
69
|
+
vias[index].viaProtection = {
|
|
70
|
+
ipc4761Type: viaProtection.ipc4761Type,
|
|
71
|
+
structureType: viaProtection.structureType,
|
|
72
|
+
features: viaProtection.features
|
|
73
|
+
}
|
|
74
|
+
vias[index].drill = {
|
|
75
|
+
holeKind: 'via',
|
|
76
|
+
plating:
|
|
77
|
+
vias[index].isPlated === false ? 'non-plated' : 'plated',
|
|
78
|
+
renderState:
|
|
79
|
+
PcbViaStructureParser.#resolveDrillRenderState(
|
|
80
|
+
viaProtection
|
|
81
|
+
),
|
|
82
|
+
ipc4761Type: viaProtection.ipc4761Type
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolves a display drill state from via-protection metadata.
|
|
89
|
+
* @param {{ ipc4761Type?: number | string, features?: object[] }} viaProtection Via-protection metadata.
|
|
90
|
+
* @returns {'open' | 'covered' | 'filled' | 'capped'}
|
|
91
|
+
*/
|
|
92
|
+
static #resolveDrillRenderState(viaProtection) {
|
|
93
|
+
const featureText = (viaProtection?.features || [])
|
|
94
|
+
.flatMap((feature) => [feature.type, feature.material])
|
|
95
|
+
.join(' ')
|
|
96
|
+
.toLowerCase()
|
|
97
|
+
|
|
98
|
+
if (/cap/u.test(featureText)) return 'capped'
|
|
99
|
+
if (/fill|plug/u.test(featureText)) return 'filled'
|
|
100
|
+
if (/cover|tent|mask/u.test(featureText)) return 'covered'
|
|
101
|
+
|
|
102
|
+
const ipcType = Number(viaProtection?.ipc4761Type)
|
|
103
|
+
if (ipcType === 6 || ipcType === 7) return 'capped'
|
|
104
|
+
if (ipcType === 3 || ipcType === 4 || ipcType === 5) return 'filled'
|
|
105
|
+
if (ipcType === 1 || ipcType === 2) return 'covered'
|
|
106
|
+
return 'open'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parses one sidecar stream into field records.
|
|
111
|
+
* @param {Uint8Array | undefined} dataBytes
|
|
112
|
+
* @param {string} sourceStream
|
|
113
|
+
* @returns {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }[]}
|
|
114
|
+
*/
|
|
115
|
+
static #parseLengthPrefixedRecords(dataBytes, sourceStream) {
|
|
116
|
+
const bytes = PcbViaStructureParser.#toUint8Array(dataBytes)
|
|
117
|
+
const records = []
|
|
118
|
+
let offset = 0
|
|
119
|
+
|
|
120
|
+
while (offset + 4 <= bytes.byteLength) {
|
|
121
|
+
const recordLength = PcbViaStructureParser.#readUint32(
|
|
122
|
+
bytes,
|
|
123
|
+
offset
|
|
124
|
+
)
|
|
125
|
+
offset += 4
|
|
126
|
+
|
|
127
|
+
if (recordLength <= 0 || offset + recordLength > bytes.byteLength) {
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const recordBytes = bytes.subarray(offset, offset + recordLength)
|
|
132
|
+
offset += recordLength
|
|
133
|
+
|
|
134
|
+
records.push({
|
|
135
|
+
fields: PcbViaStructureParser.#parseRecordFields(recordBytes),
|
|
136
|
+
sourceStream,
|
|
137
|
+
recordIndex: records.length
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return records
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parses one pipe-delimited sidecar record.
|
|
146
|
+
* @param {Uint8Array} bytes
|
|
147
|
+
* @returns {Record<string, string>}
|
|
148
|
+
*/
|
|
149
|
+
static #parseRecordFields(bytes) {
|
|
150
|
+
const text = PrintableTextDecoder.decodeBytes(bytes)
|
|
151
|
+
.replace(/\u0000/gu, '')
|
|
152
|
+
.trim()
|
|
153
|
+
const fields = {}
|
|
154
|
+
|
|
155
|
+
for (const segment of text.split('|')) {
|
|
156
|
+
const candidate = segment.trim()
|
|
157
|
+
const separatorIndex = candidate.indexOf('=')
|
|
158
|
+
if (separatorIndex <= 0) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const key = candidate.slice(0, separatorIndex).trim().toUpperCase()
|
|
163
|
+
if (!key) {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fields[key] = candidate.slice(separatorIndex + 1).trim()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return fields
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parses one via structure definition record.
|
|
175
|
+
* @param {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }} record
|
|
176
|
+
* @returns {object | null}
|
|
177
|
+
*/
|
|
178
|
+
static #parseStructureRecord(record) {
|
|
179
|
+
if (!('STRUCTURETYPE' in record.fields)) {
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const index =
|
|
184
|
+
PcbViaStructureParser.#parseInteger(
|
|
185
|
+
record.fields.VIASTRUCTUREINDEX
|
|
186
|
+
) ?? record.recordIndex
|
|
187
|
+
const structureType = PcbViaStructureParser.#parseIntegerOrString(
|
|
188
|
+
record.fields.STRUCTURETYPE
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
index,
|
|
193
|
+
ipc4761Type: structureType,
|
|
194
|
+
structureType,
|
|
195
|
+
sourceStream: record.sourceStream,
|
|
196
|
+
features: PcbViaStructureParser.#parseFeatures(record.fields)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Parses one primitive-to-structure link record.
|
|
202
|
+
* @param {{ fields: Record<string, string>, sourceStream: string }} record
|
|
203
|
+
* @returns {{ primitiveIndex: number, viaStructureIndex: number, sourceStream: string } | null}
|
|
204
|
+
*/
|
|
205
|
+
static #parseLinkRecord(record) {
|
|
206
|
+
const primitiveIndex = PcbViaStructureParser.#parseInteger(
|
|
207
|
+
record.fields.PRIMITIVEINDEX ?? record.fields.VIAINDEX
|
|
208
|
+
)
|
|
209
|
+
const viaStructureIndex = PcbViaStructureParser.#parseInteger(
|
|
210
|
+
record.fields.VIASTRUCTUREINDEX ?? record.fields.STRUCTUREINDEX
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if (primitiveIndex === null || viaStructureIndex === null) {
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
primitiveIndex,
|
|
219
|
+
viaStructureIndex,
|
|
220
|
+
sourceStream: record.sourceStream
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Parses repeated via-protection feature fields.
|
|
226
|
+
* @param {Record<string, string>} fields
|
|
227
|
+
* @returns {{ index: number, type: string, side: string, material: string }[]}
|
|
228
|
+
*/
|
|
229
|
+
static #parseFeatures(fields) {
|
|
230
|
+
const features = []
|
|
231
|
+
|
|
232
|
+
for (let index = 0; index < 16; index += 1) {
|
|
233
|
+
const type = fields[`FEATURETYPE${index}`]
|
|
234
|
+
const side = fields[`FEATURESIDE${index}`]
|
|
235
|
+
const material = fields[`FEATUREMATERIAL${index}`]
|
|
236
|
+
|
|
237
|
+
if (!type && !side && !material) {
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
features.push({
|
|
242
|
+
index,
|
|
243
|
+
type: type || '',
|
|
244
|
+
side: side || '',
|
|
245
|
+
material: material || ''
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return features
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Builds link lookups keyed by via primitive index.
|
|
254
|
+
* @param {object[]} structures
|
|
255
|
+
* @param {{ primitiveIndex: number, viaStructureIndex: number }[]} links
|
|
256
|
+
* @returns {{ structures: object[], links: object[], byPrimitiveIndex: Record<string, object> }}
|
|
257
|
+
*/
|
|
258
|
+
static #buildLookup(structures, links) {
|
|
259
|
+
const structuresByIndex = new Map(
|
|
260
|
+
structures.map((structure) => [structure.index, structure])
|
|
261
|
+
)
|
|
262
|
+
const byPrimitiveIndex = {}
|
|
263
|
+
|
|
264
|
+
for (const link of links) {
|
|
265
|
+
const structure = structuresByIndex.get(link.viaStructureIndex)
|
|
266
|
+
if (!structure) {
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
byPrimitiveIndex[String(link.primitiveIndex)] = {
|
|
271
|
+
viaStructureIndex: link.viaStructureIndex,
|
|
272
|
+
ipc4761Type: structure.ipc4761Type,
|
|
273
|
+
structureType: structure.structureType,
|
|
274
|
+
features: structure.features
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
structures,
|
|
280
|
+
links,
|
|
281
|
+
byPrimitiveIndex
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Parses a finite integer from a field value.
|
|
287
|
+
* @param {string | undefined} value
|
|
288
|
+
* @returns {number | null}
|
|
289
|
+
*/
|
|
290
|
+
static #parseInteger(value) {
|
|
291
|
+
const number = Number(value)
|
|
292
|
+
return Number.isInteger(number) ? number : null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parses a number when possible and otherwise preserves the string value.
|
|
297
|
+
* @param {string | undefined} value
|
|
298
|
+
* @returns {number | string}
|
|
299
|
+
*/
|
|
300
|
+
static #parseIntegerOrString(value) {
|
|
301
|
+
const parsed = PcbViaStructureParser.#parseInteger(value)
|
|
302
|
+
return parsed === null ? String(value || '') : parsed
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Reads one little-endian unsigned integer from a byte view.
|
|
307
|
+
* @param {Uint8Array} bytes
|
|
308
|
+
* @param {number} offset
|
|
309
|
+
* @returns {number}
|
|
310
|
+
*/
|
|
311
|
+
static #readUint32(bytes, offset) {
|
|
312
|
+
return new DataView(
|
|
313
|
+
bytes.buffer,
|
|
314
|
+
bytes.byteOffset + offset,
|
|
315
|
+
4
|
|
316
|
+
).getUint32(0, true)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Normalizes one byte-like input into a Uint8Array view.
|
|
321
|
+
* @param {Uint8Array | ArrayBuffer | undefined} bytes
|
|
322
|
+
* @returns {Uint8Array}
|
|
323
|
+
*/
|
|
324
|
+
static #toUint8Array(bytes) {
|
|
325
|
+
if (!bytes) {
|
|
326
|
+
return new Uint8Array(0)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (bytes instanceof Uint8Array) {
|
|
330
|
+
return bytes
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return new Uint8Array(bytes)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
* Extracts long printable runs from binary Altium documents.
|
|
7
7
|
*/
|
|
8
8
|
export class PrintableTextDecoder {
|
|
9
|
+
static #WINDOWS_1252_PRINTABLE_CONTROL_BYTES = new Set([
|
|
10
|
+
0x80, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c,
|
|
11
|
+
0x8e, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b,
|
|
12
|
+
0x9c, 0x9e, 0x9f
|
|
13
|
+
])
|
|
14
|
+
|
|
9
15
|
/**
|
|
10
16
|
* Returns printable ASCII-like runs from a binary buffer.
|
|
11
17
|
* @param {ArrayBuffer} arrayBuffer
|
|
@@ -67,8 +73,8 @@ export class PrintableTextDecoder {
|
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
/**
|
|
70
|
-
* Decodes one byte slice using UTF-8 first, then
|
|
71
|
-
* payloads such as legacy PCB library text.
|
|
76
|
+
* Decodes one byte slice using UTF-8 first, then Windows-1252 or GB18030
|
|
77
|
+
* for non-UTF-8 payloads such as legacy PCB library text.
|
|
72
78
|
* @param {Uint8Array} bytes
|
|
73
79
|
* @param {{ encoding?: string }} [options]
|
|
74
80
|
* @returns {string}
|
|
@@ -82,10 +88,34 @@ export class PrintableTextDecoder {
|
|
|
82
88
|
new TextDecoder('utf-8').decode(bytes)
|
|
83
89
|
)
|
|
84
90
|
}
|
|
91
|
+
if (
|
|
92
|
+
preferredEncoding === 'windows-1252' ||
|
|
93
|
+
preferredEncoding === 'cp1252'
|
|
94
|
+
) {
|
|
95
|
+
return (
|
|
96
|
+
PrintableTextDecoder.#tryDecode(bytes, 'windows-1252') ||
|
|
97
|
+
new TextDecoder('utf-8').decode(bytes)
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const utf8 = PrintableTextDecoder.#tryDecode(bytes, 'utf-8')
|
|
102
|
+
if (utf8 !== null) {
|
|
103
|
+
return utf8
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (PrintableTextDecoder.#hasWindows1252PreferredBytes(bytes)) {
|
|
107
|
+
const windows1252 = PrintableTextDecoder.#tryDecode(
|
|
108
|
+
bytes,
|
|
109
|
+
'windows-1252'
|
|
110
|
+
)
|
|
111
|
+
if (windows1252 !== null) {
|
|
112
|
+
return windows1252
|
|
113
|
+
}
|
|
114
|
+
}
|
|
85
115
|
|
|
86
116
|
return (
|
|
87
|
-
PrintableTextDecoder.#tryDecode(bytes, 'utf-8') ||
|
|
88
117
|
PrintableTextDecoder.#tryDecode(bytes, 'gb18030') ||
|
|
118
|
+
PrintableTextDecoder.#tryDecode(bytes, 'windows-1252') ||
|
|
89
119
|
new TextDecoder('utf-8').decode(bytes)
|
|
90
120
|
)
|
|
91
121
|
}
|
|
@@ -140,6 +170,26 @@ export class PrintableTextDecoder {
|
|
|
140
170
|
.trim()
|
|
141
171
|
}
|
|
142
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Returns true when bytes contain printable Windows-1252 control-range
|
|
175
|
+
* punctuation that can otherwise be misread as GB18030 pairs.
|
|
176
|
+
* @param {Uint8Array} bytes
|
|
177
|
+
* @returns {boolean}
|
|
178
|
+
*/
|
|
179
|
+
static #hasWindows1252PreferredBytes(bytes) {
|
|
180
|
+
for (const byte of bytes) {
|
|
181
|
+
if (
|
|
182
|
+
PrintableTextDecoder.#WINDOWS_1252_PRINTABLE_CONTROL_BYTES.has(
|
|
183
|
+
byte
|
|
184
|
+
)
|
|
185
|
+
) {
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
143
193
|
/**
|
|
144
194
|
* Tries one strict decode and returns null when bytes are invalid for it.
|
|
145
195
|
* @param {Uint8Array} bytes
|