altium-toolkit 1.0.9 → 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 (79) 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 +80 -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 +166 -0
  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/netlist_a1.schema.json +6 -0
  11. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
  12. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  13. package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
  14. package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
  15. package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
  16. package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
  17. package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
  18. package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
  19. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -0
  20. package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
  21. package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
  22. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +6 -0
  23. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  24. package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
  25. package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
  26. package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
  27. package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
  28. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  29. package/package.json +1 -1
  30. package/src/core/altium/AltiumParser.mjs +12 -2
  31. package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
  32. package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
  33. package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
  34. package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
  35. package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
  36. package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
  37. package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
  38. package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
  39. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  40. package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
  41. package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
  42. package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
  43. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
  44. package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
  45. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
  46. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
  47. package/src/core/altium/PcbLibModelParser.mjs +2 -0
  48. package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
  49. package/src/core/altium/PcbModelParser.mjs +211 -22
  50. package/src/core/altium/PcbPadStackParser.mjs +171 -2
  51. package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
  52. package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
  53. package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
  54. package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
  55. package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
  56. package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
  57. package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
  58. package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
  59. package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
  60. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  61. package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
  62. package/src/core/altium/PrintableTextDecoder.mjs +70 -6
  63. package/src/core/altium/PrjPcbModelParser.mjs +69 -2
  64. package/src/core/altium/PrjScrModelParser.mjs +386 -0
  65. package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
  66. package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
  67. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  68. package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
  69. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
  70. package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
  71. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
  72. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  73. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  74. package/src/parser.mjs +21 -0
  75. package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
  76. package/src/ui/PcbScene3dBuilder.mjs +26 -4
  77. package/src/ui/PcbSvgRenderer.mjs +65 -0
  78. package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
  79. package/src/ui/SchematicSvgRenderer.mjs +48 -2
@@ -165,17 +165,26 @@ export class PcbRegionPrimitiveParser {
165
165
  net: 3,
166
166
  polygon: 5
167
167
  })
168
+ const kind = PcbRegionPrimitiveParser.#numericKind(properties.KIND)
169
+ const legacyCutout =
170
+ PcbRegionPrimitiveParser.#legacyCutoutClassification(
171
+ properties,
172
+ kind
173
+ )
168
174
 
169
175
  return {
170
176
  region: {
171
177
  layerId,
172
178
  layerCode: layerId,
173
179
  ...ownershipIndexes,
174
- kind: Number(properties.KIND || 0),
180
+ kind,
181
+ ...legacyCutout.fields,
175
182
  isKeepout: flags2 === 2,
176
183
  isBoardCutout:
184
+ legacyCutout.isBoardCutout ||
177
185
  String(properties.ISBOARDCUTOUT || '').toUpperCase() ===
178
- 'TRUE',
186
+ 'TRUE',
187
+ ...legacyCutout.cutoutFlags,
179
188
  isShapeBased:
180
189
  shapeBased ||
181
190
  String(properties.ISSHAPEBASED || '').toUpperCase() ===
@@ -188,6 +197,66 @@ export class PcbRegionPrimitiveParser {
188
197
  }
189
198
  }
190
199
 
200
+ /**
201
+ * Parses a region kind while avoiding NaN for legacy symbolic labels.
202
+ * @param {string | undefined} rawKind Raw KIND value.
203
+ * @returns {number | null}
204
+ */
205
+ static #numericKind(rawKind) {
206
+ if (rawKind === undefined || rawKind === null || rawKind === '') {
207
+ return 0
208
+ }
209
+
210
+ const kind = Number(rawKind)
211
+ return Number.isFinite(kind) ? kind : null
212
+ }
213
+
214
+ /**
215
+ * Builds cutout fields from legacy string KIND labels.
216
+ * @param {Record<string, string>} properties Native property map.
217
+ * @param {number | null} numericKind Parsed numeric kind.
218
+ * @returns {{ isBoardCutout: boolean, fields: object, cutoutFlags: object }}
219
+ */
220
+ static #legacyCutoutClassification(properties, numericKind) {
221
+ const rawKind = String(properties.KIND || '').trim()
222
+ if (numericKind !== null || !rawKind) {
223
+ return {
224
+ isBoardCutout: false,
225
+ fields: {},
226
+ cutoutFlags: {}
227
+ }
228
+ }
229
+
230
+ const normalized = rawKind.replace(/[^a-z0-9]/giu, '').toLowerCase()
231
+ const isBoardCutout = normalized === 'boardcutout'
232
+ const isPolygonPourCutout =
233
+ normalized === 'polygonpourcutout' ||
234
+ normalized === 'polygoncutout' ||
235
+ normalized === 'pourcutout'
236
+ const classification =
237
+ isBoardCutout || isPolygonPourCutout
238
+ ? {
239
+ isBoardCutout,
240
+ isPolygonPourCutout,
241
+ source: 'legacy-kind',
242
+ rawKind
243
+ }
244
+ : null
245
+
246
+ return {
247
+ isBoardCutout,
248
+ fields: {
249
+ rawKind
250
+ },
251
+ cutoutFlags: classification
252
+ ? {
253
+ isPolygonPourCutout,
254
+ cutoutClassification: classification
255
+ }
256
+ : {}
257
+ }
258
+ }
259
+
191
260
  /**
192
261
  * Reads one simple double-coordinate region vertex list.
193
262
  * @param {DataView} view
@@ -0,0 +1,301 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds drill overlay and draw-order review metadata.
7
+ */
8
+ export class PcbReviewDrillMetadataBuilder {
9
+ /**
10
+ * Builds drill review and draw-order metadata.
11
+ * @param {object} pcb Normalized PCB model.
12
+ * @returns {{ overlays: object[], layerDrawOrder: object[] }}
13
+ */
14
+ static build(pcb = {}) {
15
+ return {
16
+ overlays: [
17
+ ...PcbReviewDrillMetadataBuilder.#drillRows(
18
+ 'via',
19
+ pcb.vias || []
20
+ ),
21
+ ...PcbReviewDrillMetadataBuilder.#drillRows(
22
+ 'pad',
23
+ pcb.pads || []
24
+ )
25
+ ],
26
+ layerDrawOrder: PcbReviewDrillMetadataBuilder.#layerDrawOrder(pcb)
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Builds drill overlay rows for one drill-owner collection.
32
+ * @param {'via' | 'pad'} ownerKind Drill owner kind.
33
+ * @param {object[]} owners Drill owners.
34
+ * @returns {object[]}
35
+ */
36
+ static #drillRows(ownerKind, owners) {
37
+ return (owners || [])
38
+ .map((owner, index) => {
39
+ if (!PcbReviewDrillMetadataBuilder.#hasHole(owner)) {
40
+ return null
41
+ }
42
+ const ownerKey = ownerKind + '-' + index
43
+ const holeKind = PcbReviewDrillMetadataBuilder.#holeKind(owner)
44
+ const plating =
45
+ owner?.isPlated === false ? 'non-plated' : 'plated'
46
+ const renderState =
47
+ PcbReviewDrillMetadataBuilder.#drillRenderState(owner)
48
+
49
+ return PcbReviewDrillMetadataBuilder.#stripEmpty({
50
+ elementKey: 'pcb-' + ownerKind + '-hole-' + String(index),
51
+ ownerKind,
52
+ ownerKey,
53
+ holeKind,
54
+ plating,
55
+ renderState,
56
+ overlayKind: PcbReviewDrillMetadataBuilder.#overlayKind(
57
+ ownerKind,
58
+ holeKind,
59
+ plating,
60
+ renderState
61
+ ),
62
+ layerKeys: PcbReviewDrillMetadataBuilder.#sortedStrings([
63
+ PcbReviewDrillMetadataBuilder.#layerKey(owner)
64
+ ])
65
+ })
66
+ })
67
+ .filter(Boolean)
68
+ }
69
+
70
+ /**
71
+ * Builds layer draw-order rows for visual review.
72
+ * @param {object} pcb PCB model.
73
+ * @returns {object[]}
74
+ */
75
+ static #layerDrawOrder(pcb) {
76
+ const descriptors = new Map()
77
+ for (const layer of [
78
+ ...(pcb.layers || []),
79
+ ...(pcb.primitiveLayers || [])
80
+ ]) {
81
+ const layerId = PcbReviewDrillMetadataBuilder.#layerId(layer)
82
+ if (!Number.isInteger(layerId) || descriptors.has(layerId)) {
83
+ continue
84
+ }
85
+ descriptors.set(layerId, {
86
+ layerKey: 'L' + layerId,
87
+ layerId,
88
+ displayName:
89
+ layer.displayName || layer.name || 'Layer ' + layerId,
90
+ role: PcbReviewDrillMetadataBuilder.#layerRole(layer, layerId)
91
+ })
92
+ }
93
+
94
+ let internalOrder = 0
95
+ return [...descriptors.values()]
96
+ .sort((left, right) => left.layerId - right.layerId)
97
+ .map((layer, drawOrder) => {
98
+ const row = { ...layer, drawOrder }
99
+ if (layer.role === 'internal') {
100
+ internalOrder += 1
101
+ row.internalOrder = internalOrder
102
+ }
103
+ return row
104
+ })
105
+ }
106
+
107
+ /**
108
+ * Returns true when a drill owner has a visible hole.
109
+ * @param {object} owner Drill owner primitive.
110
+ * @returns {boolean}
111
+ */
112
+ static #hasHole(owner) {
113
+ return Number(owner?.holeDiameter || owner?.drillDiameter || 0) > 0
114
+ }
115
+
116
+ /**
117
+ * Resolves a drill owner hole kind.
118
+ * @param {object} owner Drill owner primitive.
119
+ * @returns {'round' | 'slot'}
120
+ */
121
+ static #holeKind(owner) {
122
+ const holeShape = String(owner?.holeShape || '').toLowerCase()
123
+ if (
124
+ Number(owner?.holeSlotLength || owner?.slotLength || 0) > 0 ||
125
+ holeShape.includes('slot')
126
+ ) {
127
+ return 'slot'
128
+ }
129
+
130
+ return 'round'
131
+ }
132
+
133
+ /**
134
+ * Resolves drill rendering state from explicit and via-protection metadata.
135
+ * @param {object} owner Drill owner primitive.
136
+ * @returns {'open' | 'covered' | 'filled' | 'capped'}
137
+ */
138
+ static #drillRenderState(owner) {
139
+ const explicit =
140
+ owner?.drillRenderState ||
141
+ owner?.renderState ||
142
+ owner?.drill?.renderState
143
+ if (explicit) {
144
+ return PcbReviewDrillMetadataBuilder.#normalizeRenderState(explicit)
145
+ }
146
+
147
+ const featureText = (owner?.viaProtection?.features || [])
148
+ .flatMap((feature) => [feature.type, feature.material])
149
+ .join(' ')
150
+ .toLowerCase()
151
+
152
+ if (/cap/u.test(featureText)) return 'capped'
153
+ if (/fill|plug/u.test(featureText)) return 'filled'
154
+ if (/cover|tent|mask/u.test(featureText)) return 'covered'
155
+
156
+ const ipcType = Number(
157
+ owner?.ipc4761Type ?? owner?.viaProtection?.ipc4761Type
158
+ )
159
+ if (ipcType === 6 || ipcType === 7) return 'capped'
160
+ if (ipcType === 3 || ipcType === 4 || ipcType === 5) return 'filled'
161
+ if (ipcType === 1 || ipcType === 2) return 'covered'
162
+
163
+ return 'open'
164
+ }
165
+
166
+ /**
167
+ * Normalizes a render-state label.
168
+ * @param {unknown} value Raw render-state value.
169
+ * @returns {'open' | 'covered' | 'filled' | 'capped'}
170
+ */
171
+ static #normalizeRenderState(value) {
172
+ const normalized = String(value || '').toLowerCase()
173
+ if (/cap/u.test(normalized)) return 'capped'
174
+ if (/fill|plug/u.test(normalized)) return 'filled'
175
+ if (/cover|tent|mask/u.test(normalized)) return 'covered'
176
+ return 'open'
177
+ }
178
+
179
+ /**
180
+ * Resolves a deterministic overlay kind.
181
+ * @param {'via' | 'pad'} ownerKind Drill owner kind.
182
+ * @param {'round' | 'slot'} holeKind Hole kind.
183
+ * @param {'plated' | 'non-plated'} plating Plating state.
184
+ * @param {'open' | 'covered' | 'filled' | 'capped'} renderState Render state.
185
+ * @returns {string}
186
+ */
187
+ static #overlayKind(ownerKind, holeKind, plating, renderState) {
188
+ if (plating === 'non-plated') {
189
+ return holeKind === 'slot' ? 'non-plated-slot' : 'non-plated-hole'
190
+ }
191
+ if (ownerKind === 'via' && ['filled', 'capped'].includes(renderState)) {
192
+ return 'filled-or-capped-via'
193
+ }
194
+ if (ownerKind === 'via' && renderState === 'covered') {
195
+ return 'covered-via'
196
+ }
197
+ return holeKind === 'slot' ? 'plated-slot' : 'plated-hole'
198
+ }
199
+
200
+ /**
201
+ * Resolves a layer role suitable for visual draw order.
202
+ * @param {object} layer Layer row.
203
+ * @param {number} layerId Layer id.
204
+ * @returns {string}
205
+ */
206
+ static #layerRole(layer, layerId) {
207
+ const label = [layer?.role, layer?.kind, layer?.name]
208
+ .filter(Boolean)
209
+ .join(' ')
210
+ .toLowerCase()
211
+ if (label.includes('overlay') || label.includes('silk')) {
212
+ return 'overlay'
213
+ }
214
+ if (layerId === 1 || layerId === 32) {
215
+ return 'surface'
216
+ }
217
+ if (layerId > 1 && layerId < 32) {
218
+ return 'internal'
219
+ }
220
+ if (label.includes('mechanical')) {
221
+ return 'mechanical'
222
+ }
223
+ return label.includes('copper') ? 'surface' : 'other'
224
+ }
225
+
226
+ /**
227
+ * Resolves a layer key from a primitive.
228
+ * @param {object} value Primitive row.
229
+ * @returns {string}
230
+ */
231
+ static #layerKey(value) {
232
+ const layerId = PcbReviewDrillMetadataBuilder.#layerId(value)
233
+ if (Number.isInteger(layerId)) {
234
+ return 'L' + layerId
235
+ }
236
+
237
+ const layer = String(value?.layer || value?.layerName || '').trim()
238
+ return layer ? 'L-' + PcbReviewDrillMetadataBuilder.#slug(layer) : ''
239
+ }
240
+
241
+ /**
242
+ * Resolves a numeric layer id.
243
+ * @param {object} value Primitive or layer descriptor.
244
+ * @returns {number | undefined}
245
+ */
246
+ static #layerId(value) {
247
+ for (const key of ['layerId', 'layerCode', 'id', 'index']) {
248
+ const layerId = Number(value?.[key])
249
+ if (Number.isInteger(layerId)) {
250
+ return layerId
251
+ }
252
+ }
253
+
254
+ return undefined
255
+ }
256
+
257
+ /**
258
+ * Sorts and deduplicates strings naturally.
259
+ * @param {string[]} values Source values.
260
+ * @returns {string[]}
261
+ */
262
+ static #sortedStrings(values) {
263
+ return [...new Set((values || []).filter(Boolean))].sort(
264
+ (left, right) =>
265
+ left.localeCompare(right, undefined, { numeric: true })
266
+ )
267
+ }
268
+
269
+ /**
270
+ * Converts a value to a deterministic lowercase key segment.
271
+ * @param {unknown} value Source value.
272
+ * @returns {string}
273
+ */
274
+ static #slug(value) {
275
+ return (
276
+ String(value || '')
277
+ .trim()
278
+ .toLowerCase()
279
+ .replace(/[^a-z0-9]+/gu, '-')
280
+ .replace(/^-+|-+$/gu, '') || 'item'
281
+ )
282
+ }
283
+
284
+ /**
285
+ * Removes empty fields while preserving zeros and false.
286
+ * @param {Record<string, unknown>} value Candidate object.
287
+ * @returns {Record<string, unknown>}
288
+ */
289
+ static #stripEmpty(value) {
290
+ return Object.fromEntries(
291
+ Object.entries(value || {}).filter(([, entryValue]) => {
292
+ if (Array.isArray(entryValue)) return entryValue.length > 0
293
+ return (
294
+ entryValue !== null &&
295
+ entryValue !== undefined &&
296
+ entryValue !== ''
297
+ )
298
+ })
299
+ )
300
+ }
301
+ }