altium-toolkit 1.0.10 → 1.1.0
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/docs/api.md +6 -2
- package/docs/model-format.md +29 -4
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
- package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +132 -1
- package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
- package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +692 -2
- package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
- package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
- package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
- package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
- package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
- package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
- package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +7 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +16 -5
- package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
- package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +246 -7
- package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
- package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
- package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
- package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
- package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
- package/src/core/altium/PcbLibModelParser.mjs +2 -0
- package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
- package/src/core/altium/PcbModelParser.mjs +182 -18
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +3 -0
- package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
- package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
- package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
- package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
- package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
- package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
- package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
- package/src/core/altium/PrintableTextDecoder.mjs +70 -6
- package/src/core/altium/PrjPcbModelParser.mjs +45 -0
- package/src/core/altium/PrjScrModelParser.mjs +386 -0
- package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +61 -2
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +35 -2
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +164 -0
- package/src/parser.mjs +15 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
- package/src/ui/PcbScene3dBuilder.mjs +26 -4
- package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
- package/src/ui/SchematicSvgRenderer.mjs +48 -2
|
@@ -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 { DraftsmanBoardViewMetadataBuilder } from './DraftsmanBoardViewMetadataBuilder.mjs'
|
|
7
|
+
import { DraftsmanImagePayloadManifestBuilder } from './DraftsmanImagePayloadManifestBuilder.mjs'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Builds a read-only digest for Draftsman drawing containers.
|
|
@@ -35,10 +37,16 @@ export class DraftsmanDigestParser {
|
|
|
35
37
|
|
|
36
38
|
const rootFields = DraftsmanDigestParser.#rootFields(text)
|
|
37
39
|
const pages = DraftsmanDigestParser.#pages(text)
|
|
40
|
+
const styles = DraftsmanDigestParser.#styleCatalog(text)
|
|
38
41
|
const unsupportedRawItemCount = pages.reduce(
|
|
39
42
|
(total, page) => total + page.unsupportedRawItems.length,
|
|
40
43
|
0
|
|
41
44
|
)
|
|
45
|
+
const imagePayloads = DraftsmanImagePayloadManifestBuilder.build(pages)
|
|
46
|
+
const boardViewMetadata = DraftsmanBoardViewMetadataBuilder.build(
|
|
47
|
+
text,
|
|
48
|
+
pages
|
|
49
|
+
)
|
|
42
50
|
const diagnostics =
|
|
43
51
|
unsupportedRawItemCount > 0
|
|
44
52
|
? [
|
|
@@ -59,7 +67,11 @@ export class DraftsmanDigestParser {
|
|
|
59
67
|
rootFields.PcbDoc ||
|
|
60
68
|
rootFields.DocumentName ||
|
|
61
69
|
'',
|
|
70
|
+
documentOptions: DraftsmanDigestParser.#documentOptions(rootFields),
|
|
71
|
+
styles,
|
|
62
72
|
pages,
|
|
73
|
+
imagePayloads,
|
|
74
|
+
boardViewMetadata,
|
|
63
75
|
diagnostics
|
|
64
76
|
})
|
|
65
77
|
}
|
|
@@ -81,7 +93,7 @@ export class DraftsmanDigestParser {
|
|
|
81
93
|
/**
|
|
82
94
|
* Builds the normalized parser root model.
|
|
83
95
|
* @param {string} fileName File name.
|
|
84
|
-
* @param {{ sourceDocumentName: string, pages: object[], diagnostics: object[] }} digest Digest payload.
|
|
96
|
+
* @param {{ sourceDocumentName: string, documentOptions?: object, styles?: object, pages: object[], imagePayloads?: object, diagnostics: object[] }} digest Digest payload.
|
|
85
97
|
* @returns {object}
|
|
86
98
|
*/
|
|
87
99
|
static #model(fileName, digest) {
|
|
@@ -97,6 +109,7 @@ export class DraftsmanDigestParser {
|
|
|
97
109
|
(total, page) => total + page.unsupportedRawItems.length,
|
|
98
110
|
0
|
|
99
111
|
)
|
|
112
|
+
const fontStyleCount = (digest.styles?.fontStyles || []).length
|
|
100
113
|
|
|
101
114
|
return NormalizedModelSchema.attach({
|
|
102
115
|
kind: 'draftsman',
|
|
@@ -107,13 +120,20 @@ export class DraftsmanDigestParser {
|
|
|
107
120
|
pageCount: digest.pages.length,
|
|
108
121
|
noteCount,
|
|
109
122
|
imageCount,
|
|
123
|
+
fontStyleCount,
|
|
110
124
|
unsupportedRawItemCount
|
|
111
125
|
},
|
|
112
126
|
diagnostics: digest.diagnostics,
|
|
113
127
|
draftsman: {
|
|
114
128
|
schema: DraftsmanDigestParser.DIGEST_SCHEMA,
|
|
115
129
|
sourceDocumentName: digest.sourceDocumentName,
|
|
130
|
+
documentOptions: digest.documentOptions,
|
|
131
|
+
styles: digest.styles,
|
|
116
132
|
pages: digest.pages,
|
|
133
|
+
imagePayloads: digest.imagePayloads,
|
|
134
|
+
...(digest.boardViewMetadata
|
|
135
|
+
? { boardViewMetadata: digest.boardViewMetadata }
|
|
136
|
+
: {}),
|
|
117
137
|
indexes: DraftsmanDigestParser.#indexes(digest.pages)
|
|
118
138
|
},
|
|
119
139
|
bom: []
|
|
@@ -128,11 +148,21 @@ export class DraftsmanDigestParser {
|
|
|
128
148
|
static #indexes(pages) {
|
|
129
149
|
const pagesById = {}
|
|
130
150
|
const pagesByName = {}
|
|
151
|
+
const itemsById = {}
|
|
152
|
+
const imagesById = {}
|
|
131
153
|
for (const page of pages) {
|
|
132
154
|
if (page.id) pagesById[page.id] = page.index
|
|
133
155
|
if (page.name) pagesByName[page.name] = page.index
|
|
156
|
+
for (const [index, item] of (page.items || []).entries()) {
|
|
157
|
+
if (!item.id) continue
|
|
158
|
+
itemsById[item.id] = { pageIndex: page.index, index }
|
|
159
|
+
}
|
|
160
|
+
for (const [index, image] of (page.images || []).entries()) {
|
|
161
|
+
if (!image.id) continue
|
|
162
|
+
imagesById[image.id] = { pageIndex: page.index, index }
|
|
163
|
+
}
|
|
134
164
|
}
|
|
135
|
-
return { pagesById, pagesByName }
|
|
165
|
+
return { pagesById, pagesByName, itemsById, imagesById }
|
|
136
166
|
}
|
|
137
167
|
|
|
138
168
|
/**
|
|
@@ -407,6 +437,30 @@ export class DraftsmanDigestParser {
|
|
|
407
437
|
return DraftsmanDigestParser.#attributes(match?.[2] || '')
|
|
408
438
|
}
|
|
409
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Normalizes document-level display and sheet options.
|
|
442
|
+
* @param {Record<string, string>} fields Root element attributes.
|
|
443
|
+
* @returns {object}
|
|
444
|
+
*/
|
|
445
|
+
static #documentOptions(fields) {
|
|
446
|
+
return DraftsmanDigestParser.#stripEmpty({
|
|
447
|
+
defaultFontName:
|
|
448
|
+
fields.DefaultFontName ||
|
|
449
|
+
fields.FontName ||
|
|
450
|
+
fields.DefaultFont ||
|
|
451
|
+
undefined,
|
|
452
|
+
documentId: fields.DocumentId || fields.DocumentID,
|
|
453
|
+
revision: fields.Revision || fields.DocumentRevision,
|
|
454
|
+
gridSize: DraftsmanDigestParser.#number(fields.GridSize),
|
|
455
|
+
showGrid: DraftsmanDigestParser.#boolean(fields.ShowGrid),
|
|
456
|
+
sheetColor: fields.SheetColor,
|
|
457
|
+
backgroundColor: fields.BackgroundColor,
|
|
458
|
+
borderColor: fields.BorderColor,
|
|
459
|
+
gridColor: fields.GridColor,
|
|
460
|
+
fields
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
|
|
410
464
|
/**
|
|
411
465
|
* Extracts page digests.
|
|
412
466
|
* @param {string} text Decoded payload.
|
|
@@ -437,17 +491,51 @@ export class DraftsmanDigestParser {
|
|
|
437
491
|
*/
|
|
438
492
|
static #page(fields, body, index) {
|
|
439
493
|
const name = fields.Name || fields.Title || fields.Id || ''
|
|
440
|
-
return {
|
|
494
|
+
return DraftsmanDigestParser.#stripEmpty({
|
|
441
495
|
index,
|
|
442
496
|
id: fields.Id || fields.ID || '',
|
|
443
497
|
name,
|
|
444
498
|
title: fields.Title || name || 'Page ' + (index + 1),
|
|
499
|
+
pageSetup: DraftsmanDigestParser.#pageSetup(fields),
|
|
445
500
|
titleBlocks: DraftsmanDigestParser.#titleBlocks(body),
|
|
446
501
|
notes: DraftsmanDigestParser.#notes(body),
|
|
447
502
|
images: DraftsmanDigestParser.#images(body),
|
|
503
|
+
zones: DraftsmanDigestParser.#zones(body),
|
|
504
|
+
items: DraftsmanDigestParser.#items(body),
|
|
448
505
|
unsupportedRawItems:
|
|
449
506
|
DraftsmanDigestParser.#unsupportedRawItems(body)
|
|
450
|
-
}
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Normalizes page dimensions and margins.
|
|
512
|
+
* @param {Record<string, string>} fields Page attributes.
|
|
513
|
+
* @returns {object}
|
|
514
|
+
*/
|
|
515
|
+
static #pageSetup(fields) {
|
|
516
|
+
const margins = DraftsmanDigestParser.#stripEmpty({
|
|
517
|
+
left: DraftsmanDigestParser.#number(fields.MarginLeft),
|
|
518
|
+
right: DraftsmanDigestParser.#number(fields.MarginRight),
|
|
519
|
+
top: DraftsmanDigestParser.#number(fields.MarginTop),
|
|
520
|
+
bottom: DraftsmanDigestParser.#number(fields.MarginBottom)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
const setup = DraftsmanDigestParser.#stripEmpty({
|
|
524
|
+
width: DraftsmanDigestParser.#number(
|
|
525
|
+
fields.Width || fields.SheetWidth
|
|
526
|
+
),
|
|
527
|
+
height: DraftsmanDigestParser.#number(
|
|
528
|
+
fields.Height || fields.SheetHeight
|
|
529
|
+
),
|
|
530
|
+
standardSheetSize:
|
|
531
|
+
fields.StandardSheetSize || fields.SheetSize || undefined,
|
|
532
|
+
sheetTemplate: fields.SheetTemplate || fields.Template,
|
|
533
|
+
borderStyle: fields.BorderStyle,
|
|
534
|
+
orientation: fields.Orientation,
|
|
535
|
+
margins: Object.keys(margins).length ? margins : undefined
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
return Object.keys(setup).length ? setup : undefined
|
|
451
539
|
}
|
|
452
540
|
|
|
453
541
|
/**
|
|
@@ -474,14 +562,33 @@ export class DraftsmanDigestParser {
|
|
|
474
562
|
*/
|
|
475
563
|
static #notes(body) {
|
|
476
564
|
return DraftsmanDigestParser.#tagFields(body, ['Note', 'Text']).map(
|
|
477
|
-
(fields) =>
|
|
478
|
-
DraftsmanDigestParser.#stripEmpty({
|
|
565
|
+
(fields) => {
|
|
566
|
+
const border = DraftsmanDigestParser.#stripEmpty({
|
|
567
|
+
width: DraftsmanDigestParser.#number(fields.BorderWidth),
|
|
568
|
+
style: DraftsmanDigestParser.#lower(fields.BorderStyle),
|
|
569
|
+
color: fields.BorderColor,
|
|
570
|
+
visible: DraftsmanDigestParser.#boolean(fields.ShowBorder)
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
return DraftsmanDigestParser.#stripEmpty({
|
|
479
574
|
id: fields.Id || fields.ID,
|
|
480
575
|
text: fields.Text || fields.Value || fields.Name,
|
|
481
576
|
x: DraftsmanDigestParser.#number(fields.X),
|
|
482
577
|
y: DraftsmanDigestParser.#number(fields.Y),
|
|
578
|
+
width: DraftsmanDigestParser.#number(fields.Width),
|
|
579
|
+
height: DraftsmanDigestParser.#number(fields.Height),
|
|
580
|
+
alignment: DraftsmanDigestParser.#lower(
|
|
581
|
+
fields.Alignment || fields.HorizontalAlignment
|
|
582
|
+
),
|
|
583
|
+
verticalAlignment: DraftsmanDigestParser.#lower(
|
|
584
|
+
fields.VerticalAlignment
|
|
585
|
+
),
|
|
586
|
+
fontStyleId: fields.FontStyleId || fields.FontStyleID,
|
|
587
|
+
border: Object.keys(border).length ? border : undefined,
|
|
588
|
+
fillColor: fields.FillColor || fields.AreaColor,
|
|
483
589
|
fields
|
|
484
590
|
})
|
|
591
|
+
}
|
|
485
592
|
)
|
|
486
593
|
}
|
|
487
594
|
|
|
@@ -497,9 +604,103 @@ export class DraftsmanDigestParser {
|
|
|
497
604
|
id: fields.Id || fields.ID,
|
|
498
605
|
name: fields.Name || fields.FileName,
|
|
499
606
|
nativeFormat: fields.NativeFormat || fields.Format,
|
|
607
|
+
wrapperType: fields.WrapperType || fields.Wrapper,
|
|
500
608
|
byteSize: DraftsmanDigestParser.#integer(fields.ByteSize),
|
|
609
|
+
x: DraftsmanDigestParser.#number(fields.X),
|
|
610
|
+
y: DraftsmanDigestParser.#number(fields.Y),
|
|
611
|
+
width: DraftsmanDigestParser.#number(fields.Width),
|
|
612
|
+
height: DraftsmanDigestParser.#number(fields.Height),
|
|
613
|
+
rotation: DraftsmanDigestParser.#number(fields.Rotation),
|
|
614
|
+
fields
|
|
615
|
+
})
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Extracts typed style rows from a Draftsman text container.
|
|
621
|
+
* @param {string} text Decoded payload.
|
|
622
|
+
* @returns {{ fontStyles: object[] }}
|
|
623
|
+
*/
|
|
624
|
+
static #styleCatalog(text) {
|
|
625
|
+
return {
|
|
626
|
+
fontStyles: DraftsmanDigestParser.#shallowTagFields(text, [
|
|
627
|
+
'FontStyle'
|
|
628
|
+
]).map((fields) =>
|
|
629
|
+
DraftsmanDigestParser.#stripEmpty({
|
|
630
|
+
id: fields.Id || fields.ID,
|
|
631
|
+
fontName:
|
|
632
|
+
fields.FontName || fields.Name || fields.FamilyName,
|
|
633
|
+
size: DraftsmanDigestParser.#number(
|
|
634
|
+
fields.Size || fields.FontSize
|
|
635
|
+
),
|
|
636
|
+
bold: DraftsmanDigestParser.#boolean(fields.Bold),
|
|
637
|
+
italic: DraftsmanDigestParser.#boolean(fields.Italic),
|
|
638
|
+
underline: DraftsmanDigestParser.#boolean(fields.Underline),
|
|
639
|
+
color: fields.Color,
|
|
501
640
|
fields
|
|
502
641
|
})
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Extracts selected tags without consuming parent/child subtrees.
|
|
648
|
+
* @param {string} body Markup body.
|
|
649
|
+
* @param {string[]} tagNames Tag names.
|
|
650
|
+
* @returns {Record<string, string>[]}
|
|
651
|
+
*/
|
|
652
|
+
static #shallowTagFields(body, tagNames) {
|
|
653
|
+
const selected = new Set(tagNames)
|
|
654
|
+
const fields = []
|
|
655
|
+
const tagPattern = /<([A-Za-z][A-Za-z0-9_]*)\b([^>]*?)(?:\/?)>/gu
|
|
656
|
+
let match = tagPattern.exec(body || '')
|
|
657
|
+
|
|
658
|
+
while (match) {
|
|
659
|
+
if (selected.has(match[1])) {
|
|
660
|
+
fields.push(DraftsmanDigestParser.#attributes(match[2]))
|
|
661
|
+
}
|
|
662
|
+
match = tagPattern.exec(body || '')
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return fields
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Extracts page zone rows.
|
|
670
|
+
* @param {string} body Page body.
|
|
671
|
+
* @returns {object[]}
|
|
672
|
+
*/
|
|
673
|
+
static #zones(body) {
|
|
674
|
+
return DraftsmanDigestParser.#tagFields(body, [
|
|
675
|
+
'Zone',
|
|
676
|
+
'SheetZone'
|
|
677
|
+
]).map((fields) =>
|
|
678
|
+
DraftsmanDigestParser.#stripEmpty({
|
|
679
|
+
id: fields.Id || fields.ID,
|
|
680
|
+
name: fields.Name || fields.Title,
|
|
681
|
+
row: fields.Row,
|
|
682
|
+
column: fields.Column,
|
|
683
|
+
x1: DraftsmanDigestParser.#number(fields.X1),
|
|
684
|
+
y1: DraftsmanDigestParser.#number(fields.Y1),
|
|
685
|
+
x2: DraftsmanDigestParser.#number(fields.X2),
|
|
686
|
+
y2: DraftsmanDigestParser.#number(fields.Y2),
|
|
687
|
+
fields
|
|
688
|
+
})
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Extracts a stable item index for review tooling.
|
|
694
|
+
* @param {string} body Page body.
|
|
695
|
+
* @returns {object[]}
|
|
696
|
+
*/
|
|
697
|
+
static #items(body) {
|
|
698
|
+
return DraftsmanDigestParser.#tags(body).map((tag) =>
|
|
699
|
+
DraftsmanDigestParser.#stripEmpty({
|
|
700
|
+
kind: DraftsmanDigestParser.#itemKind(tag.kind),
|
|
701
|
+
id: tag.fields.Id || tag.fields.ID,
|
|
702
|
+
name: tag.fields.Name || tag.fields.Title || tag.fields.Text
|
|
703
|
+
})
|
|
503
704
|
)
|
|
504
705
|
}
|
|
505
706
|
|
|
@@ -514,7 +715,9 @@ export class DraftsmanDigestParser {
|
|
|
514
715
|
'Note',
|
|
515
716
|
'Text',
|
|
516
717
|
'Image',
|
|
517
|
-
'Picture'
|
|
718
|
+
'Picture',
|
|
719
|
+
'Zone',
|
|
720
|
+
'SheetZone'
|
|
518
721
|
])
|
|
519
722
|
return DraftsmanDigestParser.#tags(body)
|
|
520
723
|
.filter((tag) => !supported.has(tag.kind))
|
|
@@ -676,6 +879,42 @@ export class DraftsmanDigestParser {
|
|
|
676
879
|
return Number.isFinite(numeric) ? numeric : undefined
|
|
677
880
|
}
|
|
678
881
|
|
|
882
|
+
/**
|
|
883
|
+
* Parses an optional boolean value.
|
|
884
|
+
* @param {string | undefined} value Raw value.
|
|
885
|
+
* @returns {boolean | undefined}
|
|
886
|
+
*/
|
|
887
|
+
static #boolean(value) {
|
|
888
|
+
if (value === undefined || value === null || value === '') {
|
|
889
|
+
return undefined
|
|
890
|
+
}
|
|
891
|
+
return /^(?:1|true|yes)$/iu.test(String(value).trim())
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Lowercases non-empty enum-like text.
|
|
896
|
+
* @param {string | undefined} value Raw value.
|
|
897
|
+
* @returns {string | undefined}
|
|
898
|
+
*/
|
|
899
|
+
static #lower(value) {
|
|
900
|
+
const text = String(value || '').trim()
|
|
901
|
+
return text ? text.toLowerCase() : undefined
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Normalizes one XML tag name into a digest item kind.
|
|
906
|
+
* @param {string} kind Tag name.
|
|
907
|
+
* @returns {string}
|
|
908
|
+
*/
|
|
909
|
+
static #itemKind(kind) {
|
|
910
|
+
const normalized = String(kind || '')
|
|
911
|
+
if (normalized === 'TitleBlock') return 'title-block'
|
|
912
|
+
if (normalized === 'Note' || normalized === 'Text') return 'note'
|
|
913
|
+
if (normalized === 'Image' || normalized === 'Picture') return 'image'
|
|
914
|
+
if (normalized === 'Zone' || normalized === 'SheetZone') return 'zone'
|
|
915
|
+
return normalized.replace(/([a-z])([A-Z])/gu, '$1-$2').toLowerCase()
|
|
916
|
+
}
|
|
917
|
+
|
|
679
918
|
/**
|
|
680
919
|
* Removes undefined fields from one descriptor.
|
|
681
920
|
* @param {Record<string, unknown>} value Candidate object.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic image-payload manifests for Draftsman digest images.
|
|
7
|
+
*/
|
|
8
|
+
export class DraftsmanImagePayloadManifestBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.draftsman.image-payloads.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a payload manifest from parsed Draftsman pages.
|
|
13
|
+
* @param {{ index: number, images?: object[] }[]} pages Parsed page rows.
|
|
14
|
+
* @returns {{ schema: string, summary: object, payloads: object[], diagnostics: object[] }}
|
|
15
|
+
*/
|
|
16
|
+
static build(pages) {
|
|
17
|
+
const imageRows = DraftsmanImagePayloadManifestBuilder.#imageRows(pages)
|
|
18
|
+
const payloads = []
|
|
19
|
+
const diagnostics = []
|
|
20
|
+
|
|
21
|
+
for (const image of imageRows) {
|
|
22
|
+
const bytes =
|
|
23
|
+
DraftsmanImagePayloadManifestBuilder.#payloadBytes(image)
|
|
24
|
+
if (!bytes.length) {
|
|
25
|
+
diagnostics.push(
|
|
26
|
+
DraftsmanImagePayloadManifestBuilder.#missingPayloadDiagnostic(
|
|
27
|
+
image
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
payloads.push(
|
|
34
|
+
DraftsmanImagePayloadManifestBuilder.#payloadRecord(
|
|
35
|
+
image,
|
|
36
|
+
bytes
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
schema: DraftsmanImagePayloadManifestBuilder.SCHEMA,
|
|
43
|
+
summary: {
|
|
44
|
+
imageCount: imageRows.length,
|
|
45
|
+
payloadCount: payloads.length,
|
|
46
|
+
diagnosticCount: diagnostics.length
|
|
47
|
+
},
|
|
48
|
+
payloads,
|
|
49
|
+
diagnostics
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Flattens page/image rows while preserving page and image indexes.
|
|
55
|
+
* @param {{ index: number, images?: object[] }[]} pages Parsed pages.
|
|
56
|
+
* @returns {object[]}
|
|
57
|
+
*/
|
|
58
|
+
static #imageRows(pages) {
|
|
59
|
+
return (pages || []).flatMap((page) =>
|
|
60
|
+
(page.images || []).map((image, index) => ({
|
|
61
|
+
...image,
|
|
62
|
+
pageIndex: page.index,
|
|
63
|
+
imageIndex: index
|
|
64
|
+
}))
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Builds one payload manifest record.
|
|
70
|
+
* @param {object} image Image descriptor.
|
|
71
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
72
|
+
* @returns {object}
|
|
73
|
+
*/
|
|
74
|
+
static #payloadRecord(image, bytes) {
|
|
75
|
+
return DraftsmanImagePayloadManifestBuilder.#stripUndefined({
|
|
76
|
+
pageIndex: image.pageIndex,
|
|
77
|
+
imageId: image.id,
|
|
78
|
+
name: image.name,
|
|
79
|
+
nativeFormat: image.nativeFormat,
|
|
80
|
+
wrapperType:
|
|
81
|
+
image.wrapperType ||
|
|
82
|
+
image.fields?.WrapperType ||
|
|
83
|
+
image.fields?.Wrapper ||
|
|
84
|
+
undefined,
|
|
85
|
+
byteSize: bytes.byteLength,
|
|
86
|
+
checksum: {
|
|
87
|
+
algorithm: 'fnv1a32',
|
|
88
|
+
value: DraftsmanImagePayloadManifestBuilder.#fnv1a32(bytes)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Builds a structured missing-payload diagnostic.
|
|
95
|
+
* @param {object} image Image descriptor.
|
|
96
|
+
* @returns {object}
|
|
97
|
+
*/
|
|
98
|
+
static #missingPayloadDiagnostic(image) {
|
|
99
|
+
return DraftsmanImagePayloadManifestBuilder.#stripUndefined({
|
|
100
|
+
code: 'draftsman.image-payload.missing-bytes',
|
|
101
|
+
severity: 'warning',
|
|
102
|
+
pageIndex: image.pageIndex,
|
|
103
|
+
imageId: image.id,
|
|
104
|
+
name: image.name,
|
|
105
|
+
message: 'Draftsman image item did not include payload bytes.'
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extracts base64 payload bytes from known image fields.
|
|
111
|
+
* @param {object} image Image descriptor.
|
|
112
|
+
* @returns {Uint8Array}
|
|
113
|
+
*/
|
|
114
|
+
static #payloadBytes(image) {
|
|
115
|
+
const fields = image?.fields || {}
|
|
116
|
+
const value =
|
|
117
|
+
fields.PayloadBase64 ||
|
|
118
|
+
fields.DataBase64 ||
|
|
119
|
+
fields.BytesBase64 ||
|
|
120
|
+
fields.NativePayloadBase64 ||
|
|
121
|
+
fields.BitmapBase64 ||
|
|
122
|
+
''
|
|
123
|
+
|
|
124
|
+
return DraftsmanImagePayloadManifestBuilder.#decodeBase64(value)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Decodes a base64 value without depending on Node-only globals.
|
|
129
|
+
* @param {string} value Base64 text.
|
|
130
|
+
* @returns {Uint8Array}
|
|
131
|
+
*/
|
|
132
|
+
static #decodeBase64(value) {
|
|
133
|
+
const normalized = String(value || '').replace(/\s+/gu, '')
|
|
134
|
+
if (!normalized) {
|
|
135
|
+
return new Uint8Array()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const binary = globalThis.atob(normalized)
|
|
140
|
+
const bytes = new Uint8Array(binary.length)
|
|
141
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
142
|
+
bytes[index] = binary.charCodeAt(index)
|
|
143
|
+
}
|
|
144
|
+
return bytes
|
|
145
|
+
} catch {
|
|
146
|
+
return new Uint8Array()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Computes an FNV-1a 32-bit checksum.
|
|
152
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
static #fnv1a32(bytes) {
|
|
156
|
+
let hash = 0x811c9dc5
|
|
157
|
+
|
|
158
|
+
for (const value of bytes) {
|
|
159
|
+
hash ^= value
|
|
160
|
+
hash = Math.imul(hash, 0x01000193) >>> 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return hash.toString(16).padStart(8, '0')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Removes undefined object fields.
|
|
168
|
+
* @param {Record<string, unknown>} value Candidate object.
|
|
169
|
+
* @returns {Record<string, unknown>}
|
|
170
|
+
*/
|
|
171
|
+
static #stripUndefined(value) {
|
|
172
|
+
return Object.fromEntries(
|
|
173
|
+
Object.entries(value || {}).filter(
|
|
174
|
+
([, entryValue]) => entryValue !== undefined
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|