altium-toolkit 1.1.26 → 1.1.31
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 +252 -0
- package/src/ui/AltiumScene3dBottomPadRotationAdapter.mjs +127 -0
- package/src/ui/AltiumScene3dComponentBodyAdapter.mjs +153 -0
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +919 -0
- package/src/ui/AltiumScene3dIdentityTokens.mjs +105 -0
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +357 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +576 -0
- package/src/ui/PcbFootprintPadAxisNormalizer.mjs +609 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +54 -11
- package/src/ui/PcbScene3dBuilder.mjs +76 -33
- package/src/ui/PcbScene3dPadLocalSpanResolver.mjs +109 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
const CONNECTOR_TOKENS = new Set([
|
|
2
|
+
'antenna',
|
|
3
|
+
'coax',
|
|
4
|
+
'connector',
|
|
5
|
+
'edge',
|
|
6
|
+
'rf',
|
|
7
|
+
'socket'
|
|
8
|
+
])
|
|
9
|
+
const PASSIVE_BODY_PATTERN =
|
|
10
|
+
/(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|lqw|lqg)(?:$|[^a-z0-9])/i
|
|
11
|
+
const TIMING_PACKAGE_PATTERN =
|
|
12
|
+
/(?:^|[^a-z0-9])(?:clock|crystal|osc|oscillator|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
|
|
13
|
+
const TIMING_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Repairs repeated Altium model-anchor bodies by matching their shared source
|
|
17
|
+
* origin offset to repeated compatible footprint owners.
|
|
18
|
+
*/
|
|
19
|
+
export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
20
|
+
static #OFFSET_TOLERANCE_MIL = 8
|
|
21
|
+
static #MIN_OWNER_DISTANCE_MIL = 25
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Applies repeated-model owner repair to an Altium scene.
|
|
25
|
+
* @param {object} sceneDescription Scene description.
|
|
26
|
+
* @param {object} documentModel Source document model.
|
|
27
|
+
* @returns {object}
|
|
28
|
+
*/
|
|
29
|
+
static apply(sceneDescription, documentModel) {
|
|
30
|
+
if (
|
|
31
|
+
String(sceneDescription?.sourceFormat || '').toLowerCase() !==
|
|
32
|
+
'altium' ||
|
|
33
|
+
!Array.isArray(sceneDescription?.externalPlacements)
|
|
34
|
+
) {
|
|
35
|
+
return sceneDescription
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const components = Array.isArray(documentModel?.pcb?.components)
|
|
39
|
+
? documentModel.pcb.components
|
|
40
|
+
: []
|
|
41
|
+
if (!components.length) {
|
|
42
|
+
return sceneDescription
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const componentByDesignator = new Map(
|
|
46
|
+
components.map((component) => [
|
|
47
|
+
String(component?.designator || ''),
|
|
48
|
+
component
|
|
49
|
+
])
|
|
50
|
+
)
|
|
51
|
+
const placements = sceneDescription.externalPlacements.map(
|
|
52
|
+
(placement) =>
|
|
53
|
+
AltiumScene3dRepeatedModelOwnerRepair.#withPassiveOwnerCenter(
|
|
54
|
+
placement,
|
|
55
|
+
componentByDesignator.get(
|
|
56
|
+
String(placement?.designator || '')
|
|
57
|
+
),
|
|
58
|
+
sceneDescription.board
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
for (const group of AltiumScene3dRepeatedModelOwnerRepair.#groups(
|
|
63
|
+
placements,
|
|
64
|
+
componentByDesignator
|
|
65
|
+
)) {
|
|
66
|
+
const matches =
|
|
67
|
+
AltiumScene3dRepeatedModelOwnerRepair.#matchGroupOwners(
|
|
68
|
+
group.records,
|
|
69
|
+
components
|
|
70
|
+
)
|
|
71
|
+
if (!matches) {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const match of matches) {
|
|
76
|
+
placements[match.record.index] =
|
|
77
|
+
AltiumScene3dRepeatedModelOwnerRepair.#withOwner(
|
|
78
|
+
match.record.placement,
|
|
79
|
+
match.component,
|
|
80
|
+
sceneDescription.board,
|
|
81
|
+
match.offset
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { ...sceneDescription, externalPlacements: placements }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Groups repairable repeated model-anchor placements.
|
|
91
|
+
* @param {object[]} placements Scene placements.
|
|
92
|
+
* @param {Map<string, object>} componentByDesignator Components by designator.
|
|
93
|
+
* @returns {{ records: { index: number, placement: object }[] }[]}
|
|
94
|
+
*/
|
|
95
|
+
static #groups(placements, componentByDesignator) {
|
|
96
|
+
const groups = new Map()
|
|
97
|
+
|
|
98
|
+
placements.forEach((placement, index) => {
|
|
99
|
+
if (
|
|
100
|
+
!AltiumScene3dRepeatedModelOwnerRepair.#isRepairablePlacement(
|
|
101
|
+
placement,
|
|
102
|
+
componentByDesignator
|
|
103
|
+
)
|
|
104
|
+
) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const key =
|
|
109
|
+
[
|
|
110
|
+
String(placement?.externalModel?.name || ''),
|
|
111
|
+
String(placement?.mountSide || '').toLowerCase(),
|
|
112
|
+
AltiumScene3dRepeatedModelOwnerRepair.#normalizeAngle(
|
|
113
|
+
placement?.rotationDeg
|
|
114
|
+
)
|
|
115
|
+
].join('|') || 'model'
|
|
116
|
+
const records = groups.get(key) || []
|
|
117
|
+
records.push({ index, placement })
|
|
118
|
+
groups.set(key, records)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return [...groups.values()]
|
|
122
|
+
.filter((records) => records.length > 1)
|
|
123
|
+
.map((records) => ({ records }))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Checks whether one placement should participate in group owner repair.
|
|
128
|
+
* @param {object} placement Scene placement.
|
|
129
|
+
* @param {Map<string, object>} componentByDesignator Components by designator.
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
static #isRepairablePlacement(placement, componentByDesignator) {
|
|
133
|
+
if (
|
|
134
|
+
String(placement?.projection?.source || '') !==
|
|
135
|
+
'model-anchor-fallback' ||
|
|
136
|
+
!placement?.bodyPositionMil ||
|
|
137
|
+
!placement?.positionMil ||
|
|
138
|
+
!placement?.externalModel
|
|
139
|
+
) {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const component = componentByDesignator.get(
|
|
144
|
+
String(placement?.designator || '')
|
|
145
|
+
)
|
|
146
|
+
if (!component) {
|
|
147
|
+
return true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
AltiumScene3dRepeatedModelOwnerRepair.#distance(
|
|
152
|
+
placement.bodyPositionMil,
|
|
153
|
+
component
|
|
154
|
+
) > AltiumScene3dRepeatedModelOwnerRepair.#MIN_OWNER_DISTANCE_MIL
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Centers a generic passive body on its resolved owner when the body anchor
|
|
160
|
+
* carries a moderate source-origin offset.
|
|
161
|
+
* @param {object} placement Scene placement.
|
|
162
|
+
* @param {object | undefined} component Resolved owner.
|
|
163
|
+
* @param {object} board Scene board metadata.
|
|
164
|
+
* @returns {object}
|
|
165
|
+
*/
|
|
166
|
+
static #withPassiveOwnerCenter(placement, component, board) {
|
|
167
|
+
if (
|
|
168
|
+
!component ||
|
|
169
|
+
String(placement?.projection?.source || '') !== 'pad-fallback' ||
|
|
170
|
+
!AltiumScene3dRepeatedModelOwnerRepair.#isPassivePlacement(
|
|
171
|
+
placement,
|
|
172
|
+
component
|
|
173
|
+
)
|
|
174
|
+
) {
|
|
175
|
+
return placement
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const offset = {
|
|
179
|
+
x:
|
|
180
|
+
Number(placement?.bodyPositionMil?.x || 0) -
|
|
181
|
+
Number(component?.x || 0),
|
|
182
|
+
y:
|
|
183
|
+
Number(placement?.bodyPositionMil?.y || 0) -
|
|
184
|
+
Number(component?.y || 0)
|
|
185
|
+
}
|
|
186
|
+
if (
|
|
187
|
+
Math.hypot(offset.x, offset.y) <=
|
|
188
|
+
AltiumScene3dRepeatedModelOwnerRepair.#MIN_OWNER_DISTANCE_MIL
|
|
189
|
+
) {
|
|
190
|
+
return placement
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return AltiumScene3dRepeatedModelOwnerRepair.#withOwner(
|
|
194
|
+
placement,
|
|
195
|
+
component,
|
|
196
|
+
board,
|
|
197
|
+
offset
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Checks whether a placement/component pair describes a generic passive.
|
|
203
|
+
* @param {object} placement Scene placement.
|
|
204
|
+
* @param {object} component PCB component.
|
|
205
|
+
* @returns {boolean}
|
|
206
|
+
*/
|
|
207
|
+
static #isPassivePlacement(placement, component) {
|
|
208
|
+
return PASSIVE_BODY_PATTERN.test(
|
|
209
|
+
[
|
|
210
|
+
placement?.designator,
|
|
211
|
+
placement?.externalModel?.name,
|
|
212
|
+
component?.pattern,
|
|
213
|
+
component?.source,
|
|
214
|
+
component?.description
|
|
215
|
+
]
|
|
216
|
+
.map((value) => String(value || ''))
|
|
217
|
+
.join(' ')
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Matches one repeated placement group to compatible components.
|
|
223
|
+
* @param {{ placement: object }[]} records Group records.
|
|
224
|
+
* @param {object[]} components PCB components.
|
|
225
|
+
* @returns {{ record: object, component: object, offset: object }[] | null}
|
|
226
|
+
*/
|
|
227
|
+
static #matchGroupOwners(records, components) {
|
|
228
|
+
const candidates =
|
|
229
|
+
AltiumScene3dRepeatedModelOwnerRepair.#compatibleComponents(
|
|
230
|
+
records[0]?.placement,
|
|
231
|
+
components
|
|
232
|
+
)
|
|
233
|
+
const connectorMatches =
|
|
234
|
+
AltiumScene3dRepeatedModelOwnerRepair.#matchCandidateOwners(
|
|
235
|
+
records,
|
|
236
|
+
candidates
|
|
237
|
+
)
|
|
238
|
+
if (connectorMatches) {
|
|
239
|
+
return connectorMatches
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return AltiumScene3dRepeatedModelOwnerRepair.#matchCandidateOwners(
|
|
243
|
+
records,
|
|
244
|
+
AltiumScene3dRepeatedModelOwnerRepair.#compatibleTimingComponents(
|
|
245
|
+
records[0]?.placement,
|
|
246
|
+
components
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Matches records against one candidate component set.
|
|
253
|
+
* @param {{ placement: object }[]} records Group records.
|
|
254
|
+
* @param {object[]} candidates Candidate components.
|
|
255
|
+
* @returns {{ record: object, component: object, offset: object }[] | null}
|
|
256
|
+
*/
|
|
257
|
+
static #matchCandidateOwners(records, candidates) {
|
|
258
|
+
if (candidates.length < records.length) {
|
|
259
|
+
return null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const attempts = records.flatMap((record) =>
|
|
263
|
+
candidates.map((component) => ({
|
|
264
|
+
x:
|
|
265
|
+
Number(record.placement.bodyPositionMil?.x || 0) -
|
|
266
|
+
Number(component?.x || 0),
|
|
267
|
+
y:
|
|
268
|
+
Number(record.placement.bodyPositionMil?.y || 0) -
|
|
269
|
+
Number(component?.y || 0)
|
|
270
|
+
}))
|
|
271
|
+
)
|
|
272
|
+
const matches = attempts
|
|
273
|
+
.map((offset) => ({
|
|
274
|
+
offset,
|
|
275
|
+
matches:
|
|
276
|
+
AltiumScene3dRepeatedModelOwnerRepair.#matchesForOffset(
|
|
277
|
+
records,
|
|
278
|
+
candidates,
|
|
279
|
+
offset
|
|
280
|
+
)
|
|
281
|
+
}))
|
|
282
|
+
.filter((attempt) => attempt.matches.length === records.length)
|
|
283
|
+
.sort(
|
|
284
|
+
(left, right) =>
|
|
285
|
+
AltiumScene3dRepeatedModelOwnerRepair.#matchError(
|
|
286
|
+
left.matches
|
|
287
|
+
) -
|
|
288
|
+
AltiumScene3dRepeatedModelOwnerRepair.#matchError(
|
|
289
|
+
right.matches
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return matches[0]?.matches || null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Resolves compatible components for one placement group.
|
|
298
|
+
* @param {object} placement Representative placement.
|
|
299
|
+
* @param {object[]} components PCB components.
|
|
300
|
+
* @returns {object[]}
|
|
301
|
+
*/
|
|
302
|
+
static #compatibleComponents(placement, components) {
|
|
303
|
+
const mountSide = String(placement?.mountSide || '').toLowerCase()
|
|
304
|
+
const rotation = AltiumScene3dRepeatedModelOwnerRepair.#normalizeAngle(
|
|
305
|
+
placement?.rotationDeg
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return components.filter(
|
|
309
|
+
(component) =>
|
|
310
|
+
AltiumScene3dRepeatedModelOwnerRepair.#mountSide(component) ===
|
|
311
|
+
mountSide &&
|
|
312
|
+
AltiumScene3dRepeatedModelOwnerRepair.#normalizeAngle(
|
|
313
|
+
component?.rotation
|
|
314
|
+
) === rotation &&
|
|
315
|
+
AltiumScene3dRepeatedModelOwnerRepair.#connectorScore(
|
|
316
|
+
component
|
|
317
|
+
) >= 2
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Resolves timing-package candidates for repeated bodies with shared
|
|
323
|
+
* source-origin offsets.
|
|
324
|
+
* @param {object} placement Representative placement.
|
|
325
|
+
* @param {object[]} components PCB components.
|
|
326
|
+
* @returns {object[]}
|
|
327
|
+
*/
|
|
328
|
+
static #compatibleTimingComponents(placement, components) {
|
|
329
|
+
if (
|
|
330
|
+
!AltiumScene3dRepeatedModelOwnerRepair.#isTimingPlacement(placement)
|
|
331
|
+
) {
|
|
332
|
+
return []
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const mountSide = String(placement?.mountSide || '').toLowerCase()
|
|
336
|
+
const rotation = AltiumScene3dRepeatedModelOwnerRepair.#normalizeAngle(
|
|
337
|
+
placement?.rotationDeg
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
return components.filter(
|
|
341
|
+
(component) =>
|
|
342
|
+
AltiumScene3dRepeatedModelOwnerRepair.#mountSide(component) ===
|
|
343
|
+
mountSide &&
|
|
344
|
+
AltiumScene3dRepeatedModelOwnerRepair.#normalizeAngle(
|
|
345
|
+
component?.rotation
|
|
346
|
+
) === rotation &&
|
|
347
|
+
AltiumScene3dRepeatedModelOwnerRepair.#isTimingComponent(
|
|
348
|
+
component
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Matches records to components for one candidate shared offset.
|
|
355
|
+
* @param {{ placement: object }[]} records Group records.
|
|
356
|
+
* @param {object[]} components Compatible components.
|
|
357
|
+
* @param {{ x: number, y: number }} offset Candidate offset.
|
|
358
|
+
* @returns {{ record: object, component: object, offset: object, error: number }[]}
|
|
359
|
+
*/
|
|
360
|
+
static #matchesForOffset(records, components, offset) {
|
|
361
|
+
const unused = new Set(components)
|
|
362
|
+
const matches = []
|
|
363
|
+
|
|
364
|
+
for (const record of records) {
|
|
365
|
+
const match =
|
|
366
|
+
AltiumScene3dRepeatedModelOwnerRepair.#nearestOffsetComponent(
|
|
367
|
+
record.placement,
|
|
368
|
+
unused,
|
|
369
|
+
offset
|
|
370
|
+
)
|
|
371
|
+
if (!match) {
|
|
372
|
+
return matches
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
unused.delete(match.component)
|
|
376
|
+
matches.push({ record, ...match, offset })
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return matches
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Finds the unused component whose center plus offset reaches a body.
|
|
384
|
+
* @param {object} placement Scene placement.
|
|
385
|
+
* @param {Set<object>} components Unused compatible components.
|
|
386
|
+
* @param {{ x: number, y: number }} offset Candidate offset.
|
|
387
|
+
* @returns {{ component: object, error: number } | null}
|
|
388
|
+
*/
|
|
389
|
+
static #nearestOffsetComponent(placement, components, offset) {
|
|
390
|
+
const body = placement?.bodyPositionMil || {}
|
|
391
|
+
const matches = [...components]
|
|
392
|
+
.map((component) => ({
|
|
393
|
+
component,
|
|
394
|
+
error: Math.hypot(
|
|
395
|
+
Number(component?.x || 0) +
|
|
396
|
+
Number(offset?.x || 0) -
|
|
397
|
+
Number(body.x || 0),
|
|
398
|
+
Number(component?.y || 0) +
|
|
399
|
+
Number(offset?.y || 0) -
|
|
400
|
+
Number(body.y || 0)
|
|
401
|
+
)
|
|
402
|
+
}))
|
|
403
|
+
.filter(
|
|
404
|
+
(match) =>
|
|
405
|
+
match.error <=
|
|
406
|
+
AltiumScene3dRepeatedModelOwnerRepair.#OFFSET_TOLERANCE_MIL
|
|
407
|
+
)
|
|
408
|
+
.sort((left, right) => left.error - right.error)
|
|
409
|
+
|
|
410
|
+
return matches[0] || null
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Sums offset match error.
|
|
415
|
+
* @param {{ error: number }[]} matches Offset matches.
|
|
416
|
+
* @returns {number}
|
|
417
|
+
*/
|
|
418
|
+
static #matchError(matches) {
|
|
419
|
+
return matches.reduce(
|
|
420
|
+
(total, match) => total + Number(match?.error || 0),
|
|
421
|
+
0
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Applies a resolved component owner and centers the model on it.
|
|
427
|
+
* @param {object} placement Scene placement.
|
|
428
|
+
* @param {object} component Resolved owner.
|
|
429
|
+
* @param {object} board Scene board metadata.
|
|
430
|
+
* @param {{ x: number, y: number }} offset Source-origin offset.
|
|
431
|
+
* @returns {object}
|
|
432
|
+
*/
|
|
433
|
+
static #withOwner(placement, component, board, offset) {
|
|
434
|
+
const mountSide =
|
|
435
|
+
AltiumScene3dRepeatedModelOwnerRepair.#mountSide(component) ||
|
|
436
|
+
placement.mountSide
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
...placement,
|
|
440
|
+
designator: String(component?.designator || placement.designator),
|
|
441
|
+
mountSide,
|
|
442
|
+
rotationDeg: AltiumScene3dRepeatedModelOwnerRepair.#normalizeAngle(
|
|
443
|
+
component?.rotation
|
|
444
|
+
),
|
|
445
|
+
positionMil: {
|
|
446
|
+
...placement.positionMil,
|
|
447
|
+
x: Number(component?.x || 0) - Number(board?.centerX || 0),
|
|
448
|
+
y: Number(component?.y || 0) - Number(board?.centerY || 0),
|
|
449
|
+
z: AltiumScene3dRepeatedModelOwnerRepair.#faceZ(
|
|
450
|
+
mountSide,
|
|
451
|
+
board
|
|
452
|
+
)
|
|
453
|
+
},
|
|
454
|
+
modelTransform: {
|
|
455
|
+
...(placement.modelTransform || {}),
|
|
456
|
+
ownerAnchorOffsetMil: {
|
|
457
|
+
x: Number(offset?.x || 0),
|
|
458
|
+
y: Number(offset?.y || 0)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Scores connector-like identity tokens on one component.
|
|
466
|
+
* @param {object} component PCB component.
|
|
467
|
+
* @returns {number}
|
|
468
|
+
*/
|
|
469
|
+
static #connectorScore(component) {
|
|
470
|
+
return new Set(
|
|
471
|
+
AltiumScene3dRepeatedModelOwnerRepair.#identityText(component)
|
|
472
|
+
.split(/[^a-zA-Z0-9]+/gu)
|
|
473
|
+
.map((token) => token.toLowerCase())
|
|
474
|
+
.filter((token) => CONNECTOR_TOKENS.has(token))
|
|
475
|
+
).size
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Checks whether one placement identifies a timing package model.
|
|
480
|
+
* @param {object} placement External model placement.
|
|
481
|
+
* @returns {boolean}
|
|
482
|
+
*/
|
|
483
|
+
static #isTimingPlacement(placement) {
|
|
484
|
+
return TIMING_PACKAGE_PATTERN.test(
|
|
485
|
+
[
|
|
486
|
+
placement?.designator,
|
|
487
|
+
placement?.externalModel?.name,
|
|
488
|
+
placement?.externalModel?.sourceStream
|
|
489
|
+
]
|
|
490
|
+
.map((value) => String(value || ''))
|
|
491
|
+
.join(' ')
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Checks whether one component is a timing package owner.
|
|
497
|
+
* @param {object} component PCB component.
|
|
498
|
+
* @returns {boolean}
|
|
499
|
+
*/
|
|
500
|
+
static #isTimingComponent(component) {
|
|
501
|
+
const designator = String(component?.designator || '').trim()
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
TIMING_DESIGNATOR_PATTERN.test(designator) ||
|
|
505
|
+
TIMING_PACKAGE_PATTERN.test(
|
|
506
|
+
AltiumScene3dRepeatedModelOwnerRepair.#identityText(component)
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Builds component identity text.
|
|
513
|
+
* @param {object} component PCB component.
|
|
514
|
+
* @returns {string}
|
|
515
|
+
*/
|
|
516
|
+
static #identityText(component) {
|
|
517
|
+
return [
|
|
518
|
+
component?.pattern,
|
|
519
|
+
component?.source,
|
|
520
|
+
component?.description,
|
|
521
|
+
...Object.values(component?.parameters || {})
|
|
522
|
+
]
|
|
523
|
+
.map((value) => String(value || ''))
|
|
524
|
+
.join(' ')
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Resolves one component's mount side.
|
|
529
|
+
* @param {object} component PCB component.
|
|
530
|
+
* @returns {'top' | 'bottom' | ''}
|
|
531
|
+
*/
|
|
532
|
+
static #mountSide(component) {
|
|
533
|
+
const layer = String(component?.layer || '').toUpperCase()
|
|
534
|
+
|
|
535
|
+
return layer.includes('BOTTOM') || layer === 'BOT' ? 'bottom' : 'top'
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Measures XY distance between two board points.
|
|
540
|
+
* @param {object} first First point.
|
|
541
|
+
* @param {object} second Second point.
|
|
542
|
+
* @returns {number}
|
|
543
|
+
*/
|
|
544
|
+
static #distance(first, second) {
|
|
545
|
+
return Math.hypot(
|
|
546
|
+
Number(first?.x || 0) - Number(second?.x || 0),
|
|
547
|
+
Number(first?.y || 0) - Number(second?.y || 0)
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Resolves one board face Z coordinate.
|
|
553
|
+
* @param {string} mountSide Mount side.
|
|
554
|
+
* @param {object} board Board metadata.
|
|
555
|
+
* @returns {number}
|
|
556
|
+
*/
|
|
557
|
+
static #faceZ(mountSide, board) {
|
|
558
|
+
const thickness = Number(board?.thicknessMil) || 63
|
|
559
|
+
const halfThickness = thickness / 2
|
|
560
|
+
|
|
561
|
+
return String(mountSide || '').toLowerCase() === 'bottom'
|
|
562
|
+
? -halfThickness
|
|
563
|
+
: halfThickness
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Normalizes an angle into [0, 360).
|
|
568
|
+
* @param {number} angle Source angle.
|
|
569
|
+
* @returns {number}
|
|
570
|
+
*/
|
|
571
|
+
static #normalizeAngle(angle) {
|
|
572
|
+
const normalized = Number(angle || 0) % 360
|
|
573
|
+
|
|
574
|
+
return normalized < 0 ? normalized + 360 : normalized
|
|
575
|
+
}
|
|
576
|
+
}
|