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
|
@@ -24,12 +24,22 @@ export class PcbEmbeddedModelExtractor {
|
|
|
24
24
|
PcbEmbeddedModelExtractor.#parseModelMetadataStream(
|
|
25
25
|
streams.get('Models/Data')
|
|
26
26
|
)
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const modelMetadataRows = modelMetadataRecords.map((fields, index) => ({
|
|
28
|
+
fields,
|
|
29
|
+
index,
|
|
30
|
+
sourceStream: 'Models/' + index,
|
|
31
|
+
id: PcbEmbeddedModelExtractor.#getField(fields, 'ID'),
|
|
32
|
+
name: PcbEmbeddedModelExtractor.#getField(fields, 'NAME'),
|
|
33
|
+
checksum: PcbEmbeddedModelExtractor.#normalizeChecksum(
|
|
34
|
+
PcbEmbeddedModelExtractor.#parseIntegerField(fields, 'CHECKSUM')
|
|
35
|
+
)
|
|
36
|
+
}))
|
|
37
|
+
const models = modelMetadataRows
|
|
38
|
+
.map((row) =>
|
|
29
39
|
PcbEmbeddedModelExtractor.#normalizeEmbeddedModel(
|
|
30
|
-
|
|
31
|
-
streams.get(
|
|
32
|
-
|
|
40
|
+
row.fields,
|
|
41
|
+
streams.get(row.sourceStream),
|
|
42
|
+
row.sourceStream
|
|
33
43
|
)
|
|
34
44
|
)
|
|
35
45
|
.filter(Boolean)
|
|
@@ -44,10 +54,18 @@ export class PcbEmbeddedModelExtractor {
|
|
|
44
54
|
'ShapeBasedComponentBodies6/Data'
|
|
45
55
|
)
|
|
46
56
|
])
|
|
57
|
+
const integrity = PcbEmbeddedModelExtractor.#buildIntegrityReport(
|
|
58
|
+
modelMetadataRows,
|
|
59
|
+
models,
|
|
60
|
+
componentBodies,
|
|
61
|
+
streams
|
|
62
|
+
)
|
|
47
63
|
|
|
48
64
|
return {
|
|
49
65
|
models,
|
|
50
|
-
componentBodies
|
|
66
|
+
componentBodies,
|
|
67
|
+
integrity,
|
|
68
|
+
diagnostics: integrity.issues
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
|
|
@@ -350,6 +368,193 @@ export class PcbEmbeddedModelExtractor {
|
|
|
350
368
|
return [...uniqueBodies.values()]
|
|
351
369
|
}
|
|
352
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Builds model metadata and payload integrity diagnostics.
|
|
373
|
+
* @param {object[]} metadataRows Parsed model metadata rows.
|
|
374
|
+
* @param {object[]} models Recovered payload models.
|
|
375
|
+
* @param {object[]} componentBodies Recovered component bodies.
|
|
376
|
+
* @param {Map<string, Uint8Array>} streams Compound streams.
|
|
377
|
+
* @returns {{ schema: string, issues: object[] }}
|
|
378
|
+
*/
|
|
379
|
+
static #buildIntegrityReport(
|
|
380
|
+
metadataRows,
|
|
381
|
+
models,
|
|
382
|
+
componentBodies,
|
|
383
|
+
streams
|
|
384
|
+
) {
|
|
385
|
+
const issues = [
|
|
386
|
+
...PcbEmbeddedModelExtractor.#missingPayloadIssues(
|
|
387
|
+
metadataRows,
|
|
388
|
+
models,
|
|
389
|
+
streams
|
|
390
|
+
),
|
|
391
|
+
...PcbEmbeddedModelExtractor.#duplicateChecksumIssues(metadataRows),
|
|
392
|
+
...PcbEmbeddedModelExtractor.#unresolvedBodyIssues(
|
|
393
|
+
componentBodies,
|
|
394
|
+
models
|
|
395
|
+
),
|
|
396
|
+
...PcbEmbeddedModelExtractor.#unreferencedModelIssues(
|
|
397
|
+
models,
|
|
398
|
+
componentBodies
|
|
399
|
+
)
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
schema: 'altium-toolkit.pcb.embedded-model-integrity.a1',
|
|
404
|
+
issues
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Reports metadata rows without a recoverable payload stream.
|
|
410
|
+
* @param {object[]} metadataRows Parsed metadata rows.
|
|
411
|
+
* @param {object[]} models Recovered model rows.
|
|
412
|
+
* @param {Map<string, Uint8Array>} streams Compound streams.
|
|
413
|
+
* @returns {object[]}
|
|
414
|
+
*/
|
|
415
|
+
static #missingPayloadIssues(metadataRows, models, streams) {
|
|
416
|
+
return (metadataRows || [])
|
|
417
|
+
.filter(
|
|
418
|
+
(row) =>
|
|
419
|
+
!streams.has(row.sourceStream) ||
|
|
420
|
+
!models.some((model) => model.id === row.id)
|
|
421
|
+
)
|
|
422
|
+
.map((row) => ({
|
|
423
|
+
code: streams.has(row.sourceStream)
|
|
424
|
+
? 'pcb.model.payload-unreadable'
|
|
425
|
+
: 'pcb.model.payload-missing',
|
|
426
|
+
severity: 'warning',
|
|
427
|
+
modelId: row.id,
|
|
428
|
+
checksum: row.checksum,
|
|
429
|
+
name: row.name,
|
|
430
|
+
sourceStream: row.sourceStream,
|
|
431
|
+
message: streams.has(row.sourceStream)
|
|
432
|
+
? 'Embedded model payload stream could not be decoded.'
|
|
433
|
+
: 'Embedded model metadata references a missing payload stream.'
|
|
434
|
+
}))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Reports duplicate authored model checksums.
|
|
439
|
+
* @param {object[]} metadataRows Parsed metadata rows.
|
|
440
|
+
* @returns {object[]}
|
|
441
|
+
*/
|
|
442
|
+
static #duplicateChecksumIssues(metadataRows) {
|
|
443
|
+
const rowsByChecksum = new Map()
|
|
444
|
+
|
|
445
|
+
for (const row of metadataRows || []) {
|
|
446
|
+
if (!Number.isInteger(row.checksum)) {
|
|
447
|
+
continue
|
|
448
|
+
}
|
|
449
|
+
if (!rowsByChecksum.has(row.checksum)) {
|
|
450
|
+
rowsByChecksum.set(row.checksum, [])
|
|
451
|
+
}
|
|
452
|
+
rowsByChecksum.get(row.checksum).push(row)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return [...rowsByChecksum.entries()]
|
|
456
|
+
.filter(([, rows]) => rows.length > 1)
|
|
457
|
+
.map(([checksum, rows]) => ({
|
|
458
|
+
code: 'pcb.model.checksum-duplicate',
|
|
459
|
+
severity: 'warning',
|
|
460
|
+
checksum,
|
|
461
|
+
modelIds: rows.map((row) => row.id).filter(Boolean),
|
|
462
|
+
sourceStreams: rows.map((row) => row.sourceStream),
|
|
463
|
+
message:
|
|
464
|
+
'Multiple embedded model metadata rows share one checksum.'
|
|
465
|
+
}))
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Reports component bodies that reference no recovered model.
|
|
470
|
+
* @param {object[]} componentBodies Component-body rows.
|
|
471
|
+
* @param {object[]} models Recovered model rows.
|
|
472
|
+
* @returns {object[]}
|
|
473
|
+
*/
|
|
474
|
+
static #unresolvedBodyIssues(componentBodies, models) {
|
|
475
|
+
return (componentBodies || [])
|
|
476
|
+
.filter(
|
|
477
|
+
(componentBody) =>
|
|
478
|
+
componentBody.embedded &&
|
|
479
|
+
!PcbEmbeddedModelExtractor.#bodyMatchesAnyModel(
|
|
480
|
+
componentBody,
|
|
481
|
+
models
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
.map((componentBody) => ({
|
|
485
|
+
code: 'pcb.model.body-unresolved',
|
|
486
|
+
severity: 'warning',
|
|
487
|
+
modelId: componentBody.modelId,
|
|
488
|
+
checksum: componentBody.checksum,
|
|
489
|
+
name: componentBody.name,
|
|
490
|
+
sourceStream: componentBody.sourceStream,
|
|
491
|
+
message:
|
|
492
|
+
'Component body references an embedded model that was not recovered.'
|
|
493
|
+
}))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Reports recovered model payloads not referenced by any component body.
|
|
498
|
+
* @param {object[]} models Recovered model rows.
|
|
499
|
+
* @param {object[]} componentBodies Component-body rows.
|
|
500
|
+
* @returns {object[]}
|
|
501
|
+
*/
|
|
502
|
+
static #unreferencedModelIssues(models, componentBodies) {
|
|
503
|
+
if (!(componentBodies || []).length) {
|
|
504
|
+
return []
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return (models || [])
|
|
508
|
+
.filter(
|
|
509
|
+
(model) =>
|
|
510
|
+
!(componentBodies || []).some((componentBody) =>
|
|
511
|
+
PcbEmbeddedModelExtractor.#bodyMatchesModel(
|
|
512
|
+
componentBody,
|
|
513
|
+
model
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
.map((model) => ({
|
|
518
|
+
code: 'pcb.model.payload-unreferenced',
|
|
519
|
+
severity: 'info',
|
|
520
|
+
modelId: model.id,
|
|
521
|
+
checksum: model.checksum,
|
|
522
|
+
name: model.name,
|
|
523
|
+
sourceStream: model.sourceStream,
|
|
524
|
+
message:
|
|
525
|
+
'Embedded model payload was recovered but no component body references it.'
|
|
526
|
+
}))
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Returns true when a component body matches any recovered model.
|
|
531
|
+
* @param {object} componentBody Component body.
|
|
532
|
+
* @param {object[]} models Recovered models.
|
|
533
|
+
* @returns {boolean}
|
|
534
|
+
*/
|
|
535
|
+
static #bodyMatchesAnyModel(componentBody, models) {
|
|
536
|
+
return (models || []).some((model) =>
|
|
537
|
+
PcbEmbeddedModelExtractor.#bodyMatchesModel(componentBody, model)
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Returns true when a component body references a model.
|
|
543
|
+
* @param {object} componentBody Component body.
|
|
544
|
+
* @param {object} model Recovered model.
|
|
545
|
+
* @returns {boolean}
|
|
546
|
+
*/
|
|
547
|
+
static #bodyMatchesModel(componentBody, model) {
|
|
548
|
+
return (
|
|
549
|
+
(componentBody.modelId && componentBody.modelId === model.id) ||
|
|
550
|
+
(Number.isInteger(componentBody.checksum) &&
|
|
551
|
+
componentBody.checksum === model.checksum) ||
|
|
552
|
+
(componentBody.name &&
|
|
553
|
+
String(componentBody.name).toLowerCase() ===
|
|
554
|
+
String(model.name || '').toLowerCase())
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
|
|
353
558
|
/**
|
|
354
559
|
* Resolves one model format from metadata and payload text.
|
|
355
560
|
* @param {string} name
|
|
@@ -374,6 +579,27 @@ export class PcbEmbeddedModelExtractor {
|
|
|
374
579
|
return 'wrl'
|
|
375
580
|
}
|
|
376
581
|
|
|
582
|
+
if (
|
|
583
|
+
normalizedName.endsWith('.sldprt') ||
|
|
584
|
+
normalizedName.endsWith('.sldasm')
|
|
585
|
+
) {
|
|
586
|
+
return 'solidworks'
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (
|
|
590
|
+
normalizedName.endsWith('.x_t') ||
|
|
591
|
+
normalizedName.endsWith('.xmt_txt')
|
|
592
|
+
) {
|
|
593
|
+
return 'parasolid-text'
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (
|
|
597
|
+
normalizedName.endsWith('.x_b') ||
|
|
598
|
+
normalizedName.endsWith('.xmt_bin')
|
|
599
|
+
) {
|
|
600
|
+
return 'parasolid-binary'
|
|
601
|
+
}
|
|
602
|
+
|
|
377
603
|
return 'unknown'
|
|
378
604
|
}
|
|
379
605
|
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbSidecarRecordParser } from './PcbSidecarRecordParser.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Decodes extended primitive sidecar records such as mask-expansion overrides.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbExtendedPrimitiveInformationParser {
|
|
11
|
+
static #SOURCE_STREAM = 'ExtendedPrimitiveInformation/Data'
|
|
12
|
+
|
|
13
|
+
static #OBJECT_ID_TO_COLLECTION = {
|
|
14
|
+
1: ['arcs', 'arc'],
|
|
15
|
+
2: ['pads', 'pad'],
|
|
16
|
+
3: ['vias', 'via'],
|
|
17
|
+
4: ['tracks', 'track'],
|
|
18
|
+
5: ['texts', 'text'],
|
|
19
|
+
6: ['fills', 'fill'],
|
|
20
|
+
11: ['regions', 'region']
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses extended primitive information records.
|
|
25
|
+
* @param {Uint8Array | ArrayBuffer | undefined} dataBytes
|
|
26
|
+
* @param {string} [sourceStream]
|
|
27
|
+
* @returns {{ entries: object[], byPrimitiveIndex: Record<string, object>, byPrimitiveKey: Record<string, object> }}
|
|
28
|
+
*/
|
|
29
|
+
static parse(
|
|
30
|
+
dataBytes,
|
|
31
|
+
sourceStream = PcbExtendedPrimitiveInformationParser.#SOURCE_STREAM
|
|
32
|
+
) {
|
|
33
|
+
const entries = PcbSidecarRecordParser.parseLengthPrefixedRecords(
|
|
34
|
+
dataBytes,
|
|
35
|
+
sourceStream
|
|
36
|
+
)
|
|
37
|
+
.map((record) =>
|
|
38
|
+
PcbExtendedPrimitiveInformationParser.#normalizeRecord(record)
|
|
39
|
+
)
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
|
|
42
|
+
return PcbExtendedPrimitiveInformationParser.#buildLookups(entries)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Adds extended primitive information to matching decoded primitives.
|
|
47
|
+
* @param {Record<string, object[]>} binaryPrimitives
|
|
48
|
+
* @param {{ entries?: object[] }} extendedInformation
|
|
49
|
+
*/
|
|
50
|
+
static attachToPrimitives(binaryPrimitives, extendedInformation) {
|
|
51
|
+
if (!binaryPrimitives || !Array.isArray(extendedInformation?.entries)) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const entry of extendedInformation.entries) {
|
|
56
|
+
const collectionName =
|
|
57
|
+
PcbExtendedPrimitiveInformationParser.#collectionNameForEntry(
|
|
58
|
+
entry
|
|
59
|
+
)
|
|
60
|
+
const collection = binaryPrimitives[collectionName]
|
|
61
|
+
|
|
62
|
+
if (!Array.isArray(collection)) {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const primitive = collection[entry.primitiveIndex]
|
|
67
|
+
if (!primitive) {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
primitive.extendedPrimitiveInformation =
|
|
72
|
+
PcbExtendedPrimitiveInformationParser.#publicEntry(entry)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalizes one decoded sidecar record.
|
|
78
|
+
* @param {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }} record
|
|
79
|
+
* @returns {object | null}
|
|
80
|
+
*/
|
|
81
|
+
static #normalizeRecord(record) {
|
|
82
|
+
const primitiveIndex = PcbSidecarRecordParser.parseInteger(
|
|
83
|
+
PcbSidecarRecordParser.firstField(record.fields, [
|
|
84
|
+
'PRIMITIVEINDEX',
|
|
85
|
+
'INDEX'
|
|
86
|
+
])
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (primitiveIndex === null) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const primitiveObjectId = PcbSidecarRecordParser.parseInteger(
|
|
94
|
+
PcbSidecarRecordParser.firstField(record.fields, [
|
|
95
|
+
'PRIMITIVEOBJECTID',
|
|
96
|
+
'OBJECTID'
|
|
97
|
+
])
|
|
98
|
+
)
|
|
99
|
+
const objectInfo =
|
|
100
|
+
PcbExtendedPrimitiveInformationParser.#OBJECT_ID_TO_COLLECTION[
|
|
101
|
+
primitiveObjectId
|
|
102
|
+
] || []
|
|
103
|
+
const type = PcbSidecarRecordParser.firstField(record.fields, ['TYPE'])
|
|
104
|
+
const primitiveType =
|
|
105
|
+
objectInfo[1] ||
|
|
106
|
+
PcbExtendedPrimitiveInformationParser.#normalizePrimitiveType(type)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
primitiveIndex,
|
|
110
|
+
primitiveObjectId,
|
|
111
|
+
primitiveType,
|
|
112
|
+
type,
|
|
113
|
+
sourceStream: record.sourceStream,
|
|
114
|
+
maskExpansion:
|
|
115
|
+
PcbExtendedPrimitiveInformationParser.#parseMaskExpansion(
|
|
116
|
+
record.fields
|
|
117
|
+
),
|
|
118
|
+
fields: record.fields
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parses paste and solder mask-expansion fields.
|
|
124
|
+
* @param {Record<string, string>} fields
|
|
125
|
+
* @returns {{ paste: object, solder: object }}
|
|
126
|
+
*/
|
|
127
|
+
static #parseMaskExpansion(fields) {
|
|
128
|
+
const pasteMode = PcbSidecarRecordParser.parseInteger(
|
|
129
|
+
PcbSidecarRecordParser.firstField(fields, [
|
|
130
|
+
'PASTEMASKEXPANSIONMODE',
|
|
131
|
+
'PASTEMASKEXPANSION_MODE'
|
|
132
|
+
])
|
|
133
|
+
)
|
|
134
|
+
const solderMode = PcbSidecarRecordParser.parseInteger(
|
|
135
|
+
PcbSidecarRecordParser.firstField(fields, [
|
|
136
|
+
'SOLDERMASKEXPANSIONMODE',
|
|
137
|
+
'SOLDERMASKEXPANSION_MODE'
|
|
138
|
+
])
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
paste: {
|
|
143
|
+
mode: pasteMode,
|
|
144
|
+
source: PcbExtendedPrimitiveInformationParser.#maskExpansionSource(
|
|
145
|
+
pasteMode
|
|
146
|
+
),
|
|
147
|
+
manualExpansion: PcbSidecarRecordParser.parseNumber(
|
|
148
|
+
PcbSidecarRecordParser.firstField(fields, [
|
|
149
|
+
'PASTEMASKEXPANSION_MANUAL',
|
|
150
|
+
'PASTEMASKEXPANSIONMANUAL'
|
|
151
|
+
])
|
|
152
|
+
)
|
|
153
|
+
},
|
|
154
|
+
solder: {
|
|
155
|
+
mode: solderMode,
|
|
156
|
+
source: PcbExtendedPrimitiveInformationParser.#maskExpansionSource(
|
|
157
|
+
solderMode
|
|
158
|
+
),
|
|
159
|
+
manualExpansion: PcbSidecarRecordParser.parseNumber(
|
|
160
|
+
PcbSidecarRecordParser.firstField(fields, [
|
|
161
|
+
'SOLDERMASKEXPANSION_MANUAL',
|
|
162
|
+
'SOLDERMASKEXPANSIONMANUAL'
|
|
163
|
+
])
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Builds primitive-index lookups.
|
|
171
|
+
* @param {object[]} entries
|
|
172
|
+
* @returns {{ entries: object[], byPrimitiveIndex: Record<string, object>, byPrimitiveKey: Record<string, object> }}
|
|
173
|
+
*/
|
|
174
|
+
static #buildLookups(entries) {
|
|
175
|
+
const byPrimitiveIndex = {}
|
|
176
|
+
const byPrimitiveKey = {}
|
|
177
|
+
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
byPrimitiveIndex[String(entry.primitiveIndex)] = entry
|
|
180
|
+
if (Number.isInteger(entry.primitiveObjectId)) {
|
|
181
|
+
byPrimitiveKey[
|
|
182
|
+
entry.primitiveObjectId + ':' + entry.primitiveIndex
|
|
183
|
+
] = entry
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
entries,
|
|
189
|
+
byPrimitiveIndex,
|
|
190
|
+
byPrimitiveKey
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Resolves one binary primitive collection for a sidecar entry.
|
|
196
|
+
* @param {object} entry
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
static #collectionNameForEntry(entry) {
|
|
200
|
+
const objectInfo =
|
|
201
|
+
PcbExtendedPrimitiveInformationParser.#OBJECT_ID_TO_COLLECTION[
|
|
202
|
+
entry.primitiveObjectId
|
|
203
|
+
]
|
|
204
|
+
if (objectInfo) {
|
|
205
|
+
return objectInfo[0]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const primitiveType = String(entry.primitiveType || '').toLowerCase()
|
|
209
|
+
return primitiveType ? primitiveType + 's' : ''
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Builds the public primitive-attached entry.
|
|
214
|
+
* @param {object} entry
|
|
215
|
+
* @returns {object}
|
|
216
|
+
*/
|
|
217
|
+
static #publicEntry(entry) {
|
|
218
|
+
return {
|
|
219
|
+
primitiveIndex: entry.primitiveIndex,
|
|
220
|
+
primitiveObjectId: entry.primitiveObjectId,
|
|
221
|
+
primitiveType: entry.primitiveType,
|
|
222
|
+
type: entry.type,
|
|
223
|
+
sourceStream: entry.sourceStream,
|
|
224
|
+
maskExpansion: entry.maskExpansion
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Maps one mask-expansion mode to the existing source vocabulary.
|
|
230
|
+
* @param {number | null} mode
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
static #maskExpansionSource(mode) {
|
|
234
|
+
if (mode === 1) return 'rule'
|
|
235
|
+
if (mode === 2) return 'manual'
|
|
236
|
+
if (mode === 0) return 'default'
|
|
237
|
+
if (mode === null) return 'unknown'
|
|
238
|
+
|
|
239
|
+
return 'unknown-' + mode
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Normalizes one textual primitive type hint.
|
|
244
|
+
* @param {string} value
|
|
245
|
+
* @returns {string}
|
|
246
|
+
*/
|
|
247
|
+
static #normalizePrimitiveType(value) {
|
|
248
|
+
const normalized = String(value || '')
|
|
249
|
+
.trim()
|
|
250
|
+
.toLowerCase()
|
|
251
|
+
.replace(/^e/iu, '')
|
|
252
|
+
.replace(/object$/iu, '')
|
|
253
|
+
|
|
254
|
+
return normalized
|
|
255
|
+
}
|
|
256
|
+
}
|