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.
Files changed (63) hide show
  1. package/docs/api.md +6 -2
  2. package/docs/model-format.md +29 -4
  3. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +4 -0
  4. package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
  5. package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
  6. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +132 -1
  7. package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
  8. package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
  9. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
  10. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +692 -2
  11. package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
  12. package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
  13. package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
  14. package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
  15. package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
  16. package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
  17. package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
  18. package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
  19. package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
  20. package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
  21. package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
  22. package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
  23. package/package.json +1 -1
  24. package/src/core/altium/AltiumParser.mjs +7 -2
  25. package/src/core/altium/CiArtifactBundleBuilder.mjs +16 -5
  26. package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
  27. package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
  28. package/src/core/altium/DraftsmanDigestParser.mjs +246 -7
  29. package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
  30. package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
  31. package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
  32. package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
  33. package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
  34. package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
  35. package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
  36. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
  37. package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
  38. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
  39. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
  40. package/src/core/altium/PcbLibModelParser.mjs +2 -0
  41. package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
  42. package/src/core/altium/PcbModelParser.mjs +182 -18
  43. package/src/core/altium/PcbPickPlacePositionResolver.mjs +3 -0
  44. package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
  45. package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
  46. package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
  47. package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
  48. package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
  49. package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
  50. package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
  51. package/src/core/altium/PrintableTextDecoder.mjs +70 -6
  52. package/src/core/altium/PrjPcbModelParser.mjs +45 -0
  53. package/src/core/altium/PrjScrModelParser.mjs +386 -0
  54. package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
  55. package/src/core/altium/ProjectDesignBundleBuilder.mjs +61 -2
  56. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
  57. package/src/core/altium/SvgModelCrossLinkValidator.mjs +35 -2
  58. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +164 -0
  59. package/src/parser.mjs +15 -0
  60. package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
  61. package/src/ui/PcbScene3dBuilder.mjs +26 -4
  62. package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
  63. package/src/ui/SchematicSvgRenderer.mjs +48 -2
@@ -0,0 +1,338 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds read-only extraction manifests for placed PCB footprints.
7
+ */
8
+ export class PcbPlacedFootprintManifestBuilder {
9
+ static SCHEMA = 'altium-toolkit.pcb.placed-footprint-extraction.a1'
10
+
11
+ /**
12
+ * Builds a placed-footprint extraction manifest.
13
+ * @param {{ fileName?: string, components?: object[], componentPrimitiveGroups?: object[], embeddedModels?: object[] }} context Manifest context.
14
+ * @returns {object}
15
+ */
16
+ static build(context = {}) {
17
+ const outputs = (context.componentPrimitiveGroups || []).map(
18
+ (group, index) =>
19
+ PcbPlacedFootprintManifestBuilder.#output(context, group, index)
20
+ )
21
+ const embeddedAssetCount = outputs.reduce(
22
+ (total, output) => total + output.embeddedAssets.length,
23
+ 0
24
+ )
25
+
26
+ return {
27
+ schema: PcbPlacedFootprintManifestBuilder.SCHEMA,
28
+ sourceDocument: String(context.fileName || ''),
29
+ summary: {
30
+ componentCount: (context.components || []).length,
31
+ extractableFootprintCount: outputs.length,
32
+ embeddedAssetCount
33
+ },
34
+ outputs,
35
+ indexes: PcbPlacedFootprintManifestBuilder.#indexes(outputs)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Builds one placed-footprint output descriptor.
41
+ * @param {object} context Manifest context.
42
+ * @param {object} group Component primitive group.
43
+ * @param {number} index Group index.
44
+ * @returns {object}
45
+ */
46
+ static #output(context, group, index) {
47
+ const component =
48
+ (context.components || []).find(
49
+ (candidate) =>
50
+ Number(candidate.componentIndex) ===
51
+ Number(group.componentIndex)
52
+ ) || {}
53
+ const designator = group.designator || component.designator || ''
54
+ const pattern = component.pattern || ''
55
+ const footprintKey =
56
+ 'footprint-extract-' +
57
+ index +
58
+ '-' +
59
+ PcbPlacedFootprintManifestBuilder.#slug(
60
+ [designator, pattern].filter(Boolean).join('-') || index
61
+ )
62
+
63
+ return {
64
+ kind: 'placed-footprint',
65
+ footprintKey,
66
+ designator,
67
+ pattern,
68
+ componentIndex: Number(group.componentIndex),
69
+ outputLibraryKey: 'pcb-extract/' + footprintKey + '.PcbLib',
70
+ renderManifestKey: 'pcb-extract/' + footprintKey + '.render.json',
71
+ primitiveCounts:
72
+ PcbPlacedFootprintManifestBuilder.#primitiveCounts(group),
73
+ layers: PcbPlacedFootprintManifestBuilder.#layers(group),
74
+ embeddedAssets: PcbPlacedFootprintManifestBuilder.#embeddedAssets(
75
+ group,
76
+ context.embeddedModels || []
77
+ ),
78
+ diagnostics: PcbPlacedFootprintManifestBuilder.#diagnostics(group)
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Counts footprint-owned primitive families.
84
+ * @param {object} group Component primitive group.
85
+ * @returns {object}
86
+ */
87
+ static #primitiveCounts(group) {
88
+ return {
89
+ pads: (group.pads || []).length,
90
+ tracks: (group.tracks || []).length,
91
+ arcs: (group.arcs || []).length,
92
+ fills: (group.fills || []).length,
93
+ vias: (group.vias || []).length,
94
+ regions: (group.regions || []).length,
95
+ shapeBasedRegions: (group.shapeBasedRegions || []).length,
96
+ texts: (group.texts || []).length,
97
+ componentBodies: (group.componentBodies || []).length
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Builds layer descriptors touched by one footprint.
103
+ * @param {object} group Component primitive group.
104
+ * @returns {object[]}
105
+ */
106
+ static #layers(group) {
107
+ const layerMap = new Map()
108
+ for (const primitive of PcbPlacedFootprintManifestBuilder.#primitives(
109
+ group
110
+ )) {
111
+ const layer =
112
+ PcbPlacedFootprintManifestBuilder.#layerDescriptor(primitive)
113
+ if (layer) {
114
+ layerMap.set(layer.layerKey, layer)
115
+ }
116
+ }
117
+ return [...layerMap.values()].sort((left, right) =>
118
+ left.layerKey.localeCompare(right.layerKey, undefined, {
119
+ numeric: true
120
+ })
121
+ )
122
+ }
123
+
124
+ /**
125
+ * Collects embedded assets referenced by component bodies.
126
+ * @param {object} group Component primitive group.
127
+ * @param {object[]} embeddedModels Embedded model rows.
128
+ * @returns {object[]}
129
+ */
130
+ static #embeddedAssets(group, embeddedModels) {
131
+ return PcbPlacedFootprintManifestBuilder.#dedupe(
132
+ (group.componentBodies || []).flatMap((body) => {
133
+ const model =
134
+ PcbPlacedFootprintManifestBuilder.#matchingModel(
135
+ body,
136
+ embeddedModels
137
+ ) || body
138
+ return [
139
+ PcbPlacedFootprintManifestBuilder.#stripUndefined({
140
+ key: model.id || body.modelId || model.name,
141
+ format: model.format,
142
+ sourceStream: model.sourceStream,
143
+ name: model.name || body.name
144
+ })
145
+ ]
146
+ })
147
+ )
148
+ }
149
+
150
+ /**
151
+ * Builds extraction diagnostics for one group.
152
+ * @param {object} group Component primitive group.
153
+ * @returns {object[]}
154
+ */
155
+ static #diagnostics(group) {
156
+ const diagnostics = []
157
+ if (!PcbPlacedFootprintManifestBuilder.#hasOwnedGeometry(group)) {
158
+ diagnostics.push({
159
+ code: 'pcb-footprint-extract.empty-geometry',
160
+ severity: 'warning',
161
+ message: 'Placed component has no owned footprint geometry.'
162
+ })
163
+ }
164
+ return diagnostics
165
+ }
166
+
167
+ /**
168
+ * Builds manifest lookup indexes.
169
+ * @param {object[]} outputs Output descriptors.
170
+ * @returns {object}
171
+ */
172
+ static #indexes(outputs) {
173
+ const outputsByDesignator = {}
174
+ const outputsByPattern = {}
175
+
176
+ outputs.forEach((output, index) => {
177
+ if (output.designator)
178
+ outputsByDesignator[output.designator] = index
179
+ if (output.pattern) {
180
+ outputsByPattern[output.pattern] =
181
+ outputsByPattern[output.pattern] || []
182
+ outputsByPattern[output.pattern].push(index)
183
+ }
184
+ })
185
+
186
+ return {
187
+ outputsByDesignator,
188
+ outputsByPattern
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Returns all geometry primitives from a group.
194
+ * @param {object} group Component primitive group.
195
+ * @returns {object[]}
196
+ */
197
+ static #primitives(group) {
198
+ return [
199
+ ...(group.pads || []),
200
+ ...(group.tracks || []),
201
+ ...(group.arcs || []),
202
+ ...(group.fills || []),
203
+ ...(group.vias || []),
204
+ ...(group.regions || []),
205
+ ...(group.shapeBasedRegions || []),
206
+ ...(group.texts || [])
207
+ ]
208
+ }
209
+
210
+ /**
211
+ * Returns true when a component has any extractable geometry.
212
+ * @param {object} group Component primitive group.
213
+ * @returns {boolean}
214
+ */
215
+ static #hasOwnedGeometry(group) {
216
+ return (
217
+ PcbPlacedFootprintManifestBuilder.#primitives(group).length > 0 ||
218
+ (group.componentBodies || []).length > 0
219
+ )
220
+ }
221
+
222
+ /**
223
+ * Builds a normalized layer descriptor.
224
+ * @param {object} primitive Primitive row.
225
+ * @returns {object | null}
226
+ */
227
+ static #layerDescriptor(primitive) {
228
+ const layerId = Number.isInteger(primitive?.layerId)
229
+ ? primitive.layerId
230
+ : null
231
+ const displayName = String(
232
+ primitive?.layerName || primitive?.layer || ''
233
+ ).trim()
234
+ if (layerId === null && !displayName) {
235
+ return null
236
+ }
237
+ const layerKey =
238
+ layerId === null
239
+ ? 'layer-' +
240
+ PcbPlacedFootprintManifestBuilder.#slug(displayName)
241
+ : 'L' + layerId
242
+
243
+ return PcbPlacedFootprintManifestBuilder.#stripUndefined({
244
+ layerKey,
245
+ layerId: layerId === null ? undefined : layerId,
246
+ displayName: displayName || layerKey
247
+ })
248
+ }
249
+
250
+ /**
251
+ * Resolves an embedded model row for one component body.
252
+ * @param {object} body Component body row.
253
+ * @param {object[]} embeddedModels Embedded model rows.
254
+ * @returns {object | null}
255
+ */
256
+ static #matchingModel(body, embeddedModels) {
257
+ return (
258
+ (embeddedModels || []).find(
259
+ (model) =>
260
+ PcbPlacedFootprintManifestBuilder.#same(
261
+ model.id,
262
+ body.modelId
263
+ ) ||
264
+ PcbPlacedFootprintManifestBuilder.#same(
265
+ model.checksum,
266
+ body.checksum
267
+ ) ||
268
+ PcbPlacedFootprintManifestBuilder.#same(
269
+ model.name,
270
+ body.name
271
+ )
272
+ ) || null
273
+ )
274
+ }
275
+
276
+ /**
277
+ * Compares two non-empty values.
278
+ * @param {unknown} left First value.
279
+ * @param {unknown} right Second value.
280
+ * @returns {boolean}
281
+ */
282
+ static #same(left, right) {
283
+ return (
284
+ left !== null &&
285
+ left !== undefined &&
286
+ left !== '' &&
287
+ right !== null &&
288
+ right !== undefined &&
289
+ right !== '' &&
290
+ String(left) === String(right)
291
+ )
292
+ }
293
+
294
+ /**
295
+ * Deduplicates objects by their JSON form.
296
+ * @param {object[]} rows Candidate rows.
297
+ * @returns {object[]}
298
+ */
299
+ static #dedupe(rows) {
300
+ const seen = new Set()
301
+ const deduped = []
302
+ for (const row of rows || []) {
303
+ const key = JSON.stringify(row)
304
+ if (seen.has(key)) continue
305
+ seen.add(key)
306
+ deduped.push(row)
307
+ }
308
+ return deduped
309
+ }
310
+
311
+ /**
312
+ * Converts a value to a deterministic lowercase key segment.
313
+ * @param {unknown} value Source value.
314
+ * @returns {string}
315
+ */
316
+ static #slug(value) {
317
+ return (
318
+ String(value || '')
319
+ .trim()
320
+ .toLowerCase()
321
+ .replace(/[^a-z0-9]+/gu, '-')
322
+ .replace(/^-+|-+$/gu, '') || 'item'
323
+ )
324
+ }
325
+
326
+ /**
327
+ * Removes undefined fields from one object.
328
+ * @param {Record<string, unknown>} value Candidate object.
329
+ * @returns {Record<string, unknown>}
330
+ */
331
+ static #stripUndefined(value) {
332
+ return Object.fromEntries(
333
+ Object.entries(value || {}).filter(
334
+ ([, entryValue]) => entryValue !== undefined
335
+ )
336
+ )
337
+ }
338
+ }
@@ -0,0 +1,120 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { AltiumLayoutParser } from './AltiumLayoutParser.mjs'
6
+ import { ParserUtils } from './ParserUtils.mjs'
7
+
8
+ const { getField, parseBoolean, parseNumericField } = ParserUtils
9
+
10
+ /**
11
+ * Normalizes printable PCB polygon records into renderable polygon rows.
12
+ */
13
+ export class PcbPolygonRecordParser {
14
+ /**
15
+ * Parses polygon rows from printable records.
16
+ * @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records Printable records.
17
+ * @returns {object[]}
18
+ */
19
+ static parse(records) {
20
+ return (records || [])
21
+ .filter(
22
+ (record) =>
23
+ record.sourceStream === 'Polygons6/Data' &&
24
+ getField(record.fields, 'KIND0')
25
+ )
26
+ .map((record, index) =>
27
+ PcbPolygonRecordParser.#normalize(record.fields, index)
28
+ )
29
+ .filter((polygon) => polygon.segments.length > 0)
30
+ }
31
+
32
+ /**
33
+ * Normalizes one printable polygon row.
34
+ * @param {Record<string, string | string[]>} fields Record fields.
35
+ * @param {number} index Fallback row index.
36
+ * @returns {object}
37
+ */
38
+ static #normalize(fields, index) {
39
+ const outline = AltiumLayoutParser.parseBoardOutline(fields)
40
+ return PcbPolygonRecordParser.#stripEmpty({
41
+ layer: getField(fields, 'LAYER') || 'UNKNOWN',
42
+ polygonIndex: PcbPolygonRecordParser.#firstNumber(fields, [
43
+ 'POLYGONINDEX',
44
+ 'POLYGON_INDEX',
45
+ 'INDEX'
46
+ ]),
47
+ subpolygonIndex: PcbPolygonRecordParser.#firstNumber(fields, [
48
+ 'SUBPOLYINDEX',
49
+ 'SUBPOLYGONINDEX',
50
+ 'SUB_POLYGON_INDEX'
51
+ ]),
52
+ unionIndex: PcbPolygonRecordParser.#firstNumber(fields, [
53
+ 'UNIONINDEX',
54
+ 'UNION_INDEX'
55
+ ]),
56
+ isCutout: PcbPolygonRecordParser.#firstBoolean(fields, [
57
+ 'ISCUTOUT',
58
+ 'IS_CUTOUT',
59
+ 'CUTOUT'
60
+ ]),
61
+ realizationKind:
62
+ getField(fields, 'POLYGONKIND') ||
63
+ getField(fields, 'POURKIND') ||
64
+ '',
65
+ sourceRecordIndex: index,
66
+ segments: outline.segments
67
+ })
68
+ }
69
+
70
+ /**
71
+ * Reads the first numeric field from a list of native aliases.
72
+ * @param {Record<string, string | string[]>} fields Record fields.
73
+ * @param {string[]} keys Candidate keys.
74
+ * @returns {number | undefined}
75
+ */
76
+ static #firstNumber(fields, keys) {
77
+ for (const key of keys) {
78
+ const value = parseNumericField(fields, key)
79
+ if (value !== null) {
80
+ return value
81
+ }
82
+ }
83
+
84
+ return undefined
85
+ }
86
+
87
+ /**
88
+ * Reads the first boolean field from a list of native aliases.
89
+ * @param {Record<string, string | string[]>} fields Record fields.
90
+ * @param {string[]} keys Candidate keys.
91
+ * @returns {boolean | undefined}
92
+ */
93
+ static #firstBoolean(fields, keys) {
94
+ for (const key of keys) {
95
+ if (getField(fields, key)) {
96
+ return parseBoolean(fields[key])
97
+ }
98
+ }
99
+
100
+ return undefined
101
+ }
102
+
103
+ /**
104
+ * Removes empty optional fields while preserving zero and false.
105
+ * @param {Record<string, unknown>} value Candidate object.
106
+ * @returns {Record<string, unknown>}
107
+ */
108
+ static #stripEmpty(value) {
109
+ return Object.fromEntries(
110
+ Object.entries(value || {}).filter(([, entryValue]) => {
111
+ if (Array.isArray(entryValue)) return entryValue.length > 0
112
+ return (
113
+ entryValue !== null &&
114
+ entryValue !== undefined &&
115
+ entryValue !== ''
116
+ )
117
+ })
118
+ )
119
+ }
120
+ }