altium-toolkit 1.0.8 → 1.0.9
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/netlist_a1.schema.json +47 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -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 +191 -45
- 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/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 +466 -28
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +58 -0
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
- 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 +532 -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 +257 -5
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +499 -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/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +29 -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 +1187 -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
|
@@ -13,6 +13,8 @@ import { SchematicPinParser } from './SchematicPinParser.mjs'
|
|
|
13
13
|
import { SchematicPrimitiveParser } from './SchematicPrimitiveParser.mjs'
|
|
14
14
|
import { AltiumLayoutParser } from './AltiumLayoutParser.mjs'
|
|
15
15
|
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
16
|
+
import { IntLibModelParser } from './IntLibModelParser.mjs'
|
|
17
|
+
import { IntLibStreamExtractor } from './IntLibStreamExtractor.mjs'
|
|
16
18
|
import { PcbModelParser } from './PcbModelParser.mjs'
|
|
17
19
|
import { PcbLibModelParser } from './PcbLibModelParser.mjs'
|
|
18
20
|
import { PcbLibStreamExtractor } from './PcbLibStreamExtractor.mjs'
|
|
@@ -25,8 +27,20 @@ import { SchematicJunctionParser } from './SchematicJunctionParser.mjs'
|
|
|
25
27
|
import { SchematicBusEntryParser } from './SchematicBusEntryParser.mjs'
|
|
26
28
|
import { SchematicImageParser } from './SchematicImageParser.mjs'
|
|
27
29
|
import { SchematicNetlistBuilder } from './SchematicNetlistBuilder.mjs'
|
|
30
|
+
import { SchematicRecordTypeRegistry } from './SchematicRecordTypeRegistry.mjs'
|
|
28
31
|
import { SchematicComponentTextResolver } from './SchematicComponentTextResolver.mjs'
|
|
32
|
+
import { SchematicComponentOwnerTextResolver } from './SchematicComponentOwnerTextResolver.mjs'
|
|
33
|
+
import { SchematicOwnershipGraphParser } from './SchematicOwnershipGraphParser.mjs'
|
|
29
34
|
import { SchematicStreamExtractor } from './SchematicStreamExtractor.mjs'
|
|
35
|
+
import { SchematicTemplateParser } from './SchematicTemplateParser.mjs'
|
|
36
|
+
import { SchematicHarnessParser } from './SchematicHarnessParser.mjs'
|
|
37
|
+
import { SchematicImplementationParser } from './SchematicImplementationParser.mjs'
|
|
38
|
+
import { SchematicCrossSheetConnectorParser } from './SchematicCrossSheetConnectorParser.mjs'
|
|
39
|
+
import { SchematicRepeatedChannelParser } from './SchematicRepeatedChannelParser.mjs'
|
|
40
|
+
import { SchematicDisplayModeCatalogParser } from './SchematicDisplayModeCatalogParser.mjs'
|
|
41
|
+
import { SchematicBindingProvenanceParser } from './SchematicBindingProvenanceParser.mjs'
|
|
42
|
+
import { SchematicConnectivityQaBuilder } from './SchematicConnectivityQaBuilder.mjs'
|
|
43
|
+
import { SchematicQaReportBuilder } from './SchematicQaReportBuilder.mjs'
|
|
30
44
|
import { SchematicWireNormalizer } from './SchematicWireNormalizer.mjs'
|
|
31
45
|
import { CircuitJsonModelAdapter } from '../circuit-json/CircuitJsonModelAdapter.mjs'
|
|
32
46
|
const {
|
|
@@ -41,6 +55,7 @@ const {
|
|
|
41
55
|
} = ParserUtils
|
|
42
56
|
const {
|
|
43
57
|
extractSchematicFonts,
|
|
58
|
+
extractSchematicFontDiagnostics,
|
|
44
59
|
extractSchematicMetadata,
|
|
45
60
|
extractSchematicTitleBlock,
|
|
46
61
|
normalizeSchematicTextRecord
|
|
@@ -74,7 +89,7 @@ export class AltiumParser {
|
|
|
74
89
|
* Parses a native Altium buffer into the renderer compatibility model.
|
|
75
90
|
* @param {string} fileName
|
|
76
91
|
* @param {ArrayBuffer} arrayBuffer
|
|
77
|
-
* @returns {{ schema: string, kind: 'schematic' | 'pcb' | 'pcb-library' | 'project', fileType: 'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], schematic?: Record<string, unknown>, pcb?: Record<string, unknown>, pcbLibrary?: Record<string, unknown>, project?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
|
|
92
|
+
* @returns {{ schema: string, kind: 'schematic' | 'pcb' | 'pcb-library' | 'project' | 'integrated-library', fileType: 'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb' | 'IntLib', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], schematic?: Record<string, unknown>, pcb?: Record<string, unknown>, pcbLibrary?: Record<string, unknown>, project?: Record<string, unknown>, integratedLibrary?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
|
|
78
93
|
*/
|
|
79
94
|
static parseArrayBufferToRendererModel(fileName, arrayBuffer) {
|
|
80
95
|
const records = AsciiRecordParser.parse(arrayBuffer)
|
|
@@ -85,7 +100,8 @@ export class AltiumParser {
|
|
|
85
100
|
return AltiumParser.#parseSchematic(
|
|
86
101
|
fileName,
|
|
87
102
|
schematicExtraction?.records || records,
|
|
88
|
-
arrayBuffer
|
|
103
|
+
arrayBuffer,
|
|
104
|
+
schematicExtraction
|
|
89
105
|
)
|
|
90
106
|
}
|
|
91
107
|
if (fileType === 'PcbDoc') {
|
|
@@ -106,6 +122,12 @@ export class AltiumParser {
|
|
|
106
122
|
if (fileType === 'PrjPcb') {
|
|
107
123
|
return PrjPcbModelParser.parse(fileName, arrayBuffer)
|
|
108
124
|
}
|
|
125
|
+
if (fileType === 'IntLib') {
|
|
126
|
+
return IntLibModelParser.parse(
|
|
127
|
+
fileName,
|
|
128
|
+
IntLibStreamExtractor.extractFromArrayBuffer(arrayBuffer)
|
|
129
|
+
)
|
|
130
|
+
}
|
|
109
131
|
throw new Error('Unsupported file type: ' + fileName)
|
|
110
132
|
}
|
|
111
133
|
|
|
@@ -113,7 +135,7 @@ export class AltiumParser {
|
|
|
113
135
|
* Chooses the format based on extension and content.
|
|
114
136
|
* @param {string} fileName
|
|
115
137
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
116
|
-
* @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb'}
|
|
138
|
+
* @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb' | 'IntLib'}
|
|
117
139
|
*/
|
|
118
140
|
static #sniffFileType(fileName, records) {
|
|
119
141
|
const normalized = String(fileName || '').toLowerCase()
|
|
@@ -121,6 +143,7 @@ export class AltiumParser {
|
|
|
121
143
|
if (normalized.endsWith('.pcbdoc')) return 'PcbDoc'
|
|
122
144
|
if (normalized.endsWith('.pcblib')) return 'PcbLib'
|
|
123
145
|
if (normalized.endsWith('.prjpcb')) return 'PrjPcb'
|
|
146
|
+
if (normalized.endsWith('.intlib')) return 'IntLib'
|
|
124
147
|
|
|
125
148
|
const hasSchematicHeader = records.some((record) =>
|
|
126
149
|
getField(record.fields, 'HEADER').includes('Schematic')
|
|
@@ -132,12 +155,23 @@ export class AltiumParser {
|
|
|
132
155
|
* @param {string} fileName
|
|
133
156
|
* @param {{ raw: string, fields: Record<string, string | string[]> }[]} records
|
|
134
157
|
* @param {ArrayBuffer} arrayBuffer
|
|
158
|
+
* @param {{ embeddedFiles?: object } | null} schematicExtraction
|
|
135
159
|
* @returns {ReturnType<typeof AltiumParser.parseArrayBufferToRendererModel>}
|
|
136
160
|
*/
|
|
137
|
-
static #parseSchematic(
|
|
138
|
-
|
|
161
|
+
static #parseSchematic(
|
|
162
|
+
fileName,
|
|
163
|
+
records,
|
|
164
|
+
arrayBuffer,
|
|
165
|
+
schematicExtraction = null
|
|
166
|
+
) {
|
|
167
|
+
const recordIndexAwareRecords = records.map((record, recordIndex) => ({
|
|
168
|
+
...record,
|
|
169
|
+
recordIndex
|
|
170
|
+
}))
|
|
171
|
+
const componentRecords = recordIndexAwareRecords.filter(
|
|
139
172
|
(record) => getField(record.fields, 'RECORD') === '1'
|
|
140
173
|
)
|
|
174
|
+
const recordTypes = SchematicRecordTypeRegistry.summarize(records)
|
|
141
175
|
const ownersWithImplicitDisplayMode =
|
|
142
176
|
AltiumParser.#collectOwnersWithImplicitDisplayMode(records)
|
|
143
177
|
const activeMultipartOwnerParts =
|
|
@@ -176,6 +210,10 @@ export class AltiumParser {
|
|
|
176
210
|
getField(record.fields, 'RECORD') !== '30' &&
|
|
177
211
|
getField(record.fields, 'RECORD') !== '37' &&
|
|
178
212
|
!SchematicPrimitiveParser.isRectangleRecord(record.fields) &&
|
|
213
|
+
!SchematicPrimitiveParser.isRoundedRectangleRecord(
|
|
214
|
+
record.fields
|
|
215
|
+
) &&
|
|
216
|
+
!SchematicPrimitiveParser.isIeeeSymbolRecord(record.fields) &&
|
|
179
217
|
!AltiumParser.#hasDisplayText(record.fields) &&
|
|
180
218
|
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
181
219
|
AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
|
|
@@ -192,12 +230,34 @@ export class AltiumParser {
|
|
|
192
230
|
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
193
231
|
AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
|
|
194
232
|
)
|
|
233
|
+
const roundedRectangleRecords = drawableRecords.filter(
|
|
234
|
+
(record) =>
|
|
235
|
+
SchematicPrimitiveParser.isRoundedRectangleRecord(
|
|
236
|
+
record.fields
|
|
237
|
+
) &&
|
|
238
|
+
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
239
|
+
AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
|
|
240
|
+
)
|
|
241
|
+
const ieeeSymbolRecords = drawableRecords.filter(
|
|
242
|
+
(record) =>
|
|
243
|
+
SchematicPrimitiveParser.isIeeeSymbolRecord(record.fields) &&
|
|
244
|
+
AltiumParser.#hasCoordinatePair(record.fields, 'Location')
|
|
245
|
+
)
|
|
195
246
|
const arcRecords = drawableRecords.filter(
|
|
196
247
|
(record) =>
|
|
197
248
|
['11', '12'].includes(getField(record.fields, 'RECORD')) &&
|
|
198
249
|
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
199
250
|
parseNumericField(record.fields, 'Radius') !== null
|
|
200
251
|
)
|
|
252
|
+
const bezierRecords = drawableRecords.filter(
|
|
253
|
+
(record) => getField(record.fields, 'RECORD') === '5'
|
|
254
|
+
)
|
|
255
|
+
const pieRecords = drawableRecords.filter(
|
|
256
|
+
(record) =>
|
|
257
|
+
getField(record.fields, 'RECORD') === '9' &&
|
|
258
|
+
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
259
|
+
parseNumericField(record.fields, 'Radius') !== null
|
|
260
|
+
)
|
|
201
261
|
const ellipseRecords = drawableRecords.filter(
|
|
202
262
|
(record) =>
|
|
203
263
|
getField(record.fields, 'RECORD') === '8' &&
|
|
@@ -227,10 +287,26 @@ export class AltiumParser {
|
|
|
227
287
|
const crossRecords = drawableRecords.filter(
|
|
228
288
|
(record) => getField(record.fields, 'RECORD') === '22'
|
|
229
289
|
)
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
290
|
+
const ownership = SchematicOwnershipGraphParser.parse(
|
|
291
|
+
recordIndexAwareRecords
|
|
292
|
+
)
|
|
293
|
+
const harnesses = SchematicHarnessParser.parse(recordIndexAwareRecords)
|
|
294
|
+
const implementations = SchematicImplementationParser.parse(
|
|
295
|
+
recordIndexAwareRecords
|
|
296
|
+
)
|
|
297
|
+
const displayModes = SchematicDisplayModeCatalogParser.parse(
|
|
298
|
+
recordIndexAwareRecords
|
|
299
|
+
)
|
|
300
|
+
const bindings = SchematicBindingProvenanceParser.parse(
|
|
301
|
+
recordIndexAwareRecords,
|
|
302
|
+
implementations
|
|
303
|
+
)
|
|
304
|
+
const crossSheetConnectors = SchematicCrossSheetConnectorParser.parse(
|
|
305
|
+
recordIndexAwareRecords
|
|
306
|
+
)
|
|
307
|
+
const repeatedChannels = SchematicRepeatedChannelParser.parse(
|
|
308
|
+
recordIndexAwareRecords
|
|
309
|
+
)
|
|
234
310
|
const relatedTexts = new Map()
|
|
235
311
|
|
|
236
312
|
for (const record of records) {
|
|
@@ -244,6 +320,9 @@ export class AltiumParser {
|
|
|
244
320
|
|
|
245
321
|
const metadataTexts = extractSchematicMetadata(textRecords)
|
|
246
322
|
const schematicFonts = extractSchematicFonts(sheetRecord?.fields)
|
|
323
|
+
const schematicRenderDiagnostics = extractSchematicFontDiagnostics(
|
|
324
|
+
sheetRecord?.fields
|
|
325
|
+
)
|
|
247
326
|
const sheetWidth =
|
|
248
327
|
parseNumericField(sheetRecord?.fields, 'CustomX') || 1500
|
|
249
328
|
const sheetHeight =
|
|
@@ -343,6 +422,9 @@ export class AltiumParser {
|
|
|
343
422
|
polygons
|
|
344
423
|
))
|
|
345
424
|
const arcs = SchematicPrimitiveParser.parseSchematicArcs(arcRecords)
|
|
425
|
+
const beziers =
|
|
426
|
+
SchematicPrimitiveParser.parseSchematicBeziers(bezierRecords)
|
|
427
|
+
const pies = SchematicPrimitiveParser.parseSchematicPies(pieRecords)
|
|
346
428
|
const ellipses =
|
|
347
429
|
SchematicPrimitiveParser.parseSchematicEllipses(ellipseRecords)
|
|
348
430
|
const rectangles =
|
|
@@ -358,8 +440,20 @@ export class AltiumParser {
|
|
|
358
440
|
)
|
|
359
441
|
const regions =
|
|
360
442
|
SchematicPrimitiveParser.parseSchematicRegions(regionRecords)
|
|
443
|
+
const roundedRectangles =
|
|
444
|
+
SchematicPrimitiveParser.parseSchematicRoundedRectangles(
|
|
445
|
+
roundedRectangleRecords
|
|
446
|
+
)
|
|
447
|
+
const ieeeSymbols =
|
|
448
|
+
SchematicPrimitiveParser.parseSchematicIeeeSymbols(
|
|
449
|
+
ieeeSymbolRecords
|
|
450
|
+
)
|
|
361
451
|
const directives =
|
|
362
452
|
SchematicDirectiveParser.parseSchematicDirectives(directiveRecords)
|
|
453
|
+
const directiveSemantics =
|
|
454
|
+
SchematicDirectiveParser.parseDirectiveSemantics(
|
|
455
|
+
recordIndexAwareRecords
|
|
456
|
+
)
|
|
363
457
|
const { sheetSymbols, sheetEntries } = SchematicSheetParser.parse(
|
|
364
458
|
recordIndexAwareRecords
|
|
365
459
|
)
|
|
@@ -371,6 +465,11 @@ export class AltiumParser {
|
|
|
371
465
|
recordIndexAwareRecords,
|
|
372
466
|
arrayBuffer
|
|
373
467
|
)
|
|
468
|
+
const template = SchematicTemplateParser.parse(
|
|
469
|
+
recordIndexAwareRecords,
|
|
470
|
+
sheetRecord,
|
|
471
|
+
sheet
|
|
472
|
+
)
|
|
374
473
|
|
|
375
474
|
const ports = parseSchematicPorts(portRecords, lines)
|
|
376
475
|
const crosses = parseSchematicCrosses(crossRecords)
|
|
@@ -421,6 +520,8 @@ export class AltiumParser {
|
|
|
421
520
|
pins,
|
|
422
521
|
ports
|
|
423
522
|
)
|
|
523
|
+
const textFrames =
|
|
524
|
+
SchematicTextParser.extractSchematicTextFrames(anchoredTexts)
|
|
424
525
|
|
|
425
526
|
const components = componentRecords.map((record) => {
|
|
426
527
|
const x = parseNumericField(record.fields, 'Location.X') || 0
|
|
@@ -428,25 +529,28 @@ export class AltiumParser {
|
|
|
428
529
|
const libReference =
|
|
429
530
|
getField(record.fields, 'LibReference') ||
|
|
430
531
|
getField(record.fields, 'DesignItemId')
|
|
431
|
-
const
|
|
432
|
-
(
|
|
532
|
+
const ownerTexts =
|
|
533
|
+
SchematicComponentOwnerTextResolver.resolveOwnerTexts(
|
|
534
|
+
record,
|
|
535
|
+
recordIndexAwareRecords,
|
|
536
|
+
relatedTexts
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
const designator = SchematicComponentTextResolver.resolveDesignator(
|
|
540
|
+
ownerTexts,
|
|
541
|
+
anchoredTexts,
|
|
542
|
+
{
|
|
543
|
+
x,
|
|
544
|
+
y,
|
|
545
|
+
libReference
|
|
546
|
+
}
|
|
433
547
|
)
|
|
434
|
-
const ownerTexts = relatedTexts.get(ownerIndex) || []
|
|
435
548
|
|
|
436
549
|
return {
|
|
437
550
|
x,
|
|
438
551
|
y,
|
|
439
552
|
libReference,
|
|
440
|
-
designator:
|
|
441
|
-
SchematicComponentTextResolver.resolveDesignator(
|
|
442
|
-
ownerTexts,
|
|
443
|
-
anchoredTexts,
|
|
444
|
-
{
|
|
445
|
-
x,
|
|
446
|
-
y,
|
|
447
|
-
libReference
|
|
448
|
-
}
|
|
449
|
-
) || 'U?',
|
|
553
|
+
designator: designator === null ? 'U?' : designator,
|
|
450
554
|
value: SchematicComponentTextResolver.resolveValue(
|
|
451
555
|
ownerTexts,
|
|
452
556
|
anchoredTexts,
|
|
@@ -513,6 +617,16 @@ export class AltiumParser {
|
|
|
513
617
|
'Sheet metadata record 31 was not found. Using fallback dimensions.'
|
|
514
618
|
})
|
|
515
619
|
}
|
|
620
|
+
diagnostics.push(
|
|
621
|
+
...schematicRenderDiagnostics.fontFallbacks.map((fallback) => ({
|
|
622
|
+
severity: fallback.severity,
|
|
623
|
+
code: fallback.code,
|
|
624
|
+
fontId: fallback.fontId,
|
|
625
|
+
sourceFamily: fallback.sourceFamily,
|
|
626
|
+
resolvedFamily: fallback.resolvedFamily,
|
|
627
|
+
message: fallback.message
|
|
628
|
+
}))
|
|
629
|
+
)
|
|
516
630
|
diagnostics.push(...imageDiagnostics)
|
|
517
631
|
const { nets, diagnostics: netDiagnostics } =
|
|
518
632
|
SchematicNetlistBuilder.build({
|
|
@@ -520,11 +634,36 @@ export class AltiumParser {
|
|
|
520
634
|
texts: anchoredTexts,
|
|
521
635
|
pins,
|
|
522
636
|
ports,
|
|
637
|
+
crossSheetConnectors: crossSheetConnectors?.connectors || [],
|
|
523
638
|
junctions,
|
|
524
639
|
busEntries,
|
|
525
640
|
sheetEntries
|
|
526
641
|
})
|
|
527
642
|
diagnostics.push(...netDiagnostics)
|
|
643
|
+
const qa = SchematicQaReportBuilder.build({
|
|
644
|
+
records: recordIndexAwareRecords,
|
|
645
|
+
sheet: resolvedSheet,
|
|
646
|
+
lines: normalizedLines,
|
|
647
|
+
texts: anchoredTexts
|
|
648
|
+
})
|
|
649
|
+
const connectivityQa = SchematicConnectivityQaBuilder.build({
|
|
650
|
+
nets,
|
|
651
|
+
texts: anchoredTexts,
|
|
652
|
+
pins,
|
|
653
|
+
ports,
|
|
654
|
+
junctions
|
|
655
|
+
})
|
|
656
|
+
const embeddedFiles = schematicExtraction?.embeddedFiles || null
|
|
657
|
+
|
|
658
|
+
if (embeddedFiles?.diagnostics?.length) {
|
|
659
|
+
diagnostics.push(
|
|
660
|
+
...embeddedFiles.diagnostics.map((issue) => ({
|
|
661
|
+
severity: issue.severity === 'info' ? 'info' : 'warning',
|
|
662
|
+
code: issue.code,
|
|
663
|
+
message: issue.message
|
|
664
|
+
}))
|
|
665
|
+
)
|
|
666
|
+
}
|
|
528
667
|
|
|
529
668
|
return NormalizedModelSchema.attach({
|
|
530
669
|
kind: 'schematic',
|
|
@@ -535,19 +674,30 @@ export class AltiumParser {
|
|
|
535
674
|
componentCount: components.length,
|
|
536
675
|
lineCount: lines.length,
|
|
537
676
|
textCount: anchoredTexts.length,
|
|
677
|
+
recordTypeCount: recordTypes.length,
|
|
538
678
|
bomRowCount: bom.length
|
|
539
679
|
},
|
|
540
680
|
diagnostics,
|
|
541
681
|
schematic: {
|
|
542
682
|
sheet: resolvedSheet,
|
|
683
|
+
recordTypes,
|
|
684
|
+
...(schematicRenderDiagnostics.fontFallbacks.length
|
|
685
|
+
? { renderDiagnostics: schematicRenderDiagnostics }
|
|
686
|
+
: {}),
|
|
543
687
|
lines: normalizedLines,
|
|
544
688
|
polygons,
|
|
545
689
|
rectangles,
|
|
690
|
+
roundedRectangles,
|
|
546
691
|
regions,
|
|
547
692
|
ellipses,
|
|
548
693
|
arcs,
|
|
694
|
+
beziers,
|
|
695
|
+
pies,
|
|
696
|
+
ieeeSymbols,
|
|
549
697
|
directives,
|
|
698
|
+
directiveSemantics,
|
|
550
699
|
texts: anchoredTexts,
|
|
700
|
+
textFrames,
|
|
551
701
|
components,
|
|
552
702
|
pins,
|
|
553
703
|
ports,
|
|
@@ -560,7 +710,22 @@ export class AltiumParser {
|
|
|
560
710
|
junctions,
|
|
561
711
|
busEntries,
|
|
562
712
|
images,
|
|
563
|
-
nets
|
|
713
|
+
nets,
|
|
714
|
+
ownership,
|
|
715
|
+
...(template ? { template } : {}),
|
|
716
|
+
...(harnesses ? { harnesses } : {}),
|
|
717
|
+
...(implementations ? { implementations } : {}),
|
|
718
|
+
...(displayModes ? { displayModes } : {}),
|
|
719
|
+
...(bindings ? { bindings } : {}),
|
|
720
|
+
...(crossSheetConnectors ? { crossSheetConnectors } : {}),
|
|
721
|
+
...(repeatedChannels ? { repeatedChannels } : {}),
|
|
722
|
+
...(embeddedFiles &&
|
|
723
|
+
(embeddedFiles.files?.length ||
|
|
724
|
+
embeddedFiles.diagnostics?.length)
|
|
725
|
+
? { embeddedFiles }
|
|
726
|
+
: {}),
|
|
727
|
+
qa,
|
|
728
|
+
connectivityQa
|
|
564
729
|
},
|
|
565
730
|
bom
|
|
566
731
|
})
|
|
@@ -598,7 +763,9 @@ export class AltiumParser {
|
|
|
598
763
|
ownerPartId &&
|
|
599
764
|
ownerPartId !== '-1' &&
|
|
600
765
|
!getField(record.fields, 'OwnerPartDisplayMode') &&
|
|
601
|
-
|
|
766
|
+
SchematicComponentOwnerTextResolver.isDisplayModeSelectablePrimitive(
|
|
767
|
+
record.fields
|
|
768
|
+
)
|
|
602
769
|
) {
|
|
603
770
|
owners.add(ownerIndex)
|
|
604
771
|
}
|
|
@@ -652,27 +819,6 @@ export class AltiumParser {
|
|
|
652
819
|
)
|
|
653
820
|
}
|
|
654
821
|
|
|
655
|
-
/**
|
|
656
|
-
* Returns true when a schematic primitive participates in owner display
|
|
657
|
-
* mode selection.
|
|
658
|
-
* @param {Record<string, string | string[]>} fields
|
|
659
|
-
* @returns {boolean}
|
|
660
|
-
*/
|
|
661
|
-
static #isDisplayModeSelectablePrimitive(fields) {
|
|
662
|
-
const recordType = getField(fields, 'RECORD')
|
|
663
|
-
|
|
664
|
-
return (
|
|
665
|
-
recordType === '2' ||
|
|
666
|
-
recordType === '6' ||
|
|
667
|
-
recordType === '11' ||
|
|
668
|
-
recordType === '12' ||
|
|
669
|
-
recordType === '13' ||
|
|
670
|
-
recordType === '27' ||
|
|
671
|
-
(AltiumParser.#hasCoordinatePair(fields, 'Location') &&
|
|
672
|
-
AltiumParser.#hasCoordinatePair(fields, 'Corner'))
|
|
673
|
-
)
|
|
674
|
-
}
|
|
675
|
-
|
|
676
822
|
/**
|
|
677
823
|
* Groups designators into BOM rows.
|
|
678
824
|
* @param {{ designator: string, pattern: string, source: string, value: string }[]} entries
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a generic inventory for embedded payload streams.
|
|
7
|
+
*/
|
|
8
|
+
export class EmbeddedFileInventoryBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.embedded-files.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds an embedded-file inventory from a stream map.
|
|
13
|
+
* @param {Map<string, Uint8Array>} streams Compound-document streams.
|
|
14
|
+
* @param {{ skipStreamNames?: Iterable<string> }} [options] Inventory options.
|
|
15
|
+
* @returns {{ schema: string, files: object[], diagnostics: object[] }}
|
|
16
|
+
*/
|
|
17
|
+
static buildFromStreams(streams, options = {}) {
|
|
18
|
+
const skipStreamNames = new Set(options.skipStreamNames || [])
|
|
19
|
+
const files = []
|
|
20
|
+
const diagnostics = []
|
|
21
|
+
|
|
22
|
+
for (const [sourceStream, bytes] of streams || []) {
|
|
23
|
+
if (
|
|
24
|
+
skipStreamNames.has(sourceStream) ||
|
|
25
|
+
!EmbeddedFileInventoryBuilder.#isEmbeddedPayloadStream(
|
|
26
|
+
sourceStream
|
|
27
|
+
)
|
|
28
|
+
) {
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0) {
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
code: 'embedded-file.empty',
|
|
35
|
+
severity: 'warning',
|
|
36
|
+
sourceStream,
|
|
37
|
+
message: 'Embedded payload stream was empty.'
|
|
38
|
+
})
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
files.push(
|
|
43
|
+
EmbeddedFileInventoryBuilder.#fileRecord(sourceStream, bytes)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
schema: EmbeddedFileInventoryBuilder.SCHEMA_ID,
|
|
49
|
+
files: files.sort((left, right) =>
|
|
50
|
+
left.sourceStream.localeCompare(right.sourceStream)
|
|
51
|
+
),
|
|
52
|
+
diagnostics: diagnostics.sort((left, right) =>
|
|
53
|
+
left.sourceStream.localeCompare(right.sourceStream)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns true when a stream name represents a payload-style embedded file.
|
|
60
|
+
* @param {string} sourceStream Stream name.
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
static #isEmbeddedPayloadStream(sourceStream) {
|
|
64
|
+
const normalized = String(sourceStream || '')
|
|
65
|
+
if (/\/(?:Data|Header)$/i.test(normalized)) {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
/^(EmbeddedFiles|Embedded|Attachments|Images?)\//i.test(
|
|
71
|
+
normalized
|
|
72
|
+
) ||
|
|
73
|
+
/^Models\/\d+$/i.test(normalized) ||
|
|
74
|
+
/\.[A-Za-z0-9]{2,8}$/.test(normalized)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Builds one file inventory row.
|
|
80
|
+
* @param {string} sourceStream Stream name.
|
|
81
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
82
|
+
* @returns {object}
|
|
83
|
+
*/
|
|
84
|
+
static #fileRecord(sourceStream, bytes) {
|
|
85
|
+
return {
|
|
86
|
+
sourceStream,
|
|
87
|
+
name: EmbeddedFileInventoryBuilder.#basename(sourceStream),
|
|
88
|
+
format: EmbeddedFileInventoryBuilder.#format(sourceStream, bytes),
|
|
89
|
+
byteLength: bytes.byteLength,
|
|
90
|
+
checksum: {
|
|
91
|
+
algorithm: 'fnv1a32',
|
|
92
|
+
value: EmbeddedFileInventoryBuilder.#fnv1a32(bytes)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolves the payload basename from a stream path.
|
|
99
|
+
* @param {string} sourceStream Stream name.
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
static #basename(sourceStream) {
|
|
103
|
+
return (
|
|
104
|
+
String(sourceStream || '')
|
|
105
|
+
.split('/')
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.pop() || ''
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Classifies payload format from extension, magic bytes, and text probes.
|
|
113
|
+
* @param {string} sourceStream Stream name.
|
|
114
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
static #format(sourceStream, bytes) {
|
|
118
|
+
const lower =
|
|
119
|
+
EmbeddedFileInventoryBuilder.#basename(sourceStream).toLowerCase()
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
lower.endsWith('.png') ||
|
|
123
|
+
EmbeddedFileInventoryBuilder.#hasPrefix(
|
|
124
|
+
bytes,
|
|
125
|
+
[0x89, 0x50, 0x4e, 0x47]
|
|
126
|
+
)
|
|
127
|
+
) {
|
|
128
|
+
return 'png'
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
lower.endsWith('.jpg') ||
|
|
132
|
+
lower.endsWith('.jpeg') ||
|
|
133
|
+
EmbeddedFileInventoryBuilder.#hasPrefix(bytes, [0xff, 0xd8, 0xff])
|
|
134
|
+
) {
|
|
135
|
+
return 'jpeg'
|
|
136
|
+
}
|
|
137
|
+
if (
|
|
138
|
+
lower.endsWith('.gif') ||
|
|
139
|
+
EmbeddedFileInventoryBuilder.#asciiPrefix(bytes).startsWith('GIF')
|
|
140
|
+
) {
|
|
141
|
+
return 'gif'
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
lower.endsWith('.bmp') ||
|
|
145
|
+
EmbeddedFileInventoryBuilder.#asciiPrefix(bytes).startsWith('BM')
|
|
146
|
+
) {
|
|
147
|
+
return 'bmp'
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
lower.endsWith('.svg') ||
|
|
151
|
+
EmbeddedFileInventoryBuilder.#trimmedText(bytes).startsWith('<svg')
|
|
152
|
+
) {
|
|
153
|
+
return 'svg'
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
lower.endsWith('.step') ||
|
|
157
|
+
lower.endsWith('.stp') ||
|
|
158
|
+
EmbeddedFileInventoryBuilder.#trimmedText(bytes).startsWith(
|
|
159
|
+
'ISO-10303-21'
|
|
160
|
+
)
|
|
161
|
+
) {
|
|
162
|
+
return 'step'
|
|
163
|
+
}
|
|
164
|
+
if (lower.endsWith('.sldprt') || lower.endsWith('.sldasm')) {
|
|
165
|
+
return 'solidworks'
|
|
166
|
+
}
|
|
167
|
+
if (lower.endsWith('.x_t') || lower.endsWith('.xmt_txt')) {
|
|
168
|
+
return 'parasolid-text'
|
|
169
|
+
}
|
|
170
|
+
if (lower.endsWith('.x_b') || lower.endsWith('.xmt_bin')) {
|
|
171
|
+
return 'parasolid-binary'
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
lower.endsWith('.pdf') ||
|
|
175
|
+
EmbeddedFileInventoryBuilder.#asciiPrefix(bytes).startsWith('%PDF')
|
|
176
|
+
) {
|
|
177
|
+
return 'pdf'
|
|
178
|
+
}
|
|
179
|
+
if (EmbeddedFileInventoryBuilder.#isLikelyText(bytes)) {
|
|
180
|
+
return 'text'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return 'binary'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Returns true when bytes start with a prefix.
|
|
188
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
189
|
+
* @param {number[]} prefix Prefix bytes.
|
|
190
|
+
* @returns {boolean}
|
|
191
|
+
*/
|
|
192
|
+
static #hasPrefix(bytes, prefix) {
|
|
193
|
+
return prefix.every((value, index) => bytes[index] === value)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Decodes a short ASCII prefix.
|
|
198
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
201
|
+
static #asciiPrefix(bytes) {
|
|
202
|
+
return new TextDecoder('latin1').decode(bytes.slice(0, 8))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Decodes and trims a text probe.
|
|
207
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
static #trimmedText(bytes) {
|
|
211
|
+
return new TextDecoder('utf-8', { fatal: false })
|
|
212
|
+
.decode(bytes.slice(0, Math.min(bytes.byteLength, 256)))
|
|
213
|
+
.trim()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Returns true when a payload is printable enough to treat as text.
|
|
218
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
static #isLikelyText(bytes) {
|
|
222
|
+
let printable = 0
|
|
223
|
+
const length = Math.min(bytes.byteLength, 256)
|
|
224
|
+
|
|
225
|
+
for (let index = 0; index < length; index += 1) {
|
|
226
|
+
const value = bytes[index]
|
|
227
|
+
if (
|
|
228
|
+
value === 0x09 ||
|
|
229
|
+
value === 0x0a ||
|
|
230
|
+
value === 0x0d ||
|
|
231
|
+
(value >= 0x20 && value <= 0x7e)
|
|
232
|
+
) {
|
|
233
|
+
printable += 1
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return length > 0 && printable / length >= 0.9
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Computes an FNV-1a 32-bit checksum.
|
|
242
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
243
|
+
* @returns {string}
|
|
244
|
+
*/
|
|
245
|
+
static #fnv1a32(bytes) {
|
|
246
|
+
let hash = 0x811c9dc5
|
|
247
|
+
|
|
248
|
+
for (const value of bytes) {
|
|
249
|
+
hash ^= value
|
|
250
|
+
hash = Math.imul(hash, 0x01000193) >>> 0
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return hash.toString(16).padStart(8, '0')
|
|
254
|
+
}
|
|
255
|
+
}
|