altium-toolkit 0.1.20 → 0.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/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +15 -3
- package/src/core/altium/PcbFontMetricsParser.mjs +33 -6
- package/src/core/altium/PcbModelParser.mjs +82 -0
- package/src/core/altium/PcbTextPrimitiveParser.mjs +48 -3
- package/src/core/altium/SchematicPinDesignatorInferer.mjs +401 -0
- package/src/core/altium/SchematicPinParser.mjs +136 -56
- package/src/core/altium/SchematicStreamExtractor.mjs +93 -0
- package/src/core/altium/SchematicTextPostProcessor.mjs +40 -12
- package/src/scene3d.mjs +1 -0
- package/src/ui/PcbScene3dBuilder.mjs +574 -145
- package/src/ui/PcbScene3dDrillCutoutBuilder.mjs +453 -0
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +272 -0
- package/src/ui/PcbScene3dTextBoxLayoutResolver.mjs +95 -0
- package/src/ui/SchematicImageRenderer.mjs +134 -32
- package/src/ui/SchematicJunctionRenderer.mjs +22 -4
- package/src/ui/SchematicNoteRenderer.mjs +73 -9
- package/src/ui/SchematicPinSvgRenderer.mjs +25 -3
- package/src/ui/SchematicPowerPortRenderer.mjs +183 -36
- package/src/ui/SchematicSvgRenderer.mjs +2 -2
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Infers omitted numeric schematic pin designators from compact owner geometry.
|
|
7
|
+
*/
|
|
8
|
+
export class SchematicPinDesignatorInferer {
|
|
9
|
+
/**
|
|
10
|
+
* Infers omitted source-order pin numbers for compact four-pin symbols
|
|
11
|
+
* whose printable records keep enough numeric hints to prove the sequence.
|
|
12
|
+
* @param {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
13
|
+
* @returns {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[] | null}
|
|
14
|
+
*/
|
|
15
|
+
static inferSequentialCompactFourPinDesignators(pins) {
|
|
16
|
+
if (
|
|
17
|
+
pins.length !== 4 ||
|
|
18
|
+
!SchematicPinDesignatorInferer.#isCompactFourPinOwner(pins)
|
|
19
|
+
) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let explicitCount = 0
|
|
24
|
+
|
|
25
|
+
for (let index = 0; index < pins.length; index += 1) {
|
|
26
|
+
const designator = String(pins[index].designator || '').trim()
|
|
27
|
+
|
|
28
|
+
if (!designator) {
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
explicitCount += 1
|
|
33
|
+
|
|
34
|
+
if (!/^\d+$/.test(designator)) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (Number(designator) !== index + 1) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (explicitCount < 2 || explicitCount === pins.length) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return pins.map((pin, index) => ({
|
|
48
|
+
...pin,
|
|
49
|
+
designator: String(index + 1)
|
|
50
|
+
}))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Infers omitted numeric labels for compact two-column owners whose
|
|
55
|
+
* physical side geometry implies the same sequence Altium displays.
|
|
56
|
+
* @param {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
57
|
+
* @returns {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[] | null}
|
|
58
|
+
*/
|
|
59
|
+
static inferCompactTwoColumnDesignators(pins) {
|
|
60
|
+
if (pins.length < 5) {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const leftPins = pins.filter((pin) => pin.orientation === 'left')
|
|
65
|
+
const rightPins = pins.filter((pin) => pin.orientation === 'right')
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
leftPins.length < 2 ||
|
|
69
|
+
rightPins.length < 2 ||
|
|
70
|
+
leftPins.length + rightPins.length !== pins.length ||
|
|
71
|
+
!SchematicPinDesignatorInferer.#isCompactTwoColumnOwner(
|
|
72
|
+
pins,
|
|
73
|
+
leftPins,
|
|
74
|
+
rightPins
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (pins.some((pin) => String(pin.designator || '').trim())) {
|
|
81
|
+
return SchematicPinDesignatorInferer.#inferExplicitTwoColumnDesignators(
|
|
82
|
+
pins,
|
|
83
|
+
leftPins,
|
|
84
|
+
rightPins
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const designators = new Map()
|
|
89
|
+
|
|
90
|
+
SchematicPinDesignatorInferer.#sortPinsTopToBottom(leftPins).forEach(
|
|
91
|
+
(pin, index) => {
|
|
92
|
+
designators.set(pin, String(index + 1))
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
SchematicPinDesignatorInferer.#sortPinsTopToBottom(rightPins).forEach(
|
|
96
|
+
(pin, index) => {
|
|
97
|
+
designators.set(pin, String(pins.length - index))
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return pins.map((pin) => ({
|
|
102
|
+
...pin,
|
|
103
|
+
designator: designators.get(pin) || ''
|
|
104
|
+
}))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Infers omitted labels from explicit per-side arithmetic pin sequences.
|
|
109
|
+
* @param {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
110
|
+
* @param {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} leftPins
|
|
111
|
+
* @param {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} rightPins
|
|
112
|
+
* @returns {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[] | null}
|
|
113
|
+
*/
|
|
114
|
+
static #inferExplicitTwoColumnDesignators(pins, leftPins, rightPins) {
|
|
115
|
+
if (
|
|
116
|
+
pins.some((pin) => {
|
|
117
|
+
const designator = String(pin.designator || '').trim()
|
|
118
|
+
|
|
119
|
+
return designator && !/^\d+$/.test(designator)
|
|
120
|
+
})
|
|
121
|
+
) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const leftDesignators =
|
|
126
|
+
SchematicPinDesignatorInferer.#inferSideSequenceDesignators(
|
|
127
|
+
leftPins
|
|
128
|
+
)
|
|
129
|
+
const rightDesignators =
|
|
130
|
+
SchematicPinDesignatorInferer.#inferSideSequenceDesignators(
|
|
131
|
+
rightPins
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if (!leftDesignators || !rightDesignators) {
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const designators = new Map([...leftDesignators, ...rightDesignators])
|
|
139
|
+
|
|
140
|
+
if (designators.size !== pins.length) {
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return pins.map((pin) => ({
|
|
145
|
+
...pin,
|
|
146
|
+
designator: designators.get(pin) || ''
|
|
147
|
+
}))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Infers omitted numeric labels for compact one-sided connector columns
|
|
152
|
+
* when the visible labels prove a continuous top-to-bottom sequence.
|
|
153
|
+
* @param {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
154
|
+
* @returns {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[] | null}
|
|
155
|
+
*/
|
|
156
|
+
static inferSingleColumnDesignators(pins) {
|
|
157
|
+
if (
|
|
158
|
+
pins.length < 5 ||
|
|
159
|
+
pins.every((pin) => String(pin.designator || '').trim())
|
|
160
|
+
) {
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const orientations = new Set(pins.map((pin) => pin.orientation))
|
|
165
|
+
const orientation = [...orientations][0]
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
orientations.size !== 1 ||
|
|
169
|
+
(orientation !== 'left' && orientation !== 'right')
|
|
170
|
+
) {
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const maxLength = Math.max(
|
|
175
|
+
...pins.map((pin) => Number(pin.length) || 0),
|
|
176
|
+
1
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
!SchematicPinDesignatorInferer.#isCompactVerticalPinColumn(
|
|
181
|
+
pins,
|
|
182
|
+
maxLength
|
|
183
|
+
)
|
|
184
|
+
) {
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const designators =
|
|
189
|
+
SchematicPinDesignatorInferer.#inferSideSequenceDesignators(pins)
|
|
190
|
+
|
|
191
|
+
if (!designators || designators.size !== pins.length) {
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return pins.map((pin) => ({
|
|
196
|
+
...pin,
|
|
197
|
+
designator: designators.get(pin) || ''
|
|
198
|
+
}))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Infers one compact side's complete numbering when existing labels fit a
|
|
203
|
+
* top-to-bottom ascending or descending sequence.
|
|
204
|
+
* @param {{ x: number, y: number, designator: string }[]} pins
|
|
205
|
+
* @returns {Map<{ x: number, y: number, designator: string }, string> | null}
|
|
206
|
+
*/
|
|
207
|
+
static #inferSideSequenceDesignators(pins) {
|
|
208
|
+
const sortedPins =
|
|
209
|
+
SchematicPinDesignatorInferer.#sortPinsTopToBottom(pins)
|
|
210
|
+
const explicitPins = sortedPins
|
|
211
|
+
.map((pin, index) => ({
|
|
212
|
+
pin,
|
|
213
|
+
index,
|
|
214
|
+
designator: String(pin.designator || '').trim()
|
|
215
|
+
}))
|
|
216
|
+
.filter((pin) => pin.designator)
|
|
217
|
+
|
|
218
|
+
if (explicitPins.length < 2) {
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const step of [1, -1]) {
|
|
223
|
+
const offset =
|
|
224
|
+
Number(explicitPins[0].designator) -
|
|
225
|
+
explicitPins[0].index * step
|
|
226
|
+
const fits = explicitPins.every(
|
|
227
|
+
(pin) => Number(pin.designator) === offset + pin.index * step
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if (!fits) {
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const inferred = new Map()
|
|
235
|
+
const values = new Set()
|
|
236
|
+
|
|
237
|
+
for (let index = 0; index < sortedPins.length; index += 1) {
|
|
238
|
+
const value = offset + index * step
|
|
239
|
+
|
|
240
|
+
if (value <= 0 || !Number.isInteger(value)) {
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
values.add(value)
|
|
245
|
+
inferred.set(sortedPins[index], String(value))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (values.size === sortedPins.length) {
|
|
249
|
+
return inferred
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Returns true when pins form one compact rectangular two-column owner.
|
|
258
|
+
* @param {{ x: number, y: number, length: number }[]} pins
|
|
259
|
+
* @param {{ x: number, y: number, length: number }[]} leftPins
|
|
260
|
+
* @param {{ x: number, y: number, length: number }[]} rightPins
|
|
261
|
+
* @returns {boolean}
|
|
262
|
+
*/
|
|
263
|
+
static #isCompactTwoColumnOwner(pins, leftPins, rightPins) {
|
|
264
|
+
const xs = pins.map((pin) => Number(pin.x))
|
|
265
|
+
const ys = pins.map((pin) => Number(pin.y))
|
|
266
|
+
const lengths = pins.map((pin) => Number(pin.length) || 0)
|
|
267
|
+
const maxLength = Math.max(...lengths, 1)
|
|
268
|
+
const horizontalSpan = Math.max(...xs) - Math.min(...xs)
|
|
269
|
+
const verticalSpan = Math.max(...ys) - Math.min(...ys)
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
horizontalSpan >= maxLength * 2 &&
|
|
273
|
+
horizontalSpan <= maxLength * 5 &&
|
|
274
|
+
verticalSpan <= maxLength * 4 &&
|
|
275
|
+
SchematicPinDesignatorInferer.#isCompactVerticalPinColumn(
|
|
276
|
+
leftPins,
|
|
277
|
+
maxLength
|
|
278
|
+
) &&
|
|
279
|
+
SchematicPinDesignatorInferer.#isCompactVerticalPinColumn(
|
|
280
|
+
rightPins,
|
|
281
|
+
maxLength
|
|
282
|
+
) &&
|
|
283
|
+
SchematicPinDesignatorInferer.#verticalPinColumnRangesOverlap(
|
|
284
|
+
leftPins,
|
|
285
|
+
rightPins,
|
|
286
|
+
maxLength
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Returns true when a side's pins share one vertical edge with compact
|
|
293
|
+
* spacing between adjacent contacts.
|
|
294
|
+
* @param {{ x: number, y: number }[]} pins
|
|
295
|
+
* @param {number} maxLength
|
|
296
|
+
* @returns {boolean}
|
|
297
|
+
*/
|
|
298
|
+
static #isCompactVerticalPinColumn(pins, maxLength) {
|
|
299
|
+
const xs = pins.map((pin) => Number(pin.x))
|
|
300
|
+
const xSpan = Math.max(...xs) - Math.min(...xs)
|
|
301
|
+
const sortedPins =
|
|
302
|
+
SchematicPinDesignatorInferer.#sortPinsBottomToTop(pins)
|
|
303
|
+
const tolerance = 0.01
|
|
304
|
+
|
|
305
|
+
if (xSpan > Math.max(tolerance, maxLength * 0.05)) {
|
|
306
|
+
return false
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (let index = 1; index < sortedPins.length; index += 1) {
|
|
310
|
+
const gap =
|
|
311
|
+
Number(sortedPins[index].y) - Number(sortedPins[index - 1].y)
|
|
312
|
+
|
|
313
|
+
if (gap <= tolerance || gap > maxLength) {
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return true
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Returns true when the two side columns occupy the same compact body span.
|
|
323
|
+
* @param {{ y: number }[]} leftPins
|
|
324
|
+
* @param {{ y: number }[]} rightPins
|
|
325
|
+
* @param {number} maxLength
|
|
326
|
+
* @returns {boolean}
|
|
327
|
+
*/
|
|
328
|
+
static #verticalPinColumnRangesOverlap(leftPins, rightPins, maxLength) {
|
|
329
|
+
const leftRange = SchematicPinDesignatorInferer.#pinYRange(leftPins)
|
|
330
|
+
const rightRange = SchematicPinDesignatorInferer.#pinYRange(rightPins)
|
|
331
|
+
const overlap =
|
|
332
|
+
Math.min(leftRange.max, rightRange.max) -
|
|
333
|
+
Math.max(leftRange.min, rightRange.min)
|
|
334
|
+
const requiredOverlap = Math.min(
|
|
335
|
+
maxLength,
|
|
336
|
+
leftRange.max - leftRange.min,
|
|
337
|
+
rightRange.max - rightRange.min
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
return overlap >= requiredOverlap
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Sorts pins from schematic top to bottom.
|
|
345
|
+
* @param {{ x: number, y: number }[]} pins
|
|
346
|
+
* @returns {{ x: number, y: number }[]}
|
|
347
|
+
*/
|
|
348
|
+
static #sortPinsTopToBottom(pins) {
|
|
349
|
+
return [...pins].sort((left, right) => Number(right.y) - Number(left.y))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Sorts pins from schematic bottom to top.
|
|
354
|
+
* @param {{ x: number, y: number }[]} pins
|
|
355
|
+
* @returns {{ x: number, y: number }[]}
|
|
356
|
+
*/
|
|
357
|
+
static #sortPinsBottomToTop(pins) {
|
|
358
|
+
return [...pins].sort((left, right) => Number(left.y) - Number(right.y))
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Returns the vertical bounds of a pin column.
|
|
363
|
+
* @param {{ y: number }[]} pins
|
|
364
|
+
* @returns {{ min: number, max: number }}
|
|
365
|
+
*/
|
|
366
|
+
static #pinYRange(pins) {
|
|
367
|
+
const ys = pins.map((pin) => Number(pin.y))
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
min: Math.min(...ys),
|
|
371
|
+
max: Math.max(...ys)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Returns true when four pins form one compact two-sided symbol body.
|
|
377
|
+
* @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
378
|
+
* @returns {boolean}
|
|
379
|
+
*/
|
|
380
|
+
static #isCompactFourPinOwner(pins) {
|
|
381
|
+
const orientations = new Set(pins.map((pin) => pin.orientation))
|
|
382
|
+
|
|
383
|
+
if (
|
|
384
|
+
orientations.size !== 2 ||
|
|
385
|
+
!orientations.has('left') ||
|
|
386
|
+
!orientations.has('right')
|
|
387
|
+
) {
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const xs = pins.map((pin) => Number(pin.x))
|
|
392
|
+
const ys = pins.map((pin) => Number(pin.y))
|
|
393
|
+
const lengths = pins.map((pin) => Number(pin.length) || 0)
|
|
394
|
+
const maxLength = Math.max(...lengths, 1)
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
Math.max(...xs) - Math.min(...xs) <= maxLength * 3 &&
|
|
398
|
+
Math.max(...ys) - Math.min(...ys) <= maxLength * 3
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
import { SchematicPinDesignatorInferer } from './SchematicPinDesignatorInferer.mjs'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Helpers for normalized schematic pins, ports, and crosses.
|
|
@@ -395,31 +396,11 @@ export class SchematicPinParser {
|
|
|
395
396
|
/**
|
|
396
397
|
* Expands a schematic polyline record into drawable line segments.
|
|
397
398
|
* @param {Record<string, string | string[]>} fields
|
|
398
|
-
* @param {{ isBus?: boolean }} [options]
|
|
399
|
-
* @returns {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle: number, isBus?: boolean }[]}
|
|
399
|
+
* @param {{ isBus?: boolean, recordType?: string }} [options]
|
|
400
|
+
* @returns {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle: number, isBus?: boolean, recordType?: string }[]}
|
|
400
401
|
*/
|
|
401
402
|
static parseSchematicPolyline(fields, options = {}) {
|
|
402
|
-
const
|
|
403
|
-
fields,
|
|
404
|
-
'LocationCount'
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
if (locationCount === null || locationCount < 2) {
|
|
408
|
-
return []
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const points = []
|
|
412
|
-
|
|
413
|
-
for (let index = 1; index <= locationCount; index += 1) {
|
|
414
|
-
const x = ParserUtils.parseNumericField(fields, 'X' + index)
|
|
415
|
-
const y = ParserUtils.parseNumericField(fields, 'Y' + index)
|
|
416
|
-
|
|
417
|
-
if (x === null || y === null) {
|
|
418
|
-
break
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
points.push({ x, y })
|
|
422
|
-
}
|
|
403
|
+
const points = SchematicPinParser.#collectSchematicPointList(fields)
|
|
423
404
|
|
|
424
405
|
const segments = []
|
|
425
406
|
const lineStyle = SchematicPinParser.#resolveSchematicLineStyle(fields)
|
|
@@ -436,7 +417,8 @@ export class SchematicPinParser {
|
|
|
436
417
|
color: ParserUtils.toColor(fields.Color, '#a44a1b'),
|
|
437
418
|
width: ParserUtils.parseNumericField(fields, 'LineWidth') || 1,
|
|
438
419
|
lineStyle,
|
|
439
|
-
isBus: options.isBus === true ? true : undefined
|
|
420
|
+
isBus: options.isBus === true ? true : undefined,
|
|
421
|
+
recordType: options.recordType || undefined
|
|
440
422
|
})
|
|
441
423
|
}
|
|
442
424
|
|
|
@@ -449,27 +431,7 @@ export class SchematicPinParser {
|
|
|
449
431
|
* @returns {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle: number }[]}
|
|
450
432
|
*/
|
|
451
433
|
static parseSchematicPolygon(fields) {
|
|
452
|
-
const
|
|
453
|
-
fields,
|
|
454
|
-
'LocationCount'
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
if (locationCount === null || locationCount < 2) {
|
|
458
|
-
return []
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const points = []
|
|
462
|
-
|
|
463
|
-
for (let index = 1; index <= locationCount; index += 1) {
|
|
464
|
-
const x = ParserUtils.parseNumericField(fields, 'X' + index)
|
|
465
|
-
const y = ParserUtils.parseNumericField(fields, 'Y' + index)
|
|
466
|
-
|
|
467
|
-
if (x === null || y === null) {
|
|
468
|
-
break
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
points.push({ x, y })
|
|
472
|
-
}
|
|
434
|
+
const points = SchematicPinParser.#collectSchematicPointList(fields)
|
|
473
435
|
|
|
474
436
|
if (points.length < 2) {
|
|
475
437
|
return []
|
|
@@ -527,6 +489,49 @@ export class SchematicPinParser {
|
|
|
527
489
|
return ParserUtils.parseNumericField(fields, 'LineStyle') || 0
|
|
528
490
|
}
|
|
529
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Collects a schematic point list, carrying forward a missing coordinate
|
|
494
|
+
* axis from the preceding point when Altium omitted an unchanged value.
|
|
495
|
+
* @param {Record<string, string | string[]>} fields
|
|
496
|
+
* @returns {{ x: number, y: number }[]}
|
|
497
|
+
*/
|
|
498
|
+
static #collectSchematicPointList(fields) {
|
|
499
|
+
const locationCount = ParserUtils.parseNumericField(
|
|
500
|
+
fields,
|
|
501
|
+
'LocationCount'
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if (locationCount === null || locationCount < 2) {
|
|
505
|
+
return []
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const points = []
|
|
509
|
+
let previousX = null
|
|
510
|
+
let previousY = null
|
|
511
|
+
|
|
512
|
+
for (let index = 1; index <= locationCount; index += 1) {
|
|
513
|
+
const x = ParserUtils.parseNumericField(fields, 'X' + index)
|
|
514
|
+
const y = ParserUtils.parseNumericField(fields, 'Y' + index)
|
|
515
|
+
|
|
516
|
+
if (x === null && y === null) {
|
|
517
|
+
break
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const pointX = x === null ? previousX : x
|
|
521
|
+
const pointY = y === null ? previousY : y
|
|
522
|
+
|
|
523
|
+
if (pointX === null || pointY === null) {
|
|
524
|
+
break
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
points.push({ x: pointX, y: pointY })
|
|
528
|
+
previousX = pointX
|
|
529
|
+
previousY = pointY
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return points
|
|
533
|
+
}
|
|
534
|
+
|
|
530
535
|
/**
|
|
531
536
|
* Deduces the visible pins for one schematic symbol owner.
|
|
532
537
|
* @param {{ x: number, y: number, length: number, conglomerate?: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, ownerIndex: string }[]} pins
|
|
@@ -534,11 +539,32 @@ export class SchematicPinParser {
|
|
|
534
539
|
*/
|
|
535
540
|
static #normalizeSchematicPinGroup(pins) {
|
|
536
541
|
const deduped = SchematicPinParser.#dedupeSchematicPins(pins)
|
|
542
|
+
const inferredSequentialDesignators =
|
|
543
|
+
SchematicPinDesignatorInferer.inferSequentialCompactFourPinDesignators(
|
|
544
|
+
deduped
|
|
545
|
+
)
|
|
546
|
+
const inferredTwoColumnDesignators = inferredSequentialDesignators
|
|
547
|
+
? null
|
|
548
|
+
: SchematicPinDesignatorInferer.inferCompactTwoColumnDesignators(
|
|
549
|
+
deduped
|
|
550
|
+
)
|
|
551
|
+
const inferredSingleColumnDesignators =
|
|
552
|
+
inferredSequentialDesignators || inferredTwoColumnDesignators
|
|
553
|
+
? null
|
|
554
|
+
: SchematicPinDesignatorInferer.inferSingleColumnDesignators(
|
|
555
|
+
deduped
|
|
556
|
+
)
|
|
557
|
+
const normalizedPins =
|
|
558
|
+
inferredSequentialDesignators ||
|
|
559
|
+
inferredTwoColumnDesignators ||
|
|
560
|
+
inferredSingleColumnDesignators ||
|
|
561
|
+
deduped
|
|
537
562
|
const names = [
|
|
538
|
-
...new Set(
|
|
563
|
+
...new Set(normalizedPins.map((pin) => pin.name).filter(Boolean))
|
|
539
564
|
]
|
|
540
|
-
const orientationCount = new Set(
|
|
541
|
-
.
|
|
565
|
+
const orientationCount = new Set(
|
|
566
|
+
normalizedPins.map((pin) => pin.orientation)
|
|
567
|
+
).size
|
|
542
568
|
const allPassive = names.every((name) =>
|
|
543
569
|
SchematicPinParser.#isPassivePinName(name)
|
|
544
570
|
)
|
|
@@ -546,15 +572,20 @@ export class SchematicPinParser {
|
|
|
546
572
|
(name) => !SchematicPinParser.#isPassivePinName(name)
|
|
547
573
|
)
|
|
548
574
|
const allNumberedPins =
|
|
549
|
-
|
|
550
|
-
|
|
575
|
+
normalizedPins.length > 0 &&
|
|
576
|
+
normalizedPins.every(
|
|
551
577
|
(pin) =>
|
|
552
578
|
/^\d+$/.test(String(pin.designator || '').trim()) &&
|
|
553
579
|
(!pin.name || /^\d+$/.test(String(pin.name || '').trim()))
|
|
554
580
|
)
|
|
555
581
|
let labelMode = 'name-and-number'
|
|
556
582
|
|
|
557
|
-
if (
|
|
583
|
+
if (
|
|
584
|
+
inferredSequentialDesignators ||
|
|
585
|
+
SchematicPinParser.#isDenseTwoSidedHorizontal4850Family(
|
|
586
|
+
normalizedPins
|
|
587
|
+
)
|
|
588
|
+
) {
|
|
558
589
|
labelMode = 'number-only'
|
|
559
590
|
}
|
|
560
591
|
|
|
@@ -562,30 +593,38 @@ export class SchematicPinParser {
|
|
|
562
593
|
// Keep dense multi-side connector symbols whose contacts are only
|
|
563
594
|
// identified by numbers; dropping them loses both pin numbers and
|
|
564
595
|
// any power-port attachment geometry recovered from those pins.
|
|
565
|
-
if (
|
|
596
|
+
if (normalizedPins.length > 4 && !allNumberedPins) {
|
|
566
597
|
return []
|
|
567
598
|
}
|
|
568
599
|
|
|
569
600
|
labelMode = 'number-only'
|
|
570
601
|
}
|
|
571
602
|
|
|
572
|
-
if (allPassive &&
|
|
603
|
+
if (allPassive && normalizedPins.length <= 2) {
|
|
573
604
|
labelMode = SchematicPinParser.#isCanonicalPassiveTwoPinGroup(
|
|
574
|
-
|
|
605
|
+
normalizedPins
|
|
575
606
|
)
|
|
576
607
|
? 'hidden'
|
|
577
608
|
: 'number-only'
|
|
609
|
+
} else if (
|
|
610
|
+
SchematicPinParser.#isOwnerDrawnTerminalGlyphGroup(
|
|
611
|
+
normalizedPins,
|
|
612
|
+
semanticNames,
|
|
613
|
+
orientationCount
|
|
614
|
+
)
|
|
615
|
+
) {
|
|
616
|
+
labelMode = 'hidden'
|
|
578
617
|
} else if (!semanticNames.length && orientationCount <= 2) {
|
|
579
618
|
labelMode = 'number-only'
|
|
580
619
|
} else if (
|
|
581
620
|
semanticNames.length >= Math.max(names.length - 1, 3) &&
|
|
582
621
|
orientationCount <= 2 &&
|
|
583
|
-
|
|
622
|
+
normalizedPins.length <= 4
|
|
584
623
|
) {
|
|
585
624
|
labelMode = 'name-only'
|
|
586
625
|
}
|
|
587
626
|
|
|
588
|
-
return
|
|
627
|
+
return normalizedPins.map(({ conglomerate, ...pin }) => ({
|
|
589
628
|
...pin,
|
|
590
629
|
color: '#0000ff',
|
|
591
630
|
labelColor: '#1f1f1f',
|
|
@@ -593,6 +632,47 @@ export class SchematicPinParser {
|
|
|
593
632
|
}))
|
|
594
633
|
}
|
|
595
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Returns true when a compact multi-side owner has transistor-like terminal
|
|
637
|
+
* letters that are part of the drawn symbol body, not external pin labels.
|
|
638
|
+
* @param {{ designator: string, name: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
639
|
+
* @param {string[]} semanticNames
|
|
640
|
+
* @param {number} orientationCount
|
|
641
|
+
* @returns {boolean}
|
|
642
|
+
*/
|
|
643
|
+
static #isOwnerDrawnTerminalGlyphGroup(
|
|
644
|
+
pins,
|
|
645
|
+
semanticNames,
|
|
646
|
+
orientationCount
|
|
647
|
+
) {
|
|
648
|
+
if (
|
|
649
|
+
pins.length < 3 ||
|
|
650
|
+
pins.length > 4 ||
|
|
651
|
+
orientationCount < 3 ||
|
|
652
|
+
pins.some((pin) => String(pin.designator || '').trim())
|
|
653
|
+
) {
|
|
654
|
+
return false
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (semanticNames.length !== pins.length) {
|
|
658
|
+
return false
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return semanticNames.every((name) =>
|
|
662
|
+
SchematicPinParser.#isTransistorTerminalName(name)
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Returns true for one-letter terminal glyphs commonly drawn inside
|
|
668
|
+
* transistor-style schematic symbols.
|
|
669
|
+
* @param {string} name
|
|
670
|
+
* @returns {boolean}
|
|
671
|
+
*/
|
|
672
|
+
static #isTransistorTerminalName(name) {
|
|
673
|
+
return /^[BCDEGS]$/i.test(String(name || '').trim())
|
|
674
|
+
}
|
|
675
|
+
|
|
596
676
|
/**
|
|
597
677
|
* Returns true when one passive two-pin symbol uses the ordinary 1/2 pin
|
|
598
678
|
* numbering that should stay hidden for simple resistor-like parts.
|