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,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.#tryDecode(bytes, 'windows-1252') ||
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 = PrintableTextDecoder.#tryDecode(
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.#tryDecode(bytes, 'windows-1252') ||
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