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
|
@@ -8,8 +8,17 @@ import { PcbBoardRegionSemanticsParser } from './PcbBoardRegionSemanticsParser.m
|
|
|
8
8
|
import { PcbComponentAnnotationNormalizer } from './PcbComponentAnnotationNormalizer.mjs'
|
|
9
9
|
import { PcbComponentBodyPlacementNormalizer } from './PcbComponentBodyPlacementNormalizer.mjs'
|
|
10
10
|
import { PcbComponentPrimitiveIndexer } from './PcbComponentPrimitiveIndexer.mjs'
|
|
11
|
+
import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
|
|
12
|
+
import { PcbDimensionParser } from './PcbDimensionParser.mjs'
|
|
13
|
+
import { PcbMechanicalLayerPairParser } from './PcbMechanicalLayerPairParser.mjs'
|
|
14
|
+
import { PcbDefaultsParser } from './PcbDefaultsParser.mjs'
|
|
15
|
+
import { PcbMaskPasteResolver } from './PcbMaskPasteResolver.mjs'
|
|
11
16
|
import { PcbOutlineRecovery } from './PcbOutlineRecovery.mjs'
|
|
17
|
+
import { PcbOwnershipGraphBuilder } from './PcbOwnershipGraphBuilder.mjs'
|
|
18
|
+
import { PcbPickPlacePositionResolver } from './PcbPickPlacePositionResolver.mjs'
|
|
12
19
|
import { PcbRuleParser } from './PcbRuleParser.mjs'
|
|
20
|
+
import { PcbSpecialStringResolver } from './PcbSpecialStringResolver.mjs'
|
|
21
|
+
import { PcbStatisticsBuilder } from './PcbStatisticsBuilder.mjs'
|
|
13
22
|
import { ParserUtils } from './ParserUtils.mjs'
|
|
14
23
|
|
|
15
24
|
const {
|
|
@@ -33,7 +42,7 @@ export class PcbModelParser {
|
|
|
33
42
|
* resolved primitive netName fields keyed by numeric netIndex values.
|
|
34
43
|
* @param {string} fileName
|
|
35
44
|
* @param {{ raw: string, fields: Record<string, string | string[]>, sourceStream?: string }[]} records
|
|
36
|
-
* @param {{ streamNames: string[], binaryPrimitives: Record<string, object[]>, primitiveParameters?: object, diagnostics: { printableRecordCount: number, printableStreamCount: number, binaryPrimitiveCount: number } } | null} pcbExtraction
|
|
45
|
+
* @param {{ streamNames: string[], binaryPrimitives: Record<string, object[]>, primitiveParameters?: object, viaStructures?: object, customPadShapes?: object, extendedPrimitiveInformation?: object, unions?: object, diagnostics: { printableRecordCount: number, printableStreamCount: number, binaryPrimitiveCount: number } } | null} pcbExtraction
|
|
37
46
|
* @returns {{ schema: string, kind: 'pcb', fileType: 'PcbDoc', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], pcb: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
|
|
38
47
|
*/
|
|
39
48
|
static parse(fileName, records, pcbExtraction = null) {
|
|
@@ -93,11 +102,27 @@ export class PcbModelParser {
|
|
|
93
102
|
const primitiveLayers = AltiumLayoutParser.parsePrimitiveLayerNames(
|
|
94
103
|
boardRecords.map((record) => record.fields)
|
|
95
104
|
)
|
|
105
|
+
const mechanicalLayerPairs = PcbMechanicalLayerPairParser.parse(
|
|
106
|
+
boardRecords.map((record) => record.fields),
|
|
107
|
+
layers,
|
|
108
|
+
primitiveLayers
|
|
109
|
+
)
|
|
110
|
+
const layerFlipMetadata =
|
|
111
|
+
PcbMechanicalLayerPairParser.buildFlipMetadata(mechanicalLayerPairs)
|
|
96
112
|
const appearance3d = PcbModelParser.#parseAppearance3d(boardRecords)
|
|
97
113
|
const nets = PcbModelParser.#parseNetRecords(records)
|
|
98
114
|
const netNameByIndex = PcbModelParser.#buildNetNameMap(nets)
|
|
99
115
|
const classes = PcbModelParser.#parseClassRecords(records)
|
|
116
|
+
const differentialPairData = PcbModelParser.#buildDifferentialPairData(
|
|
117
|
+
PcbModelParser.#parseDifferentialPairRecords(records),
|
|
118
|
+
classes
|
|
119
|
+
)
|
|
100
120
|
const rules = PcbRuleParser.parse(records)
|
|
121
|
+
const defaults = PcbDefaultsParser.parse(
|
|
122
|
+
boardRecord?.fields || {},
|
|
123
|
+
'pcb-document'
|
|
124
|
+
)
|
|
125
|
+
const dimensions = PcbDimensionParser.parse(records)
|
|
101
126
|
const polygons = polygonRecords
|
|
102
127
|
.map((record) => ({
|
|
103
128
|
layer: getField(record.fields, 'LAYER') || 'UNKNOWN',
|
|
@@ -155,15 +180,50 @@ export class PcbModelParser {
|
|
|
155
180
|
)
|
|
156
181
|
? pcbExtraction.embeddedFonts.fonts
|
|
157
182
|
: []
|
|
183
|
+
const embeddedFiles = pcbExtraction?.embeddedFiles || {
|
|
184
|
+
schema: 'altium-toolkit.embedded-files.a1',
|
|
185
|
+
files: [],
|
|
186
|
+
diagnostics: []
|
|
187
|
+
}
|
|
188
|
+
const embeddedModelIntegrity = pcbExtraction?.embeddedModels
|
|
189
|
+
?.integrity || {
|
|
190
|
+
schema: 'altium-toolkit.pcb.embedded-model-integrity.a1',
|
|
191
|
+
issues: []
|
|
192
|
+
}
|
|
158
193
|
const rawRecords = Array.isArray(pcbExtraction?.rawRecords)
|
|
159
194
|
? pcbExtraction.rawRecords
|
|
160
195
|
: []
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
196
|
+
const viaStructures = pcbExtraction?.viaStructures || {
|
|
197
|
+
structures: [],
|
|
198
|
+
links: [],
|
|
199
|
+
byPrimitiveIndex: {}
|
|
200
|
+
}
|
|
201
|
+
const customPadShapes = pcbExtraction?.customPadShapes || {
|
|
202
|
+
entries: [],
|
|
203
|
+
byPrimitiveIndex: {}
|
|
204
|
+
}
|
|
205
|
+
const extendedPrimitiveInformation =
|
|
206
|
+
pcbExtraction?.extendedPrimitiveInformation || {
|
|
207
|
+
entries: [],
|
|
208
|
+
byPrimitiveIndex: {},
|
|
209
|
+
byPrimitiveKey: {}
|
|
210
|
+
}
|
|
211
|
+
const unions = pcbExtraction?.unions || {
|
|
212
|
+
userUnions: [],
|
|
213
|
+
smartUnions: [],
|
|
214
|
+
byIndex: {},
|
|
215
|
+
smartByIndex: {},
|
|
216
|
+
membersByPrimitiveKey: {}
|
|
217
|
+
}
|
|
218
|
+
const texts = PcbSpecialStringResolver.annotateTexts(
|
|
219
|
+
PcbModelParser.#annotateTextFontMetrics(
|
|
220
|
+
PcbComponentAnnotationNormalizer.normalizeTexts(
|
|
221
|
+
rawTextPrimitives,
|
|
222
|
+
rawComponentRecords
|
|
223
|
+
),
|
|
224
|
+
extractedEmbeddedFonts
|
|
165
225
|
),
|
|
166
|
-
|
|
226
|
+
pcbExtraction?.specialStringParameters || {}
|
|
167
227
|
)
|
|
168
228
|
const recoveredOutline = PcbOutlineRecovery.recoverOutline({
|
|
169
229
|
fallbackOutline: fallbackBoardOutline,
|
|
@@ -185,6 +245,11 @@ export class PcbModelParser {
|
|
|
185
245
|
texts,
|
|
186
246
|
components: componentRecords
|
|
187
247
|
})
|
|
248
|
+
PcbCustomPadShapeParser.attachToPads(
|
|
249
|
+
normalizedPcb.pads,
|
|
250
|
+
customPadShapes,
|
|
251
|
+
normalizedPcb
|
|
252
|
+
)
|
|
188
253
|
const boardRegionContexts =
|
|
189
254
|
PcbBoardRegionSemanticsParser.buildBoardRegionContexts(
|
|
190
255
|
normalizedPcb.boardRegions
|
|
@@ -207,6 +272,27 @@ export class PcbModelParser {
|
|
|
207
272
|
const componentPrimitives = PcbComponentPrimitiveIndexer.indexGroups(
|
|
208
273
|
componentPrimitiveGroups
|
|
209
274
|
)
|
|
275
|
+
const ownership = PcbOwnershipGraphBuilder.build({
|
|
276
|
+
...normalizedPcb,
|
|
277
|
+
nets
|
|
278
|
+
})
|
|
279
|
+
const pnp = PcbPickPlacePositionResolver.buildModel(
|
|
280
|
+
normalizedPcb.components,
|
|
281
|
+
componentPrimitiveGroups,
|
|
282
|
+
{ sourceComponents: componentRecords }
|
|
283
|
+
)
|
|
284
|
+
const statistics = PcbStatisticsBuilder.build({
|
|
285
|
+
...normalizedPcb,
|
|
286
|
+
layers,
|
|
287
|
+
primitiveLayers,
|
|
288
|
+
rules
|
|
289
|
+
})
|
|
290
|
+
const maskPaste = PcbMaskPasteResolver.build({
|
|
291
|
+
pads: normalizedPcb.pads,
|
|
292
|
+
vias: normalizedPcb.vias,
|
|
293
|
+
rules,
|
|
294
|
+
defaults
|
|
295
|
+
})
|
|
210
296
|
const bom = PcbModelParser.#groupBomRows(
|
|
211
297
|
componentRecords.map((component) => ({
|
|
212
298
|
designator: component.designator,
|
|
@@ -325,6 +411,38 @@ export class PcbModelParser {
|
|
|
325
411
|
})
|
|
326
412
|
}
|
|
327
413
|
|
|
414
|
+
if (embeddedFiles.files.length) {
|
|
415
|
+
diagnostics.push({
|
|
416
|
+
severity: 'info',
|
|
417
|
+
message:
|
|
418
|
+
'Inventoried ' +
|
|
419
|
+
embeddedFiles.files.length +
|
|
420
|
+
' generic embedded payload ' +
|
|
421
|
+
PcbModelParser.#plural(
|
|
422
|
+
embeddedFiles.files.length,
|
|
423
|
+
'stream',
|
|
424
|
+
'streams'
|
|
425
|
+
) +
|
|
426
|
+
'.'
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (const issue of embeddedModelIntegrity.issues || []) {
|
|
431
|
+
diagnostics.push({
|
|
432
|
+
severity: issue.severity === 'info' ? 'info' : 'warning',
|
|
433
|
+
code: issue.code,
|
|
434
|
+
message: issue.message
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const issue of embeddedFiles.diagnostics || []) {
|
|
439
|
+
diagnostics.push({
|
|
440
|
+
severity: issue.severity === 'info' ? 'info' : 'warning',
|
|
441
|
+
code: issue.code,
|
|
442
|
+
message: issue.message
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
|
|
328
446
|
if (rawRecords.length) {
|
|
329
447
|
diagnostics.push({
|
|
330
448
|
severity: 'info',
|
|
@@ -335,6 +453,54 @@ export class PcbModelParser {
|
|
|
335
453
|
})
|
|
336
454
|
}
|
|
337
455
|
|
|
456
|
+
if (viaStructures.structures?.length) {
|
|
457
|
+
diagnostics.push({
|
|
458
|
+
severity: 'info',
|
|
459
|
+
message:
|
|
460
|
+
'Recovered ' +
|
|
461
|
+
viaStructures.structures.length +
|
|
462
|
+
' PCB via protection structure ' +
|
|
463
|
+
PcbModelParser.#plural(
|
|
464
|
+
viaStructures.structures.length,
|
|
465
|
+
'definition',
|
|
466
|
+
'definitions'
|
|
467
|
+
) +
|
|
468
|
+
'.'
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (extendedPrimitiveInformation.entries?.length) {
|
|
473
|
+
diagnostics.push({
|
|
474
|
+
severity: 'info',
|
|
475
|
+
message:
|
|
476
|
+
'Recovered ' +
|
|
477
|
+
extendedPrimitiveInformation.entries.length +
|
|
478
|
+
' extended PCB primitive information records.'
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (customPadShapes.entries?.length) {
|
|
483
|
+
diagnostics.push({
|
|
484
|
+
severity: 'info',
|
|
485
|
+
message:
|
|
486
|
+
'Recovered ' +
|
|
487
|
+
customPadShapes.entries.length +
|
|
488
|
+
' custom pad shape sidecar records.'
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (unions.userUnions?.length || unions.smartUnions?.length) {
|
|
493
|
+
diagnostics.push({
|
|
494
|
+
severity: 'info',
|
|
495
|
+
message:
|
|
496
|
+
'Recovered ' +
|
|
497
|
+
unions.userUnions.length +
|
|
498
|
+
' user unions and ' +
|
|
499
|
+
unions.smartUnions.length +
|
|
500
|
+
' smart unions.'
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
|
|
338
504
|
if (recoveredOutline.source === 'board-route') {
|
|
339
505
|
diagnostics.push({
|
|
340
506
|
severity: 'info',
|
|
@@ -373,15 +539,30 @@ export class PcbModelParser {
|
|
|
373
539
|
bomRowCount: bom.length,
|
|
374
540
|
netCount: nets.length,
|
|
375
541
|
classCount: classes.length,
|
|
542
|
+
differentialPairCount:
|
|
543
|
+
differentialPairData.differentialPairs.length,
|
|
544
|
+
differentialPairClassCount:
|
|
545
|
+
differentialPairData.differentialPairClasses.length,
|
|
376
546
|
ruleCount: rules.length,
|
|
547
|
+
dimensionCount: dimensions.length,
|
|
548
|
+
mechanicalLayerPairCount: mechanicalLayerPairs.length,
|
|
377
549
|
polygonCount: polygons.length,
|
|
378
550
|
trackCount: tracks.length,
|
|
379
551
|
arcCount: arcs.length,
|
|
380
552
|
viaCount: vias.length,
|
|
553
|
+
viaStructureCount: viaStructures.structures?.length || 0,
|
|
554
|
+
extendedPrimitiveInformationCount:
|
|
555
|
+
extendedPrimitiveInformation.entries?.length || 0,
|
|
556
|
+
customPadShapeCount: customPadShapes.entries?.length || 0,
|
|
557
|
+
userUnionCount: unions.userUnions?.length || 0,
|
|
558
|
+
smartUnionCount: unions.smartUnions?.length || 0,
|
|
381
559
|
boardRegionCount: boardRegionSummary.boardRegionCount,
|
|
382
560
|
flexRegionCount: boardRegionSummary.flexRegionCount,
|
|
383
561
|
bendingLineCount: boardRegionSummary.bendingLineCount,
|
|
562
|
+
embeddedModelIssueCount:
|
|
563
|
+
embeddedModelIntegrity.issues?.length || 0,
|
|
384
564
|
embeddedFontCount: extractedEmbeddedFonts.length,
|
|
565
|
+
embeddedFileCount: embeddedFiles.files.length,
|
|
385
566
|
rawRecordCount: rawRecords.length,
|
|
386
567
|
boardWidthMil: Math.round(boardOutline.widthMil),
|
|
387
568
|
boardHeightMil: Math.round(boardOutline.heightMil)
|
|
@@ -391,30 +572,48 @@ export class PcbModelParser {
|
|
|
391
572
|
boardOutline: normalizedPcb.boardOutline,
|
|
392
573
|
layers,
|
|
393
574
|
layerSubstacks,
|
|
575
|
+
mechanicalLayerPairs,
|
|
576
|
+
layerFlipMetadata,
|
|
394
577
|
boardRegionContexts,
|
|
395
578
|
primitiveLayers,
|
|
396
579
|
appearance3d,
|
|
397
580
|
nets,
|
|
398
581
|
classes,
|
|
582
|
+
differentialPairs: differentialPairData.differentialPairs,
|
|
583
|
+
differentialPairClasses:
|
|
584
|
+
differentialPairData.differentialPairClasses,
|
|
399
585
|
rules,
|
|
586
|
+
...(defaults ? { defaults } : {}),
|
|
587
|
+
maskPaste,
|
|
588
|
+
dimensions,
|
|
400
589
|
components: normalizedPcb.components,
|
|
590
|
+
pickPlace: pnp,
|
|
401
591
|
polygons: normalizedPcb.polygons,
|
|
402
592
|
fills: normalizedPcb.fills,
|
|
403
593
|
tracks: normalizedPcb.tracks,
|
|
404
594
|
arcs: normalizedPcb.arcs,
|
|
405
595
|
vias: normalizedPcb.vias,
|
|
596
|
+
viaStructures,
|
|
597
|
+
extendedPrimitiveInformation,
|
|
598
|
+
customPadShapes,
|
|
599
|
+
unions,
|
|
406
600
|
pads: normalizedPcb.pads,
|
|
407
601
|
regions: normalizedPcb.regions,
|
|
408
602
|
shapeBasedRegions: normalizedPcb.shapeBasedRegions,
|
|
409
603
|
boardRegions: normalizedPcb.boardRegions,
|
|
410
604
|
texts: normalizedPcb.texts,
|
|
411
605
|
embeddedModels: extractedEmbeddedModels,
|
|
606
|
+
embeddedModelIntegrity,
|
|
412
607
|
embeddedFonts: extractedEmbeddedFonts,
|
|
608
|
+
embeddedFiles,
|
|
413
609
|
rawRecords,
|
|
414
610
|
componentBodies,
|
|
415
611
|
componentPrimitives,
|
|
416
|
-
componentPrimitiveGroups
|
|
612
|
+
componentPrimitiveGroups,
|
|
613
|
+
ownership,
|
|
614
|
+
statistics
|
|
417
615
|
},
|
|
616
|
+
pnp,
|
|
418
617
|
bom
|
|
419
618
|
})
|
|
420
619
|
}
|
|
@@ -444,33 +643,106 @@ export class PcbModelParser {
|
|
|
444
643
|
/**
|
|
445
644
|
* Normalizes component placement fields while preserving native index order.
|
|
446
645
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
447
|
-
* @returns {{ componentIndex: number, designator: string, uniqueId: string, x: number, y: number, layer: string, pattern: string, rotation: number, source: string, description: string, height: number | null, nameOn: boolean, commentOn: boolean }[]}
|
|
646
|
+
* @returns {{ componentIndex: number, designator: string, uniqueId: string, x: number, y: number, layer: string, pattern: string, rotation: number, source: string, description: string, height: number | null, provenance: object, nameOn: boolean, commentOn: boolean }[]}
|
|
448
647
|
*/
|
|
449
648
|
static #normalizeComponentRecords(records) {
|
|
450
649
|
return records
|
|
451
|
-
.map((record, index) =>
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
getField(record.fields, '
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
650
|
+
.map((record, index) => {
|
|
651
|
+
const provenance = PcbModelParser.#parseComponentProvenance(
|
|
652
|
+
record.fields
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
componentIndex: index,
|
|
657
|
+
designator: getField(record.fields, 'SOURCEDESIGNATOR'),
|
|
658
|
+
uniqueId:
|
|
659
|
+
getField(record.fields, 'UNIQUEID') ||
|
|
660
|
+
getField(record.fields, 'UID') ||
|
|
661
|
+
getField(record.fields, 'UNIQUEIDPRIMITIVEINFORMATION'),
|
|
662
|
+
x: parseNumericField(record.fields, 'X') || 0,
|
|
663
|
+
y: parseNumericField(record.fields, 'Y') || 0,
|
|
664
|
+
layer: getField(record.fields, 'LAYER') || 'TOP',
|
|
665
|
+
pattern: getField(record.fields, 'PATTERN'),
|
|
666
|
+
rotation: parseNumericField(record.fields, 'ROTATION') || 0,
|
|
667
|
+
source:
|
|
668
|
+
getField(record.fields, 'SOURCELIBREFERENCE') ||
|
|
669
|
+
getField(record.fields, 'SOURCEFOOTPRINTLIBRARY'),
|
|
670
|
+
description: getField(record.fields, 'SOURCEDESCRIPTION'),
|
|
671
|
+
height: parseNumericField(record.fields, 'HEIGHT'),
|
|
672
|
+
...(Object.keys(provenance).length ? { provenance } : {}),
|
|
673
|
+
nameOn: parseBoolean(record.fields.NAMEON),
|
|
674
|
+
commentOn: parseBoolean(record.fields.COMMENTON)
|
|
675
|
+
}
|
|
676
|
+
})
|
|
471
677
|
.filter((component) => component.pattern && component.designator)
|
|
472
678
|
}
|
|
473
679
|
|
|
680
|
+
/**
|
|
681
|
+
* Parses schematic/project provenance from one PCB component row.
|
|
682
|
+
* @param {Record<string, string | string[]>} fields
|
|
683
|
+
* @returns {Record<string, unknown>}
|
|
684
|
+
*/
|
|
685
|
+
static #parseComponentProvenance(fields) {
|
|
686
|
+
const sourceUniqueId = getField(fields, 'SOURCEUNIQUEID')
|
|
687
|
+
const sourceHierarchicalPath = getField(
|
|
688
|
+
fields,
|
|
689
|
+
'SOURCEHIERARCHICALPATH'
|
|
690
|
+
)
|
|
691
|
+
const sourceFootprintLibrary = getField(
|
|
692
|
+
fields,
|
|
693
|
+
'SOURCEFOOTPRINTLIBRARY'
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
const provenance = PcbModelParser.#stripEmptyObject({
|
|
697
|
+
channelOffset: parseNumericField(fields, 'CHANNELOFFSET'),
|
|
698
|
+
sourceDesignator: getField(fields, 'SOURCEDESIGNATOR'),
|
|
699
|
+
sourceUniqueId,
|
|
700
|
+
sourceUniqueIdSegments:
|
|
701
|
+
PcbModelParser.#splitAltiumPath(sourceUniqueId),
|
|
702
|
+
sourceHierarchicalPath,
|
|
703
|
+
sourceHierarchySegments: PcbModelParser.#splitAltiumPath(
|
|
704
|
+
sourceHierarchicalPath
|
|
705
|
+
),
|
|
706
|
+
sourceFootprintLibrary,
|
|
707
|
+
sourceFootprintLibraryName: PcbModelParser.#basenameFromAltiumPath(
|
|
708
|
+
sourceFootprintLibrary
|
|
709
|
+
),
|
|
710
|
+
sourceLibReference: getField(fields, 'SOURCELIBREFERENCE'),
|
|
711
|
+
sourceComponentLibrary: getField(fields, 'SOURCECOMPONENTLIBRARY'),
|
|
712
|
+
sourceComponentLibraryIdentifierKind: parseNumericField(
|
|
713
|
+
fields,
|
|
714
|
+
'SOURCECOMPLIBIDENTIFIERKIND'
|
|
715
|
+
),
|
|
716
|
+
sourceComponentLibraryIdentifier: getField(
|
|
717
|
+
fields,
|
|
718
|
+
'SOURCECOMPLIBRARYIDENTIFIER'
|
|
719
|
+
),
|
|
720
|
+
footprintDescription: getField(fields, 'FOOTPRINTDESCRIPTION'),
|
|
721
|
+
nameAutoPosition: parseNumericField(fields, 'NAMEAUTOPOSITION'),
|
|
722
|
+
commentAutoPosition: parseNumericField(
|
|
723
|
+
fields,
|
|
724
|
+
'COMMENTAUTOPOSITION'
|
|
725
|
+
),
|
|
726
|
+
lockStrings: PcbModelParser.#optionalBooleanField(
|
|
727
|
+
fields,
|
|
728
|
+
'LOCKSTRINGS'
|
|
729
|
+
),
|
|
730
|
+
enablePinSwapping: PcbModelParser.#optionalBooleanField(
|
|
731
|
+
fields,
|
|
732
|
+
'ENABLEPINSWAPPING'
|
|
733
|
+
),
|
|
734
|
+
enablePartSwapping: PcbModelParser.#optionalBooleanField(
|
|
735
|
+
fields,
|
|
736
|
+
'ENABLEPARTSWAPPING'
|
|
737
|
+
)
|
|
738
|
+
})
|
|
739
|
+
const nonRedundantKeys = Object.keys(provenance).filter(
|
|
740
|
+
(key) => !['sourceDesignator', 'sourceLibReference'].includes(key)
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
return nonRedundantKeys.length ? provenance : {}
|
|
744
|
+
}
|
|
745
|
+
|
|
474
746
|
/**
|
|
475
747
|
* Normalizes native Nets6/Data records in stream order.
|
|
476
748
|
* @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records
|
|
@@ -560,6 +832,172 @@ export class PcbModelParser {
|
|
|
560
832
|
return netNameByIndex
|
|
561
833
|
}
|
|
562
834
|
|
|
835
|
+
/**
|
|
836
|
+
* Parses native DifferentialPairs6/Data records in stream order.
|
|
837
|
+
* @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records
|
|
838
|
+
* @returns {{ pairIndex: number, name: string, positiveNetName: string, negativeNetName: string, netNames: string[], gatherControl: boolean, uniqueId: string }[]}
|
|
839
|
+
*/
|
|
840
|
+
static #parseDifferentialPairRecords(records) {
|
|
841
|
+
return records
|
|
842
|
+
.filter(
|
|
843
|
+
(record) => record.sourceStream === 'DifferentialPairs6/Data'
|
|
844
|
+
)
|
|
845
|
+
.map((record, index) => {
|
|
846
|
+
const positiveNetName = getField(
|
|
847
|
+
record.fields,
|
|
848
|
+
'POSITIVENETNAME'
|
|
849
|
+
)
|
|
850
|
+
const negativeNetName = getField(
|
|
851
|
+
record.fields,
|
|
852
|
+
'NEGATIVENETNAME'
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
pairIndex: index,
|
|
857
|
+
name: getField(record.fields, 'NAME'),
|
|
858
|
+
positiveNetName,
|
|
859
|
+
negativeNetName,
|
|
860
|
+
netNames: [positiveNetName, negativeNetName].filter(
|
|
861
|
+
Boolean
|
|
862
|
+
),
|
|
863
|
+
gatherControl: PcbModelParser.#parseBooleanField(
|
|
864
|
+
record.fields,
|
|
865
|
+
'GATHERCONTROL',
|
|
866
|
+
false
|
|
867
|
+
),
|
|
868
|
+
uniqueId:
|
|
869
|
+
getField(record.fields, 'UNIQUEID') ||
|
|
870
|
+
getField(record.fields, 'UID')
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
.filter(
|
|
874
|
+
(pair) =>
|
|
875
|
+
pair.name ||
|
|
876
|
+
pair.positiveNetName ||
|
|
877
|
+
pair.negativeNetName ||
|
|
878
|
+
pair.uniqueId
|
|
879
|
+
)
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Joins differential-pair class members to concrete pair records.
|
|
884
|
+
* @param {{ pairIndex: number, name: string, positiveNetName: string, negativeNetName: string, netNames: string[], gatherControl: boolean, uniqueId: string }[]} pairs
|
|
885
|
+
* @param {{ classIndex: number, name: string, kindName: string, members: string[] }[]} classes
|
|
886
|
+
* @returns {{ differentialPairs: object[], differentialPairClasses: object[] }}
|
|
887
|
+
*/
|
|
888
|
+
static #buildDifferentialPairData(pairs, classes) {
|
|
889
|
+
const pairsByName = new Map(
|
|
890
|
+
(pairs || []).map((pair) => [
|
|
891
|
+
PcbModelParser.#normalizeLookupName(pair.name),
|
|
892
|
+
pair
|
|
893
|
+
])
|
|
894
|
+
)
|
|
895
|
+
const classNamesByPair = new Map()
|
|
896
|
+
const differentialPairClasses = (classes || [])
|
|
897
|
+
.filter((classRecord) => classRecord.kindName === 'diff-pair')
|
|
898
|
+
.map((classRecord) => {
|
|
899
|
+
const pairNames = []
|
|
900
|
+
const unresolvedMembers = []
|
|
901
|
+
|
|
902
|
+
for (const member of classRecord.members || []) {
|
|
903
|
+
const pair = pairsByName.get(
|
|
904
|
+
PcbModelParser.#normalizeLookupName(member)
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
if (!pair) {
|
|
908
|
+
unresolvedMembers.push(member)
|
|
909
|
+
continue
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
pairNames.push(pair.name)
|
|
913
|
+
const classNames = classNamesByPair.get(pair.name) || []
|
|
914
|
+
classNames.push(classRecord.name)
|
|
915
|
+
classNamesByPair.set(pair.name, classNames)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
classIndex: classRecord.classIndex,
|
|
920
|
+
name: classRecord.name,
|
|
921
|
+
members: [...classRecord.members],
|
|
922
|
+
pairNames,
|
|
923
|
+
unresolvedMembers
|
|
924
|
+
}
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
return {
|
|
928
|
+
differentialPairs: (pairs || []).map((pair) => ({
|
|
929
|
+
...pair,
|
|
930
|
+
classNames: classNamesByPair.get(pair.name) || []
|
|
931
|
+
})),
|
|
932
|
+
differentialPairClasses
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Splits an authored hierarchy or unique-id path into stable segments.
|
|
938
|
+
* @param {string | undefined} value Path-like field value.
|
|
939
|
+
* @returns {string[]}
|
|
940
|
+
*/
|
|
941
|
+
static #splitAltiumPath(value) {
|
|
942
|
+
return String(value || '')
|
|
943
|
+
.split(/[\\/]+/u)
|
|
944
|
+
.map((segment) => segment.trim())
|
|
945
|
+
.filter(Boolean)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Returns the terminal segment from a native path-like field value.
|
|
950
|
+
* @param {string | undefined} value Path-like field value.
|
|
951
|
+
* @returns {string}
|
|
952
|
+
*/
|
|
953
|
+
static #basenameFromAltiumPath(value) {
|
|
954
|
+
const segments = PcbModelParser.#splitAltiumPath(value)
|
|
955
|
+
|
|
956
|
+
return segments.length ? segments[segments.length - 1] : ''
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Removes empty values while preserving explicit false and zero values.
|
|
961
|
+
* @param {Record<string, unknown>} value Object to normalize.
|
|
962
|
+
* @returns {Record<string, unknown>}
|
|
963
|
+
*/
|
|
964
|
+
static #stripEmptyObject(value) {
|
|
965
|
+
return Object.fromEntries(
|
|
966
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
967
|
+
if (Array.isArray(entryValue)) {
|
|
968
|
+
return entryValue.length > 0
|
|
969
|
+
}
|
|
970
|
+
if (typeof entryValue === 'string') {
|
|
971
|
+
return entryValue.length > 0
|
|
972
|
+
}
|
|
973
|
+
return entryValue !== null && entryValue !== undefined
|
|
974
|
+
})
|
|
975
|
+
)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Parses an optional boolean field without inventing a default value.
|
|
980
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
981
|
+
* @param {string} key Field name.
|
|
982
|
+
* @returns {boolean | null}
|
|
983
|
+
*/
|
|
984
|
+
static #optionalBooleanField(fields, key) {
|
|
985
|
+
const raw = getField(fields, key)
|
|
986
|
+
|
|
987
|
+
return raw ? parseBoolean(raw) : null
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Builds a case-insensitive lookup key for class and pair names.
|
|
992
|
+
* @param {string | undefined} value Raw lookup value.
|
|
993
|
+
* @returns {string}
|
|
994
|
+
*/
|
|
995
|
+
static #normalizeLookupName(value) {
|
|
996
|
+
return String(value || '')
|
|
997
|
+
.trim()
|
|
998
|
+
.toUpperCase()
|
|
999
|
+
}
|
|
1000
|
+
|
|
563
1001
|
/**
|
|
564
1002
|
* Extracts authored Altium 3D appearance colors from board metadata.
|
|
565
1003
|
* @param {{ fields: Record<string, string | string[]> }[]} boardRecords
|