altium-toolkit 1.1.30 → 1.1.32
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 +34 -1
- package/src/core/altium/AltiumSchematicArcAngleNormalizer.mjs +82 -0
- package/src/core/altium/AltiumSchematicFreeGraphicStrokeNormalizer.mjs +105 -0
- package/src/core/altium/AltiumSchematicHiddenDesignatorResolver.mjs +191 -0
- package/src/core/altium/AltiumSchematicPackedImageResolver.mjs +433 -0
- package/src/core/altium/AltiumSchematicSheetBoundsNormalizer.mjs +287 -0
- package/src/parser.mjs +5 -0
- package/src/renderers.mjs +2 -0
- package/src/scene3d.mjs +1 -0
- package/src/ui/AltiumPcbBottomViewMirror.mjs +394 -0
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +300 -0
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +364 -22
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +568 -4
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +149 -23
- package/src/ui/PcbFootprintPadAxisNormalizer.mjs +609 -0
- package/src/ui/PcbScene3dBuilder.mjs +313 -15
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +34 -2
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
const MIN_GROUP_PAD_COUNT = 4
|
|
2
|
+
const MIN_ASPECT_RATIO = 1.4
|
|
3
|
+
const MIN_DIRECTION_STABILITY = 0.35
|
|
4
|
+
const PARALLEL_TOLERANCE_DEG = 30
|
|
5
|
+
const MIN_PARALLEL_RATIO = 0.6
|
|
6
|
+
const ROW_GAP_SHORT_SIZE_FACTOR = 1.5
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalizes rectangular pad axes for parsed two-row surface-mount footprints.
|
|
10
|
+
*/
|
|
11
|
+
export class PcbFootprintPadAxisNormalizer {
|
|
12
|
+
static #documentCache = new WeakMap()
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns a document model with row-parallel rectangular pads turned across
|
|
16
|
+
* their owning two-row footprint.
|
|
17
|
+
* @param {object} documentModel Document model.
|
|
18
|
+
* @returns {object}
|
|
19
|
+
*/
|
|
20
|
+
static apply(documentModel) {
|
|
21
|
+
if (!PcbFootprintPadAxisNormalizer.#canCache(documentModel)) {
|
|
22
|
+
return documentModel
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const cachedDocument =
|
|
26
|
+
PcbFootprintPadAxisNormalizer.#documentCache.get(documentModel)
|
|
27
|
+
if (cachedDocument) return cachedDocument
|
|
28
|
+
|
|
29
|
+
const pcb = documentModel?.pcb
|
|
30
|
+
const sourcePads =
|
|
31
|
+
PcbFootprintPadAxisNormalizer.#firstPadList(pcb) || []
|
|
32
|
+
const updates =
|
|
33
|
+
PcbFootprintPadAxisNormalizer.#resolvePadRotationUpdates(sourcePads)
|
|
34
|
+
if (!updates.objects.size && !updates.signatures.size) {
|
|
35
|
+
PcbFootprintPadAxisNormalizer.#documentCache.set(
|
|
36
|
+
documentModel,
|
|
37
|
+
documentModel
|
|
38
|
+
)
|
|
39
|
+
return documentModel
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const normalizedDocument =
|
|
43
|
+
PcbFootprintPadAxisNormalizer.#applyPadUpdatesToDocument(
|
|
44
|
+
documentModel,
|
|
45
|
+
updates
|
|
46
|
+
)
|
|
47
|
+
PcbFootprintPadAxisNormalizer.#documentCache.set(
|
|
48
|
+
documentModel,
|
|
49
|
+
normalizedDocument
|
|
50
|
+
)
|
|
51
|
+
return normalizedDocument
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns true when a value can be used as a WeakMap key.
|
|
56
|
+
* @param {unknown} value Value to test.
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
static #canCache(value) {
|
|
60
|
+
return (
|
|
61
|
+
value !== null &&
|
|
62
|
+
(typeof value === 'object' || typeof value === 'function')
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the first PCB pad list present in the document model.
|
|
68
|
+
* @param {object | null | undefined} pcb PCB model.
|
|
69
|
+
* @returns {object[] | null}
|
|
70
|
+
*/
|
|
71
|
+
static #firstPadList(pcb) {
|
|
72
|
+
if (Array.isArray(pcb?.pads)) return pcb.pads
|
|
73
|
+
if (Array.isArray(pcb?.kicadBoard?.pads)) return pcb.kicadBoard.pads
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolves pad rotation updates keyed by object identity and signature.
|
|
79
|
+
* @param {object[]} pads PCB pad list.
|
|
80
|
+
* @returns {{ objects: Map<object, number>, signatures: Map<string, number> }}
|
|
81
|
+
*/
|
|
82
|
+
static #resolvePadRotationUpdates(pads) {
|
|
83
|
+
const updates = {
|
|
84
|
+
objects: new Map(),
|
|
85
|
+
signatures: new Map()
|
|
86
|
+
}
|
|
87
|
+
const groups = PcbFootprintPadAxisNormalizer.#groupPadsByOwner(pads)
|
|
88
|
+
|
|
89
|
+
for (const groupPads of groups.values()) {
|
|
90
|
+
PcbFootprintPadAxisNormalizer.#addGroupPadRotationUpdates(
|
|
91
|
+
groupPads,
|
|
92
|
+
updates
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return updates
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Groups pads by their owning component or footprint.
|
|
101
|
+
* @param {object[]} pads PCB pad list.
|
|
102
|
+
* @returns {Map<string, object[]>}
|
|
103
|
+
*/
|
|
104
|
+
static #groupPadsByOwner(pads) {
|
|
105
|
+
const groups = new Map()
|
|
106
|
+
|
|
107
|
+
for (const pad of pads || []) {
|
|
108
|
+
const key = PcbFootprintPadAxisNormalizer.#padOwnerKey(pad)
|
|
109
|
+
if (!key) continue
|
|
110
|
+
|
|
111
|
+
const group = groups.get(key) || []
|
|
112
|
+
group.push(pad)
|
|
113
|
+
groups.set(key, group)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return groups
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Adds inferred rotation updates for one footprint pad group.
|
|
121
|
+
* @param {object[]} groupPads Pads owned by one footprint.
|
|
122
|
+
* @param {{ objects: Map<object, number>, signatures: Map<string, number> }} updates Rotation updates.
|
|
123
|
+
* @returns {void}
|
|
124
|
+
*/
|
|
125
|
+
static #addGroupPadRotationUpdates(groupPads, updates) {
|
|
126
|
+
const candidates = groupPads.filter((pad) =>
|
|
127
|
+
PcbFootprintPadAxisNormalizer.#isCandidateSurfacePad(pad)
|
|
128
|
+
)
|
|
129
|
+
if (candidates.length < MIN_GROUP_PAD_COUNT) return
|
|
130
|
+
|
|
131
|
+
const rowAngle =
|
|
132
|
+
PcbFootprintPadAxisNormalizer.#resolveNearestNeighborLineAngle(
|
|
133
|
+
candidates
|
|
134
|
+
)
|
|
135
|
+
if (rowAngle === null) return
|
|
136
|
+
if (
|
|
137
|
+
!PcbFootprintPadAxisNormalizer.#hasSeparatedRows(
|
|
138
|
+
candidates,
|
|
139
|
+
rowAngle
|
|
140
|
+
)
|
|
141
|
+
) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const parallelPads = candidates.filter((pad) =>
|
|
146
|
+
PcbFootprintPadAxisNormalizer.#isPadLongAxisParallelToRow(
|
|
147
|
+
pad,
|
|
148
|
+
rowAngle
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
const minimumParallelPads = Math.max(
|
|
152
|
+
MIN_GROUP_PAD_COUNT,
|
|
153
|
+
Math.ceil(candidates.length * MIN_PARALLEL_RATIO)
|
|
154
|
+
)
|
|
155
|
+
if (parallelPads.length < minimumParallelPads) return
|
|
156
|
+
|
|
157
|
+
for (const pad of parallelPads) {
|
|
158
|
+
const rotation = PcbFootprintPadAxisNormalizer.#normalizeAngle(
|
|
159
|
+
Number(pad?.rotation || 0) + 90
|
|
160
|
+
)
|
|
161
|
+
updates.objects.set(pad, rotation)
|
|
162
|
+
updates.signatures.set(
|
|
163
|
+
PcbFootprintPadAxisNormalizer.#padSignature(pad),
|
|
164
|
+
rotation
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Returns true when one pad is a rectangular-ish surface pad with a clear
|
|
171
|
+
* long axis.
|
|
172
|
+
* @param {object} pad PCB pad.
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
static #isCandidateSurfacePad(pad) {
|
|
176
|
+
if (PcbFootprintPadAxisNormalizer.#hasDrill(pad)) return false
|
|
177
|
+
|
|
178
|
+
const dimensions = PcbFootprintPadAxisNormalizer.#padDimensions(pad)
|
|
179
|
+
if (!dimensions) return false
|
|
180
|
+
|
|
181
|
+
const shortest = Math.min(dimensions.width, dimensions.height)
|
|
182
|
+
const longest = Math.max(dimensions.width, dimensions.height)
|
|
183
|
+
if (shortest <= 0) return false
|
|
184
|
+
|
|
185
|
+
return longest / shortest >= MIN_ASPECT_RATIO
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Returns true when one pad has a drill hole or slot.
|
|
190
|
+
* @param {object} pad PCB pad.
|
|
191
|
+
* @returns {boolean}
|
|
192
|
+
*/
|
|
193
|
+
static #hasDrill(pad) {
|
|
194
|
+
const values = [
|
|
195
|
+
pad?.holeDiameter,
|
|
196
|
+
pad?.drillDiameter,
|
|
197
|
+
pad?.holeSize,
|
|
198
|
+
pad?.slotLength,
|
|
199
|
+
pad?.holeSlotLength
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
return values.some(
|
|
203
|
+
(value) =>
|
|
204
|
+
Number.isFinite(Number(value)) && Math.abs(Number(value)) > 0
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolves the visible rectangular pad dimensions.
|
|
210
|
+
* @param {object} pad PCB pad.
|
|
211
|
+
* @returns {{ width: number, height: number } | null}
|
|
212
|
+
*/
|
|
213
|
+
static #padDimensions(pad) {
|
|
214
|
+
const width = PcbFootprintPadAxisNormalizer.#firstFiniteNumber([
|
|
215
|
+
pad?.width,
|
|
216
|
+
pad?.sizeTopX,
|
|
217
|
+
pad?.sizeBottomX
|
|
218
|
+
])
|
|
219
|
+
const height = PcbFootprintPadAxisNormalizer.#firstFiniteNumber([
|
|
220
|
+
pad?.height,
|
|
221
|
+
pad?.sizeTopY,
|
|
222
|
+
pad?.sizeBottomY
|
|
223
|
+
])
|
|
224
|
+
|
|
225
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null
|
|
226
|
+
if (width <= 0 || height <= 0) return null
|
|
227
|
+
|
|
228
|
+
return { width, height }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Returns the first finite numeric value in a list.
|
|
233
|
+
* @param {unknown[]} values Values to scan.
|
|
234
|
+
* @returns {number}
|
|
235
|
+
*/
|
|
236
|
+
static #firstFiniteNumber(values) {
|
|
237
|
+
for (const value of values) {
|
|
238
|
+
const number = Number(value)
|
|
239
|
+
if (Number.isFinite(number)) return number
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return NaN
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Resolves the dominant nearest-neighbor line angle for one two-row group.
|
|
247
|
+
* @param {object[]} pads Candidate pads.
|
|
248
|
+
* @returns {number | null}
|
|
249
|
+
*/
|
|
250
|
+
static #resolveNearestNeighborLineAngle(pads) {
|
|
251
|
+
const angles = []
|
|
252
|
+
|
|
253
|
+
for (const pad of pads) {
|
|
254
|
+
const neighbor = PcbFootprintPadAxisNormalizer.#nearestNeighbor(
|
|
255
|
+
pad,
|
|
256
|
+
pads
|
|
257
|
+
)
|
|
258
|
+
if (!neighbor) continue
|
|
259
|
+
|
|
260
|
+
const angle =
|
|
261
|
+
(Math.atan2(
|
|
262
|
+
Number(neighbor.y) - Number(pad.y),
|
|
263
|
+
Number(neighbor.x) - Number(pad.x)
|
|
264
|
+
) *
|
|
265
|
+
180) /
|
|
266
|
+
Math.PI
|
|
267
|
+
angles.push(
|
|
268
|
+
PcbFootprintPadAxisNormalizer.#normalizeLineAngle(angle)
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return PcbFootprintPadAxisNormalizer.#meanLineAngle(angles)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Finds the nearest distinct neighboring pad.
|
|
277
|
+
* @param {object} pad Source pad.
|
|
278
|
+
* @param {object[]} pads Candidate pads.
|
|
279
|
+
* @returns {object | null}
|
|
280
|
+
*/
|
|
281
|
+
static #nearestNeighbor(pad, pads) {
|
|
282
|
+
let nearest = null
|
|
283
|
+
let nearestDistance = Infinity
|
|
284
|
+
|
|
285
|
+
for (const candidate of pads) {
|
|
286
|
+
if (candidate === pad) continue
|
|
287
|
+
|
|
288
|
+
const dx = Number(candidate?.x) - Number(pad?.x)
|
|
289
|
+
const dy = Number(candidate?.y) - Number(pad?.y)
|
|
290
|
+
const distance = dx * dx + dy * dy
|
|
291
|
+
if (!Number.isFinite(distance) || distance <= 0) continue
|
|
292
|
+
if (distance >= nearestDistance) continue
|
|
293
|
+
|
|
294
|
+
nearest = candidate
|
|
295
|
+
nearestDistance = distance
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return nearest
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Returns the mean line angle using doubled-angle vector averaging.
|
|
303
|
+
* @param {number[]} angles Line angles in degrees.
|
|
304
|
+
* @returns {number | null}
|
|
305
|
+
*/
|
|
306
|
+
static #meanLineAngle(angles) {
|
|
307
|
+
if (!angles.length) return null
|
|
308
|
+
|
|
309
|
+
let x = 0
|
|
310
|
+
let y = 0
|
|
311
|
+
for (const angle of angles) {
|
|
312
|
+
const radians =
|
|
313
|
+
(PcbFootprintPadAxisNormalizer.#normalizeLineAngle(angle) *
|
|
314
|
+
Math.PI *
|
|
315
|
+
2) /
|
|
316
|
+
180
|
|
317
|
+
x += Math.cos(radians)
|
|
318
|
+
y += Math.sin(radians)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const magnitude = Math.hypot(x, y) / angles.length
|
|
322
|
+
if (magnitude < MIN_DIRECTION_STABILITY) return null
|
|
323
|
+
|
|
324
|
+
return PcbFootprintPadAxisNormalizer.#normalizeLineAngle(
|
|
325
|
+
(Math.atan2(y, x) * 90) / Math.PI
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Returns true when candidate pads form at least two separated rows.
|
|
331
|
+
* @param {object[]} pads Candidate pads.
|
|
332
|
+
* @param {number} rowAngle Row line angle in degrees.
|
|
333
|
+
* @returns {boolean}
|
|
334
|
+
*/
|
|
335
|
+
static #hasSeparatedRows(pads, rowAngle) {
|
|
336
|
+
const radians = (rowAngle * Math.PI) / 180
|
|
337
|
+
const sin = Math.sin(radians)
|
|
338
|
+
const cos = Math.cos(radians)
|
|
339
|
+
const values = pads
|
|
340
|
+
.map((pad) => -sin * Number(pad?.x) + cos * Number(pad?.y))
|
|
341
|
+
.filter((value) => Number.isFinite(value))
|
|
342
|
+
.sort((a, b) => a - b)
|
|
343
|
+
if (values.length < MIN_GROUP_PAD_COUNT) return false
|
|
344
|
+
|
|
345
|
+
let gap = 0
|
|
346
|
+
let gapIndex = -1
|
|
347
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
348
|
+
const nextGap = values[index] - values[index - 1]
|
|
349
|
+
if (nextGap > gap) {
|
|
350
|
+
gap = nextGap
|
|
351
|
+
gapIndex = index - 1
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (gapIndex < 1) return false
|
|
356
|
+
if (values.length - gapIndex - 1 < 2) return false
|
|
357
|
+
|
|
358
|
+
const shortSize =
|
|
359
|
+
PcbFootprintPadAxisNormalizer.#medianPadShortSize(pads)
|
|
360
|
+
return gap > Math.max(shortSize * ROW_GAP_SHORT_SIZE_FACTOR, 0.001)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Resolves the median short side for candidate pad rectangles.
|
|
365
|
+
* @param {object[]} pads Candidate pads.
|
|
366
|
+
* @returns {number}
|
|
367
|
+
*/
|
|
368
|
+
static #medianPadShortSize(pads) {
|
|
369
|
+
const values = pads
|
|
370
|
+
.map((pad) => {
|
|
371
|
+
const dimensions =
|
|
372
|
+
PcbFootprintPadAxisNormalizer.#padDimensions(pad)
|
|
373
|
+
if (!dimensions) return null
|
|
374
|
+
return Math.min(dimensions.width, dimensions.height)
|
|
375
|
+
})
|
|
376
|
+
.filter((value) => Number.isFinite(value))
|
|
377
|
+
.sort((a, b) => a - b)
|
|
378
|
+
if (!values.length) return 0
|
|
379
|
+
|
|
380
|
+
return values[Math.floor(values.length / 2)]
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Returns true when a pad's long rectangle axis is parallel to its row.
|
|
385
|
+
* @param {object} pad PCB pad.
|
|
386
|
+
* @param {number} rowAngle Row line angle.
|
|
387
|
+
* @returns {boolean}
|
|
388
|
+
*/
|
|
389
|
+
static #isPadLongAxisParallelToRow(pad, rowAngle) {
|
|
390
|
+
const dimensions = PcbFootprintPadAxisNormalizer.#padDimensions(pad)
|
|
391
|
+
if (!dimensions) return false
|
|
392
|
+
|
|
393
|
+
const longAxisAngle =
|
|
394
|
+
Number(pad?.rotation || 0) +
|
|
395
|
+
(dimensions.height > dimensions.width ? 90 : 0)
|
|
396
|
+
const delta = PcbFootprintPadAxisNormalizer.#lineAngleDelta(
|
|
397
|
+
longAxisAngle,
|
|
398
|
+
rowAngle
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return delta <= PARALLEL_TOLERANCE_DEG
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Applies resolved pad updates to all pad lists in a document.
|
|
406
|
+
* @param {object} documentModel Document model.
|
|
407
|
+
* @param {{ objects: Map<object, number>, signatures: Map<string, number> }} updates Rotation updates.
|
|
408
|
+
* @returns {object}
|
|
409
|
+
*/
|
|
410
|
+
static #applyPadUpdatesToDocument(documentModel, updates) {
|
|
411
|
+
const pcb = documentModel?.pcb || {}
|
|
412
|
+
const pads = Array.isArray(pcb.pads)
|
|
413
|
+
? PcbFootprintPadAxisNormalizer.#applyPadUpdates(pcb.pads, updates)
|
|
414
|
+
: pcb.pads
|
|
415
|
+
const kicadBoard =
|
|
416
|
+
PcbFootprintPadAxisNormalizer.#applyKicadBoardPadUpdates(
|
|
417
|
+
pcb,
|
|
418
|
+
pads,
|
|
419
|
+
updates
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
...documentModel,
|
|
424
|
+
pcb: {
|
|
425
|
+
...pcb,
|
|
426
|
+
pads,
|
|
427
|
+
kicadBoard
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Applies resolved pad updates to a KiCad board pad list.
|
|
434
|
+
* @param {object} pcb PCB model.
|
|
435
|
+
* @param {object[] | undefined} pads Updated top-level pad list.
|
|
436
|
+
* @param {{ objects: Map<object, number>, signatures: Map<string, number> }} updates Rotation updates.
|
|
437
|
+
* @returns {object | undefined}
|
|
438
|
+
*/
|
|
439
|
+
static #applyKicadBoardPadUpdates(pcb, pads, updates) {
|
|
440
|
+
const kicadBoard = pcb?.kicadBoard
|
|
441
|
+
if (!kicadBoard || !Array.isArray(kicadBoard.pads)) {
|
|
442
|
+
return kicadBoard
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
...kicadBoard,
|
|
447
|
+
pads:
|
|
448
|
+
kicadBoard.pads === pcb.pads
|
|
449
|
+
? pads
|
|
450
|
+
: PcbFootprintPadAxisNormalizer.#applyPadUpdates(
|
|
451
|
+
kicadBoard.pads,
|
|
452
|
+
updates
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Applies resolved pad updates to one pad list.
|
|
459
|
+
* @param {object[] | undefined} pads PCB pads.
|
|
460
|
+
* @param {{ objects: Map<object, number>, signatures: Map<string, number> }} updates Rotation updates.
|
|
461
|
+
* @returns {object[] | undefined}
|
|
462
|
+
*/
|
|
463
|
+
static #applyPadUpdates(pads, updates) {
|
|
464
|
+
if (!Array.isArray(pads)) return pads
|
|
465
|
+
|
|
466
|
+
return pads.map((pad) => {
|
|
467
|
+
const rotation =
|
|
468
|
+
updates.objects.get(pad) ??
|
|
469
|
+
updates.signatures.get(
|
|
470
|
+
PcbFootprintPadAxisNormalizer.#padSignature(pad)
|
|
471
|
+
)
|
|
472
|
+
if (rotation === undefined) return pad
|
|
473
|
+
if (
|
|
474
|
+
PcbFootprintPadAxisNormalizer.#anglesEqual(
|
|
475
|
+
rotation,
|
|
476
|
+
pad?.rotation
|
|
477
|
+
)
|
|
478
|
+
) {
|
|
479
|
+
return pad
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
...pad,
|
|
484
|
+
rotation
|
|
485
|
+
}
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Resolves a stable owner key for one pad.
|
|
491
|
+
* @param {object} pad PCB pad.
|
|
492
|
+
* @returns {string}
|
|
493
|
+
*/
|
|
494
|
+
static #padOwnerKey(pad) {
|
|
495
|
+
const componentIndex = PcbFootprintPadAxisNormalizer.#firstPresent([
|
|
496
|
+
pad?.componentIndex,
|
|
497
|
+
pad?.ownerIndex
|
|
498
|
+
])
|
|
499
|
+
if (componentIndex !== null) return 'component:' + componentIndex
|
|
500
|
+
|
|
501
|
+
const footprintId = PcbFootprintPadAxisNormalizer.#trimmedString(
|
|
502
|
+
pad?.footprintId
|
|
503
|
+
)
|
|
504
|
+
if (footprintId) return 'footprint:' + footprintId
|
|
505
|
+
|
|
506
|
+
const reference = PcbFootprintPadAxisNormalizer.#trimmedString(
|
|
507
|
+
pad?.footprintReference ?? pad?.designator ?? pad?.ref
|
|
508
|
+
)
|
|
509
|
+
return reference ? 'reference:' + reference : ''
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Resolves a stable signature for matching cloned pad lists.
|
|
514
|
+
* @param {object} pad PCB pad.
|
|
515
|
+
* @returns {string}
|
|
516
|
+
*/
|
|
517
|
+
static #padSignature(pad) {
|
|
518
|
+
return [
|
|
519
|
+
PcbFootprintPadAxisNormalizer.#padOwnerKey(pad),
|
|
520
|
+
PcbFootprintPadAxisNormalizer.#trimmedString(
|
|
521
|
+
pad?.number ?? pad?.name ?? pad?.id
|
|
522
|
+
),
|
|
523
|
+
PcbFootprintPadAxisNormalizer.#signatureNumber(pad?.x),
|
|
524
|
+
PcbFootprintPadAxisNormalizer.#signatureNumber(pad?.y),
|
|
525
|
+
PcbFootprintPadAxisNormalizer.#signatureNumber(pad?.rotation)
|
|
526
|
+
].join('|')
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Resolves the first non-empty value.
|
|
531
|
+
* @param {unknown[]} values Values to scan.
|
|
532
|
+
* @returns {string | number | null}
|
|
533
|
+
*/
|
|
534
|
+
static #firstPresent(values) {
|
|
535
|
+
for (const value of values) {
|
|
536
|
+
if (value === undefined || value === null || value === '') continue
|
|
537
|
+
return value
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return null
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Converts one value to a trimmed string.
|
|
545
|
+
* @param {unknown} value Value to stringify.
|
|
546
|
+
* @returns {string}
|
|
547
|
+
*/
|
|
548
|
+
static #trimmedString(value) {
|
|
549
|
+
return String(value ?? '').trim()
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Formats a number for a cloned-pad signature.
|
|
554
|
+
* @param {unknown} value Numeric value.
|
|
555
|
+
* @returns {string}
|
|
556
|
+
*/
|
|
557
|
+
static #signatureNumber(value) {
|
|
558
|
+
const number = Number(value)
|
|
559
|
+
return Number.isFinite(number) ? number.toFixed(6) : ''
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Returns true when two rotations are equal after normalization.
|
|
564
|
+
* @param {unknown} left First angle.
|
|
565
|
+
* @param {unknown} right Second angle.
|
|
566
|
+
* @returns {boolean}
|
|
567
|
+
*/
|
|
568
|
+
static #anglesEqual(left, right) {
|
|
569
|
+
return (
|
|
570
|
+
Math.abs(
|
|
571
|
+
PcbFootprintPadAxisNormalizer.#normalizeAngle(left) -
|
|
572
|
+
PcbFootprintPadAxisNormalizer.#normalizeAngle(right)
|
|
573
|
+
) < 0.001
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Resolves the acute delta between two line angles.
|
|
579
|
+
* @param {number} left First angle.
|
|
580
|
+
* @param {number} right Second angle.
|
|
581
|
+
* @returns {number}
|
|
582
|
+
*/
|
|
583
|
+
static #lineAngleDelta(left, right) {
|
|
584
|
+
const delta = Math.abs(
|
|
585
|
+
PcbFootprintPadAxisNormalizer.#normalizeLineAngle(left) -
|
|
586
|
+
PcbFootprintPadAxisNormalizer.#normalizeLineAngle(right)
|
|
587
|
+
)
|
|
588
|
+
return Math.min(delta, 180 - delta)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Normalizes an angle into [0, 360).
|
|
593
|
+
* @param {unknown} angle Angle in degrees.
|
|
594
|
+
* @returns {number}
|
|
595
|
+
*/
|
|
596
|
+
static #normalizeAngle(angle) {
|
|
597
|
+
const value = Number(angle) || 0
|
|
598
|
+
return ((value % 360) + 360) % 360
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Normalizes an angle into [0, 180).
|
|
603
|
+
* @param {unknown} angle Angle in degrees.
|
|
604
|
+
* @returns {number}
|
|
605
|
+
*/
|
|
606
|
+
static #normalizeLineAngle(angle) {
|
|
607
|
+
return PcbFootprintPadAxisNormalizer.#normalizeAngle(angle) % 180
|
|
608
|
+
}
|
|
609
|
+
}
|