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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
|
+
import { ProjectOutJobDigestBuilder } from './ProjectOutJobDigestBuilder.mjs'
|
|
7
|
+
import { ProjectDocumentGraphBuilder } from './ProjectDocumentGraphBuilder.mjs'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Parses Altium PrjPcb INI-style project files into a normalized project
|
|
@@ -35,7 +37,15 @@ export class PrjPcbModelParser {
|
|
|
35
37
|
)
|
|
36
38
|
const currentVariant =
|
|
37
39
|
PrjPcbModelParser.#stringField(design, 'CurrentVariant') || ''
|
|
38
|
-
|
|
40
|
+
let documents = PrjPcbModelParser.#extractDocuments(sections)
|
|
41
|
+
const classGeneration = PrjPcbModelParser.#extractClassGeneration(
|
|
42
|
+
sections,
|
|
43
|
+
documents
|
|
44
|
+
)
|
|
45
|
+
documents = PrjPcbModelParser.#attachDocumentClassGeneration(
|
|
46
|
+
documents,
|
|
47
|
+
classGeneration
|
|
48
|
+
)
|
|
39
49
|
const documentGroups = PrjPcbModelParser.#buildDocumentGroups(documents)
|
|
40
50
|
const parameters = PrjPcbModelParser.#extractParameters(sections)
|
|
41
51
|
const variants = PrjPcbModelParser.#extractVariants(
|
|
@@ -45,6 +55,15 @@ export class PrjPcbModelParser {
|
|
|
45
55
|
const configurations =
|
|
46
56
|
PrjPcbModelParser.#extractConfigurations(sections)
|
|
47
57
|
const outputGroups = PrjPcbModelParser.#extractOutputGroups(sections)
|
|
58
|
+
const outJobDigest = ProjectOutJobDigestBuilder.build({
|
|
59
|
+
documents,
|
|
60
|
+
outputGroups
|
|
61
|
+
})
|
|
62
|
+
const documentGraph = ProjectDocumentGraphBuilder.build({
|
|
63
|
+
documents,
|
|
64
|
+
documentGroups,
|
|
65
|
+
outputGroups
|
|
66
|
+
})
|
|
48
67
|
const summary = PrjPcbModelParser.#buildSummary(
|
|
49
68
|
fileName,
|
|
50
69
|
documents,
|
|
@@ -73,6 +92,9 @@ export class PrjPcbModelParser {
|
|
|
73
92
|
variants,
|
|
74
93
|
configurations,
|
|
75
94
|
outputGroups,
|
|
95
|
+
outJobDigest,
|
|
96
|
+
documentGraph,
|
|
97
|
+
classGeneration,
|
|
76
98
|
sections: PrjPcbModelParser.#serializeSections(sections)
|
|
77
99
|
},
|
|
78
100
|
bom: []
|
|
@@ -214,6 +236,9 @@ export class PrjPcbModelParser {
|
|
|
214
236
|
integratedLibraries: documents.filter(
|
|
215
237
|
(document) => document.kind === 'integrated-library'
|
|
216
238
|
),
|
|
239
|
+
harnessFiles: documents.filter(
|
|
240
|
+
(document) => document.kind === 'harness'
|
|
241
|
+
),
|
|
217
242
|
outJobs: documents.filter(
|
|
218
243
|
(document) => document.kind === 'output-job'
|
|
219
244
|
),
|
|
@@ -251,6 +276,188 @@ export class PrjPcbModelParser {
|
|
|
251
276
|
return { list, map }
|
|
252
277
|
}
|
|
253
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Extracts project and per-document class-generation policy sections.
|
|
281
|
+
* @param {{ name: string, entries: { key: string, value: string }[] }[]} sections
|
|
282
|
+
* @param {object[]} documents
|
|
283
|
+
* @returns {{ section: string, policies: object, options: Record<string, string | string[]>, documents: object[], byDocumentPath: Record<string, object>, byDocumentIndex: Record<string, object> }}
|
|
284
|
+
*/
|
|
285
|
+
static #extractClassGeneration(sections, documents) {
|
|
286
|
+
const projectSection =
|
|
287
|
+
PrjPcbModelParser.#findSection(sections, 'PrjClassGen') || null
|
|
288
|
+
const projectFields = PrjPcbModelParser.#sectionFields(projectSection)
|
|
289
|
+
const documentPolicies = PrjPcbModelParser.#numberedSections(
|
|
290
|
+
sections,
|
|
291
|
+
'DocumentClassGen'
|
|
292
|
+
).map(({ section, number }) => {
|
|
293
|
+
const fields = PrjPcbModelParser.#sectionFields(section)
|
|
294
|
+
const documentPath =
|
|
295
|
+
PrjPcbModelParser.#stringField(fields, 'DocumentPath') || ''
|
|
296
|
+
const normalizedPath =
|
|
297
|
+
PrjPcbModelParser.#normalizeDocumentPath(documentPath)
|
|
298
|
+
const documentIndex =
|
|
299
|
+
PrjPcbModelParser.#integerField(fields, 'DocumentIndex') ||
|
|
300
|
+
PrjPcbModelParser.#documentIndexForPath(
|
|
301
|
+
documents,
|
|
302
|
+
normalizedPath
|
|
303
|
+
) ||
|
|
304
|
+
number
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
index: number,
|
|
308
|
+
section: section.name,
|
|
309
|
+
documentIndex,
|
|
310
|
+
documentPath,
|
|
311
|
+
normalizedPath,
|
|
312
|
+
policies: PrjPcbModelParser.#classGenerationPolicies(fields),
|
|
313
|
+
options: fields
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
const byDocumentPath = {}
|
|
317
|
+
const byDocumentIndex = {}
|
|
318
|
+
|
|
319
|
+
for (const policy of documentPolicies) {
|
|
320
|
+
if (policy.normalizedPath) {
|
|
321
|
+
byDocumentPath[policy.normalizedPath] = policy
|
|
322
|
+
}
|
|
323
|
+
byDocumentIndex[String(policy.documentIndex)] = policy
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
section: projectSection?.name || '',
|
|
328
|
+
policies: PrjPcbModelParser.#classGenerationPolicies(projectFields),
|
|
329
|
+
options: projectFields,
|
|
330
|
+
documents: documentPolicies,
|
|
331
|
+
byDocumentPath,
|
|
332
|
+
byDocumentIndex
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Attaches per-document class-generation options to document rows.
|
|
338
|
+
* @param {object[]} documents Project documents.
|
|
339
|
+
* @param {object} classGeneration Class-generation model.
|
|
340
|
+
* @returns {object[]}
|
|
341
|
+
*/
|
|
342
|
+
static #attachDocumentClassGeneration(documents, classGeneration) {
|
|
343
|
+
return (documents || []).map((document) => {
|
|
344
|
+
const inlinePolicies =
|
|
345
|
+
PrjPcbModelParser.#classGenerationPoliciesFromDocument(
|
|
346
|
+
document.options || {}
|
|
347
|
+
)
|
|
348
|
+
const sectionPolicy =
|
|
349
|
+
classGeneration.byDocumentPath?.[document.normalizedPath] ||
|
|
350
|
+
classGeneration.byDocumentIndex?.[String(document.index)] ||
|
|
351
|
+
null
|
|
352
|
+
const policies = {
|
|
353
|
+
...inlinePolicies,
|
|
354
|
+
...(sectionPolicy?.policies || {})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return Object.keys(policies).length
|
|
358
|
+
? {
|
|
359
|
+
...document,
|
|
360
|
+
classGeneration: {
|
|
361
|
+
documentIndex: document.index,
|
|
362
|
+
policies,
|
|
363
|
+
section: sectionPolicy?.section || '',
|
|
364
|
+
options:
|
|
365
|
+
sectionPolicy?.options ||
|
|
366
|
+
PrjPcbModelParser.#classGenerationDocumentOptions(
|
|
367
|
+
document.options || {}
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
: document
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Extracts class-generation policies from inline document options.
|
|
377
|
+
* @param {Record<string, string | string[]>} fields Document option fields.
|
|
378
|
+
* @returns {Record<string, boolean>}
|
|
379
|
+
*/
|
|
380
|
+
static #classGenerationPoliciesFromDocument(fields) {
|
|
381
|
+
const options =
|
|
382
|
+
PrjPcbModelParser.#classGenerationDocumentOptions(fields)
|
|
383
|
+
return PrjPcbModelParser.#classGenerationPolicies(options)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Selects only inline class-generation options from one document section.
|
|
388
|
+
* @param {Record<string, string | string[]>} fields Document option fields.
|
|
389
|
+
* @returns {Record<string, string | string[]>}
|
|
390
|
+
*/
|
|
391
|
+
static #classGenerationDocumentOptions(fields) {
|
|
392
|
+
const options = {}
|
|
393
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
394
|
+
if (/^ClassGen/i.test(key)) {
|
|
395
|
+
options[key] = value
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return options
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Converts class-generation option fields into stable camelCase policy
|
|
403
|
+
* names.
|
|
404
|
+
* @param {Record<string, string | string[]>} fields Class-generation fields.
|
|
405
|
+
* @returns {Record<string, boolean>}
|
|
406
|
+
*/
|
|
407
|
+
static #classGenerationPolicies(fields) {
|
|
408
|
+
const policies = {}
|
|
409
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
410
|
+
const policyName = PrjPcbModelParser.#classGenerationPolicyName(key)
|
|
411
|
+
if (!policyName) continue
|
|
412
|
+
policies[policyName] = PrjPcbModelParser.#booleanValue(value)
|
|
413
|
+
}
|
|
414
|
+
return policies
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Resolves a public class-generation policy name.
|
|
419
|
+
* @param {string} key Raw option key.
|
|
420
|
+
* @returns {string}
|
|
421
|
+
*/
|
|
422
|
+
static #classGenerationPolicyName(key) {
|
|
423
|
+
const normalized = String(key || '')
|
|
424
|
+
.replace(/^ClassGen/i, '')
|
|
425
|
+
.replace(/[^a-z0-9]/gi, '')
|
|
426
|
+
.toLowerCase()
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
{
|
|
430
|
+
generateclasses: 'generateClasses',
|
|
431
|
+
generatenetclasses: 'generateNetClasses',
|
|
432
|
+
generatecomponentclasses: 'generateComponentClasses',
|
|
433
|
+
generatedifferentialpairclasses:
|
|
434
|
+
'generateDifferentialPairClasses',
|
|
435
|
+
generaterooms: 'generateRooms',
|
|
436
|
+
generatesheetclasses: 'generateSheetClasses',
|
|
437
|
+
generatepolygonclasses: 'generatePolygonClasses',
|
|
438
|
+
transfernetclasses: 'transferNetClasses',
|
|
439
|
+
transfercomponentclasses: 'transferComponentClasses',
|
|
440
|
+
transferdifferentialpairclasses:
|
|
441
|
+
'transferDifferentialPairClasses',
|
|
442
|
+
transferroomdirectives: 'transferRoomDirectives'
|
|
443
|
+
}[normalized] || ''
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Finds a document index for a normalized path.
|
|
449
|
+
* @param {object[]} documents Project document rows.
|
|
450
|
+
* @param {string} normalizedPath Normalized document path.
|
|
451
|
+
* @returns {number}
|
|
452
|
+
*/
|
|
453
|
+
static #documentIndexForPath(documents, normalizedPath) {
|
|
454
|
+
if (!normalizedPath) return 0
|
|
455
|
+
const match = (documents || []).find(
|
|
456
|
+
(document) => document.normalizedPath === normalizedPath
|
|
457
|
+
)
|
|
458
|
+
return Number(match?.index) || 0
|
|
459
|
+
}
|
|
460
|
+
|
|
254
461
|
/**
|
|
255
462
|
* Extracts project variants and their variation rows.
|
|
256
463
|
* @param {{ name: string, entries: { key: string, value: string }[] }[]} sections
|
|
@@ -316,6 +523,8 @@ export class PrjPcbModelParser {
|
|
|
316
523
|
PrjPcbModelParser.#buildParameterOverrideMap(
|
|
317
524
|
paramVariations
|
|
318
525
|
),
|
|
526
|
+
alternateFitted:
|
|
527
|
+
PrjPcbModelParser.#buildAlternateFittedMap(variations),
|
|
319
528
|
dnp: variations
|
|
320
529
|
.filter((variation) => variation.Kind === '1')
|
|
321
530
|
.map((variation) => variation.Designator || '')
|
|
@@ -448,6 +657,41 @@ export class PrjPcbModelParser {
|
|
|
448
657
|
return overrides
|
|
449
658
|
}
|
|
450
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Groups alternate fitted component rows by designator.
|
|
662
|
+
* @param {Record<string, string>[]} rows Variant variation rows.
|
|
663
|
+
* @returns {Record<string, object>}
|
|
664
|
+
*/
|
|
665
|
+
static #buildAlternateFittedMap(rows) {
|
|
666
|
+
const alternates = {}
|
|
667
|
+
for (const row of rows || []) {
|
|
668
|
+
const designator = String(row.Designator || '').trim()
|
|
669
|
+
if (!designator) continue
|
|
670
|
+
const alternatePart = String(row.AlternatePart || '').trim()
|
|
671
|
+
const isAlternate =
|
|
672
|
+
String(row.Kind || '').trim() === '2' || Boolean(alternatePart)
|
|
673
|
+
if (!isAlternate) continue
|
|
674
|
+
|
|
675
|
+
alternates[designator] = {
|
|
676
|
+
designator,
|
|
677
|
+
alternatePart,
|
|
678
|
+
libReference:
|
|
679
|
+
row.AlternateLibReference ||
|
|
680
|
+
row.AlternateLibraryRef ||
|
|
681
|
+
row.LibraryRef ||
|
|
682
|
+
'',
|
|
683
|
+
footprint:
|
|
684
|
+
row.AlternateFootprint ||
|
|
685
|
+
row.Footprint ||
|
|
686
|
+
row.Pattern ||
|
|
687
|
+
'',
|
|
688
|
+
comment: row.AlternateComment || row.Comment || '',
|
|
689
|
+
description: row.AlternateDescription || row.Description || ''
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return alternates
|
|
693
|
+
}
|
|
694
|
+
|
|
451
695
|
/**
|
|
452
696
|
* Extracts numbered output rows from one OutputGroup section.
|
|
453
697
|
* @param {Record<string, string | string[]>} fields
|
|
@@ -465,7 +709,14 @@ export class PrjPcbModelParser {
|
|
|
465
709
|
PrjPcbModelParser.#stringField(fields, 'OutputType' + index) ||
|
|
466
710
|
''
|
|
467
711
|
if (!type) continue
|
|
468
|
-
|
|
712
|
+
const targetPath =
|
|
713
|
+
PrjPcbModelParser.#stringField(
|
|
714
|
+
fields,
|
|
715
|
+
'OutputTargetPath' + index
|
|
716
|
+
) ||
|
|
717
|
+
PrjPcbModelParser.#stringField(fields, 'OutputPath' + index) ||
|
|
718
|
+
''
|
|
719
|
+
const row = {
|
|
469
720
|
index,
|
|
470
721
|
type,
|
|
471
722
|
name:
|
|
@@ -487,7 +738,9 @@ export class PrjPcbModelParser {
|
|
|
487
738
|
fields,
|
|
488
739
|
'OutputDefault' + index
|
|
489
740
|
)
|
|
490
|
-
}
|
|
741
|
+
}
|
|
742
|
+
if (targetPath) row.targetPath = targetPath
|
|
743
|
+
rows.push(row)
|
|
491
744
|
}
|
|
492
745
|
|
|
493
746
|
return rows
|
|
@@ -662,10 +915,28 @@ export class PrjPcbModelParser {
|
|
|
662
915
|
* @returns {boolean}
|
|
663
916
|
*/
|
|
664
917
|
static #booleanField(fields, key) {
|
|
665
|
-
|
|
666
|
-
PrjPcbModelParser.#stringField(fields, key)
|
|
667
|
-
)
|
|
668
|
-
|
|
918
|
+
return PrjPcbModelParser.#booleanValue(
|
|
919
|
+
PrjPcbModelParser.#stringField(fields, key)
|
|
920
|
+
)
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Parses one boolean-ish value.
|
|
925
|
+
* @param {unknown} value Raw field value.
|
|
926
|
+
* @returns {boolean}
|
|
927
|
+
*/
|
|
928
|
+
static #booleanValue(value) {
|
|
929
|
+
const raw = Array.isArray(value)
|
|
930
|
+
? String(value[0] || '')
|
|
931
|
+
: String(value || '')
|
|
932
|
+
const normalized = raw.toLowerCase()
|
|
933
|
+
|
|
934
|
+
return (
|
|
935
|
+
normalized === '1' ||
|
|
936
|
+
normalized === 't' ||
|
|
937
|
+
normalized === 'true' ||
|
|
938
|
+
normalized === 'yes'
|
|
939
|
+
)
|
|
669
940
|
}
|
|
670
941
|
|
|
671
942
|
/**
|
|
@@ -734,6 +1005,9 @@ export class PrjPcbModelParser {
|
|
|
734
1005
|
return 'pcb-library'
|
|
735
1006
|
case '.intlib':
|
|
736
1007
|
return 'integrated-library'
|
|
1008
|
+
case '.harness':
|
|
1009
|
+
case '.harnessdoc':
|
|
1010
|
+
return 'harness'
|
|
737
1011
|
case '.outjob':
|
|
738
1012
|
return 'output-job'
|
|
739
1013
|
default:
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses annotation mapping files used to relate logical and compiled
|
|
9
|
+
* designators.
|
|
10
|
+
*/
|
|
11
|
+
export class ProjectAnnotationParser {
|
|
12
|
+
/**
|
|
13
|
+
* Parses one annotation ArrayBuffer.
|
|
14
|
+
* @param {string} fileName Annotation file name.
|
|
15
|
+
* @param {ArrayBuffer} arrayBuffer Annotation bytes.
|
|
16
|
+
* @returns {object}
|
|
17
|
+
*/
|
|
18
|
+
static parse(fileName, arrayBuffer) {
|
|
19
|
+
return ProjectAnnotationParser.parseText(
|
|
20
|
+
fileName,
|
|
21
|
+
new TextDecoder('windows-1252')
|
|
22
|
+
.decode(arrayBuffer || new ArrayBuffer(0))
|
|
23
|
+
.replace(/^\uFEFF/u, '')
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses one annotation text payload.
|
|
29
|
+
* @param {string} fileName Annotation file name.
|
|
30
|
+
* @param {string} text Annotation text.
|
|
31
|
+
* @returns {object}
|
|
32
|
+
*/
|
|
33
|
+
static parseText(fileName, text) {
|
|
34
|
+
const sections = ProjectAnnotationParser.#parseIniSections(text)
|
|
35
|
+
const mappings = ProjectAnnotationParser.#extractMappings(
|
|
36
|
+
fileName,
|
|
37
|
+
sections
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return NormalizedModelSchema.attach({
|
|
41
|
+
kind: 'project-annotation',
|
|
42
|
+
fileType: 'Annotation',
|
|
43
|
+
fileName,
|
|
44
|
+
summary: {
|
|
45
|
+
title: fileName,
|
|
46
|
+
mappingCount: mappings.length
|
|
47
|
+
},
|
|
48
|
+
diagnostics: [
|
|
49
|
+
{
|
|
50
|
+
severity: 'info',
|
|
51
|
+
message:
|
|
52
|
+
'Recovered ' +
|
|
53
|
+
mappings.length +
|
|
54
|
+
' annotation designator mappings.'
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
annotations: {
|
|
58
|
+
mappings,
|
|
59
|
+
bySourceDesignator: ProjectAnnotationParser.#indexBy(
|
|
60
|
+
mappings,
|
|
61
|
+
'sourceDesignator'
|
|
62
|
+
),
|
|
63
|
+
byCompiledDesignator: ProjectAnnotationParser.#indexBy(
|
|
64
|
+
mappings,
|
|
65
|
+
'compiledDesignator'
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
bom: []
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses INI-like sections.
|
|
74
|
+
* @param {string} text Text payload.
|
|
75
|
+
* @returns {{ name: string, entries: { key: string, value: string }[] }[]}
|
|
76
|
+
*/
|
|
77
|
+
static #parseIniSections(text) {
|
|
78
|
+
const sections = []
|
|
79
|
+
let current = null
|
|
80
|
+
|
|
81
|
+
for (const rawLine of String(text || '')
|
|
82
|
+
.replace(/\r\n?/gu, '\n')
|
|
83
|
+
.split('\n')) {
|
|
84
|
+
const trimmed = rawLine.trim()
|
|
85
|
+
if (
|
|
86
|
+
!trimmed ||
|
|
87
|
+
trimmed.startsWith(';') ||
|
|
88
|
+
trimmed.startsWith('#')
|
|
89
|
+
) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/u)
|
|
94
|
+
if (sectionMatch) {
|
|
95
|
+
current = {
|
|
96
|
+
name: sectionMatch[1].trim(),
|
|
97
|
+
entries: []
|
|
98
|
+
}
|
|
99
|
+
sections.push(current)
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!current) continue
|
|
104
|
+
const separatorIndex = rawLine.indexOf('=')
|
|
105
|
+
if (separatorIndex === -1) continue
|
|
106
|
+
current.entries.push({
|
|
107
|
+
key: rawLine.slice(0, separatorIndex).trim(),
|
|
108
|
+
value: rawLine.slice(separatorIndex + 1).trim()
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return sections
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extracts designator mapping rows.
|
|
117
|
+
* @param {string} fileName Annotation file name.
|
|
118
|
+
* @param {{ name: string, entries: { key: string, value: string }[] }[]} sections Parsed sections.
|
|
119
|
+
* @returns {object[]}
|
|
120
|
+
*/
|
|
121
|
+
static #extractMappings(fileName, sections) {
|
|
122
|
+
return (sections || [])
|
|
123
|
+
.map((section, sectionIndex) => {
|
|
124
|
+
const match = String(section.name || '').match(
|
|
125
|
+
/^Annotation(\d*)$/iu
|
|
126
|
+
)
|
|
127
|
+
if (!match) return null
|
|
128
|
+
const fields = ProjectAnnotationParser.#sectionFields(section)
|
|
129
|
+
const sourceDesignator =
|
|
130
|
+
ProjectAnnotationParser.#field(fields, [
|
|
131
|
+
'SourceDesignator',
|
|
132
|
+
'LogicalDesignator',
|
|
133
|
+
'OriginalDesignator'
|
|
134
|
+
]) || ''
|
|
135
|
+
const compiledDesignator =
|
|
136
|
+
ProjectAnnotationParser.#field(fields, [
|
|
137
|
+
'CompiledDesignator',
|
|
138
|
+
'PhysicalDesignator',
|
|
139
|
+
'NewDesignator'
|
|
140
|
+
]) || ''
|
|
141
|
+
if (!sourceDesignator || !compiledDesignator) return null
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
index: Number.parseInt(match[1] || '', 10) || sectionIndex,
|
|
145
|
+
sourceDesignator,
|
|
146
|
+
compiledDesignator,
|
|
147
|
+
uniqueId:
|
|
148
|
+
ProjectAnnotationParser.#field(fields, [
|
|
149
|
+
'UniqueId',
|
|
150
|
+
'UniqueID'
|
|
151
|
+
]) || '',
|
|
152
|
+
sourceFileName: fileName,
|
|
153
|
+
options: fields
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.sort((left, right) => left.index - right.index)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Builds a field map from one section.
|
|
162
|
+
* @param {{ entries: { key: string, value: string }[] }} section Section row.
|
|
163
|
+
* @returns {Record<string, string>}
|
|
164
|
+
*/
|
|
165
|
+
static #sectionFields(section) {
|
|
166
|
+
const fields = {}
|
|
167
|
+
for (const entry of section?.entries || []) {
|
|
168
|
+
fields[entry.key] = entry.value
|
|
169
|
+
}
|
|
170
|
+
return fields
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Reads the first matching field.
|
|
175
|
+
* @param {Record<string, string>} fields Field map.
|
|
176
|
+
* @param {string[]} names Candidate names.
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
static #field(fields, names) {
|
|
180
|
+
for (const name of names) {
|
|
181
|
+
const lower = name.toLowerCase()
|
|
182
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
183
|
+
if (key.toLowerCase() === lower) {
|
|
184
|
+
return String(value || '').trim()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return ''
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Builds a compact index by field.
|
|
193
|
+
* @param {object[]} rows Rows.
|
|
194
|
+
* @param {string} key Key.
|
|
195
|
+
* @returns {Record<string, object>}
|
|
196
|
+
*/
|
|
197
|
+
static #indexBy(rows, key) {
|
|
198
|
+
const index = {}
|
|
199
|
+
for (const row of rows || []) {
|
|
200
|
+
const value = String(row?.[key] || '').trim()
|
|
201
|
+
if (value) index[value] = row
|
|
202
|
+
}
|
|
203
|
+
return index
|
|
204
|
+
}
|
|
205
|
+
}
|