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.
- package/README.md +34 -5
- package/docs/api.md +171 -23
- package/docs/model-format.md +192 -20
- package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
- package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
- package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
- package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
- package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
- package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
- package/docs/testing.md +7 -0
- package/examples/README.md +21 -0
- package/examples/cli-utils.mjs +148 -0
- package/examples/corpus-smoke.mjs +523 -0
- package/examples/extract-bom.mjs +47 -0
- package/examples/generate-pnp.mjs +59 -0
- package/examples/inspect-board.mjs +70 -0
- package/examples/inspect-schematic.mjs +406 -0
- package/examples/library-catalog.mjs +115 -0
- package/examples/net-report.mjs +61 -0
- package/examples/validate-library.mjs +59 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +357 -16
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +52 -11
- package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
- package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
- package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
- package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
- package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
- package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
- package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
- package/src/core/altium/NormalizedModelSchema.mjs +3 -31
- package/src/core/altium/ParameterCollection.mjs +431 -0
- package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
- package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
- package/src/core/altium/ParserErrors.mjs +90 -0
- package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
- package/src/core/altium/ParserUtils.mjs +24 -0
- package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
- package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
- package/src/core/altium/PcbLayerGroups.mjs +308 -0
- package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
- package/src/core/altium/PcbLibModelParser.mjs +20 -4
- package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
- package/src/core/altium/PcbModelParser.mjs +223 -4
- package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PcbStreamExtractor.mjs +130 -6
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
- package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
- package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
- package/src/core/altium/SchLibModelParser.mjs +840 -0
- package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
- package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
- package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
- package/src/core/altium/SchematicHarnessParser.mjs +9 -3
- package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +99 -65
- package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
- package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
- package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
- package/src/core/altium/SchematicSheetParser.mjs +8 -2
- package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
- package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
- package/src/core/altium/SchematicTextParser.mjs +28 -12
- package/src/core/altium/SchematicTextRunParser.mjs +81 -0
- package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +43 -1
- package/src/renderers.mjs +1 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +185 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNoteRenderer.mjs +5 -1
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
- package/src/ui/SchematicShapeRenderer.mjs +86 -17
- package/src/ui/SchematicSvgRenderer.mjs +980 -58
- 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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
}
|