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
|
@@ -13,6 +13,9 @@ 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'
|
|
18
|
+
import { DraftsmanDigestParser } from './DraftsmanDigestParser.mjs'
|
|
16
19
|
import { PcbModelParser } from './PcbModelParser.mjs'
|
|
17
20
|
import { PcbLibModelParser } from './PcbLibModelParser.mjs'
|
|
18
21
|
import { PcbLibStreamExtractor } from './PcbLibStreamExtractor.mjs'
|
|
@@ -25,8 +28,20 @@ import { SchematicJunctionParser } from './SchematicJunctionParser.mjs'
|
|
|
25
28
|
import { SchematicBusEntryParser } from './SchematicBusEntryParser.mjs'
|
|
26
29
|
import { SchematicImageParser } from './SchematicImageParser.mjs'
|
|
27
30
|
import { SchematicNetlistBuilder } from './SchematicNetlistBuilder.mjs'
|
|
31
|
+
import { SchematicRecordTypeRegistry } from './SchematicRecordTypeRegistry.mjs'
|
|
28
32
|
import { SchematicComponentTextResolver } from './SchematicComponentTextResolver.mjs'
|
|
33
|
+
import { SchematicComponentOwnerTextResolver } from './SchematicComponentOwnerTextResolver.mjs'
|
|
34
|
+
import { SchematicOwnershipGraphParser } from './SchematicOwnershipGraphParser.mjs'
|
|
29
35
|
import { SchematicStreamExtractor } from './SchematicStreamExtractor.mjs'
|
|
36
|
+
import { SchematicTemplateParser } from './SchematicTemplateParser.mjs'
|
|
37
|
+
import { SchematicHarnessParser } from './SchematicHarnessParser.mjs'
|
|
38
|
+
import { SchematicImplementationParser } from './SchematicImplementationParser.mjs'
|
|
39
|
+
import { SchematicCrossSheetConnectorParser } from './SchematicCrossSheetConnectorParser.mjs'
|
|
40
|
+
import { SchematicRepeatedChannelParser } from './SchematicRepeatedChannelParser.mjs'
|
|
41
|
+
import { SchematicDisplayModeCatalogParser } from './SchematicDisplayModeCatalogParser.mjs'
|
|
42
|
+
import { SchematicBindingProvenanceParser } from './SchematicBindingProvenanceParser.mjs'
|
|
43
|
+
import { SchematicConnectivityQaBuilder } from './SchematicConnectivityQaBuilder.mjs'
|
|
44
|
+
import { SchematicQaReportBuilder } from './SchematicQaReportBuilder.mjs'
|
|
30
45
|
import { SchematicWireNormalizer } from './SchematicWireNormalizer.mjs'
|
|
31
46
|
import { CircuitJsonModelAdapter } from '../circuit-json/CircuitJsonModelAdapter.mjs'
|
|
32
47
|
const {
|
|
@@ -41,6 +56,7 @@ const {
|
|
|
41
56
|
} = ParserUtils
|
|
42
57
|
const {
|
|
43
58
|
extractSchematicFonts,
|
|
59
|
+
extractSchematicFontDiagnostics,
|
|
44
60
|
extractSchematicMetadata,
|
|
45
61
|
extractSchematicTitleBlock,
|
|
46
62
|
normalizeSchematicTextRecord
|
|
@@ -74,7 +90,7 @@ export class AltiumParser {
|
|
|
74
90
|
* Parses a native Altium buffer into the renderer compatibility model.
|
|
75
91
|
* @param {string} fileName
|
|
76
92
|
* @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 }[] }}
|
|
93
|
+
* @returns {{ schema: string, kind: 'schematic' | 'pcb' | 'pcb-library' | 'project' | 'integrated-library' | 'draftsman', fileType: 'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb' | 'IntLib' | 'PCBDwf', 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>, draftsman?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
|
|
78
94
|
*/
|
|
79
95
|
static parseArrayBufferToRendererModel(fileName, arrayBuffer) {
|
|
80
96
|
const records = AsciiRecordParser.parse(arrayBuffer)
|
|
@@ -85,7 +101,8 @@ export class AltiumParser {
|
|
|
85
101
|
return AltiumParser.#parseSchematic(
|
|
86
102
|
fileName,
|
|
87
103
|
schematicExtraction?.records || records,
|
|
88
|
-
arrayBuffer
|
|
104
|
+
arrayBuffer,
|
|
105
|
+
schematicExtraction
|
|
89
106
|
)
|
|
90
107
|
}
|
|
91
108
|
if (fileType === 'PcbDoc') {
|
|
@@ -106,6 +123,15 @@ export class AltiumParser {
|
|
|
106
123
|
if (fileType === 'PrjPcb') {
|
|
107
124
|
return PrjPcbModelParser.parse(fileName, arrayBuffer)
|
|
108
125
|
}
|
|
126
|
+
if (fileType === 'IntLib') {
|
|
127
|
+
return IntLibModelParser.parse(
|
|
128
|
+
fileName,
|
|
129
|
+
IntLibStreamExtractor.extractFromArrayBuffer(arrayBuffer)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
if (fileType === 'PCBDwf') {
|
|
133
|
+
return DraftsmanDigestParser.parse(fileName, arrayBuffer)
|
|
134
|
+
}
|
|
109
135
|
throw new Error('Unsupported file type: ' + fileName)
|
|
110
136
|
}
|
|
111
137
|
|
|
@@ -113,7 +139,7 @@ export class AltiumParser {
|
|
|
113
139
|
* Chooses the format based on extension and content.
|
|
114
140
|
* @param {string} fileName
|
|
115
141
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
116
|
-
* @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb'}
|
|
142
|
+
* @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb' | 'IntLib' | 'PCBDwf'}
|
|
117
143
|
*/
|
|
118
144
|
static #sniffFileType(fileName, records) {
|
|
119
145
|
const normalized = String(fileName || '').toLowerCase()
|
|
@@ -121,6 +147,8 @@ export class AltiumParser {
|
|
|
121
147
|
if (normalized.endsWith('.pcbdoc')) return 'PcbDoc'
|
|
122
148
|
if (normalized.endsWith('.pcblib')) return 'PcbLib'
|
|
123
149
|
if (normalized.endsWith('.prjpcb')) return 'PrjPcb'
|
|
150
|
+
if (normalized.endsWith('.intlib')) return 'IntLib'
|
|
151
|
+
if (normalized.endsWith('.pcbdwf')) return 'PCBDwf'
|
|
124
152
|
|
|
125
153
|
const hasSchematicHeader = records.some((record) =>
|
|
126
154
|
getField(record.fields, 'HEADER').includes('Schematic')
|
|
@@ -132,12 +160,23 @@ export class AltiumParser {
|
|
|
132
160
|
* @param {string} fileName
|
|
133
161
|
* @param {{ raw: string, fields: Record<string, string | string[]> }[]} records
|
|
134
162
|
* @param {ArrayBuffer} arrayBuffer
|
|
163
|
+
* @param {{ embeddedFiles?: object } | null} schematicExtraction
|
|
135
164
|
* @returns {ReturnType<typeof AltiumParser.parseArrayBufferToRendererModel>}
|
|
136
165
|
*/
|
|
137
|
-
static #parseSchematic(
|
|
138
|
-
|
|
166
|
+
static #parseSchematic(
|
|
167
|
+
fileName,
|
|
168
|
+
records,
|
|
169
|
+
arrayBuffer,
|
|
170
|
+
schematicExtraction = null
|
|
171
|
+
) {
|
|
172
|
+
const recordIndexAwareRecords = records.map((record, recordIndex) => ({
|
|
173
|
+
...record,
|
|
174
|
+
recordIndex
|
|
175
|
+
}))
|
|
176
|
+
const componentRecords = recordIndexAwareRecords.filter(
|
|
139
177
|
(record) => getField(record.fields, 'RECORD') === '1'
|
|
140
178
|
)
|
|
179
|
+
const recordTypes = SchematicRecordTypeRegistry.summarize(records)
|
|
141
180
|
const ownersWithImplicitDisplayMode =
|
|
142
181
|
AltiumParser.#collectOwnersWithImplicitDisplayMode(records)
|
|
143
182
|
const activeMultipartOwnerParts =
|
|
@@ -176,6 +215,10 @@ export class AltiumParser {
|
|
|
176
215
|
getField(record.fields, 'RECORD') !== '30' &&
|
|
177
216
|
getField(record.fields, 'RECORD') !== '37' &&
|
|
178
217
|
!SchematicPrimitiveParser.isRectangleRecord(record.fields) &&
|
|
218
|
+
!SchematicPrimitiveParser.isRoundedRectangleRecord(
|
|
219
|
+
record.fields
|
|
220
|
+
) &&
|
|
221
|
+
!SchematicPrimitiveParser.isIeeeSymbolRecord(record.fields) &&
|
|
179
222
|
!AltiumParser.#hasDisplayText(record.fields) &&
|
|
180
223
|
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
181
224
|
AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
|
|
@@ -192,12 +235,34 @@ export class AltiumParser {
|
|
|
192
235
|
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
193
236
|
AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
|
|
194
237
|
)
|
|
238
|
+
const roundedRectangleRecords = drawableRecords.filter(
|
|
239
|
+
(record) =>
|
|
240
|
+
SchematicPrimitiveParser.isRoundedRectangleRecord(
|
|
241
|
+
record.fields
|
|
242
|
+
) &&
|
|
243
|
+
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
244
|
+
AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
|
|
245
|
+
)
|
|
246
|
+
const ieeeSymbolRecords = drawableRecords.filter(
|
|
247
|
+
(record) =>
|
|
248
|
+
SchematicPrimitiveParser.isIeeeSymbolRecord(record.fields) &&
|
|
249
|
+
AltiumParser.#hasCoordinatePair(record.fields, 'Location')
|
|
250
|
+
)
|
|
195
251
|
const arcRecords = drawableRecords.filter(
|
|
196
252
|
(record) =>
|
|
197
253
|
['11', '12'].includes(getField(record.fields, 'RECORD')) &&
|
|
198
254
|
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
199
255
|
parseNumericField(record.fields, 'Radius') !== null
|
|
200
256
|
)
|
|
257
|
+
const bezierRecords = drawableRecords.filter(
|
|
258
|
+
(record) => getField(record.fields, 'RECORD') === '5'
|
|
259
|
+
)
|
|
260
|
+
const pieRecords = drawableRecords.filter(
|
|
261
|
+
(record) =>
|
|
262
|
+
getField(record.fields, 'RECORD') === '9' &&
|
|
263
|
+
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
264
|
+
parseNumericField(record.fields, 'Radius') !== null
|
|
265
|
+
)
|
|
201
266
|
const ellipseRecords = drawableRecords.filter(
|
|
202
267
|
(record) =>
|
|
203
268
|
getField(record.fields, 'RECORD') === '8' &&
|
|
@@ -227,10 +292,26 @@ export class AltiumParser {
|
|
|
227
292
|
const crossRecords = drawableRecords.filter(
|
|
228
293
|
(record) => getField(record.fields, 'RECORD') === '22'
|
|
229
294
|
)
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
295
|
+
const ownership = SchematicOwnershipGraphParser.parse(
|
|
296
|
+
recordIndexAwareRecords
|
|
297
|
+
)
|
|
298
|
+
const harnesses = SchematicHarnessParser.parse(recordIndexAwareRecords)
|
|
299
|
+
const implementations = SchematicImplementationParser.parse(
|
|
300
|
+
recordIndexAwareRecords
|
|
301
|
+
)
|
|
302
|
+
const displayModes = SchematicDisplayModeCatalogParser.parse(
|
|
303
|
+
recordIndexAwareRecords
|
|
304
|
+
)
|
|
305
|
+
const bindings = SchematicBindingProvenanceParser.parse(
|
|
306
|
+
recordIndexAwareRecords,
|
|
307
|
+
implementations
|
|
308
|
+
)
|
|
309
|
+
const crossSheetConnectors = SchematicCrossSheetConnectorParser.parse(
|
|
310
|
+
recordIndexAwareRecords
|
|
311
|
+
)
|
|
312
|
+
const repeatedChannels = SchematicRepeatedChannelParser.parse(
|
|
313
|
+
recordIndexAwareRecords
|
|
314
|
+
)
|
|
234
315
|
const relatedTexts = new Map()
|
|
235
316
|
|
|
236
317
|
for (const record of records) {
|
|
@@ -244,6 +325,9 @@ export class AltiumParser {
|
|
|
244
325
|
|
|
245
326
|
const metadataTexts = extractSchematicMetadata(textRecords)
|
|
246
327
|
const schematicFonts = extractSchematicFonts(sheetRecord?.fields)
|
|
328
|
+
const schematicRenderDiagnostics = extractSchematicFontDiagnostics(
|
|
329
|
+
sheetRecord?.fields
|
|
330
|
+
)
|
|
247
331
|
const sheetWidth =
|
|
248
332
|
parseNumericField(sheetRecord?.fields, 'CustomX') || 1500
|
|
249
333
|
const sheetHeight =
|
|
@@ -343,6 +427,9 @@ export class AltiumParser {
|
|
|
343
427
|
polygons
|
|
344
428
|
))
|
|
345
429
|
const arcs = SchematicPrimitiveParser.parseSchematicArcs(arcRecords)
|
|
430
|
+
const beziers =
|
|
431
|
+
SchematicPrimitiveParser.parseSchematicBeziers(bezierRecords)
|
|
432
|
+
const pies = SchematicPrimitiveParser.parseSchematicPies(pieRecords)
|
|
346
433
|
const ellipses =
|
|
347
434
|
SchematicPrimitiveParser.parseSchematicEllipses(ellipseRecords)
|
|
348
435
|
const rectangles =
|
|
@@ -358,8 +445,20 @@ export class AltiumParser {
|
|
|
358
445
|
)
|
|
359
446
|
const regions =
|
|
360
447
|
SchematicPrimitiveParser.parseSchematicRegions(regionRecords)
|
|
448
|
+
const roundedRectangles =
|
|
449
|
+
SchematicPrimitiveParser.parseSchematicRoundedRectangles(
|
|
450
|
+
roundedRectangleRecords
|
|
451
|
+
)
|
|
452
|
+
const ieeeSymbols =
|
|
453
|
+
SchematicPrimitiveParser.parseSchematicIeeeSymbols(
|
|
454
|
+
ieeeSymbolRecords
|
|
455
|
+
)
|
|
361
456
|
const directives =
|
|
362
457
|
SchematicDirectiveParser.parseSchematicDirectives(directiveRecords)
|
|
458
|
+
const directiveSemantics =
|
|
459
|
+
SchematicDirectiveParser.parseDirectiveSemantics(
|
|
460
|
+
recordIndexAwareRecords
|
|
461
|
+
)
|
|
363
462
|
const { sheetSymbols, sheetEntries } = SchematicSheetParser.parse(
|
|
364
463
|
recordIndexAwareRecords
|
|
365
464
|
)
|
|
@@ -371,6 +470,11 @@ export class AltiumParser {
|
|
|
371
470
|
recordIndexAwareRecords,
|
|
372
471
|
arrayBuffer
|
|
373
472
|
)
|
|
473
|
+
const template = SchematicTemplateParser.parse(
|
|
474
|
+
recordIndexAwareRecords,
|
|
475
|
+
sheetRecord,
|
|
476
|
+
sheet
|
|
477
|
+
)
|
|
374
478
|
|
|
375
479
|
const ports = parseSchematicPorts(portRecords, lines)
|
|
376
480
|
const crosses = parseSchematicCrosses(crossRecords)
|
|
@@ -421,6 +525,8 @@ export class AltiumParser {
|
|
|
421
525
|
pins,
|
|
422
526
|
ports
|
|
423
527
|
)
|
|
528
|
+
const textFrames =
|
|
529
|
+
SchematicTextParser.extractSchematicTextFrames(anchoredTexts)
|
|
424
530
|
|
|
425
531
|
const components = componentRecords.map((record) => {
|
|
426
532
|
const x = parseNumericField(record.fields, 'Location.X') || 0
|
|
@@ -428,25 +534,28 @@ export class AltiumParser {
|
|
|
428
534
|
const libReference =
|
|
429
535
|
getField(record.fields, 'LibReference') ||
|
|
430
536
|
getField(record.fields, 'DesignItemId')
|
|
431
|
-
const
|
|
432
|
-
(
|
|
537
|
+
const ownerTexts =
|
|
538
|
+
SchematicComponentOwnerTextResolver.resolveOwnerTexts(
|
|
539
|
+
record,
|
|
540
|
+
recordIndexAwareRecords,
|
|
541
|
+
relatedTexts
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
const designator = SchematicComponentTextResolver.resolveDesignator(
|
|
545
|
+
ownerTexts,
|
|
546
|
+
anchoredTexts,
|
|
547
|
+
{
|
|
548
|
+
x,
|
|
549
|
+
y,
|
|
550
|
+
libReference
|
|
551
|
+
}
|
|
433
552
|
)
|
|
434
|
-
const ownerTexts = relatedTexts.get(ownerIndex) || []
|
|
435
553
|
|
|
436
554
|
return {
|
|
437
555
|
x,
|
|
438
556
|
y,
|
|
439
557
|
libReference,
|
|
440
|
-
designator:
|
|
441
|
-
SchematicComponentTextResolver.resolveDesignator(
|
|
442
|
-
ownerTexts,
|
|
443
|
-
anchoredTexts,
|
|
444
|
-
{
|
|
445
|
-
x,
|
|
446
|
-
y,
|
|
447
|
-
libReference
|
|
448
|
-
}
|
|
449
|
-
) || 'U?',
|
|
558
|
+
designator: designator === null ? 'U?' : designator,
|
|
450
559
|
value: SchematicComponentTextResolver.resolveValue(
|
|
451
560
|
ownerTexts,
|
|
452
561
|
anchoredTexts,
|
|
@@ -513,6 +622,16 @@ export class AltiumParser {
|
|
|
513
622
|
'Sheet metadata record 31 was not found. Using fallback dimensions.'
|
|
514
623
|
})
|
|
515
624
|
}
|
|
625
|
+
diagnostics.push(
|
|
626
|
+
...schematicRenderDiagnostics.fontFallbacks.map((fallback) => ({
|
|
627
|
+
severity: fallback.severity,
|
|
628
|
+
code: fallback.code,
|
|
629
|
+
fontId: fallback.fontId,
|
|
630
|
+
sourceFamily: fallback.sourceFamily,
|
|
631
|
+
resolvedFamily: fallback.resolvedFamily,
|
|
632
|
+
message: fallback.message
|
|
633
|
+
}))
|
|
634
|
+
)
|
|
516
635
|
diagnostics.push(...imageDiagnostics)
|
|
517
636
|
const { nets, diagnostics: netDiagnostics } =
|
|
518
637
|
SchematicNetlistBuilder.build({
|
|
@@ -520,11 +639,36 @@ export class AltiumParser {
|
|
|
520
639
|
texts: anchoredTexts,
|
|
521
640
|
pins,
|
|
522
641
|
ports,
|
|
642
|
+
crossSheetConnectors: crossSheetConnectors?.connectors || [],
|
|
523
643
|
junctions,
|
|
524
644
|
busEntries,
|
|
525
645
|
sheetEntries
|
|
526
646
|
})
|
|
527
647
|
diagnostics.push(...netDiagnostics)
|
|
648
|
+
const qa = SchematicQaReportBuilder.build({
|
|
649
|
+
records: recordIndexAwareRecords,
|
|
650
|
+
sheet: resolvedSheet,
|
|
651
|
+
lines: normalizedLines,
|
|
652
|
+
texts: anchoredTexts
|
|
653
|
+
})
|
|
654
|
+
const connectivityQa = SchematicConnectivityQaBuilder.build({
|
|
655
|
+
nets,
|
|
656
|
+
texts: anchoredTexts,
|
|
657
|
+
pins,
|
|
658
|
+
ports,
|
|
659
|
+
junctions
|
|
660
|
+
})
|
|
661
|
+
const embeddedFiles = schematicExtraction?.embeddedFiles || null
|
|
662
|
+
|
|
663
|
+
if (embeddedFiles?.diagnostics?.length) {
|
|
664
|
+
diagnostics.push(
|
|
665
|
+
...embeddedFiles.diagnostics.map((issue) => ({
|
|
666
|
+
severity: issue.severity === 'info' ? 'info' : 'warning',
|
|
667
|
+
code: issue.code,
|
|
668
|
+
message: issue.message
|
|
669
|
+
}))
|
|
670
|
+
)
|
|
671
|
+
}
|
|
528
672
|
|
|
529
673
|
return NormalizedModelSchema.attach({
|
|
530
674
|
kind: 'schematic',
|
|
@@ -535,19 +679,30 @@ export class AltiumParser {
|
|
|
535
679
|
componentCount: components.length,
|
|
536
680
|
lineCount: lines.length,
|
|
537
681
|
textCount: anchoredTexts.length,
|
|
682
|
+
recordTypeCount: recordTypes.length,
|
|
538
683
|
bomRowCount: bom.length
|
|
539
684
|
},
|
|
540
685
|
diagnostics,
|
|
541
686
|
schematic: {
|
|
542
687
|
sheet: resolvedSheet,
|
|
688
|
+
recordTypes,
|
|
689
|
+
...(schematicRenderDiagnostics.fontFallbacks.length
|
|
690
|
+
? { renderDiagnostics: schematicRenderDiagnostics }
|
|
691
|
+
: {}),
|
|
543
692
|
lines: normalizedLines,
|
|
544
693
|
polygons,
|
|
545
694
|
rectangles,
|
|
695
|
+
roundedRectangles,
|
|
546
696
|
regions,
|
|
547
697
|
ellipses,
|
|
548
698
|
arcs,
|
|
699
|
+
beziers,
|
|
700
|
+
pies,
|
|
701
|
+
ieeeSymbols,
|
|
549
702
|
directives,
|
|
703
|
+
directiveSemantics,
|
|
550
704
|
texts: anchoredTexts,
|
|
705
|
+
textFrames,
|
|
551
706
|
components,
|
|
552
707
|
pins,
|
|
553
708
|
ports,
|
|
@@ -560,7 +715,22 @@ export class AltiumParser {
|
|
|
560
715
|
junctions,
|
|
561
716
|
busEntries,
|
|
562
717
|
images,
|
|
563
|
-
nets
|
|
718
|
+
nets,
|
|
719
|
+
ownership,
|
|
720
|
+
...(template ? { template } : {}),
|
|
721
|
+
...(harnesses ? { harnesses } : {}),
|
|
722
|
+
...(implementations ? { implementations } : {}),
|
|
723
|
+
...(displayModes ? { displayModes } : {}),
|
|
724
|
+
...(bindings ? { bindings } : {}),
|
|
725
|
+
...(crossSheetConnectors ? { crossSheetConnectors } : {}),
|
|
726
|
+
...(repeatedChannels ? { repeatedChannels } : {}),
|
|
727
|
+
...(embeddedFiles &&
|
|
728
|
+
(embeddedFiles.files?.length ||
|
|
729
|
+
embeddedFiles.diagnostics?.length)
|
|
730
|
+
? { embeddedFiles }
|
|
731
|
+
: {}),
|
|
732
|
+
qa,
|
|
733
|
+
connectivityQa
|
|
564
734
|
},
|
|
565
735
|
bom
|
|
566
736
|
})
|
|
@@ -598,7 +768,9 @@ export class AltiumParser {
|
|
|
598
768
|
ownerPartId &&
|
|
599
769
|
ownerPartId !== '-1' &&
|
|
600
770
|
!getField(record.fields, 'OwnerPartDisplayMode') &&
|
|
601
|
-
|
|
771
|
+
SchematicComponentOwnerTextResolver.isDisplayModeSelectablePrimitive(
|
|
772
|
+
record.fields
|
|
773
|
+
)
|
|
602
774
|
) {
|
|
603
775
|
owners.add(ownerIndex)
|
|
604
776
|
}
|
|
@@ -652,27 +824,6 @@ export class AltiumParser {
|
|
|
652
824
|
)
|
|
653
825
|
}
|
|
654
826
|
|
|
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
827
|
/**
|
|
677
828
|
* Groups designators into BOM rows.
|
|
678
829
|
* @param {{ designator: string, pattern: string, source: string, value: string }[]} entries
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbSvgRenderer } from '../../ui/PcbSvgRenderer.mjs'
|
|
6
|
+
import { SchematicSvgRenderer } from '../../ui/SchematicSvgRenderer.mjs'
|
|
7
|
+
import { PcbStatisticsBuilder } from './PcbStatisticsBuilder.mjs'
|
|
8
|
+
import { ProjectDesignBundleBuilder } from './ProjectDesignBundleBuilder.mjs'
|
|
9
|
+
import { ProjectDocumentGraphBuilder } from './ProjectDocumentGraphBuilder.mjs'
|
|
10
|
+
import { ProjectNetlistExporter } from './ProjectNetlistExporter.mjs'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds one deterministic CI artifact package from parsed project documents.
|
|
14
|
+
*/
|
|
15
|
+
export class CiArtifactBundleBuilder {
|
|
16
|
+
static SCHEMA = 'altium-toolkit.ci.artifact-bundle.a1'
|
|
17
|
+
|
|
18
|
+
static #UNITS = {
|
|
19
|
+
coordinate: 'mil',
|
|
20
|
+
length: 'mil',
|
|
21
|
+
board: 'mil',
|
|
22
|
+
pnp: 'mil',
|
|
23
|
+
angle: 'deg'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static #PNP_UNITS = {
|
|
27
|
+
coordinate: 'mil',
|
|
28
|
+
angle: 'deg'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Builds a deterministic bundle of normalized, rendered, and report outputs.
|
|
33
|
+
* @param {{ projectModel?: object, documentModels?: object[], designBundle?: object, annotationModels?: object[], variantName?: string, renderSchematicSvg?: boolean, renderPcbLayerSvgs?: boolean, schematicSvgOptions?: object }} options Bundle options.
|
|
34
|
+
* @returns {object}
|
|
35
|
+
*/
|
|
36
|
+
static build(options = {}) {
|
|
37
|
+
const documentModels = Array.isArray(options.documentModels)
|
|
38
|
+
? options.documentModels
|
|
39
|
+
: []
|
|
40
|
+
const designBundle =
|
|
41
|
+
options.designBundle ||
|
|
42
|
+
ProjectDesignBundleBuilder.build({
|
|
43
|
+
projectModel: options.projectModel,
|
|
44
|
+
documentModels,
|
|
45
|
+
annotationModels: options.annotationModels || [],
|
|
46
|
+
variantName: options.variantName
|
|
47
|
+
})
|
|
48
|
+
const activeBundle = designBundle.effectiveVariant || designBundle
|
|
49
|
+
const schematicSvgs =
|
|
50
|
+
options.renderSchematicSvg === false
|
|
51
|
+
? []
|
|
52
|
+
: CiArtifactBundleBuilder.#schematicSvgs(
|
|
53
|
+
documentModels,
|
|
54
|
+
options.schematicSvgOptions || {}
|
|
55
|
+
)
|
|
56
|
+
const pcbLayerSvgs =
|
|
57
|
+
options.renderPcbLayerSvgs === false
|
|
58
|
+
? []
|
|
59
|
+
: CiArtifactBundleBuilder.#pcbLayerSvgs(documentModels)
|
|
60
|
+
const statistics = CiArtifactBundleBuilder.#statistics(documentModels)
|
|
61
|
+
const diagnostics = CiArtifactBundleBuilder.#diagnostics(
|
|
62
|
+
designBundle,
|
|
63
|
+
documentModels
|
|
64
|
+
)
|
|
65
|
+
const netlistJson =
|
|
66
|
+
ProjectNetlistExporter.buildNetlistJson(activeBundle)
|
|
67
|
+
const documentGraph =
|
|
68
|
+
designBundle.project?.documentGraph ||
|
|
69
|
+
ProjectDocumentGraphBuilder.build(
|
|
70
|
+
options.projectModel?.project || designBundle.project || {}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
schema: CiArtifactBundleBuilder.SCHEMA,
|
|
75
|
+
summary: {
|
|
76
|
+
normalizedModelCount: documentModels.length,
|
|
77
|
+
schematicSvgCount: schematicSvgs.length,
|
|
78
|
+
pcbLayerSvgCount: pcbLayerSvgs.reduce(
|
|
79
|
+
(total, entry) => total + entry.layers.length,
|
|
80
|
+
0
|
|
81
|
+
),
|
|
82
|
+
netCount: netlistJson.nets.length,
|
|
83
|
+
bomRowCount: (activeBundle.bom || designBundle.bom || [])
|
|
84
|
+
.length,
|
|
85
|
+
pnpCount: (activeBundle.pnp?.entries || []).length,
|
|
86
|
+
diagnosticCount: diagnostics.length
|
|
87
|
+
},
|
|
88
|
+
units: designBundle.units || CiArtifactBundleBuilder.#UNITS,
|
|
89
|
+
designBundle,
|
|
90
|
+
documentGraph,
|
|
91
|
+
normalizedModels: documentModels,
|
|
92
|
+
netlist: {
|
|
93
|
+
json: netlistJson,
|
|
94
|
+
wirelist: ProjectNetlistExporter.buildWirelist(activeBundle)
|
|
95
|
+
},
|
|
96
|
+
bom: {
|
|
97
|
+
rows: activeBundle.bom || designBundle.bom || []
|
|
98
|
+
},
|
|
99
|
+
pnp: CiArtifactBundleBuilder.#pnp(activeBundle, designBundle),
|
|
100
|
+
schematicSvgs,
|
|
101
|
+
pcbLayerSvgs,
|
|
102
|
+
statistics,
|
|
103
|
+
diagnostics
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolves a PnP payload with explicit output units.
|
|
109
|
+
* @param {object} activeBundle Effective bundle or variant.
|
|
110
|
+
* @param {object} designBundle Source design bundle.
|
|
111
|
+
* @returns {object}
|
|
112
|
+
*/
|
|
113
|
+
static #pnp(activeBundle, designBundle) {
|
|
114
|
+
const pnp = activeBundle.pnp || designBundle.pnp || { entries: [] }
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
units: pnp.units || CiArtifactBundleBuilder.#PNP_UNITS,
|
|
118
|
+
...pnp
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Renders schematic SVG entries.
|
|
124
|
+
* @param {object[]} documentModels Parsed document models.
|
|
125
|
+
* @param {object} renderOptions Schematic SVG render options.
|
|
126
|
+
* @returns {object[]}
|
|
127
|
+
*/
|
|
128
|
+
static #schematicSvgs(documentModels, renderOptions) {
|
|
129
|
+
return documentModels
|
|
130
|
+
.filter((model) => model?.kind === 'schematic')
|
|
131
|
+
.map((model) => ({
|
|
132
|
+
fileName: model.fileName || '',
|
|
133
|
+
svg: SchematicSvgRenderer.render(model, renderOptions)
|
|
134
|
+
}))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Renders per-layer PCB SVG entries.
|
|
139
|
+
* @param {object[]} documentModels Parsed document models.
|
|
140
|
+
* @returns {object[]}
|
|
141
|
+
*/
|
|
142
|
+
static #pcbLayerSvgs(documentModels) {
|
|
143
|
+
return documentModels
|
|
144
|
+
.filter((model) => model?.kind === 'pcb')
|
|
145
|
+
.map((model) => ({
|
|
146
|
+
fileName: model.fileName || '',
|
|
147
|
+
layers: PcbSvgRenderer.renderLayerSvgs(model)
|
|
148
|
+
}))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Builds statistics package entries.
|
|
153
|
+
* @param {object[]} documentModels Parsed document models.
|
|
154
|
+
* @returns {{ pcb: object[] }}
|
|
155
|
+
*/
|
|
156
|
+
static #statistics(documentModels) {
|
|
157
|
+
return {
|
|
158
|
+
pcb: documentModels
|
|
159
|
+
.filter((model) => model?.kind === 'pcb')
|
|
160
|
+
.map((model) => ({
|
|
161
|
+
fileName: model.fileName || '',
|
|
162
|
+
statistics:
|
|
163
|
+
model.pcb?.statistics ||
|
|
164
|
+
PcbStatisticsBuilder.build(model.pcb || {})
|
|
165
|
+
}))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Collects diagnostics from the bundle and source documents.
|
|
171
|
+
* @param {object} designBundle Composed design bundle.
|
|
172
|
+
* @param {object[]} documentModels Parsed document models.
|
|
173
|
+
* @returns {object[]}
|
|
174
|
+
*/
|
|
175
|
+
static #diagnostics(designBundle, documentModels) {
|
|
176
|
+
return [
|
|
177
|
+
...CiArtifactBundleBuilder.#sourceDiagnostics(
|
|
178
|
+
'design-bundle',
|
|
179
|
+
designBundle.diagnostics || []
|
|
180
|
+
),
|
|
181
|
+
...documentModels.flatMap((model) =>
|
|
182
|
+
CiArtifactBundleBuilder.#sourceDiagnostics(
|
|
183
|
+
model.fileName || model.kind || 'document',
|
|
184
|
+
model.diagnostics || []
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Adds source labels to diagnostics without changing their codes.
|
|
192
|
+
* @param {string} source Diagnostic source label.
|
|
193
|
+
* @param {object[]} diagnostics Source diagnostics.
|
|
194
|
+
* @returns {object[]}
|
|
195
|
+
*/
|
|
196
|
+
static #sourceDiagnostics(source, diagnostics) {
|
|
197
|
+
return (diagnostics || []).map((diagnostic) => ({
|
|
198
|
+
source,
|
|
199
|
+
...diagnostic
|
|
200
|
+
}))
|
|
201
|
+
}
|
|
202
|
+
}
|