altium-toolkit 1.0.8 → 1.0.10
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/ci_artifact_bundle_a1.schema.json +76 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -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 +196 -45
- package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
- 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/ParserCompatibilityFuzzer.mjs +192 -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 +495 -32
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +229 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- 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 +541 -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 +281 -7
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +503 -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/altium/SvgModelCrossLinkValidator.mjs +402 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +35 -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 +1252 -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,18 @@ 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'
|
|
19
|
+
import { PcbRouteAnalysisBuilder } from './PcbRouteAnalysisBuilder.mjs'
|
|
12
20
|
import { PcbRuleParser } from './PcbRuleParser.mjs'
|
|
21
|
+
import { PcbSpecialStringResolver } from './PcbSpecialStringResolver.mjs'
|
|
22
|
+
import { PcbStatisticsBuilder } from './PcbStatisticsBuilder.mjs'
|
|
13
23
|
import { ParserUtils } from './ParserUtils.mjs'
|
|
14
24
|
|
|
15
25
|
const {
|
|
@@ -33,7 +43,7 @@ export class PcbModelParser {
|
|
|
33
43
|
* resolved primitive netName fields keyed by numeric netIndex values.
|
|
34
44
|
* @param {string} fileName
|
|
35
45
|
* @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
|
|
46
|
+
* @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
47
|
* @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
48
|
*/
|
|
39
49
|
static parse(fileName, records, pcbExtraction = null) {
|
|
@@ -93,11 +103,27 @@ export class PcbModelParser {
|
|
|
93
103
|
const primitiveLayers = AltiumLayoutParser.parsePrimitiveLayerNames(
|
|
94
104
|
boardRecords.map((record) => record.fields)
|
|
95
105
|
)
|
|
106
|
+
const mechanicalLayerPairs = PcbMechanicalLayerPairParser.parse(
|
|
107
|
+
boardRecords.map((record) => record.fields),
|
|
108
|
+
layers,
|
|
109
|
+
primitiveLayers
|
|
110
|
+
)
|
|
111
|
+
const layerFlipMetadata =
|
|
112
|
+
PcbMechanicalLayerPairParser.buildFlipMetadata(mechanicalLayerPairs)
|
|
96
113
|
const appearance3d = PcbModelParser.#parseAppearance3d(boardRecords)
|
|
97
114
|
const nets = PcbModelParser.#parseNetRecords(records)
|
|
98
115
|
const netNameByIndex = PcbModelParser.#buildNetNameMap(nets)
|
|
99
116
|
const classes = PcbModelParser.#parseClassRecords(records)
|
|
117
|
+
const differentialPairData = PcbModelParser.#buildDifferentialPairData(
|
|
118
|
+
PcbModelParser.#parseDifferentialPairRecords(records),
|
|
119
|
+
classes
|
|
120
|
+
)
|
|
100
121
|
const rules = PcbRuleParser.parse(records)
|
|
122
|
+
const defaults = PcbDefaultsParser.parse(
|
|
123
|
+
boardRecord?.fields || {},
|
|
124
|
+
'pcb-document'
|
|
125
|
+
)
|
|
126
|
+
const dimensions = PcbDimensionParser.parse(records)
|
|
101
127
|
const polygons = polygonRecords
|
|
102
128
|
.map((record) => ({
|
|
103
129
|
layer: getField(record.fields, 'LAYER') || 'UNKNOWN',
|
|
@@ -155,15 +181,50 @@ export class PcbModelParser {
|
|
|
155
181
|
)
|
|
156
182
|
? pcbExtraction.embeddedFonts.fonts
|
|
157
183
|
: []
|
|
184
|
+
const embeddedFiles = pcbExtraction?.embeddedFiles || {
|
|
185
|
+
schema: 'altium-toolkit.embedded-files.a1',
|
|
186
|
+
files: [],
|
|
187
|
+
diagnostics: []
|
|
188
|
+
}
|
|
189
|
+
const embeddedModelIntegrity = pcbExtraction?.embeddedModels
|
|
190
|
+
?.integrity || {
|
|
191
|
+
schema: 'altium-toolkit.pcb.embedded-model-integrity.a1',
|
|
192
|
+
issues: []
|
|
193
|
+
}
|
|
158
194
|
const rawRecords = Array.isArray(pcbExtraction?.rawRecords)
|
|
159
195
|
? pcbExtraction.rawRecords
|
|
160
196
|
: []
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
197
|
+
const viaStructures = pcbExtraction?.viaStructures || {
|
|
198
|
+
structures: [],
|
|
199
|
+
links: [],
|
|
200
|
+
byPrimitiveIndex: {}
|
|
201
|
+
}
|
|
202
|
+
const customPadShapes = pcbExtraction?.customPadShapes || {
|
|
203
|
+
entries: [],
|
|
204
|
+
byPrimitiveIndex: {}
|
|
205
|
+
}
|
|
206
|
+
const extendedPrimitiveInformation =
|
|
207
|
+
pcbExtraction?.extendedPrimitiveInformation || {
|
|
208
|
+
entries: [],
|
|
209
|
+
byPrimitiveIndex: {},
|
|
210
|
+
byPrimitiveKey: {}
|
|
211
|
+
}
|
|
212
|
+
const unions = pcbExtraction?.unions || {
|
|
213
|
+
userUnions: [],
|
|
214
|
+
smartUnions: [],
|
|
215
|
+
byIndex: {},
|
|
216
|
+
smartByIndex: {},
|
|
217
|
+
membersByPrimitiveKey: {}
|
|
218
|
+
}
|
|
219
|
+
const texts = PcbSpecialStringResolver.annotateTexts(
|
|
220
|
+
PcbModelParser.#annotateTextFontMetrics(
|
|
221
|
+
PcbComponentAnnotationNormalizer.normalizeTexts(
|
|
222
|
+
rawTextPrimitives,
|
|
223
|
+
rawComponentRecords
|
|
224
|
+
),
|
|
225
|
+
extractedEmbeddedFonts
|
|
165
226
|
),
|
|
166
|
-
|
|
227
|
+
pcbExtraction?.specialStringParameters || {}
|
|
167
228
|
)
|
|
168
229
|
const recoveredOutline = PcbOutlineRecovery.recoverOutline({
|
|
169
230
|
fallbackOutline: fallbackBoardOutline,
|
|
@@ -185,6 +246,11 @@ export class PcbModelParser {
|
|
|
185
246
|
texts,
|
|
186
247
|
components: componentRecords
|
|
187
248
|
})
|
|
249
|
+
PcbCustomPadShapeParser.attachToPads(
|
|
250
|
+
normalizedPcb.pads,
|
|
251
|
+
customPadShapes,
|
|
252
|
+
normalizedPcb
|
|
253
|
+
)
|
|
188
254
|
const boardRegionContexts =
|
|
189
255
|
PcbBoardRegionSemanticsParser.buildBoardRegionContexts(
|
|
190
256
|
normalizedPcb.boardRegions
|
|
@@ -207,6 +273,37 @@ export class PcbModelParser {
|
|
|
207
273
|
const componentPrimitives = PcbComponentPrimitiveIndexer.indexGroups(
|
|
208
274
|
componentPrimitiveGroups
|
|
209
275
|
)
|
|
276
|
+
const ownership = PcbOwnershipGraphBuilder.build({
|
|
277
|
+
...normalizedPcb,
|
|
278
|
+
nets
|
|
279
|
+
})
|
|
280
|
+
const pnp = PcbPickPlacePositionResolver.buildModel(
|
|
281
|
+
normalizedPcb.components,
|
|
282
|
+
componentPrimitiveGroups,
|
|
283
|
+
{ sourceComponents: componentRecords }
|
|
284
|
+
)
|
|
285
|
+
const routeAnalysis = PcbRouteAnalysisBuilder.build({
|
|
286
|
+
...normalizedPcb,
|
|
287
|
+
layers,
|
|
288
|
+
primitiveLayers,
|
|
289
|
+
nets,
|
|
290
|
+
classes,
|
|
291
|
+
differentialPairs: differentialPairData.differentialPairs,
|
|
292
|
+
differentialPairClasses:
|
|
293
|
+
differentialPairData.differentialPairClasses
|
|
294
|
+
})
|
|
295
|
+
const statistics = PcbStatisticsBuilder.build({
|
|
296
|
+
...normalizedPcb,
|
|
297
|
+
layers,
|
|
298
|
+
primitiveLayers,
|
|
299
|
+
rules
|
|
300
|
+
})
|
|
301
|
+
const maskPaste = PcbMaskPasteResolver.build({
|
|
302
|
+
pads: normalizedPcb.pads,
|
|
303
|
+
vias: normalizedPcb.vias,
|
|
304
|
+
rules,
|
|
305
|
+
defaults
|
|
306
|
+
})
|
|
210
307
|
const bom = PcbModelParser.#groupBomRows(
|
|
211
308
|
componentRecords.map((component) => ({
|
|
212
309
|
designator: component.designator,
|
|
@@ -325,6 +422,38 @@ export class PcbModelParser {
|
|
|
325
422
|
})
|
|
326
423
|
}
|
|
327
424
|
|
|
425
|
+
if (embeddedFiles.files.length) {
|
|
426
|
+
diagnostics.push({
|
|
427
|
+
severity: 'info',
|
|
428
|
+
message:
|
|
429
|
+
'Inventoried ' +
|
|
430
|
+
embeddedFiles.files.length +
|
|
431
|
+
' generic embedded payload ' +
|
|
432
|
+
PcbModelParser.#plural(
|
|
433
|
+
embeddedFiles.files.length,
|
|
434
|
+
'stream',
|
|
435
|
+
'streams'
|
|
436
|
+
) +
|
|
437
|
+
'.'
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (const issue of embeddedModelIntegrity.issues || []) {
|
|
442
|
+
diagnostics.push({
|
|
443
|
+
severity: issue.severity === 'info' ? 'info' : 'warning',
|
|
444
|
+
code: issue.code,
|
|
445
|
+
message: issue.message
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
for (const issue of embeddedFiles.diagnostics || []) {
|
|
450
|
+
diagnostics.push({
|
|
451
|
+
severity: issue.severity === 'info' ? 'info' : 'warning',
|
|
452
|
+
code: issue.code,
|
|
453
|
+
message: issue.message
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
|
|
328
457
|
if (rawRecords.length) {
|
|
329
458
|
diagnostics.push({
|
|
330
459
|
severity: 'info',
|
|
@@ -335,6 +464,54 @@ export class PcbModelParser {
|
|
|
335
464
|
})
|
|
336
465
|
}
|
|
337
466
|
|
|
467
|
+
if (viaStructures.structures?.length) {
|
|
468
|
+
diagnostics.push({
|
|
469
|
+
severity: 'info',
|
|
470
|
+
message:
|
|
471
|
+
'Recovered ' +
|
|
472
|
+
viaStructures.structures.length +
|
|
473
|
+
' PCB via protection structure ' +
|
|
474
|
+
PcbModelParser.#plural(
|
|
475
|
+
viaStructures.structures.length,
|
|
476
|
+
'definition',
|
|
477
|
+
'definitions'
|
|
478
|
+
) +
|
|
479
|
+
'.'
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (extendedPrimitiveInformation.entries?.length) {
|
|
484
|
+
diagnostics.push({
|
|
485
|
+
severity: 'info',
|
|
486
|
+
message:
|
|
487
|
+
'Recovered ' +
|
|
488
|
+
extendedPrimitiveInformation.entries.length +
|
|
489
|
+
' extended PCB primitive information records.'
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (customPadShapes.entries?.length) {
|
|
494
|
+
diagnostics.push({
|
|
495
|
+
severity: 'info',
|
|
496
|
+
message:
|
|
497
|
+
'Recovered ' +
|
|
498
|
+
customPadShapes.entries.length +
|
|
499
|
+
' custom pad shape sidecar records.'
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (unions.userUnions?.length || unions.smartUnions?.length) {
|
|
504
|
+
diagnostics.push({
|
|
505
|
+
severity: 'info',
|
|
506
|
+
message:
|
|
507
|
+
'Recovered ' +
|
|
508
|
+
unions.userUnions.length +
|
|
509
|
+
' user unions and ' +
|
|
510
|
+
unions.smartUnions.length +
|
|
511
|
+
' smart unions.'
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
|
|
338
515
|
if (recoveredOutline.source === 'board-route') {
|
|
339
516
|
diagnostics.push({
|
|
340
517
|
severity: 'info',
|
|
@@ -373,15 +550,32 @@ export class PcbModelParser {
|
|
|
373
550
|
bomRowCount: bom.length,
|
|
374
551
|
netCount: nets.length,
|
|
375
552
|
classCount: classes.length,
|
|
553
|
+
differentialPairCount:
|
|
554
|
+
differentialPairData.differentialPairs.length,
|
|
555
|
+
differentialPairClassCount:
|
|
556
|
+
differentialPairData.differentialPairClasses.length,
|
|
376
557
|
ruleCount: rules.length,
|
|
558
|
+
dimensionCount: dimensions.length,
|
|
559
|
+
mechanicalLayerPairCount: mechanicalLayerPairs.length,
|
|
377
560
|
polygonCount: polygons.length,
|
|
378
561
|
trackCount: tracks.length,
|
|
379
562
|
arcCount: arcs.length,
|
|
380
563
|
viaCount: vias.length,
|
|
564
|
+
viaStructureCount: viaStructures.structures?.length || 0,
|
|
565
|
+
extendedPrimitiveInformationCount:
|
|
566
|
+
extendedPrimitiveInformation.entries?.length || 0,
|
|
567
|
+
customPadShapeCount: customPadShapes.entries?.length || 0,
|
|
568
|
+
userUnionCount: unions.userUnions?.length || 0,
|
|
569
|
+
smartUnionCount: unions.smartUnions?.length || 0,
|
|
570
|
+
routedNetCount: routeAnalysis.summary.routedNetCount,
|
|
571
|
+
routedLengthMil: routeAnalysis.summary.totalLengthMil,
|
|
381
572
|
boardRegionCount: boardRegionSummary.boardRegionCount,
|
|
382
573
|
flexRegionCount: boardRegionSummary.flexRegionCount,
|
|
383
574
|
bendingLineCount: boardRegionSummary.bendingLineCount,
|
|
575
|
+
embeddedModelIssueCount:
|
|
576
|
+
embeddedModelIntegrity.issues?.length || 0,
|
|
384
577
|
embeddedFontCount: extractedEmbeddedFonts.length,
|
|
578
|
+
embeddedFileCount: embeddedFiles.files.length,
|
|
385
579
|
rawRecordCount: rawRecords.length,
|
|
386
580
|
boardWidthMil: Math.round(boardOutline.widthMil),
|
|
387
581
|
boardHeightMil: Math.round(boardOutline.heightMil)
|
|
@@ -391,30 +585,49 @@ export class PcbModelParser {
|
|
|
391
585
|
boardOutline: normalizedPcb.boardOutline,
|
|
392
586
|
layers,
|
|
393
587
|
layerSubstacks,
|
|
588
|
+
mechanicalLayerPairs,
|
|
589
|
+
layerFlipMetadata,
|
|
394
590
|
boardRegionContexts,
|
|
395
591
|
primitiveLayers,
|
|
396
592
|
appearance3d,
|
|
397
593
|
nets,
|
|
398
594
|
classes,
|
|
595
|
+
differentialPairs: differentialPairData.differentialPairs,
|
|
596
|
+
differentialPairClasses:
|
|
597
|
+
differentialPairData.differentialPairClasses,
|
|
399
598
|
rules,
|
|
599
|
+
...(defaults ? { defaults } : {}),
|
|
600
|
+
maskPaste,
|
|
601
|
+
dimensions,
|
|
400
602
|
components: normalizedPcb.components,
|
|
603
|
+
pickPlace: pnp,
|
|
604
|
+
routeAnalysis,
|
|
401
605
|
polygons: normalizedPcb.polygons,
|
|
402
606
|
fills: normalizedPcb.fills,
|
|
403
607
|
tracks: normalizedPcb.tracks,
|
|
404
608
|
arcs: normalizedPcb.arcs,
|
|
405
609
|
vias: normalizedPcb.vias,
|
|
610
|
+
viaStructures,
|
|
611
|
+
extendedPrimitiveInformation,
|
|
612
|
+
customPadShapes,
|
|
613
|
+
unions,
|
|
406
614
|
pads: normalizedPcb.pads,
|
|
407
615
|
regions: normalizedPcb.regions,
|
|
408
616
|
shapeBasedRegions: normalizedPcb.shapeBasedRegions,
|
|
409
617
|
boardRegions: normalizedPcb.boardRegions,
|
|
410
618
|
texts: normalizedPcb.texts,
|
|
411
619
|
embeddedModels: extractedEmbeddedModels,
|
|
620
|
+
embeddedModelIntegrity,
|
|
412
621
|
embeddedFonts: extractedEmbeddedFonts,
|
|
622
|
+
embeddedFiles,
|
|
413
623
|
rawRecords,
|
|
414
624
|
componentBodies,
|
|
415
625
|
componentPrimitives,
|
|
416
|
-
componentPrimitiveGroups
|
|
626
|
+
componentPrimitiveGroups,
|
|
627
|
+
ownership,
|
|
628
|
+
statistics
|
|
417
629
|
},
|
|
630
|
+
pnp,
|
|
418
631
|
bom
|
|
419
632
|
})
|
|
420
633
|
}
|
|
@@ -444,33 +657,106 @@ export class PcbModelParser {
|
|
|
444
657
|
/**
|
|
445
658
|
* Normalizes component placement fields while preserving native index order.
|
|
446
659
|
* @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 }[]}
|
|
660
|
+
* @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
661
|
*/
|
|
449
662
|
static #normalizeComponentRecords(records) {
|
|
450
663
|
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
|
-
|
|
664
|
+
.map((record, index) => {
|
|
665
|
+
const provenance = PcbModelParser.#parseComponentProvenance(
|
|
666
|
+
record.fields
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
componentIndex: index,
|
|
671
|
+
designator: getField(record.fields, 'SOURCEDESIGNATOR'),
|
|
672
|
+
uniqueId:
|
|
673
|
+
getField(record.fields, 'UNIQUEID') ||
|
|
674
|
+
getField(record.fields, 'UID') ||
|
|
675
|
+
getField(record.fields, 'UNIQUEIDPRIMITIVEINFORMATION'),
|
|
676
|
+
x: parseNumericField(record.fields, 'X') || 0,
|
|
677
|
+
y: parseNumericField(record.fields, 'Y') || 0,
|
|
678
|
+
layer: getField(record.fields, 'LAYER') || 'TOP',
|
|
679
|
+
pattern: getField(record.fields, 'PATTERN'),
|
|
680
|
+
rotation: parseNumericField(record.fields, 'ROTATION') || 0,
|
|
681
|
+
source:
|
|
682
|
+
getField(record.fields, 'SOURCELIBREFERENCE') ||
|
|
683
|
+
getField(record.fields, 'SOURCEFOOTPRINTLIBRARY'),
|
|
684
|
+
description: getField(record.fields, 'SOURCEDESCRIPTION'),
|
|
685
|
+
height: parseNumericField(record.fields, 'HEIGHT'),
|
|
686
|
+
...(Object.keys(provenance).length ? { provenance } : {}),
|
|
687
|
+
nameOn: parseBoolean(record.fields.NAMEON),
|
|
688
|
+
commentOn: parseBoolean(record.fields.COMMENTON)
|
|
689
|
+
}
|
|
690
|
+
})
|
|
471
691
|
.filter((component) => component.pattern && component.designator)
|
|
472
692
|
}
|
|
473
693
|
|
|
694
|
+
/**
|
|
695
|
+
* Parses schematic/project provenance from one PCB component row.
|
|
696
|
+
* @param {Record<string, string | string[]>} fields
|
|
697
|
+
* @returns {Record<string, unknown>}
|
|
698
|
+
*/
|
|
699
|
+
static #parseComponentProvenance(fields) {
|
|
700
|
+
const sourceUniqueId = getField(fields, 'SOURCEUNIQUEID')
|
|
701
|
+
const sourceHierarchicalPath = getField(
|
|
702
|
+
fields,
|
|
703
|
+
'SOURCEHIERARCHICALPATH'
|
|
704
|
+
)
|
|
705
|
+
const sourceFootprintLibrary = getField(
|
|
706
|
+
fields,
|
|
707
|
+
'SOURCEFOOTPRINTLIBRARY'
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
const provenance = PcbModelParser.#stripEmptyObject({
|
|
711
|
+
channelOffset: parseNumericField(fields, 'CHANNELOFFSET'),
|
|
712
|
+
sourceDesignator: getField(fields, 'SOURCEDESIGNATOR'),
|
|
713
|
+
sourceUniqueId,
|
|
714
|
+
sourceUniqueIdSegments:
|
|
715
|
+
PcbModelParser.#splitAltiumPath(sourceUniqueId),
|
|
716
|
+
sourceHierarchicalPath,
|
|
717
|
+
sourceHierarchySegments: PcbModelParser.#splitAltiumPath(
|
|
718
|
+
sourceHierarchicalPath
|
|
719
|
+
),
|
|
720
|
+
sourceFootprintLibrary,
|
|
721
|
+
sourceFootprintLibraryName: PcbModelParser.#basenameFromAltiumPath(
|
|
722
|
+
sourceFootprintLibrary
|
|
723
|
+
),
|
|
724
|
+
sourceLibReference: getField(fields, 'SOURCELIBREFERENCE'),
|
|
725
|
+
sourceComponentLibrary: getField(fields, 'SOURCECOMPONENTLIBRARY'),
|
|
726
|
+
sourceComponentLibraryIdentifierKind: parseNumericField(
|
|
727
|
+
fields,
|
|
728
|
+
'SOURCECOMPLIBIDENTIFIERKIND'
|
|
729
|
+
),
|
|
730
|
+
sourceComponentLibraryIdentifier: getField(
|
|
731
|
+
fields,
|
|
732
|
+
'SOURCECOMPLIBRARYIDENTIFIER'
|
|
733
|
+
),
|
|
734
|
+
footprintDescription: getField(fields, 'FOOTPRINTDESCRIPTION'),
|
|
735
|
+
nameAutoPosition: parseNumericField(fields, 'NAMEAUTOPOSITION'),
|
|
736
|
+
commentAutoPosition: parseNumericField(
|
|
737
|
+
fields,
|
|
738
|
+
'COMMENTAUTOPOSITION'
|
|
739
|
+
),
|
|
740
|
+
lockStrings: PcbModelParser.#optionalBooleanField(
|
|
741
|
+
fields,
|
|
742
|
+
'LOCKSTRINGS'
|
|
743
|
+
),
|
|
744
|
+
enablePinSwapping: PcbModelParser.#optionalBooleanField(
|
|
745
|
+
fields,
|
|
746
|
+
'ENABLEPINSWAPPING'
|
|
747
|
+
),
|
|
748
|
+
enablePartSwapping: PcbModelParser.#optionalBooleanField(
|
|
749
|
+
fields,
|
|
750
|
+
'ENABLEPARTSWAPPING'
|
|
751
|
+
)
|
|
752
|
+
})
|
|
753
|
+
const nonRedundantKeys = Object.keys(provenance).filter(
|
|
754
|
+
(key) => !['sourceDesignator', 'sourceLibReference'].includes(key)
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
return nonRedundantKeys.length ? provenance : {}
|
|
758
|
+
}
|
|
759
|
+
|
|
474
760
|
/**
|
|
475
761
|
* Normalizes native Nets6/Data records in stream order.
|
|
476
762
|
* @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records
|
|
@@ -560,6 +846,172 @@ export class PcbModelParser {
|
|
|
560
846
|
return netNameByIndex
|
|
561
847
|
}
|
|
562
848
|
|
|
849
|
+
/**
|
|
850
|
+
* Parses native DifferentialPairs6/Data records in stream order.
|
|
851
|
+
* @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records
|
|
852
|
+
* @returns {{ pairIndex: number, name: string, positiveNetName: string, negativeNetName: string, netNames: string[], gatherControl: boolean, uniqueId: string }[]}
|
|
853
|
+
*/
|
|
854
|
+
static #parseDifferentialPairRecords(records) {
|
|
855
|
+
return records
|
|
856
|
+
.filter(
|
|
857
|
+
(record) => record.sourceStream === 'DifferentialPairs6/Data'
|
|
858
|
+
)
|
|
859
|
+
.map((record, index) => {
|
|
860
|
+
const positiveNetName = getField(
|
|
861
|
+
record.fields,
|
|
862
|
+
'POSITIVENETNAME'
|
|
863
|
+
)
|
|
864
|
+
const negativeNetName = getField(
|
|
865
|
+
record.fields,
|
|
866
|
+
'NEGATIVENETNAME'
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
return {
|
|
870
|
+
pairIndex: index,
|
|
871
|
+
name: getField(record.fields, 'NAME'),
|
|
872
|
+
positiveNetName,
|
|
873
|
+
negativeNetName,
|
|
874
|
+
netNames: [positiveNetName, negativeNetName].filter(
|
|
875
|
+
Boolean
|
|
876
|
+
),
|
|
877
|
+
gatherControl: PcbModelParser.#parseBooleanField(
|
|
878
|
+
record.fields,
|
|
879
|
+
'GATHERCONTROL',
|
|
880
|
+
false
|
|
881
|
+
),
|
|
882
|
+
uniqueId:
|
|
883
|
+
getField(record.fields, 'UNIQUEID') ||
|
|
884
|
+
getField(record.fields, 'UID')
|
|
885
|
+
}
|
|
886
|
+
})
|
|
887
|
+
.filter(
|
|
888
|
+
(pair) =>
|
|
889
|
+
pair.name ||
|
|
890
|
+
pair.positiveNetName ||
|
|
891
|
+
pair.negativeNetName ||
|
|
892
|
+
pair.uniqueId
|
|
893
|
+
)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Joins differential-pair class members to concrete pair records.
|
|
898
|
+
* @param {{ pairIndex: number, name: string, positiveNetName: string, negativeNetName: string, netNames: string[], gatherControl: boolean, uniqueId: string }[]} pairs
|
|
899
|
+
* @param {{ classIndex: number, name: string, kindName: string, members: string[] }[]} classes
|
|
900
|
+
* @returns {{ differentialPairs: object[], differentialPairClasses: object[] }}
|
|
901
|
+
*/
|
|
902
|
+
static #buildDifferentialPairData(pairs, classes) {
|
|
903
|
+
const pairsByName = new Map(
|
|
904
|
+
(pairs || []).map((pair) => [
|
|
905
|
+
PcbModelParser.#normalizeLookupName(pair.name),
|
|
906
|
+
pair
|
|
907
|
+
])
|
|
908
|
+
)
|
|
909
|
+
const classNamesByPair = new Map()
|
|
910
|
+
const differentialPairClasses = (classes || [])
|
|
911
|
+
.filter((classRecord) => classRecord.kindName === 'diff-pair')
|
|
912
|
+
.map((classRecord) => {
|
|
913
|
+
const pairNames = []
|
|
914
|
+
const unresolvedMembers = []
|
|
915
|
+
|
|
916
|
+
for (const member of classRecord.members || []) {
|
|
917
|
+
const pair = pairsByName.get(
|
|
918
|
+
PcbModelParser.#normalizeLookupName(member)
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
if (!pair) {
|
|
922
|
+
unresolvedMembers.push(member)
|
|
923
|
+
continue
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
pairNames.push(pair.name)
|
|
927
|
+
const classNames = classNamesByPair.get(pair.name) || []
|
|
928
|
+
classNames.push(classRecord.name)
|
|
929
|
+
classNamesByPair.set(pair.name, classNames)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return {
|
|
933
|
+
classIndex: classRecord.classIndex,
|
|
934
|
+
name: classRecord.name,
|
|
935
|
+
members: [...classRecord.members],
|
|
936
|
+
pairNames,
|
|
937
|
+
unresolvedMembers
|
|
938
|
+
}
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
return {
|
|
942
|
+
differentialPairs: (pairs || []).map((pair) => ({
|
|
943
|
+
...pair,
|
|
944
|
+
classNames: classNamesByPair.get(pair.name) || []
|
|
945
|
+
})),
|
|
946
|
+
differentialPairClasses
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Splits an authored hierarchy or unique-id path into stable segments.
|
|
952
|
+
* @param {string | undefined} value Path-like field value.
|
|
953
|
+
* @returns {string[]}
|
|
954
|
+
*/
|
|
955
|
+
static #splitAltiumPath(value) {
|
|
956
|
+
return String(value || '')
|
|
957
|
+
.split(/[\\/]+/u)
|
|
958
|
+
.map((segment) => segment.trim())
|
|
959
|
+
.filter(Boolean)
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Returns the terminal segment from a native path-like field value.
|
|
964
|
+
* @param {string | undefined} value Path-like field value.
|
|
965
|
+
* @returns {string}
|
|
966
|
+
*/
|
|
967
|
+
static #basenameFromAltiumPath(value) {
|
|
968
|
+
const segments = PcbModelParser.#splitAltiumPath(value)
|
|
969
|
+
|
|
970
|
+
return segments.length ? segments[segments.length - 1] : ''
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Removes empty values while preserving explicit false and zero values.
|
|
975
|
+
* @param {Record<string, unknown>} value Object to normalize.
|
|
976
|
+
* @returns {Record<string, unknown>}
|
|
977
|
+
*/
|
|
978
|
+
static #stripEmptyObject(value) {
|
|
979
|
+
return Object.fromEntries(
|
|
980
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
981
|
+
if (Array.isArray(entryValue)) {
|
|
982
|
+
return entryValue.length > 0
|
|
983
|
+
}
|
|
984
|
+
if (typeof entryValue === 'string') {
|
|
985
|
+
return entryValue.length > 0
|
|
986
|
+
}
|
|
987
|
+
return entryValue !== null && entryValue !== undefined
|
|
988
|
+
})
|
|
989
|
+
)
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Parses an optional boolean field without inventing a default value.
|
|
994
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
995
|
+
* @param {string} key Field name.
|
|
996
|
+
* @returns {boolean | null}
|
|
997
|
+
*/
|
|
998
|
+
static #optionalBooleanField(fields, key) {
|
|
999
|
+
const raw = getField(fields, key)
|
|
1000
|
+
|
|
1001
|
+
return raw ? parseBoolean(raw) : null
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Builds a case-insensitive lookup key for class and pair names.
|
|
1006
|
+
* @param {string | undefined} value Raw lookup value.
|
|
1007
|
+
* @returns {string}
|
|
1008
|
+
*/
|
|
1009
|
+
static #normalizeLookupName(value) {
|
|
1010
|
+
return String(value || '')
|
|
1011
|
+
.trim()
|
|
1012
|
+
.toUpperCase()
|
|
1013
|
+
}
|
|
1014
|
+
|
|
563
1015
|
/**
|
|
564
1016
|
* Extracts authored Altium 3D appearance colors from board metadata.
|
|
565
1017
|
* @param {{ fields: Record<string, string | string[]> }[]} boardRecords
|
|
@@ -755,25 +1207,36 @@ export class PcbModelParser {
|
|
|
755
1207
|
key === 'MEMBERCOUNT' ||
|
|
756
1208
|
key === 'ENABLED' ||
|
|
757
1209
|
key === 'UNIQUEID' ||
|
|
758
|
-
/^M\d+$/.test(key)
|
|
1210
|
+
/^(?:M|MEMBER)\d+$/.test(key)
|
|
759
1211
|
)
|
|
760
1212
|
}
|
|
761
1213
|
|
|
762
1214
|
/**
|
|
763
|
-
* Extracts ordered class members from M0
|
|
1215
|
+
* Extracts ordered class members from M0/MEMBER0-style fields.
|
|
764
1216
|
* @param {Record<string, string | string[]>} fields
|
|
765
1217
|
* @returns {string[]}
|
|
766
1218
|
*/
|
|
767
1219
|
static #parseClassMembers(fields) {
|
|
768
1220
|
return Object.keys(fields || {})
|
|
769
|
-
.filter((key) => /^M\d+$/.test(key))
|
|
1221
|
+
.filter((key) => /^(?:M|MEMBER)\d+$/.test(key))
|
|
770
1222
|
.sort(
|
|
771
|
-
(left, right) =>
|
|
1223
|
+
(left, right) =>
|
|
1224
|
+
PcbModelParser.#classMemberIndex(left) -
|
|
1225
|
+
PcbModelParser.#classMemberIndex(right)
|
|
772
1226
|
)
|
|
773
1227
|
.map((key) => getField(fields, key))
|
|
774
1228
|
.filter(Boolean)
|
|
775
1229
|
}
|
|
776
1230
|
|
|
1231
|
+
/**
|
|
1232
|
+
* Extracts the numeric index from a class member field name.
|
|
1233
|
+
* @param {string} key Field key.
|
|
1234
|
+
* @returns {number}
|
|
1235
|
+
*/
|
|
1236
|
+
static #classMemberIndex(key) {
|
|
1237
|
+
return Number(String(key).replace(/^(?:M|MEMBER)/u, ''))
|
|
1238
|
+
}
|
|
1239
|
+
|
|
777
1240
|
/**
|
|
778
1241
|
* Returns a stable display name for one native PCB class kind.
|
|
779
1242
|
* @param {number} kind
|