altium-toolkit 1.1.3 → 1.1.23

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 (125) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +171 -23
  3. package/docs/model-format.md +192 -20
  4. package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
  5. package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
  6. package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
  7. package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
  8. package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
  9. package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
  10. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
  11. package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
  12. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
  13. package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
  14. package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
  15. package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
  16. package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
  17. package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
  18. package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
  19. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
  20. package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
  21. package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
  22. package/docs/testing.md +7 -0
  23. package/examples/README.md +21 -0
  24. package/examples/cli-utils.mjs +148 -0
  25. package/examples/corpus-smoke.mjs +523 -0
  26. package/examples/extract-bom.mjs +47 -0
  27. package/examples/generate-pnp.mjs +59 -0
  28. package/examples/inspect-board.mjs +70 -0
  29. package/examples/inspect-schematic.mjs +406 -0
  30. package/examples/library-catalog.mjs +115 -0
  31. package/examples/net-report.mjs +61 -0
  32. package/examples/validate-library.mjs +59 -0
  33. package/package.json +1 -1
  34. package/spec/library-scope.md +5 -0
  35. package/src/core/BinaryReader.mjs +213 -2
  36. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  37. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  38. package/src/core/altium/AltiumParser.mjs +357 -16
  39. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  40. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  41. package/src/core/altium/AltiumUnits.mjs +205 -0
  42. package/src/core/altium/AsciiRecordParser.mjs +52 -11
  43. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  44. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  45. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  46. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  47. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  48. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  49. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  50. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  51. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  52. package/src/core/altium/ParameterCollection.mjs +431 -0
  53. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  54. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  55. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  56. package/src/core/altium/ParserErrors.mjs +90 -0
  57. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  58. package/src/core/altium/ParserUtils.mjs +24 -0
  59. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  60. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  61. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  62. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  63. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  64. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  65. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  66. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  67. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  68. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  69. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  70. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  71. package/src/core/altium/PcbModelParser.mjs +223 -4
  72. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  73. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  74. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  75. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  76. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  77. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  78. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  79. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  80. package/src/core/altium/SchLibModelParser.mjs +840 -0
  81. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  82. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  83. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  84. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  85. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  86. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  87. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  88. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  89. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  90. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  91. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  92. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  93. package/src/core/altium/SchematicPinParser.mjs +99 -65
  94. package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
  95. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  96. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  97. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  98. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  99. package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
  100. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  101. package/src/core/altium/SchematicTextParser.mjs +28 -12
  102. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  103. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  104. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  105. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  106. package/src/core/altium/SourceComponentClient.mjs +239 -0
  107. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  108. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  109. package/src/parser.mjs +43 -1
  110. package/src/renderers.mjs +1 -0
  111. package/src/styles/altium-renderers.css +6 -6
  112. package/src/ui/PcbArcUtils.mjs +19 -2
  113. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  114. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  115. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  116. package/src/ui/SchematicColorResolver.mjs +185 -0
  117. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  118. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  119. package/src/ui/SchematicNoteRenderer.mjs +5 -1
  120. package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
  121. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  122. package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
  123. package/src/ui/SchematicShapeRenderer.mjs +86 -17
  124. package/src/ui/SchematicSvgRenderer.mjs +980 -58
  125. package/src/ui/SchematicTypography.mjs +4 -3
@@ -0,0 +1,270 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds deterministic per-net primitive ownership summaries for PCB models.
7
+ */
8
+ export class PcbNetMembershipReportBuilder {
9
+ static SCHEMA = 'altium-toolkit.pcb.net-membership.a1'
10
+
11
+ /**
12
+ * Builds a PCB net-membership report from normalized PCB primitives.
13
+ * @param {object} pcb Normalized PCB model.
14
+ * @returns {object}
15
+ */
16
+ static build(pcb = {}) {
17
+ const declaredNetNames =
18
+ PcbNetMembershipReportBuilder.#declaredNetNames(pcb)
19
+ const rowsByNet = new Map()
20
+ const unownedPrimitives = []
21
+ let primitiveCount = 0
22
+
23
+ for (const family of PcbNetMembershipReportBuilder.#families()) {
24
+ const rows = Array.isArray(pcb?.[family]) ? pcb[family] : []
25
+ primitiveCount += rows.length
26
+
27
+ rows.forEach((primitive, index) => {
28
+ const row = PcbNetMembershipReportBuilder.#primitiveRow(
29
+ family,
30
+ primitive,
31
+ index
32
+ )
33
+
34
+ if (!row.netName) {
35
+ unownedPrimitives.push(
36
+ PcbNetMembershipReportBuilder.#stripEmpty({
37
+ primitiveKey: row.primitiveKey,
38
+ family,
39
+ index,
40
+ layerId: row.layerId
41
+ })
42
+ )
43
+ return
44
+ }
45
+
46
+ if (!rowsByNet.has(row.netName)) {
47
+ rowsByNet.set(row.netName, [])
48
+ }
49
+ rowsByNet.get(row.netName).push(row)
50
+ })
51
+ }
52
+
53
+ const observedNetNames = new Set(rowsByNet.keys())
54
+ const byNet = [...rowsByNet.entries()]
55
+ .map(([netName, rows]) =>
56
+ PcbNetMembershipReportBuilder.#netRow(
57
+ netName,
58
+ rows,
59
+ declaredNetNames.has(netName)
60
+ )
61
+ )
62
+ .sort((left, right) =>
63
+ PcbNetMembershipReportBuilder.#naturalCompare(
64
+ left.netName,
65
+ right.netName
66
+ )
67
+ )
68
+ const emptyDeclaredNets = [...declaredNetNames]
69
+ .filter((netName) => !observedNetNames.has(netName))
70
+ .sort(PcbNetMembershipReportBuilder.#naturalCompare)
71
+ const undeclaredNets = [...observedNetNames]
72
+ .filter((netName) => !declaredNetNames.has(netName))
73
+ .sort(PcbNetMembershipReportBuilder.#naturalCompare)
74
+ const possibleUnroutedNets = byNet
75
+ .filter(
76
+ (net) =>
77
+ net.declared &&
78
+ net.padCount >= 2 &&
79
+ net.trackCount + net.arcCount + net.viaCount === 0
80
+ )
81
+ .map((net) => net.netName)
82
+
83
+ return {
84
+ schema: PcbNetMembershipReportBuilder.SCHEMA,
85
+ summary: {
86
+ declaredNetCount: declaredNetNames.size,
87
+ observedNetCount: observedNetNames.size,
88
+ matchedNetCount: [...observedNetNames].filter((netName) =>
89
+ declaredNetNames.has(netName)
90
+ ).length,
91
+ undeclaredNetCount: undeclaredNets.length,
92
+ emptyDeclaredNetCount: emptyDeclaredNets.length,
93
+ netCount: new Set([...declaredNetNames, ...observedNetNames])
94
+ .size,
95
+ primitiveCount,
96
+ ownedPrimitiveCount: primitiveCount - unownedPrimitives.length,
97
+ unownedPrimitiveCount: unownedPrimitives.length,
98
+ possibleUnroutedNetCount: possibleUnroutedNets.length
99
+ },
100
+ byNet,
101
+ emptyDeclaredNets,
102
+ undeclaredNets,
103
+ possibleUnroutedNets,
104
+ unownedPrimitives
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Returns primitive families that can carry net ownership.
110
+ * @returns {string[]}
111
+ */
112
+ static #families() {
113
+ return [
114
+ 'pads',
115
+ 'tracks',
116
+ 'arcs',
117
+ 'vias',
118
+ 'fills',
119
+ 'regions',
120
+ 'shapeBasedRegions',
121
+ 'polygons'
122
+ ]
123
+ }
124
+
125
+ /**
126
+ * Collects declared net names from the normalized net table.
127
+ * @param {object} pcb Normalized PCB model.
128
+ * @returns {Set<string>}
129
+ */
130
+ static #declaredNetNames(pcb) {
131
+ return new Set(
132
+ (Array.isArray(pcb?.nets) ? pcb.nets : [])
133
+ .map((net) => String(net?.name || '').trim())
134
+ .filter(Boolean)
135
+ )
136
+ }
137
+
138
+ /**
139
+ * Builds one normalized primitive membership row.
140
+ * @param {string} family Primitive family name.
141
+ * @param {object} primitive Primitive row.
142
+ * @param {number} index Primitive index within its family.
143
+ * @returns {object}
144
+ */
145
+ static #primitiveRow(family, primitive, index) {
146
+ return PcbNetMembershipReportBuilder.#stripEmpty({
147
+ primitiveKey: family + '-' + index,
148
+ family,
149
+ index,
150
+ netName: PcbNetMembershipReportBuilder.#netName(primitive),
151
+ layerId: PcbNetMembershipReportBuilder.#layerId(primitive),
152
+ padDesignator:
153
+ family === 'pads'
154
+ ? PcbNetMembershipReportBuilder.#padDesignator(primitive)
155
+ : undefined
156
+ })
157
+ }
158
+
159
+ /**
160
+ * Builds one per-net aggregate row.
161
+ * @param {string} netName Net name.
162
+ * @param {object[]} rows Primitive rows owned by the net.
163
+ * @param {boolean} declared Whether the net is declared in the net table.
164
+ * @returns {object}
165
+ */
166
+ static #netRow(netName, rows, declared) {
167
+ const counts = Object.fromEntries(
168
+ PcbNetMembershipReportBuilder.#families().map((family) => [
169
+ family,
170
+ 0
171
+ ])
172
+ )
173
+ const layerIds = new Set()
174
+ const padDesignators = new Set()
175
+ const memberKeys = []
176
+
177
+ for (const row of rows) {
178
+ counts[row.family] = Number(counts[row.family] || 0) + 1
179
+ memberKeys.push(row.primitiveKey)
180
+ if (Number.isInteger(row.layerId)) {
181
+ layerIds.add(row.layerId)
182
+ }
183
+ if (row.padDesignator) {
184
+ padDesignators.add(row.padDesignator)
185
+ }
186
+ }
187
+
188
+ return {
189
+ netName,
190
+ declared,
191
+ totalPrimitiveCount: rows.length,
192
+ padCount: counts.pads,
193
+ trackCount: counts.tracks,
194
+ arcCount: counts.arcs,
195
+ viaCount: counts.vias,
196
+ fillCount: counts.fills,
197
+ regionCount: counts.regions,
198
+ shapeBasedRegionCount: counts.shapeBasedRegions,
199
+ polygonCount: counts.polygons,
200
+ layers: [...layerIds].sort((left, right) => left - right),
201
+ padDesignators: [...padDesignators].sort(
202
+ PcbNetMembershipReportBuilder.#naturalCompare
203
+ ),
204
+ memberKeys
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Resolves a primitive net name from normalized field spellings.
210
+ * @param {object} primitive Primitive row.
211
+ * @returns {string}
212
+ */
213
+ static #netName(primitive) {
214
+ return String(
215
+ primitive?.netName || primitive?.net || primitive?.netLabel || ''
216
+ ).trim()
217
+ }
218
+
219
+ /**
220
+ * Resolves a primitive layer id from normalized field spellings.
221
+ * @param {object} primitive Primitive row.
222
+ * @returns {number | undefined}
223
+ */
224
+ static #layerId(primitive) {
225
+ for (const key of ['layerId', 'layerCode', 'id', 'index']) {
226
+ const number = Number(primitive?.[key])
227
+ if (Number.isInteger(number)) {
228
+ return number
229
+ }
230
+ }
231
+
232
+ return undefined
233
+ }
234
+
235
+ /**
236
+ * Resolves a pad designator from normalized field spellings.
237
+ * @param {object} pad Pad row.
238
+ * @returns {string}
239
+ */
240
+ static #padDesignator(pad) {
241
+ return String(
242
+ pad?.designator || pad?.padNumber || pad?.pinName || pad?.name || ''
243
+ ).trim()
244
+ }
245
+
246
+ /**
247
+ * Sorts strings with numeric chunks in human order.
248
+ * @param {string} left Left value.
249
+ * @param {string} right Right value.
250
+ * @returns {number}
251
+ */
252
+ static #naturalCompare(left, right) {
253
+ return String(left).localeCompare(String(right), undefined, {
254
+ numeric: true
255
+ })
256
+ }
257
+
258
+ /**
259
+ * Removes undefined and blank-string values from a shallow object.
260
+ * @param {Record<string, unknown>} row Input row.
261
+ * @returns {Record<string, unknown>}
262
+ */
263
+ static #stripEmpty(row) {
264
+ return Object.fromEntries(
265
+ Object.entries(row).filter(
266
+ ([, value]) => value !== undefined && value !== ''
267
+ )
268
+ )
269
+ }
270
+ }
@@ -24,6 +24,8 @@ export class PcbOutlineRecovery {
24
24
 
25
25
  static #MAX_DIRECT_RENDER_ARC_SWEEP_DEGREES = 120
26
26
 
27
+ static #MAX_MECHANICAL_FRAME_TO_AUTHORED_AREA_RATIO = 16
28
+
27
29
  /**
28
30
  * Selects a recoverable board outline from mechanical track layers.
29
31
  * @param {{ fallbackOutline: { minX: number, minY: number, widthMil: number, heightMil: number, segments: Array<Record<string, number | string>> }, components: { x: number, y: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerId?: number }[] }} options
@@ -76,6 +78,20 @@ export class PcbOutlineRecovery {
76
78
  }
77
79
  }
78
80
 
81
+ if (
82
+ PcbOutlineRecovery.#shouldKeepAuthoredOutline(
83
+ fallbackOutline,
84
+ boundaryLayer.bounds,
85
+ componentBounds
86
+ )
87
+ ) {
88
+ return {
89
+ source: 'fallback',
90
+ layerId: null,
91
+ outline: fallbackOutline
92
+ }
93
+ }
94
+
79
95
  const recoveredOutline =
80
96
  PcbOutlineRecovery.#traceTrackOutline(
81
97
  boundaryLayer.tracks,
@@ -166,6 +182,84 @@ export class PcbOutlineRecovery {
166
182
  return candidates[0] || null
167
183
  }
168
184
 
185
+ /**
186
+ * Returns true when the mechanical candidate is likely a drawing frame
187
+ * around an otherwise plausible authored board route.
188
+ * @param {{ minX: number, minY: number, widthMil: number, heightMil: number, segments?: Array<Record<string, number | string>> }} authoredOutline Authored board-route outline.
189
+ * @param {{ widthMil: number, heightMil: number }} mechanicalBounds Mechanical track-layer bounds.
190
+ * @param {{ centerX: number, centerY: number }} componentBounds Component placement envelope.
191
+ * @returns {boolean}
192
+ */
193
+ static #shouldKeepAuthoredOutline(
194
+ authoredOutline,
195
+ mechanicalBounds,
196
+ componentBounds
197
+ ) {
198
+ if (
199
+ !PcbOutlineRecovery.#isClosedOutlinePath(
200
+ authoredOutline?.segments || []
201
+ )
202
+ ) {
203
+ return false
204
+ }
205
+
206
+ const authoredArea =
207
+ Number(authoredOutline?.widthMil || 0) *
208
+ Number(authoredOutline?.heightMil || 0)
209
+ const mechanicalArea =
210
+ Number(mechanicalBounds?.widthMil || 0) *
211
+ Number(mechanicalBounds?.heightMil || 0)
212
+
213
+ if (!authoredArea || !mechanicalArea) {
214
+ return false
215
+ }
216
+
217
+ if (
218
+ mechanicalArea / authoredArea <=
219
+ PcbOutlineRecovery.#MAX_MECHANICAL_FRAME_TO_AUTHORED_AREA_RATIO
220
+ ) {
221
+ return false
222
+ }
223
+
224
+ return PcbOutlineRecovery.#outlineEnvelopeContainsPoint(
225
+ authoredOutline,
226
+ componentBounds?.centerX,
227
+ componentBounds?.centerY
228
+ )
229
+ }
230
+
231
+ /**
232
+ * Returns true when one point falls within an outline's bounding envelope.
233
+ * @param {{ minX?: number, minY?: number, widthMil?: number, heightMil?: number }} outline Outline bounds.
234
+ * @param {number} x Point X coordinate.
235
+ * @param {number} y Point Y coordinate.
236
+ * @returns {boolean}
237
+ */
238
+ static #outlineEnvelopeContainsPoint(outline, x, y) {
239
+ const minX = Number(outline?.minX)
240
+ const minY = Number(outline?.minY)
241
+ const widthMil = Number(outline?.widthMil)
242
+ const heightMil = Number(outline?.heightMil)
243
+
244
+ if (
245
+ !Number.isFinite(minX) ||
246
+ !Number.isFinite(minY) ||
247
+ !Number.isFinite(widthMil) ||
248
+ !Number.isFinite(heightMil) ||
249
+ !Number.isFinite(x) ||
250
+ !Number.isFinite(y)
251
+ ) {
252
+ return false
253
+ }
254
+
255
+ return (
256
+ x >= minX &&
257
+ x <= minX + widthMil &&
258
+ y >= minY &&
259
+ y <= minY + heightMil
260
+ )
261
+ }
262
+
169
263
  /**
170
264
  * Builds one track-bounds envelope.
171
265
  * @param {{ x1: number, y1: number, x2: number, y2: number }[]} tracks
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { AsciiRecordParser } from './AsciiRecordParser.mjs'
6
6
  import { EmbeddedFileInventoryBuilder } from './EmbeddedFileInventoryBuilder.mjs'
7
+ import { NativeStreamInventoryBuilder } from './NativeStreamInventoryBuilder.mjs'
7
8
  import { PcbBinaryPrimitiveParser } from './PcbBinaryPrimitiveParser.mjs'
8
9
  import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
9
10
  import { PcbEmbeddedFontExtractor } from './PcbEmbeddedFontExtractor.mjs'
@@ -11,12 +12,45 @@ import { PcbEmbeddedModelExtractor } from './PcbEmbeddedModelExtractor.mjs'
11
12
  import { PcbExtendedPrimitiveInformationParser } from './PcbExtendedPrimitiveInformationParser.mjs'
12
13
  import { PcbPrimitiveParameterParser } from './PcbPrimitiveParameterParser.mjs'
13
14
  import { PcbRawRecordRegistry } from './PcbRawRecordRegistry.mjs'
15
+ import { PcbSidecarRecordParser } from './PcbSidecarRecordParser.mjs'
14
16
  import { PcbUnionParser } from './PcbUnionParser.mjs'
15
17
  import { PcbViaStructureParser } from './PcbViaStructureParser.mjs'
16
18
  import { PcbWideStringTableParser } from './PcbWideStringTableParser.mjs'
17
19
  import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
18
20
  import { OleConstants } from '../ole/OleConstants.mjs'
19
21
 
22
+ const KNOWN_PCB_STREAM_STORAGES = new Set([
23
+ 'Board6',
24
+ 'Nets6',
25
+ 'Arcs6',
26
+ 'Pads6',
27
+ 'Vias6',
28
+ 'Tracks6',
29
+ 'Texts',
30
+ 'Texts6',
31
+ 'Fills6',
32
+ 'Regions6',
33
+ 'ShapeBasedRegions6',
34
+ 'BoardRegions',
35
+ 'ComponentBodies6',
36
+ 'Polygons6',
37
+ 'Components6',
38
+ 'Classes6',
39
+ 'DifferentialPairs6',
40
+ 'EmbeddedBoards6',
41
+ 'Rooms6',
42
+ 'Rules6',
43
+ 'WideStrings6',
44
+ 'PrimitiveParameters',
45
+ 'ViaStructures',
46
+ 'ViaStructureManager',
47
+ 'ExtendedPrimitiveInformation',
48
+ 'CustomShapes',
49
+ 'UnionNames',
50
+ 'SmartUnions',
51
+ 'Models'
52
+ ])
53
+
20
54
  /**
21
55
  * Extracts stream-scoped printable and binary PCB content from OLE-backed
22
56
  * PcbDoc containers.
@@ -99,12 +133,15 @@ export class PcbStreamExtractor {
99
133
  continue
100
134
  }
101
135
 
102
- const recordBuffer = PcbStreamExtractor.#toArrayBuffer(bytes)
103
- const streamRecords = AsciiRecordParser.parse(recordBuffer).map(
104
- (record) => ({
105
- ...record,
106
- sourceStream: name
107
- })
136
+ if (
137
+ PcbStreamExtractor.#isSupersededLegacyDataStream(name, streams)
138
+ ) {
139
+ continue
140
+ }
141
+
142
+ const streamRecords = PcbStreamExtractor.#parseDataStreamRecords(
143
+ name,
144
+ bytes
108
145
  )
109
146
 
110
147
  if (!streamRecords.length) {
@@ -335,11 +372,23 @@ export class PcbStreamExtractor {
335
372
  usedStreamNames.add(diagnostic.sourceStream)
336
373
  )
337
374
 
375
+ const nativeStreams = NativeStreamInventoryBuilder.buildFromStreams(
376
+ streams,
377
+ {
378
+ source: 'pcbdoc',
379
+ consumedStreamNames: usedStreamNames,
380
+ knownStreamNames: [...streams.keys()].filter((streamName) =>
381
+ PcbStreamExtractor.#isKnownNativeStream(streamName)
382
+ )
383
+ }
384
+ )
385
+
338
386
  return {
339
387
  records,
340
388
  streamNames: [...usedStreamNames].sort((left, right) =>
341
389
  left.localeCompare(right)
342
390
  ),
391
+ nativeStreams,
343
392
  binaryPrimitives,
344
393
  primitiveParameters,
345
394
  wideStrings,
@@ -395,6 +444,38 @@ export class PcbStreamExtractor {
395
444
  )
396
445
  }
397
446
 
447
+ /**
448
+ * Parses a data stream as length-prefixed sidecar records when needed, with
449
+ * printable-record fallback for regular pipe-delimited streams.
450
+ * @param {string} name Stream path.
451
+ * @param {Uint8Array} bytes Stream bytes.
452
+ * @returns {Array<{ raw?: string, fields: Record<string, string | string[]>, sourceStream: string, recordIndex?: number }>}
453
+ */
454
+ static #parseDataStreamRecords(name, bytes) {
455
+ if (PcbStreamExtractor.#isLengthPrefixedPrintableStream(name)) {
456
+ const sidecarRecords =
457
+ PcbSidecarRecordParser.parseLengthPrefixedRecords(bytes, name)
458
+ if (sidecarRecords.length) {
459
+ return sidecarRecords
460
+ }
461
+ }
462
+
463
+ const recordBuffer = PcbStreamExtractor.#toArrayBuffer(bytes)
464
+ return AsciiRecordParser.parse(recordBuffer).map((record) => ({
465
+ ...record,
466
+ sourceStream: name
467
+ }))
468
+ }
469
+
470
+ /**
471
+ * Returns true for printable sidecar streams that may use length prefixes.
472
+ * @param {string} name Stream path.
473
+ * @returns {boolean}
474
+ */
475
+ static #isLengthPrefixedPrintableStream(name) {
476
+ return name === 'EmbeddedBoards6/Data' || name === 'Rooms6/Data'
477
+ }
478
+
398
479
  /**
399
480
  * Returns true for binary sidecar streams with printable-looking payloads.
400
481
  * @param {string} name
@@ -413,6 +494,34 @@ export class PcbStreamExtractor {
413
494
  )
414
495
  }
415
496
 
497
+ /**
498
+ * Returns true when a legacy printable stream has a versioned companion.
499
+ * @param {string} name Stream path.
500
+ * @param {Map<string, Uint8Array>} streams Source stream map.
501
+ * @returns {boolean}
502
+ */
503
+ static #isSupersededLegacyDataStream(name, streams) {
504
+ const suffix = '/Data'
505
+ if (!name.endsWith(suffix)) {
506
+ return false
507
+ }
508
+
509
+ const storagePath = name.slice(0, -suffix.length)
510
+ const separatorIndex = storagePath.lastIndexOf('/')
511
+ const parentPath =
512
+ separatorIndex >= 0 ? storagePath.slice(0, separatorIndex + 1) : ''
513
+ const storageName =
514
+ separatorIndex >= 0
515
+ ? storagePath.slice(separatorIndex + 1)
516
+ : storagePath
517
+
518
+ if (!storageName || /\d$/u.test(storageName)) {
519
+ return false
520
+ }
521
+
522
+ return streams.has(parentPath + storageName + '6' + suffix)
523
+ }
524
+
416
525
  /**
417
526
  * Collects sidecar source stream names that produced via-structure data.
418
527
  * @param {{ structures?: { sourceStream?: string }[], links?: { sourceStream?: string }[] }} viaStructures
@@ -437,4 +546,19 @@ export class PcbStreamExtractor {
437
546
  .map((record) => record.sourceStream)
438
547
  .filter(Boolean)
439
548
  }
549
+
550
+ /**
551
+ * Returns true when a native stream name belongs to a recognized PCB
552
+ * storage.
553
+ * @param {string} streamName Native stream name.
554
+ * @returns {boolean}
555
+ */
556
+ static #isKnownNativeStream(streamName) {
557
+ if (streamName === 'FileHeader') {
558
+ return true
559
+ }
560
+
561
+ const storageName = String(streamName || '').split('/')[0]
562
+ return KNOWN_PCB_STREAM_STORAGES.has(storageName)
563
+ }
440
564
  }
@@ -19,7 +19,7 @@ export class PcbTrackPrimitiveParser {
19
19
  * Decodes one track stream.
20
20
  * @param {Uint8Array | ArrayBuffer} headerBytes
21
21
  * @param {Uint8Array | ArrayBuffer} dataBytes
22
- * @returns {{ x1: number, y1: number, x2: number, y2: number, width: number, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number, layerId: number }[]}
22
+ * @returns {{ x1: number, y1: number, x2: number, y2: number, width: number, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number, layerId: number, [key: string]: unknown }[]}
23
23
  */
24
24
  static parseTrackStream(headerBytes, dataBytes) {
25
25
  return PcbTrackPrimitiveParser.#sliceTrackRecords(
@@ -31,7 +31,7 @@ export class PcbTrackPrimitiveParser {
31
31
  /**
32
32
  * Decodes one track record view into a normalized primitive.
33
33
  * @param {DataView} view
34
- * @returns {{ x1: number, y1: number, x2: number, y2: number, width: number, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number, layerId: number }}
34
+ * @returns {{ x1: number, y1: number, x2: number, y2: number, width: number, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number, layerId: number, [key: string]: unknown }}
35
35
  */
36
36
  static #parseTrackRecord(view) {
37
37
  const layerId = view.getUint8(0)
@@ -49,11 +49,61 @@ export class PcbTrackPrimitiveParser {
49
49
  y2: PcbTrackPrimitiveParser.#readMil(view, 25),
50
50
  width: PcbTrackPrimitiveParser.#readMil(view, 29),
51
51
  ...ownershipIndexes,
52
+ ...PcbTrackPrimitiveParser.#parseTrackMetadata(view),
52
53
  layerCode: layerId,
53
54
  layerId
54
55
  }
55
56
  }
56
57
 
58
+ /**
59
+ * Decodes optional route-state metadata from one track record.
60
+ * @param {DataView} view Track record view.
61
+ * @returns {{ trackFlags?: number, isSelected?: boolean, isLocked?: boolean, isPartOfComponent?: boolean, isKeepout?: boolean, unionIndex?: number, isLengthTuning?: boolean, isUserRouted?: boolean }}
62
+ */
63
+ static #parseTrackMetadata(view) {
64
+ const flags = PcbTrackPrimitiveParser.#readByteIfAvailable(view, 1)
65
+ const keepout = PcbTrackPrimitiveParser.#readByteIfAvailable(view, 2)
66
+ const unionIndex = PcbTrackPrimitiveParser.#readByteIfAvailable(
67
+ view,
68
+ 36
69
+ )
70
+ const lengthTuning = PcbTrackPrimitiveParser.#readByteIfAvailable(
71
+ view,
72
+ 37
73
+ )
74
+ const userRouted = PcbTrackPrimitiveParser.#readByteIfAvailable(
75
+ view,
76
+ 44
77
+ )
78
+
79
+ if (!flags && !keepout && !unionIndex && !lengthTuning && !userRouted) {
80
+ return {}
81
+ }
82
+
83
+ const result = {}
84
+
85
+ if (flags) {
86
+ result.trackFlags = flags
87
+ result.isSelected = (flags & 0x01) !== 0
88
+ result.isLocked = (flags & 0x04) === 0
89
+ result.isPartOfComponent = (flags & 0x10) !== 0
90
+ }
91
+ if (keepout) {
92
+ result.isKeepout = true
93
+ }
94
+ if (unionIndex) {
95
+ result.unionIndex = unionIndex
96
+ }
97
+ if (lengthTuning) {
98
+ result.isLengthTuning = true
99
+ }
100
+ if (userRouted) {
101
+ result.isUserRouted = true
102
+ }
103
+
104
+ return result
105
+ }
106
+
57
107
  /**
58
108
  * Splits a track stream into record views, preserving variable payload
59
109
  * lengths when tracks are stored with object-id and payload-length
@@ -84,4 +134,18 @@ export class PcbTrackPrimitiveParser {
84
134
  static #readMil(view, offset) {
85
135
  return view.getInt32(offset, true) / 10000
86
136
  }
137
+
138
+ /**
139
+ * Reads one byte when it is present in a variable-length record.
140
+ * @param {DataView} view Record view.
141
+ * @param {number} offset Byte offset.
142
+ * @returns {number}
143
+ */
144
+ static #readByteIfAvailable(view, offset) {
145
+ if (!view || offset >= view.byteLength) {
146
+ return 0
147
+ }
148
+
149
+ return view.getUint8(offset)
150
+ }
87
151
  }