altium-toolkit 1.1.2 → 1.1.22
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/docs/api.md +37 -0
- package/docs/model-format.md +18 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
- package/docs/testing.md +5 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/altium/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +245 -10
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AsciiRecordParser.mjs +43 -11
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PrintableTextDecoder.mjs +133 -13
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicImageParser.mjs +291 -6
- package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicPinParser.mjs +262 -24
- package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +263 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +87 -7
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
- package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
- package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +109 -24
- package/src/ui/SchematicSvgRenderer.mjs +1210 -71
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic textual and byte records for generated Altium libraries.
|
|
7
|
+
*/
|
|
8
|
+
export class AltiumLibraryRecordBuilder {
|
|
9
|
+
/**
|
|
10
|
+
* Builds a schematic component record.
|
|
11
|
+
* @param {object} bundle Normalized component bundle.
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
static buildSchematicComponentRecord(bundle) {
|
|
15
|
+
return AltiumLibraryRecordBuilder.#pipeRecord({
|
|
16
|
+
RECORD: 'Component',
|
|
17
|
+
Name: bundle.symbol.name,
|
|
18
|
+
SourceId: bundle.id,
|
|
19
|
+
DisplayName: bundle.name,
|
|
20
|
+
PinCount: String(bundle.symbol.pins.length),
|
|
21
|
+
PrimitiveCount: String(bundle.symbol.primitives.length)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds a PCB footprint record.
|
|
27
|
+
* @param {object} bundle Normalized component bundle.
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
static buildPcbFootprintRecord(bundle) {
|
|
31
|
+
return AltiumLibraryRecordBuilder.#pipeRecord({
|
|
32
|
+
RECORD: 'Footprint',
|
|
33
|
+
Name: bundle.footprint.name,
|
|
34
|
+
SourceId: bundle.id,
|
|
35
|
+
DisplayName: bundle.name,
|
|
36
|
+
PadCount: String(bundle.footprint.pads.length),
|
|
37
|
+
TrackCount: String(bundle.footprint.tracks.length),
|
|
38
|
+
ModelCount: String(bundle.models.length)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Builds a PcbLib Library/Data stream.
|
|
44
|
+
* @param {object[]} bundles Normalized component bundles.
|
|
45
|
+
* @returns {Uint8Array}
|
|
46
|
+
*/
|
|
47
|
+
static buildPcbLibraryData(bundles) {
|
|
48
|
+
const countBytes = AltiumLibraryRecordBuilder.createCountHeader(
|
|
49
|
+
bundles.length
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return AltiumLibraryRecordBuilder.concatBytes([
|
|
53
|
+
AltiumLibraryRecordBuilder.createProperties({
|
|
54
|
+
HEADER: 'PCB 6.0 Binary Library File',
|
|
55
|
+
WEIGHT: '0',
|
|
56
|
+
GENERATEDBY: 'ECAD Forge'
|
|
57
|
+
}),
|
|
58
|
+
countBytes,
|
|
59
|
+
...bundles.map((bundle) =>
|
|
60
|
+
AltiumLibraryRecordBuilder.createStringBlock(
|
|
61
|
+
bundle.footprint.name
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Builds a PcbLib component parameters table.
|
|
69
|
+
* @param {object[]} bundles Normalized component bundles.
|
|
70
|
+
* @returns {Uint8Array}
|
|
71
|
+
*/
|
|
72
|
+
static buildComponentParamsToc(bundles) {
|
|
73
|
+
return AltiumLibraryRecordBuilder.concatBytes(
|
|
74
|
+
bundles.map((bundle) =>
|
|
75
|
+
AltiumLibraryRecordBuilder.createLengthPrefixedAscii(
|
|
76
|
+
AltiumLibraryRecordBuilder.#pipeRecord({
|
|
77
|
+
Name: bundle.footprint.name,
|
|
78
|
+
'Pad Count': String(bundle.footprint.pads.length),
|
|
79
|
+
Height: String(bundle.metadata.height || ''),
|
|
80
|
+
Description: String(bundle.metadata.description || '')
|
|
81
|
+
}) + '\r\n\u0000'
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Builds a SectionKeys stream.
|
|
89
|
+
* @param {object[]} bundles Normalized component bundles.
|
|
90
|
+
* @returns {Uint8Array}
|
|
91
|
+
*/
|
|
92
|
+
static buildSectionKeys(bundles) {
|
|
93
|
+
const entries = bundles.map((bundle) => ({
|
|
94
|
+
fullName: bundle.footprint.name,
|
|
95
|
+
storageName: AltiumLibraryRecordBuilder.sanitizeStorageName(
|
|
96
|
+
bundle.footprint.name
|
|
97
|
+
)
|
|
98
|
+
}))
|
|
99
|
+
|
|
100
|
+
return AltiumLibraryRecordBuilder.concatBytes([
|
|
101
|
+
AltiumLibraryRecordBuilder.createCountHeader(entries.length),
|
|
102
|
+
...entries.flatMap((entry) => [
|
|
103
|
+
AltiumLibraryRecordBuilder.createStringBlock(entry.fullName),
|
|
104
|
+
AltiumLibraryRecordBuilder.createStringBlock(entry.storageName)
|
|
105
|
+
])
|
|
106
|
+
])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Builds a footprint Data stream.
|
|
111
|
+
* @param {object} bundle Normalized component bundle.
|
|
112
|
+
* @returns {Uint8Array}
|
|
113
|
+
*/
|
|
114
|
+
static buildFootprintData(bundle) {
|
|
115
|
+
return AltiumLibraryRecordBuilder.concatBytes([
|
|
116
|
+
AltiumLibraryRecordBuilder.createStringBlock(bundle.footprint.name)
|
|
117
|
+
])
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Builds a footprint Parameters stream.
|
|
122
|
+
* @param {object} bundle Normalized component bundle.
|
|
123
|
+
* @returns {Uint8Array}
|
|
124
|
+
*/
|
|
125
|
+
static buildFootprintParameters(bundle) {
|
|
126
|
+
return AltiumLibraryRecordBuilder.createProperties({
|
|
127
|
+
PATTERN: bundle.footprint.name,
|
|
128
|
+
DESCRIPTION: String(bundle.metadata.description || ''),
|
|
129
|
+
HEIGHT: String(bundle.metadata.height || ''),
|
|
130
|
+
ITEMGUID: AltiumLibraryRecordBuilder.#guidFromText(bundle.id)
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Builds model metadata entries.
|
|
136
|
+
* @param {{ model: object, id: string, checksum: number }[]} models Model rows.
|
|
137
|
+
* @returns {Uint8Array}
|
|
138
|
+
*/
|
|
139
|
+
static buildModelsData(models) {
|
|
140
|
+
return AltiumLibraryRecordBuilder.concatBytes(
|
|
141
|
+
models.map((row) =>
|
|
142
|
+
AltiumLibraryRecordBuilder.createLengthPrefixedAscii(
|
|
143
|
+
AltiumLibraryRecordBuilder.#pipeRecord({
|
|
144
|
+
ID: row.id,
|
|
145
|
+
NAME: row.model.name,
|
|
146
|
+
CHECKSUM: String(row.checksum),
|
|
147
|
+
FORMAT: row.model.format
|
|
148
|
+
}) + '\u0000'
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Creates a little-endian count header.
|
|
156
|
+
* @param {number} count Count value.
|
|
157
|
+
* @returns {Uint8Array}
|
|
158
|
+
*/
|
|
159
|
+
static createCountHeader(count) {
|
|
160
|
+
const bytes = new Uint8Array(4)
|
|
161
|
+
new DataView(bytes.buffer).setUint32(0, Number(count || 0), true)
|
|
162
|
+
return bytes
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Creates a PcbLib property stream.
|
|
167
|
+
* @param {Record<string, string>} properties Properties.
|
|
168
|
+
* @returns {Uint8Array}
|
|
169
|
+
*/
|
|
170
|
+
static createProperties(properties) {
|
|
171
|
+
return AltiumLibraryRecordBuilder.createLengthPrefixedAscii(
|
|
172
|
+
AltiumLibraryRecordBuilder.#pipeRecord(properties) + '\u0000'
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Creates a Pascal-style string block.
|
|
178
|
+
* @param {string} text Text value.
|
|
179
|
+
* @returns {Uint8Array}
|
|
180
|
+
*/
|
|
181
|
+
static createStringBlock(text) {
|
|
182
|
+
const encoded = new TextEncoder().encode(String(text || ''))
|
|
183
|
+
const bytes = new Uint8Array(4 + 1 + encoded.byteLength)
|
|
184
|
+
const view = new DataView(bytes.buffer)
|
|
185
|
+
|
|
186
|
+
view.setUint32(0, 1 + encoded.byteLength, true)
|
|
187
|
+
bytes[4] = encoded.byteLength
|
|
188
|
+
bytes.set(encoded, 5)
|
|
189
|
+
|
|
190
|
+
return bytes
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Creates a length-prefixed ASCII/UTF-8 byte block.
|
|
195
|
+
* @param {string} text Text body.
|
|
196
|
+
* @returns {Uint8Array}
|
|
197
|
+
*/
|
|
198
|
+
static createLengthPrefixedAscii(text) {
|
|
199
|
+
const encoded = new TextEncoder().encode(String(text || ''))
|
|
200
|
+
const bytes = new Uint8Array(4 + encoded.byteLength)
|
|
201
|
+
|
|
202
|
+
new DataView(bytes.buffer).setUint32(0, encoded.byteLength, true)
|
|
203
|
+
bytes.set(encoded, 4)
|
|
204
|
+
|
|
205
|
+
return bytes
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Concatenates byte chunks.
|
|
210
|
+
* @param {Uint8Array[]} chunks Byte chunks.
|
|
211
|
+
* @returns {Uint8Array}
|
|
212
|
+
*/
|
|
213
|
+
static concatBytes(chunks) {
|
|
214
|
+
const byteLength = chunks.reduce(
|
|
215
|
+
(sum, chunk) => sum + chunk.byteLength,
|
|
216
|
+
0
|
|
217
|
+
)
|
|
218
|
+
const bytes = new Uint8Array(byteLength)
|
|
219
|
+
let offset = 0
|
|
220
|
+
|
|
221
|
+
for (const chunk of chunks) {
|
|
222
|
+
bytes.set(chunk, offset)
|
|
223
|
+
offset += chunk.byteLength
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return bytes
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Sanitizes one OLE storage name.
|
|
231
|
+
* @param {string} name Storage name.
|
|
232
|
+
* @returns {string}
|
|
233
|
+
*/
|
|
234
|
+
static sanitizeStorageName(name) {
|
|
235
|
+
return String(name || 'Component')
|
|
236
|
+
.replace(/[\\/:\u0000-\u001f]/gu, '_')
|
|
237
|
+
.slice(0, 31)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Computes a simple deterministic checksum for generated model metadata.
|
|
242
|
+
* @param {Uint8Array} bytes Model bytes.
|
|
243
|
+
* @returns {number}
|
|
244
|
+
*/
|
|
245
|
+
static checksumBytes(bytes) {
|
|
246
|
+
return [...bytes].reduce(
|
|
247
|
+
(checksum, value) => (checksum + value) >>> 0,
|
|
248
|
+
0
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Builds one pipe-delimited record.
|
|
254
|
+
* @param {Record<string, string>} fields Record fields.
|
|
255
|
+
* @returns {string}
|
|
256
|
+
*/
|
|
257
|
+
static #pipeRecord(fields) {
|
|
258
|
+
return (
|
|
259
|
+
'|' +
|
|
260
|
+
Object.entries(fields)
|
|
261
|
+
.filter(([, value]) => String(value ?? '') !== '')
|
|
262
|
+
.map(([key, value]) => key + '=' + String(value))
|
|
263
|
+
.join('|')
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Builds a deterministic GUID-like id from text.
|
|
269
|
+
* @param {string} text Source text.
|
|
270
|
+
* @returns {string}
|
|
271
|
+
*/
|
|
272
|
+
static #guidFromText(text) {
|
|
273
|
+
const hex = [...new TextEncoder().encode(String(text || ''))]
|
|
274
|
+
.map((value) => value.toString(16).padStart(2, '0'))
|
|
275
|
+
.join('')
|
|
276
|
+
.padEnd(32, '0')
|
|
277
|
+
.slice(0, 32)
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
'{' +
|
|
281
|
+
hex.slice(0, 8) +
|
|
282
|
+
'-' +
|
|
283
|
+
hex.slice(8, 12) +
|
|
284
|
+
'-' +
|
|
285
|
+
hex.slice(12, 16) +
|
|
286
|
+
'-' +
|
|
287
|
+
hex.slice(16, 20) +
|
|
288
|
+
'-' +
|
|
289
|
+
hex.slice(20) +
|
|
290
|
+
'}'
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -59,6 +59,7 @@ const {
|
|
|
59
59
|
extractSchematicFonts,
|
|
60
60
|
extractSchematicFontDiagnostics,
|
|
61
61
|
extractSchematicMetadata,
|
|
62
|
+
extractSchematicOwnerMetadata,
|
|
62
63
|
extractSchematicTitleBlock,
|
|
63
64
|
normalizeSchematicTextRecord
|
|
64
65
|
} = SchematicTextParser
|
|
@@ -186,7 +187,7 @@ export class AltiumParser {
|
|
|
186
187
|
AltiumParser.#collectOwnersWithImplicitDisplayMode(records)
|
|
187
188
|
const activeMultipartOwnerParts =
|
|
188
189
|
SchematicMultipartOwnerMatcher.collectActiveMultipartOwnerParts(
|
|
189
|
-
|
|
190
|
+
recordIndexAwareRecords,
|
|
190
191
|
componentRecords
|
|
191
192
|
)
|
|
192
193
|
const sheetRecord = records.find(
|
|
@@ -237,8 +238,11 @@ export class AltiumParser {
|
|
|
237
238
|
const rectangleRecords = drawableRecords.filter(
|
|
238
239
|
(record) =>
|
|
239
240
|
SchematicPrimitiveParser.isRectangleRecord(record.fields) &&
|
|
240
|
-
AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
241
|
-
|
|
241
|
+
((AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
|
|
242
|
+
AltiumParser.#hasCoordinatePair(record.fields, 'Corner')) ||
|
|
243
|
+
SchematicPrimitiveParser.isPointListedRectangleRecord(
|
|
244
|
+
record.fields
|
|
245
|
+
))
|
|
242
246
|
)
|
|
243
247
|
const roundedRectangleRecords = drawableRecords.filter(
|
|
244
248
|
(record) =>
|
|
@@ -329,21 +333,30 @@ export class AltiumParser {
|
|
|
329
333
|
}
|
|
330
334
|
|
|
331
335
|
const metadataTexts = extractSchematicMetadata(textRecords)
|
|
336
|
+
const ownerMetadataTexts = extractSchematicOwnerMetadata(textRecords)
|
|
332
337
|
const schematicFonts = extractSchematicFonts(sheetRecord?.fields)
|
|
333
338
|
const schematicRenderDiagnostics = extractSchematicFontDiagnostics(
|
|
334
339
|
sheetRecord?.fields
|
|
335
340
|
)
|
|
336
|
-
const
|
|
341
|
+
const storedSheetWidth =
|
|
337
342
|
parseNumericField(sheetRecord?.fields, 'CustomX') || 1500
|
|
338
|
-
const
|
|
343
|
+
const storedSheetHeight =
|
|
339
344
|
parseNumericField(sheetRecord?.fields, 'CustomY') || 950
|
|
345
|
+
const templatePageSize =
|
|
346
|
+
AltiumLayoutParser.resolveSchematicTemplatePageSize(
|
|
347
|
+
sheetRecord?.fields,
|
|
348
|
+
storedSheetWidth,
|
|
349
|
+
storedSheetHeight
|
|
350
|
+
)
|
|
351
|
+
const sheetWidth = templatePageSize?.width || storedSheetWidth
|
|
352
|
+
const sheetHeight = templatePageSize?.height || storedSheetHeight
|
|
340
353
|
const sheetMargin =
|
|
341
354
|
parseNumericField(sheetRecord?.fields, 'CustomMarginWidth') || 20
|
|
342
355
|
const sheet = {
|
|
343
356
|
width: sheetWidth,
|
|
344
357
|
height: sheetHeight,
|
|
345
|
-
sourceWidth:
|
|
346
|
-
sourceHeight:
|
|
358
|
+
sourceWidth: templatePageSize?.sourceWidth || storedSheetWidth,
|
|
359
|
+
sourceHeight: templatePageSize?.sourceHeight || storedSheetHeight,
|
|
347
360
|
visibleGrid:
|
|
348
361
|
parseNumericField(sheetRecord?.fields, 'VisibleGridSize') || 10,
|
|
349
362
|
snapGrid:
|
|
@@ -363,6 +376,9 @@ export class AltiumParser {
|
|
|
363
376
|
fonts: schematicFonts,
|
|
364
377
|
sheetStyle:
|
|
365
378
|
parseNumericField(sheetRecord?.fields, 'SheetStyle') || 0,
|
|
379
|
+
...(templatePageSize?.paperSize
|
|
380
|
+
? { paperSize: templatePageSize.paperSize }
|
|
381
|
+
: {}),
|
|
366
382
|
titleBlock: extractSchematicTitleBlock(
|
|
367
383
|
textRecords,
|
|
368
384
|
metadataTexts,
|
|
@@ -415,7 +431,19 @@ export class AltiumParser {
|
|
|
415
431
|
)
|
|
416
432
|
)
|
|
417
433
|
]
|
|
418
|
-
const
|
|
434
|
+
const ownerDrawnInternalPinOwners =
|
|
435
|
+
AltiumParser.#collectInaccessibleOwnerDrawnPrimitiveOwners(
|
|
436
|
+
drawableRecords
|
|
437
|
+
)
|
|
438
|
+
const numericEndpointLabelOwners =
|
|
439
|
+
AltiumParser.#collectNumericEndpointLabelOwners(
|
|
440
|
+
componentRecords,
|
|
441
|
+
recordIndexAwareRecords
|
|
442
|
+
)
|
|
443
|
+
const pins = parseSchematicPins(pinRecords, {
|
|
444
|
+
ownerDrawnInternalPinOwners,
|
|
445
|
+
numericEndpointLabelOwners
|
|
446
|
+
})
|
|
419
447
|
const junctions = SchematicJunctionParser.parseSchematicJunctions(
|
|
420
448
|
recordIndexAwareRecords
|
|
421
449
|
)
|
|
@@ -489,7 +517,8 @@ export class AltiumParser {
|
|
|
489
517
|
record.fields,
|
|
490
518
|
metadataTexts,
|
|
491
519
|
sheet,
|
|
492
|
-
schematicFonts
|
|
520
|
+
schematicFonts,
|
|
521
|
+
ownerMetadataTexts
|
|
493
522
|
)
|
|
494
523
|
)
|
|
495
524
|
.filter(Boolean)
|
|
@@ -524,7 +553,13 @@ export class AltiumParser {
|
|
|
524
553
|
normalizedLines,
|
|
525
554
|
pins,
|
|
526
555
|
ports,
|
|
527
|
-
|
|
556
|
+
{
|
|
557
|
+
rectangles,
|
|
558
|
+
roundedRectangles,
|
|
559
|
+
ellipses,
|
|
560
|
+
arcs,
|
|
561
|
+
pies
|
|
562
|
+
}
|
|
528
563
|
),
|
|
529
564
|
normalizedLines,
|
|
530
565
|
pins,
|
|
@@ -584,6 +619,8 @@ export class AltiumParser {
|
|
|
584
619
|
|
|
585
620
|
resolvedSheet.xZones =
|
|
586
621
|
SchematicSheetStyleResolver.resolveXZones(resolvedSheet)
|
|
622
|
+
resolvedSheet.yZones =
|
|
623
|
+
SchematicSheetStyleResolver.resolveYZones(resolvedSheet)
|
|
587
624
|
delete resolvedSheet.sheetStyle
|
|
588
625
|
|
|
589
626
|
const title =
|
|
@@ -649,6 +686,11 @@ export class AltiumParser {
|
|
|
649
686
|
busEntries,
|
|
650
687
|
sheetEntries
|
|
651
688
|
})
|
|
689
|
+
AltiumParser.#suppressRedundantSinglePinPowerNetNames(
|
|
690
|
+
pins,
|
|
691
|
+
nets,
|
|
692
|
+
anchoredTexts
|
|
693
|
+
)
|
|
652
694
|
diagnostics.push(...netDiagnostics)
|
|
653
695
|
const qa = SchematicQaReportBuilder.build({
|
|
654
696
|
records: recordIndexAwareRecords,
|
|
@@ -741,6 +783,142 @@ export class AltiumParser {
|
|
|
741
783
|
})
|
|
742
784
|
}
|
|
743
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Hides one-pin owner pin names when a connected power port already labels
|
|
788
|
+
* the same net, preserving the numeric pin endpoint label.
|
|
789
|
+
* @param {{ ownerIndex?: string, name?: string, labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }[]} pins
|
|
790
|
+
* @param {{ powerPorts?: { text?: string }[], pins?: { ownerIndex?: string, name?: string, labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }[] }[]} nets
|
|
791
|
+
* @param {{ x: number, y: number, text?: string, recordType?: string }[]} texts
|
|
792
|
+
* @returns {void}
|
|
793
|
+
*/
|
|
794
|
+
static #suppressRedundantSinglePinPowerNetNames(pins, nets, texts = []) {
|
|
795
|
+
const ownerPinCounts = new Map()
|
|
796
|
+
|
|
797
|
+
for (const pin of pins || []) {
|
|
798
|
+
const ownerIndex = String(pin.ownerIndex || '').trim()
|
|
799
|
+
|
|
800
|
+
if (!ownerIndex) continue
|
|
801
|
+
|
|
802
|
+
ownerPinCounts.set(
|
|
803
|
+
ownerIndex,
|
|
804
|
+
(ownerPinCounts.get(ownerIndex) || 0) + 1
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
for (const net of nets || []) {
|
|
809
|
+
const powerPortNames = new Set(
|
|
810
|
+
(net.powerPorts || [])
|
|
811
|
+
.map((powerPort) =>
|
|
812
|
+
AltiumParser.#normalizePowerNetLabel(powerPort.text)
|
|
813
|
+
)
|
|
814
|
+
.filter(Boolean)
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
if (!powerPortNames.size) continue
|
|
818
|
+
|
|
819
|
+
for (const pin of net.pins || []) {
|
|
820
|
+
if (
|
|
821
|
+
AltiumParser.#canSuppressSinglePinName(
|
|
822
|
+
pin,
|
|
823
|
+
ownerPinCounts
|
|
824
|
+
) &&
|
|
825
|
+
powerPortNames.has(
|
|
826
|
+
AltiumParser.#normalizePowerNetLabel(pin.name)
|
|
827
|
+
)
|
|
828
|
+
) {
|
|
829
|
+
pin.labelMode = 'number-only'
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const powerPorts = (texts || []).filter(
|
|
835
|
+
(text) => text?.recordType === '17'
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
for (const pin of pins || []) {
|
|
839
|
+
if (!AltiumParser.#canSuppressSinglePinName(pin, ownerPinCounts)) {
|
|
840
|
+
continue
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const pinName = AltiumParser.#normalizePowerNetLabel(pin.name)
|
|
844
|
+
const connectionPoint = AltiumParser.#resolvePinConnectionPoint(pin)
|
|
845
|
+
const matchesDirectPowerPort = powerPorts.some(
|
|
846
|
+
(powerPort) =>
|
|
847
|
+
pinName ===
|
|
848
|
+
AltiumParser.#normalizePowerNetLabel(powerPort.text) &&
|
|
849
|
+
AltiumParser.#pointsAreNear(connectionPoint, powerPort, 2)
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
if (matchesDirectPowerPort) {
|
|
853
|
+
pin.labelMode = 'number-only'
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Returns true when a pin is the only pin on its owner and currently shows
|
|
860
|
+
* a name label.
|
|
861
|
+
* @param {{ ownerIndex?: string, labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }} pin
|
|
862
|
+
* @param {Map<string, number>} ownerPinCounts
|
|
863
|
+
* @returns {boolean}
|
|
864
|
+
*/
|
|
865
|
+
static #canSuppressSinglePinName(pin, ownerPinCounts) {
|
|
866
|
+
const ownerIndex = String(pin?.ownerIndex || '').trim()
|
|
867
|
+
const labelMode = pin?.labelMode || 'name-and-number'
|
|
868
|
+
|
|
869
|
+
return (
|
|
870
|
+
ownerIndex &&
|
|
871
|
+
ownerPinCounts.get(ownerIndex) === 1 &&
|
|
872
|
+
labelMode !== 'hidden' &&
|
|
873
|
+
labelMode !== 'number-only'
|
|
874
|
+
)
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Resolves the outer electrical endpoint for a normalized schematic pin.
|
|
879
|
+
* @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
|
|
880
|
+
* @returns {{ x: number, y: number }}
|
|
881
|
+
*/
|
|
882
|
+
static #resolvePinConnectionPoint(pin) {
|
|
883
|
+
switch (pin.orientation) {
|
|
884
|
+
case 'right':
|
|
885
|
+
return { x: pin.x + pin.length, y: pin.y }
|
|
886
|
+
case 'top':
|
|
887
|
+
return { x: pin.x, y: pin.y + pin.length }
|
|
888
|
+
case 'bottom':
|
|
889
|
+
return { x: pin.x, y: pin.y - pin.length }
|
|
890
|
+
case 'left':
|
|
891
|
+
default:
|
|
892
|
+
return { x: pin.x - pin.length, y: pin.y }
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Returns true when two schematic points are close enough to represent the
|
|
898
|
+
* same recovered connection endpoint.
|
|
899
|
+
* @param {{ x: number, y: number }} left
|
|
900
|
+
* @param {{ x: number, y: number }} right
|
|
901
|
+
* @param {number} tolerance
|
|
902
|
+
* @returns {boolean}
|
|
903
|
+
*/
|
|
904
|
+
static #pointsAreNear(left, right, tolerance) {
|
|
905
|
+
return (
|
|
906
|
+
Math.abs(Number(left.x) - Number(right.x)) <= tolerance &&
|
|
907
|
+
Math.abs(Number(left.y) - Number(right.y)) <= tolerance
|
|
908
|
+
)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Normalizes a power-net label for structural comparisons.
|
|
913
|
+
* @param {string | undefined} label
|
|
914
|
+
* @returns {string}
|
|
915
|
+
*/
|
|
916
|
+
static #normalizePowerNetLabel(label) {
|
|
917
|
+
return String(label || '')
|
|
918
|
+
.trim()
|
|
919
|
+
.toUpperCase()
|
|
920
|
+
}
|
|
921
|
+
|
|
744
922
|
/**
|
|
745
923
|
* Finds a visible text string with a given logical name.
|
|
746
924
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
@@ -784,6 +962,63 @@ export class AltiumParser {
|
|
|
784
962
|
return owners
|
|
785
963
|
}
|
|
786
964
|
|
|
965
|
+
/**
|
|
966
|
+
* Collects symbol owners that draw an inaccessible body around compact
|
|
967
|
+
* internal pins.
|
|
968
|
+
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
969
|
+
* @returns {Set<string>}
|
|
970
|
+
*/
|
|
971
|
+
static #collectInaccessibleOwnerDrawnPrimitiveOwners(records) {
|
|
972
|
+
const owners = new Set()
|
|
973
|
+
|
|
974
|
+
for (const record of records) {
|
|
975
|
+
const ownerIndex = getField(record.fields, 'OwnerIndex')
|
|
976
|
+
const ownerPartId = getField(record.fields, 'OwnerPartId')
|
|
977
|
+
|
|
978
|
+
if (
|
|
979
|
+
ownerIndex &&
|
|
980
|
+
ownerPartId &&
|
|
981
|
+
ownerPartId !== '-1' &&
|
|
982
|
+
parseBoolean(record.fields.IsNotAccesible) &&
|
|
983
|
+
SchematicComponentOwnerTextResolver.isDisplayModeSelectablePrimitive(
|
|
984
|
+
record.fields
|
|
985
|
+
)
|
|
986
|
+
) {
|
|
987
|
+
owners.add(ownerIndex)
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return owners
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Collects component owners whose passive two-pin numeric endpoints are
|
|
996
|
+
* intended to remain visible in the schematic body.
|
|
997
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} componentRecords Component records.
|
|
998
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Indexed schematic records.
|
|
999
|
+
* @returns {Set<string>}
|
|
1000
|
+
*/
|
|
1001
|
+
static #collectNumericEndpointLabelOwners(componentRecords, records) {
|
|
1002
|
+
const owners = new Set()
|
|
1003
|
+
|
|
1004
|
+
for (const componentRecord of componentRecords) {
|
|
1005
|
+
if (
|
|
1006
|
+
parseNumericField(componentRecord.fields, 'ComponentKind') !== 4
|
|
1007
|
+
) {
|
|
1008
|
+
continue
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
for (const ownerIndex of SchematicComponentOwnerTextResolver.resolveOwnerIndexes(
|
|
1012
|
+
componentRecord,
|
|
1013
|
+
records
|
|
1014
|
+
)) {
|
|
1015
|
+
owners.add(ownerIndex)
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
return owners
|
|
1020
|
+
}
|
|
1021
|
+
|
|
787
1022
|
/**
|
|
788
1023
|
* Returns true when one schematic record belongs to the active symbol
|
|
789
1024
|
* display mode for its owner.
|