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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derives a rigid-flex topology report from the PCB layer-stack read model.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbRigidFlexTopologyBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.pcb.rigid-flex-topology.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a rigid-flex topology sidecar.
|
|
13
|
+
* @param {object | undefined} layerStackReadModel Layer-stack sidecar.
|
|
14
|
+
* @returns {object | undefined}
|
|
15
|
+
*/
|
|
16
|
+
static build(layerStackReadModel) {
|
|
17
|
+
if (!layerStackReadModel) return undefined
|
|
18
|
+
|
|
19
|
+
const substacks = layerStackReadModel.substacks || []
|
|
20
|
+
const boardRegions = layerStackReadModel.boardRegions || []
|
|
21
|
+
const substackRegionJoins =
|
|
22
|
+
PcbRigidFlexTopologyBuilder.#substackRegionJoins(substacks)
|
|
23
|
+
const branchGraph = PcbRigidFlexTopologyBuilder.#branchGraph(
|
|
24
|
+
layerStackReadModel.branches || [],
|
|
25
|
+
substacks
|
|
26
|
+
)
|
|
27
|
+
const bendLines = PcbRigidFlexTopologyBuilder.#bendLines(
|
|
28
|
+
substacks,
|
|
29
|
+
boardRegions
|
|
30
|
+
)
|
|
31
|
+
const diagnostics = PcbRigidFlexTopologyBuilder.#diagnostics({
|
|
32
|
+
substacks,
|
|
33
|
+
branchGraph
|
|
34
|
+
})
|
|
35
|
+
const summary = {
|
|
36
|
+
substackCount: substacks.length,
|
|
37
|
+
flexSubstackCount: substacks.filter((substack) => substack.isFlex)
|
|
38
|
+
.length,
|
|
39
|
+
boardRegionCount:
|
|
40
|
+
layerStackReadModel.summary?.boardRegionCount || 0,
|
|
41
|
+
branchCount: branchGraph.length,
|
|
42
|
+
bendLineCount: bendLines.length,
|
|
43
|
+
diagnosticCount: diagnostics.length
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
schema: PcbRigidFlexTopologyBuilder.SCHEMA_ID,
|
|
48
|
+
summary,
|
|
49
|
+
substackRegionJoins,
|
|
50
|
+
branchGraph,
|
|
51
|
+
bendLines,
|
|
52
|
+
diagnostics
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds substack-to-board-region join rows.
|
|
58
|
+
* @param {object[]} substacks Layer substacks.
|
|
59
|
+
* @returns {object[]}
|
|
60
|
+
*/
|
|
61
|
+
static #substackRegionJoins(substacks) {
|
|
62
|
+
return substacks.map((substack) => ({
|
|
63
|
+
substackId: substack.id,
|
|
64
|
+
substackName: substack.name,
|
|
65
|
+
isFlex: substack.isFlex,
|
|
66
|
+
layerKeys: substack.layerKeys || [],
|
|
67
|
+
regionIndexes: substack.boardRegionIndexes || [],
|
|
68
|
+
regionNames: substack.boardRegionNames || []
|
|
69
|
+
}))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Builds a branch graph with resolved child substack summaries.
|
|
74
|
+
* @param {object[]} branches Stack branches.
|
|
75
|
+
* @param {object[]} substacks Layer substacks.
|
|
76
|
+
* @returns {object[]}
|
|
77
|
+
*/
|
|
78
|
+
static #branchGraph(branches, substacks) {
|
|
79
|
+
const substackById = new Map(
|
|
80
|
+
substacks
|
|
81
|
+
.filter((substack) => substack.id)
|
|
82
|
+
.map((substack) => [substack.id, substack])
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return branches.map((branch) => ({
|
|
86
|
+
branchId: branch.id,
|
|
87
|
+
branchName: branch.name,
|
|
88
|
+
rootStackRef: branch.rootStackRef,
|
|
89
|
+
stackRefs: branch.stackRefs || [],
|
|
90
|
+
...(branch.sections ? { sections: branch.sections } : {}),
|
|
91
|
+
childSubstacks: (branch.stackRefs || []).flatMap((stackRef) => {
|
|
92
|
+
const substack = substackById.get(stackRef)
|
|
93
|
+
if (!substack) return []
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
id: substack.id,
|
|
98
|
+
name: substack.name,
|
|
99
|
+
isFlex: substack.isFlex
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
})
|
|
103
|
+
}))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Builds bend-line summaries.
|
|
108
|
+
* @param {object[]} substacks Layer substacks.
|
|
109
|
+
* @param {object[]} boardRegions Board-region summaries.
|
|
110
|
+
* @returns {object[]}
|
|
111
|
+
*/
|
|
112
|
+
static #bendLines(substacks, boardRegions) {
|
|
113
|
+
const regionNameByIndex = new Map(
|
|
114
|
+
boardRegions.map((region) => [region.regionIndex, region.name])
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return substacks.flatMap((substack) =>
|
|
118
|
+
(substack.boardRegionIndexes || []).flatMap((regionIndex) =>
|
|
119
|
+
Array.from({ length: substack.bendingLineCount || 0 }).map(
|
|
120
|
+
(_, lineIndex) =>
|
|
121
|
+
PcbRigidFlexTopologyBuilder.#stripUndefined({
|
|
122
|
+
substackId: substack.id,
|
|
123
|
+
substackName: substack.name,
|
|
124
|
+
regionIndex,
|
|
125
|
+
regionName: regionNameByIndex.get(regionIndex),
|
|
126
|
+
lineIndex
|
|
127
|
+
})
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Builds topology diagnostics.
|
|
135
|
+
* @param {{ substacks: object[], branchGraph: object[] }} input Topology sections.
|
|
136
|
+
* @returns {object[]}
|
|
137
|
+
*/
|
|
138
|
+
static #diagnostics(input) {
|
|
139
|
+
const diagnostics = []
|
|
140
|
+
const substackIds = new Set(
|
|
141
|
+
input.substacks.map((substack) => substack.id).filter(Boolean)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
for (const branch of input.branchGraph) {
|
|
145
|
+
for (const stackRef of branch.stackRefs || []) {
|
|
146
|
+
if (substackIds.has(stackRef)) continue
|
|
147
|
+
diagnostics.push({
|
|
148
|
+
code: 'pcb.rigid-flex.unresolved-branch-substack',
|
|
149
|
+
severity: 'warning',
|
|
150
|
+
message:
|
|
151
|
+
'Rigid-flex branch graph references an unknown substack.',
|
|
152
|
+
branchId: branch.branchId,
|
|
153
|
+
stackRef
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return diagnostics
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Removes undefined values from an object.
|
|
163
|
+
* @param {Record<string, unknown>} object Source object.
|
|
164
|
+
* @returns {object}
|
|
165
|
+
*/
|
|
166
|
+
static #stripUndefined(object) {
|
|
167
|
+
return Object.fromEntries(
|
|
168
|
+
Object.entries(object).filter(([, value]) => value !== undefined)
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -12,6 +12,36 @@ export class PrintableTextDecoder {
|
|
|
12
12
|
0x9c, 0x9e, 0x9f
|
|
13
13
|
])
|
|
14
14
|
|
|
15
|
+
static #WINDOWS_1252_CONTROL_CODE_POINTS = new Map([
|
|
16
|
+
[0x80, 0x20ac],
|
|
17
|
+
[0x82, 0x201a],
|
|
18
|
+
[0x83, 0x0192],
|
|
19
|
+
[0x84, 0x201e],
|
|
20
|
+
[0x85, 0x2026],
|
|
21
|
+
[0x86, 0x2020],
|
|
22
|
+
[0x87, 0x2021],
|
|
23
|
+
[0x88, 0x02c6],
|
|
24
|
+
[0x89, 0x2030],
|
|
25
|
+
[0x8a, 0x0160],
|
|
26
|
+
[0x8b, 0x2039],
|
|
27
|
+
[0x8c, 0x0152],
|
|
28
|
+
[0x8e, 0x017d],
|
|
29
|
+
[0x91, 0x2018],
|
|
30
|
+
[0x92, 0x2019],
|
|
31
|
+
[0x93, 0x201c],
|
|
32
|
+
[0x94, 0x201d],
|
|
33
|
+
[0x95, 0x2022],
|
|
34
|
+
[0x96, 0x2013],
|
|
35
|
+
[0x97, 0x2014],
|
|
36
|
+
[0x98, 0x02dc],
|
|
37
|
+
[0x99, 0x2122],
|
|
38
|
+
[0x9a, 0x0161],
|
|
39
|
+
[0x9b, 0x203a],
|
|
40
|
+
[0x9c, 0x0153],
|
|
41
|
+
[0x9e, 0x017e],
|
|
42
|
+
[0x9f, 0x0178]
|
|
43
|
+
])
|
|
44
|
+
|
|
15
45
|
/**
|
|
16
46
|
* Returns printable ASCII-like runs from a binary buffer.
|
|
17
47
|
* @param {ArrayBuffer} arrayBuffer
|
|
@@ -93,7 +123,7 @@ export class PrintableTextDecoder {
|
|
|
93
123
|
preferredEncoding === 'cp1252'
|
|
94
124
|
) {
|
|
95
125
|
return (
|
|
96
|
-
PrintableTextDecoder.#
|
|
126
|
+
PrintableTextDecoder.#tryDecodeWindows1252(bytes) ||
|
|
97
127
|
new TextDecoder('utf-8').decode(bytes)
|
|
98
128
|
)
|
|
99
129
|
}
|
|
@@ -104,10 +134,8 @@ export class PrintableTextDecoder {
|
|
|
104
134
|
}
|
|
105
135
|
|
|
106
136
|
if (PrintableTextDecoder.#hasWindows1252PreferredBytes(bytes)) {
|
|
107
|
-
const windows1252 =
|
|
108
|
-
bytes
|
|
109
|
-
'windows-1252'
|
|
110
|
-
)
|
|
137
|
+
const windows1252 =
|
|
138
|
+
PrintableTextDecoder.#tryDecodeWindows1252(bytes)
|
|
111
139
|
if (windows1252 !== null) {
|
|
112
140
|
return windows1252
|
|
113
141
|
}
|
|
@@ -115,7 +143,7 @@ export class PrintableTextDecoder {
|
|
|
115
143
|
|
|
116
144
|
return (
|
|
117
145
|
PrintableTextDecoder.#tryDecode(bytes, 'gb18030') ||
|
|
118
|
-
PrintableTextDecoder.#
|
|
146
|
+
PrintableTextDecoder.#tryDecodeWindows1252(bytes) ||
|
|
119
147
|
new TextDecoder('utf-8').decode(bytes)
|
|
120
148
|
)
|
|
121
149
|
}
|
|
@@ -203,4 +231,40 @@ export class PrintableTextDecoder {
|
|
|
203
231
|
return null
|
|
204
232
|
}
|
|
205
233
|
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Tries a Windows-1252 decode and normalizes runtimes that expose C1 bytes
|
|
237
|
+
* as control characters instead of punctuation.
|
|
238
|
+
* @param {Uint8Array} bytes
|
|
239
|
+
* @returns {string | null}
|
|
240
|
+
*/
|
|
241
|
+
static #tryDecodeWindows1252(bytes) {
|
|
242
|
+
const decoded = PrintableTextDecoder.#tryDecode(bytes, 'windows-1252')
|
|
243
|
+
if (decoded === null) return null
|
|
244
|
+
|
|
245
|
+
return PrintableTextDecoder.#normalizeWindows1252Controls(decoded)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Maps Windows-1252 control-range punctuation to stable Unicode code
|
|
250
|
+
* points across Node/ICU builds.
|
|
251
|
+
* @param {string} text
|
|
252
|
+
* @returns {string}
|
|
253
|
+
*/
|
|
254
|
+
static #normalizeWindows1252Controls(text) {
|
|
255
|
+
let normalized = ''
|
|
256
|
+
|
|
257
|
+
for (const character of text) {
|
|
258
|
+
const codePoint = character.codePointAt(0)
|
|
259
|
+
const windows1252CodePoint =
|
|
260
|
+
PrintableTextDecoder.#WINDOWS_1252_CONTROL_CODE_POINTS.get(
|
|
261
|
+
codePoint
|
|
262
|
+
)
|
|
263
|
+
normalized += String.fromCodePoint(
|
|
264
|
+
windows1252CodePoint || codePoint
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return normalized
|
|
269
|
+
}
|
|
206
270
|
}
|
|
@@ -716,6 +716,10 @@ export class PrjPcbModelParser {
|
|
|
716
716
|
) ||
|
|
717
717
|
PrjPcbModelParser.#stringField(fields, 'OutputPath' + index) ||
|
|
718
718
|
''
|
|
719
|
+
const configRows = PrjPcbModelParser.#extractOutputConfigRows(
|
|
720
|
+
fields,
|
|
721
|
+
index
|
|
722
|
+
)
|
|
719
723
|
const row = {
|
|
720
724
|
index,
|
|
721
725
|
type,
|
|
@@ -740,12 +744,53 @@ export class PrjPcbModelParser {
|
|
|
740
744
|
)
|
|
741
745
|
}
|
|
742
746
|
if (targetPath) row.targetPath = targetPath
|
|
747
|
+
if (configRows.length) row.configRows = configRows
|
|
743
748
|
rows.push(row)
|
|
744
749
|
}
|
|
745
750
|
|
|
746
751
|
return rows
|
|
747
752
|
}
|
|
748
753
|
|
|
754
|
+
/**
|
|
755
|
+
* Extracts output configuration rows associated with one output index.
|
|
756
|
+
* @param {Record<string, string | string[]>} fields Output group fields.
|
|
757
|
+
* @param {number} outputIndex Output row index.
|
|
758
|
+
* @returns {{ key: string, record: string, fields: Record<string, string> }[]}
|
|
759
|
+
*/
|
|
760
|
+
static #extractOutputConfigRows(fields, outputIndex) {
|
|
761
|
+
const rows = []
|
|
762
|
+
const patterns = [
|
|
763
|
+
new RegExp('^Configuration' + outputIndex + '_Item\\d+$', 'i'),
|
|
764
|
+
new RegExp(
|
|
765
|
+
'^OutputConfiguration(?:Parameter)?' +
|
|
766
|
+
outputIndex +
|
|
767
|
+
'(?:_Item\\d+)?$',
|
|
768
|
+
'i'
|
|
769
|
+
)
|
|
770
|
+
]
|
|
771
|
+
|
|
772
|
+
for (const key of Object.keys(fields || {}).sort((left, right) =>
|
|
773
|
+
left.localeCompare(right, undefined, { numeric: true })
|
|
774
|
+
)) {
|
|
775
|
+
if (!patterns.some((pattern) => pattern.test(key))) continue
|
|
776
|
+
const values = Array.isArray(fields[key])
|
|
777
|
+
? fields[key]
|
|
778
|
+
: [fields[key]]
|
|
779
|
+
for (const value of values) {
|
|
780
|
+
const parsed = PrjPcbModelParser.#parsePipeFields(value)
|
|
781
|
+
const record = parsed.Record || ''
|
|
782
|
+
delete parsed.Record
|
|
783
|
+
rows.push({
|
|
784
|
+
key,
|
|
785
|
+
record,
|
|
786
|
+
fields: parsed
|
|
787
|
+
})
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return rows
|
|
792
|
+
}
|
|
793
|
+
|
|
749
794
|
/**
|
|
750
795
|
* Builds model summary counts.
|
|
751
796
|
* @param {string} fileName
|