altium-toolkit 1.0.8 → 1.0.10
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 +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +196 -45
- package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +495 -32
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +229 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +281 -7
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +35 -0
- package/src/styles/altium-renderers.css +19 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionIndex.mjs +9 -4
- package/src/ui/PcbScene3dBuilder.mjs +137 -3
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1252 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { AsciiRecordParser } from './AsciiRecordParser.mjs'
|
|
6
|
+
import { EmbeddedFileInventoryBuilder } from './EmbeddedFileInventoryBuilder.mjs'
|
|
6
7
|
import { PcbBinaryPrimitiveParser } from './PcbBinaryPrimitiveParser.mjs'
|
|
8
|
+
import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
|
|
7
9
|
import { PcbEmbeddedFontExtractor } from './PcbEmbeddedFontExtractor.mjs'
|
|
8
10
|
import { PcbEmbeddedModelExtractor } from './PcbEmbeddedModelExtractor.mjs'
|
|
11
|
+
import { PcbExtendedPrimitiveInformationParser } from './PcbExtendedPrimitiveInformationParser.mjs'
|
|
9
12
|
import { PcbPrimitiveParameterParser } from './PcbPrimitiveParameterParser.mjs'
|
|
10
13
|
import { PcbRawRecordRegistry } from './PcbRawRecordRegistry.mjs'
|
|
14
|
+
import { PcbUnionParser } from './PcbUnionParser.mjs'
|
|
15
|
+
import { PcbViaStructureParser } from './PcbViaStructureParser.mjs'
|
|
11
16
|
import { PcbWideStringTableParser } from './PcbWideStringTableParser.mjs'
|
|
12
17
|
import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
|
|
13
18
|
import { OleConstants } from '../ole/OleConstants.mjs'
|
|
@@ -45,7 +50,7 @@ export class PcbStreamExtractor {
|
|
|
45
50
|
/**
|
|
46
51
|
* Extracts PCB content directly from one OLE-backed PcbDoc buffer.
|
|
47
52
|
* @param {ArrayBuffer} arrayBuffer
|
|
48
|
-
* @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, streamNames: string[], binaryPrimitives: Record<string, object[]>, primitiveParameters: object, wideStrings: object, diagnostics: { printableRecordCount: number, printableStreamCount: number, binaryPrimitiveCount: number, primitiveParameterGroupCount: number, wideStringCount: number } } | null}
|
|
53
|
+
* @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, streamNames: string[], binaryPrimitives: Record<string, object[]>, primitiveParameters: object, wideStrings: object, viaStructures: object, extendedPrimitiveInformation: object, customPadShapes: object, unions: object, diagnostics: { printableRecordCount: number, printableStreamCount: number, binaryPrimitiveCount: number, primitiveParameterGroupCount: number, wideStringCount: number, viaStructureCount: number } } | null}
|
|
49
54
|
*/
|
|
50
55
|
static extractFromArrayBuffer(arrayBuffer) {
|
|
51
56
|
if (!PcbStreamExtractor.isCompoundDocument(arrayBuffer)) {
|
|
@@ -67,7 +72,7 @@ export class PcbStreamExtractor {
|
|
|
67
72
|
* Extracts stream-scoped printable records and known binary primitives from
|
|
68
73
|
* a stream map.
|
|
69
74
|
* @param {Map<string, Uint8Array>} streams
|
|
70
|
-
* @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, streamNames: string[], binaryPrimitives: Record<string, object[]>, primitiveParameters: object, wideStrings: object, diagnostics: { printableRecordCount: number, printableStreamCount: number, binaryPrimitiveCount: number, primitiveParameterGroupCount: number, wideStringCount: number } }}
|
|
75
|
+
* @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, streamNames: string[], binaryPrimitives: Record<string, object[]>, primitiveParameters: object, wideStrings: object, viaStructures: object, extendedPrimitiveInformation: object, customPadShapes: object, unions: object, diagnostics: { printableRecordCount: number, printableStreamCount: number, binaryPrimitiveCount: number, primitiveParameterGroupCount: number, wideStringCount: number, viaStructureCount: number } }}
|
|
71
76
|
*/
|
|
72
77
|
static extractFromStreams(streams) {
|
|
73
78
|
const records = []
|
|
@@ -106,7 +111,9 @@ export class PcbStreamExtractor {
|
|
|
106
111
|
continue
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
|
|
114
|
+
for (const record of streamRecords) {
|
|
115
|
+
records.push(record)
|
|
116
|
+
}
|
|
110
117
|
printableStreamNames.add(name)
|
|
111
118
|
usedStreamNames.add(name)
|
|
112
119
|
}
|
|
@@ -142,6 +149,15 @@ export class PcbStreamExtractor {
|
|
|
142
149
|
const wideStrings = PcbWideStringTableParser.parse(
|
|
143
150
|
streams.get('WideStrings6/Data')
|
|
144
151
|
)
|
|
152
|
+
const viaStructures = PcbViaStructureParser.extractFromStreams(streams)
|
|
153
|
+
const extendedPrimitiveInformation =
|
|
154
|
+
PcbExtendedPrimitiveInformationParser.parse(
|
|
155
|
+
streams.get('ExtendedPrimitiveInformation/Data')
|
|
156
|
+
)
|
|
157
|
+
const customPadShapes = PcbCustomPadShapeParser.parse(
|
|
158
|
+
streams.get('CustomShapes/Data')
|
|
159
|
+
)
|
|
160
|
+
const unions = PcbUnionParser.extractFromStreams(streams)
|
|
145
161
|
|
|
146
162
|
if (primitiveParameters.groups.length) {
|
|
147
163
|
usedStreamNames.add('PrimitiveParameters/Data')
|
|
@@ -151,6 +167,26 @@ export class PcbStreamExtractor {
|
|
|
151
167
|
usedStreamNames.add('WideStrings6/Data')
|
|
152
168
|
}
|
|
153
169
|
|
|
170
|
+
for (const sourceStream of PcbStreamExtractor.#viaStructureStreamNames(
|
|
171
|
+
viaStructures
|
|
172
|
+
)) {
|
|
173
|
+
usedStreamNames.add(sourceStream)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (extendedPrimitiveInformation.entries.length) {
|
|
177
|
+
usedStreamNames.add('ExtendedPrimitiveInformation/Data')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (customPadShapes.entries.length) {
|
|
181
|
+
usedStreamNames.add('CustomShapes/Data')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const sourceStream of PcbStreamExtractor.#unionStreamNames(
|
|
185
|
+
unions
|
|
186
|
+
)) {
|
|
187
|
+
usedStreamNames.add(sourceStream)
|
|
188
|
+
}
|
|
189
|
+
|
|
154
190
|
if (arcHeaderBytes && arcDataBytes) {
|
|
155
191
|
binaryPrimitives.arcs = PcbBinaryPrimitiveParser.parseArcStream(
|
|
156
192
|
arcHeaderBytes,
|
|
@@ -176,6 +212,10 @@ export class PcbStreamExtractor {
|
|
|
176
212
|
viaHeaderBytes,
|
|
177
213
|
viaDataBytes
|
|
178
214
|
)
|
|
215
|
+
PcbViaStructureParser.attachToVias(
|
|
216
|
+
binaryPrimitives.vias,
|
|
217
|
+
viaStructures
|
|
218
|
+
)
|
|
179
219
|
if (binaryPrimitives.vias.length) {
|
|
180
220
|
usedStreamNames.add('Vias6/Data')
|
|
181
221
|
}
|
|
@@ -246,6 +286,12 @@ export class PcbStreamExtractor {
|
|
|
246
286
|
}
|
|
247
287
|
}
|
|
248
288
|
|
|
289
|
+
PcbExtendedPrimitiveInformationParser.attachToPrimitives(
|
|
290
|
+
binaryPrimitives,
|
|
291
|
+
extendedPrimitiveInformation
|
|
292
|
+
)
|
|
293
|
+
PcbUnionParser.attachToPrimitives(binaryPrimitives, unions)
|
|
294
|
+
|
|
249
295
|
const embeddedModels =
|
|
250
296
|
PcbEmbeddedModelExtractor.extractFromStreams(streams)
|
|
251
297
|
const embeddedFonts =
|
|
@@ -276,6 +322,19 @@ export class PcbStreamExtractor {
|
|
|
276
322
|
)
|
|
277
323
|
}
|
|
278
324
|
|
|
325
|
+
const embeddedFiles = EmbeddedFileInventoryBuilder.buildFromStreams(
|
|
326
|
+
streams,
|
|
327
|
+
{
|
|
328
|
+
skipStreamNames: usedStreamNames
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
embeddedFiles.files.forEach((file) =>
|
|
332
|
+
usedStreamNames.add(file.sourceStream)
|
|
333
|
+
)
|
|
334
|
+
embeddedFiles.diagnostics.forEach((diagnostic) =>
|
|
335
|
+
usedStreamNames.add(diagnostic.sourceStream)
|
|
336
|
+
)
|
|
337
|
+
|
|
279
338
|
return {
|
|
280
339
|
records,
|
|
281
340
|
streamNames: [...usedStreamNames].sort((left, right) =>
|
|
@@ -284,16 +343,32 @@ export class PcbStreamExtractor {
|
|
|
284
343
|
binaryPrimitives,
|
|
285
344
|
primitiveParameters,
|
|
286
345
|
wideStrings,
|
|
346
|
+
viaStructures,
|
|
347
|
+
extendedPrimitiveInformation,
|
|
348
|
+
customPadShapes,
|
|
349
|
+
unions,
|
|
287
350
|
embeddedModels,
|
|
288
351
|
embeddedFonts,
|
|
352
|
+
embeddedFiles,
|
|
289
353
|
rawRecords,
|
|
290
354
|
diagnostics: {
|
|
291
355
|
printableRecordCount: records.length,
|
|
292
356
|
printableStreamCount: printableStreamNames.size,
|
|
293
357
|
embeddedFontCount: embeddedFonts.fonts.length,
|
|
358
|
+
embeddedFileCount: embeddedFiles.files.length,
|
|
359
|
+
embeddedFileIssueCount: embeddedFiles.diagnostics.length,
|
|
360
|
+
embeddedModelIssueCount:
|
|
361
|
+
embeddedModels.integrity?.issues?.length || 0,
|
|
294
362
|
rawRecordCount: rawRecords.length,
|
|
295
363
|
primitiveParameterGroupCount: primitiveParameters.groups.length,
|
|
296
364
|
wideStringCount: wideStrings.entries.length,
|
|
365
|
+
viaStructureCount: viaStructures.structures.length,
|
|
366
|
+
viaStructureLinkCount: viaStructures.links.length,
|
|
367
|
+
extendedPrimitiveInformationCount:
|
|
368
|
+
extendedPrimitiveInformation.entries.length,
|
|
369
|
+
customPadShapeCount: customPadShapes.entries.length,
|
|
370
|
+
userUnionCount: unions.userUnions.length,
|
|
371
|
+
smartUnionCount: unions.smartUnions.length,
|
|
297
372
|
binaryPrimitiveCount:
|
|
298
373
|
binaryPrimitives.arcs.length +
|
|
299
374
|
binaryPrimitives.tracks.length +
|
|
@@ -327,7 +402,39 @@ export class PcbStreamExtractor {
|
|
|
327
402
|
*/
|
|
328
403
|
static #isBinarySidecarDataStream(name) {
|
|
329
404
|
return (
|
|
330
|
-
name === 'PrimitiveParameters/Data' ||
|
|
405
|
+
name === 'PrimitiveParameters/Data' ||
|
|
406
|
+
name === 'WideStrings6/Data' ||
|
|
407
|
+
name === 'ViaStructures/Data' ||
|
|
408
|
+
name === 'ViaStructureManager/Data' ||
|
|
409
|
+
name === 'ExtendedPrimitiveInformation/Data' ||
|
|
410
|
+
name === 'CustomShapes/Data' ||
|
|
411
|
+
name === 'UnionNames/Data' ||
|
|
412
|
+
name === 'SmartUnions/Data'
|
|
331
413
|
)
|
|
332
414
|
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Collects sidecar source stream names that produced via-structure data.
|
|
418
|
+
* @param {{ structures?: { sourceStream?: string }[], links?: { sourceStream?: string }[] }} viaStructures
|
|
419
|
+
* @returns {string[]}
|
|
420
|
+
*/
|
|
421
|
+
static #viaStructureStreamNames(viaStructures) {
|
|
422
|
+
return [
|
|
423
|
+
...(viaStructures.structures || []),
|
|
424
|
+
...(viaStructures.links || [])
|
|
425
|
+
]
|
|
426
|
+
.map((record) => record.sourceStream)
|
|
427
|
+
.filter(Boolean)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Collects sidecar source stream names that produced union metadata.
|
|
432
|
+
* @param {{ userUnions?: { sourceStream?: string }[], smartUnions?: { sourceStream?: string }[] }} unions
|
|
433
|
+
* @returns {string[]}
|
|
434
|
+
*/
|
|
435
|
+
static #unionStreamNames(unions) {
|
|
436
|
+
return [...(unions.userUnions || []), ...(unions.smartUnions || [])]
|
|
437
|
+
.map((record) => record.sourceStream)
|
|
438
|
+
.filter(Boolean)
|
|
439
|
+
}
|
|
333
440
|
}
|
|
@@ -235,10 +235,70 @@ export class PcbTextPrimitiveParser {
|
|
|
235
235
|
if (payload.byteLength >= 133) {
|
|
236
236
|
extendedFields.textboxRectJustification = payload.getUint8(132)
|
|
237
237
|
}
|
|
238
|
+
if (fontType === 2 && payload.byteLength >= 157) {
|
|
239
|
+
extendedFields.barcode =
|
|
240
|
+
PcbTextPrimitiveParser.#parseBarcodeFields(payload)
|
|
241
|
+
}
|
|
238
242
|
|
|
239
243
|
return extendedFields
|
|
240
244
|
}
|
|
241
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Parses barcode-specific fields from modern barcode text records.
|
|
248
|
+
* @param {DataView} payload Text payload.
|
|
249
|
+
* @returns {{ kind: number, kindName: string, renderMode: number, renderModeName: string, fullWidth: number, fullHeight: number, marginX: number, marginY: number, minBarWidth: number, showText: boolean, inverted: boolean }}
|
|
250
|
+
*/
|
|
251
|
+
static #parseBarcodeFields(payload) {
|
|
252
|
+
const kind = payload.getUint8(133)
|
|
253
|
+
const renderMode = payload.getUint8(134)
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
kind,
|
|
257
|
+
kindName: PcbTextPrimitiveParser.#barcodeKindName(kind),
|
|
258
|
+
renderMode,
|
|
259
|
+
renderModeName:
|
|
260
|
+
PcbTextPrimitiveParser.#barcodeRenderModeName(renderMode),
|
|
261
|
+
fullWidth: PcbTextPrimitiveParser.#readMil(payload, 135),
|
|
262
|
+
fullHeight: PcbTextPrimitiveParser.#readMil(payload, 139),
|
|
263
|
+
marginX: PcbTextPrimitiveParser.#readMil(payload, 143),
|
|
264
|
+
marginY: PcbTextPrimitiveParser.#readMil(payload, 147),
|
|
265
|
+
minBarWidth: PcbTextPrimitiveParser.#readMil(payload, 151),
|
|
266
|
+
showText: payload.getUint8(155) !== 0,
|
|
267
|
+
inverted: payload.getUint8(156) !== 0
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Resolves a barcode kind label.
|
|
273
|
+
* @param {number} kind Barcode kind id.
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
static #barcodeKindName(kind) {
|
|
277
|
+
return (
|
|
278
|
+
{
|
|
279
|
+
0: 'code39',
|
|
280
|
+
1: 'code128',
|
|
281
|
+
2: 'ean13',
|
|
282
|
+
3: 'qr'
|
|
283
|
+
}[Number(kind)] || 'unknown'
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Resolves a barcode render-mode label.
|
|
289
|
+
* @param {number} mode Barcode render mode id.
|
|
290
|
+
* @returns {string}
|
|
291
|
+
*/
|
|
292
|
+
static #barcodeRenderModeName(mode) {
|
|
293
|
+
return (
|
|
294
|
+
{
|
|
295
|
+
0: 'minimum',
|
|
296
|
+
1: 'fit-text',
|
|
297
|
+
2: 'full-size'
|
|
298
|
+
}[Number(mode)] || 'unknown'
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
242
302
|
/**
|
|
243
303
|
* Decodes one fixed-length UTF-16LE field from a payload view.
|
|
244
304
|
* @param {DataView} payload
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbSidecarRecordParser } from './PcbSidecarRecordParser.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Decodes PCB user-union and smart-union sidecar streams.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbUnionParser {
|
|
11
|
+
static #OBJECT_ID_TO_COLLECTION = {
|
|
12
|
+
1: 'arcs',
|
|
13
|
+
2: 'pads',
|
|
14
|
+
3: 'vias',
|
|
15
|
+
4: 'tracks',
|
|
16
|
+
5: 'texts',
|
|
17
|
+
6: 'fills',
|
|
18
|
+
11: 'regions'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extracts user and smart union records from a stream map.
|
|
23
|
+
* @param {Map<string, Uint8Array>} streams
|
|
24
|
+
* @returns {{ userUnions: object[], smartUnions: object[], byIndex: Record<string, object>, smartByIndex: Record<string, object>, membersByPrimitiveKey: Record<string, object[]> }}
|
|
25
|
+
*/
|
|
26
|
+
static extractFromStreams(streams) {
|
|
27
|
+
const userUnions = PcbUnionParser.#parseUserUnions(
|
|
28
|
+
streams.get('UnionNames/Data')
|
|
29
|
+
)
|
|
30
|
+
const smartUnions = PcbUnionParser.#parseSmartUnions(
|
|
31
|
+
streams.get('SmartUnions/Data')
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return PcbUnionParser.#buildLookups(userUnions, smartUnions)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Adds smart-union memberships to decoded primitives in place.
|
|
39
|
+
* @param {Record<string, object[]>} binaryPrimitives
|
|
40
|
+
* @param {{ smartUnions?: object[] }} unions
|
|
41
|
+
*/
|
|
42
|
+
static attachToPrimitives(binaryPrimitives, unions) {
|
|
43
|
+
if (!binaryPrimitives || !Array.isArray(unions?.smartUnions)) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const smartUnion of unions.smartUnions) {
|
|
48
|
+
for (const member of smartUnion.members || []) {
|
|
49
|
+
const collectionName =
|
|
50
|
+
PcbUnionParser.#OBJECT_ID_TO_COLLECTION[
|
|
51
|
+
member.primitiveObjectId
|
|
52
|
+
]
|
|
53
|
+
const collection = binaryPrimitives[collectionName]
|
|
54
|
+
const primitive = Array.isArray(collection)
|
|
55
|
+
? collection[member.primitiveIndex]
|
|
56
|
+
: null
|
|
57
|
+
|
|
58
|
+
if (!primitive) {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
primitive.unionMemberships = primitive.unionMemberships || []
|
|
63
|
+
primitive.unionMemberships.push(
|
|
64
|
+
PcbUnionParser.#publicMembership(smartUnion)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parses UnionNames/Data entries.
|
|
72
|
+
* @param {Uint8Array | undefined} bytes
|
|
73
|
+
* @returns {object[]}
|
|
74
|
+
*/
|
|
75
|
+
static #parseUserUnions(bytes) {
|
|
76
|
+
return PcbSidecarRecordParser.parseLengthPrefixedRecords(
|
|
77
|
+
bytes,
|
|
78
|
+
'UnionNames/Data'
|
|
79
|
+
)
|
|
80
|
+
.map((record) => PcbUnionParser.#normalizeUserUnion(record))
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parses SmartUnions/Data entries.
|
|
86
|
+
* @param {Uint8Array | undefined} bytes
|
|
87
|
+
* @returns {object[]}
|
|
88
|
+
*/
|
|
89
|
+
static #parseSmartUnions(bytes) {
|
|
90
|
+
return PcbSidecarRecordParser.parseLengthPrefixedRecords(
|
|
91
|
+
bytes,
|
|
92
|
+
'SmartUnions/Data'
|
|
93
|
+
)
|
|
94
|
+
.map((record) => PcbUnionParser.#normalizeSmartUnion(record))
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Normalizes one user-union record.
|
|
100
|
+
* @param {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }} record
|
|
101
|
+
* @returns {object}
|
|
102
|
+
*/
|
|
103
|
+
static #normalizeUserUnion(record) {
|
|
104
|
+
const index =
|
|
105
|
+
PcbSidecarRecordParser.parseInteger(
|
|
106
|
+
PcbSidecarRecordParser.firstField(record.fields, [
|
|
107
|
+
'UNIONINDEX',
|
|
108
|
+
'INDEX'
|
|
109
|
+
])
|
|
110
|
+
) ?? record.recordIndex
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
index,
|
|
114
|
+
name: PcbSidecarRecordParser.firstField(record.fields, ['NAME']),
|
|
115
|
+
sourceStream: record.sourceStream,
|
|
116
|
+
fields: record.fields
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Normalizes one smart-union record.
|
|
122
|
+
* @param {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }} record
|
|
123
|
+
* @returns {object}
|
|
124
|
+
*/
|
|
125
|
+
static #normalizeSmartUnion(record) {
|
|
126
|
+
const index =
|
|
127
|
+
PcbSidecarRecordParser.parseInteger(
|
|
128
|
+
PcbSidecarRecordParser.firstField(record.fields, [
|
|
129
|
+
'UNIONINDEX',
|
|
130
|
+
'SMARTUNIONINDEX',
|
|
131
|
+
'INDEX'
|
|
132
|
+
])
|
|
133
|
+
) ?? record.recordIndex
|
|
134
|
+
const type = PcbUnionParser.#parseType(record.fields)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
index,
|
|
138
|
+
name: PcbSidecarRecordParser.firstField(record.fields, ['NAME']),
|
|
139
|
+
type,
|
|
140
|
+
typeName: PcbUnionParser.#smartUnionTypeName(type),
|
|
141
|
+
sourceStream: record.sourceStream,
|
|
142
|
+
members: PcbUnionParser.#parseMembers(record.fields),
|
|
143
|
+
fields: record.fields
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parses a smart-union type value.
|
|
149
|
+
* @param {Record<string, string>} fields
|
|
150
|
+
* @returns {number | string}
|
|
151
|
+
*/
|
|
152
|
+
static #parseType(fields) {
|
|
153
|
+
const value = PcbSidecarRecordParser.firstField(fields, [
|
|
154
|
+
'UNIONTYPE',
|
|
155
|
+
'TYPE',
|
|
156
|
+
'SMARTUNIONTYPE'
|
|
157
|
+
])
|
|
158
|
+
const parsed = PcbSidecarRecordParser.parseInteger(value)
|
|
159
|
+
|
|
160
|
+
return parsed === null ? value : parsed
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parses member primitive references from numbered sidecar fields.
|
|
165
|
+
* @param {Record<string, string>} fields
|
|
166
|
+
* @returns {{ primitiveObjectId: number, primitiveIndex: number }[]}
|
|
167
|
+
*/
|
|
168
|
+
static #parseMembers(fields) {
|
|
169
|
+
const members = []
|
|
170
|
+
const memberNumbers = new Set()
|
|
171
|
+
|
|
172
|
+
for (const key of Object.keys(fields)) {
|
|
173
|
+
const match = key.match(/^PRIMITIVEOBJECTID(\d+)$/u)
|
|
174
|
+
if (match) {
|
|
175
|
+
memberNumbers.add(match[1])
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const memberNumber of [...memberNumbers].sort(
|
|
180
|
+
(left, right) => Number(left) - Number(right)
|
|
181
|
+
)) {
|
|
182
|
+
const primitiveObjectId = PcbSidecarRecordParser.parseInteger(
|
|
183
|
+
fields['PRIMITIVEOBJECTID' + memberNumber]
|
|
184
|
+
)
|
|
185
|
+
const primitiveIndex = PcbSidecarRecordParser.parseInteger(
|
|
186
|
+
fields['PRIMITIVEINDEX' + memberNumber]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
Number.isInteger(primitiveObjectId) &&
|
|
191
|
+
Number.isInteger(primitiveIndex)
|
|
192
|
+
) {
|
|
193
|
+
members.push({ primitiveObjectId, primitiveIndex })
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!members.length) {
|
|
198
|
+
members.push(...PcbUnionParser.#parseFlatMembers(fields))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return members
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parses flat primitive member fields used by compact fake fixtures.
|
|
206
|
+
* @param {Record<string, string>} fields
|
|
207
|
+
* @returns {{ primitiveObjectId: number, primitiveIndex: number }[]}
|
|
208
|
+
*/
|
|
209
|
+
static #parseFlatMembers(fields) {
|
|
210
|
+
const primitiveObjectId = PcbSidecarRecordParser.parseInteger(
|
|
211
|
+
PcbSidecarRecordParser.firstField(fields, [
|
|
212
|
+
'PRIMITIVEOBJECTID',
|
|
213
|
+
'OBJECTID'
|
|
214
|
+
])
|
|
215
|
+
)
|
|
216
|
+
const primitiveIndexes = String(
|
|
217
|
+
PcbSidecarRecordParser.firstField(fields, [
|
|
218
|
+
'PRIMITIVEINDEXES',
|
|
219
|
+
'PRIMITIVEINDEX'
|
|
220
|
+
])
|
|
221
|
+
)
|
|
222
|
+
.split(/[;,\s]+/u)
|
|
223
|
+
.map((value) => PcbSidecarRecordParser.parseInteger(value))
|
|
224
|
+
.filter(Number.isInteger)
|
|
225
|
+
|
|
226
|
+
if (!Number.isInteger(primitiveObjectId)) {
|
|
227
|
+
return []
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return primitiveIndexes.map((primitiveIndex) => ({
|
|
231
|
+
primitiveObjectId,
|
|
232
|
+
primitiveIndex
|
|
233
|
+
}))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Builds union lookups.
|
|
238
|
+
* @param {object[]} userUnions
|
|
239
|
+
* @param {object[]} smartUnions
|
|
240
|
+
* @returns {{ userUnions: object[], smartUnions: object[], byIndex: Record<string, object>, smartByIndex: Record<string, object>, membersByPrimitiveKey: Record<string, object[]> }}
|
|
241
|
+
*/
|
|
242
|
+
static #buildLookups(userUnions, smartUnions) {
|
|
243
|
+
const byIndex = {}
|
|
244
|
+
const smartByIndex = {}
|
|
245
|
+
const membersByPrimitiveKey = {}
|
|
246
|
+
|
|
247
|
+
for (const union of userUnions) {
|
|
248
|
+
byIndex[String(union.index)] = union
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const smartUnion of smartUnions) {
|
|
252
|
+
smartByIndex[String(smartUnion.index)] = smartUnion
|
|
253
|
+
for (const member of smartUnion.members || []) {
|
|
254
|
+
const key =
|
|
255
|
+
member.primitiveObjectId + ':' + member.primitiveIndex
|
|
256
|
+
membersByPrimitiveKey[key] = membersByPrimitiveKey[key] || []
|
|
257
|
+
membersByPrimitiveKey[key].push(smartUnion)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
userUnions,
|
|
263
|
+
smartUnions,
|
|
264
|
+
byIndex,
|
|
265
|
+
smartByIndex,
|
|
266
|
+
membersByPrimitiveKey
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Returns the public membership shape attached to primitives.
|
|
272
|
+
* @param {object} smartUnion
|
|
273
|
+
* @returns {object}
|
|
274
|
+
*/
|
|
275
|
+
static #publicMembership(smartUnion) {
|
|
276
|
+
return {
|
|
277
|
+
index: smartUnion.index,
|
|
278
|
+
name: smartUnion.name,
|
|
279
|
+
type: smartUnion.type,
|
|
280
|
+
typeName: smartUnion.typeName,
|
|
281
|
+
sourceStream: smartUnion.sourceStream
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Maps known smart-union type ids to stable labels.
|
|
287
|
+
* @param {number | string} type
|
|
288
|
+
* @returns {string}
|
|
289
|
+
*/
|
|
290
|
+
static #smartUnionTypeName(type) {
|
|
291
|
+
if (typeof type === 'string' && type) {
|
|
292
|
+
return type
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
{
|
|
297
|
+
1: 'drill-table',
|
|
298
|
+
2: 'via-stitching',
|
|
299
|
+
3: 'layer-stack-table',
|
|
300
|
+
4: 'length-tuning',
|
|
301
|
+
5: 'metadata-ole-object',
|
|
302
|
+
6: 'via-shielding',
|
|
303
|
+
9: 'rectangle'
|
|
304
|
+
}[Number(type)] || 'unknown'
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
}
|