altium-toolkit 1.1.31 → 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/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +54 -6
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +364 -22
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +568 -4
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +149 -23
- package/src/ui/PcbScene3dBuilder.mjs +297 -5
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +34 -2
|
@@ -8,9 +8,15 @@ const CONNECTOR_TOKENS = new Set([
|
|
|
8
8
|
])
|
|
9
9
|
const PASSIVE_BODY_PATTERN =
|
|
10
10
|
/(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|lqw|lqg)(?:$|[^a-z0-9])/i
|
|
11
|
+
const COMPACT_IC_BODY_PATTERN =
|
|
12
|
+
/(?:^|[^a-z0-9])(?:u?qfn(?:[-_ ]?\d+)?|dfn(?:[-_ ]?\d+)?|qfp(?:[-_ ]?\d+)?|bga(?:[-_ ]?\d+)?|lga(?:[-_ ]?\d+)?)(?:$|[^a-z0-9])/i
|
|
11
13
|
const TIMING_PACKAGE_PATTERN =
|
|
12
14
|
/(?:^|[^a-z0-9])(?:clock|crystal|osc|oscillator|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
|
|
13
15
|
const TIMING_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
|
|
16
|
+
const AUTHORED_ANCHOR_IDENTITY_PATTERN =
|
|
17
|
+
/(?:^|[^a-z0-9])(?:antenna|coax|connector|edge|header|jack|mechanical|module|mount|shield|sma|socket|usb)(?:$|[^a-z0-9])/i
|
|
18
|
+
const PAD_FALLBACK_AUTHORED_ANCHOR_PATTERN =
|
|
19
|
+
/(?:^|[^a-z0-9])(?:antenna|coax|conn|connector|edge|flex|fpc|frame|hardware|header|jack|mechanical|module|shield|sma|socket|usb)(?:$|[^a-z0-9])/i
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* Repairs repeated Altium model-anchor bodies by matching their shared source
|
|
@@ -19,6 +25,7 @@ const TIMING_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
|
|
|
19
25
|
export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
20
26
|
static #OFFSET_TOLERANCE_MIL = 8
|
|
21
27
|
static #MIN_OWNER_DISTANCE_MIL = 25
|
|
28
|
+
static #MIN_PAD_FALLBACK_OWNER_OFFSET_MIL = 1
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
31
|
* Applies repeated-model owner repair to an Altium scene.
|
|
@@ -50,7 +57,7 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
50
57
|
)
|
|
51
58
|
const placements = sceneDescription.externalPlacements.map(
|
|
52
59
|
(placement) =>
|
|
53
|
-
AltiumScene3dRepeatedModelOwnerRepair.#
|
|
60
|
+
AltiumScene3dRepeatedModelOwnerRepair.#withSingleOwnerCenter(
|
|
54
61
|
placement,
|
|
55
62
|
componentByDesignator.get(
|
|
56
63
|
String(placement?.designator || '')
|
|
@@ -156,18 +163,18 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
/**
|
|
159
|
-
* Centers a
|
|
160
|
-
*
|
|
166
|
+
* Centers a single body on its resolved owner when the body anchor carries
|
|
167
|
+
* a source-origin offset that is not an authored mechanical anchor.
|
|
161
168
|
* @param {object} placement Scene placement.
|
|
162
169
|
* @param {object | undefined} component Resolved owner.
|
|
163
170
|
* @param {object} board Scene board metadata.
|
|
164
171
|
* @returns {object}
|
|
165
172
|
*/
|
|
166
|
-
static #
|
|
173
|
+
static #withSingleOwnerCenter(placement, component, board) {
|
|
167
174
|
if (
|
|
168
175
|
!component ||
|
|
169
176
|
String(placement?.projection?.source || '') !== 'pad-fallback' ||
|
|
170
|
-
|
|
177
|
+
AltiumScene3dRepeatedModelOwnerRepair.#isAuthoredAnchorPlacement(
|
|
171
178
|
placement,
|
|
172
179
|
component
|
|
173
180
|
)
|
|
@@ -185,7 +192,8 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
185
192
|
}
|
|
186
193
|
if (
|
|
187
194
|
Math.hypot(offset.x, offset.y) <=
|
|
188
|
-
AltiumScene3dRepeatedModelOwnerRepair
|
|
195
|
+
AltiumScene3dRepeatedModelOwnerRepair
|
|
196
|
+
.#MIN_PAD_FALLBACK_OWNER_OFFSET_MIL
|
|
189
197
|
) {
|
|
190
198
|
return placement
|
|
191
199
|
}
|
|
@@ -194,28 +202,87 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
194
202
|
placement,
|
|
195
203
|
component,
|
|
196
204
|
board,
|
|
197
|
-
offset
|
|
205
|
+
offset,
|
|
206
|
+
{ preserveRotation: true }
|
|
198
207
|
)
|
|
199
208
|
}
|
|
200
209
|
|
|
201
210
|
/**
|
|
202
|
-
* Checks whether a placement/component pair describes
|
|
211
|
+
* Checks whether a placement/component pair describes an authored hardware
|
|
212
|
+
* anchor whose source body position should remain authoritative.
|
|
203
213
|
* @param {object} placement Scene placement.
|
|
204
214
|
* @param {object} component PCB component.
|
|
205
215
|
* @returns {boolean}
|
|
206
216
|
*/
|
|
207
|
-
static #
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
placement
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
static #isAuthoredAnchorPlacement(placement, component) {
|
|
218
|
+
const identityText =
|
|
219
|
+
AltiumScene3dRepeatedModelOwnerRepair.#identityTextForPlacement(
|
|
220
|
+
placement,
|
|
221
|
+
component
|
|
222
|
+
)
|
|
223
|
+
if (PASSIVE_BODY_PATTERN.test(identityText)) {
|
|
224
|
+
return false
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (
|
|
228
|
+
String(placement?.projection?.source || '').toLowerCase() ===
|
|
229
|
+
'pad-fallback'
|
|
230
|
+
) {
|
|
231
|
+
return (
|
|
232
|
+
PAD_FALLBACK_AUTHORED_ANCHOR_PATTERN.test(identityText) ||
|
|
233
|
+
AltiumScene3dRepeatedModelOwnerRepair.#hasCompactIcSourceOriginOffset(
|
|
234
|
+
placement,
|
|
235
|
+
component,
|
|
236
|
+
identityText
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return AUTHORED_ANCHOR_IDENTITY_PATTERN.test(identityText)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Checks whether a compact IC uses a small authored source-origin offset.
|
|
246
|
+
* @param {object} placement Scene placement.
|
|
247
|
+
* @param {object} component PCB component.
|
|
248
|
+
* @param {string} identityText Searchable package identity.
|
|
249
|
+
* @returns {boolean}
|
|
250
|
+
*/
|
|
251
|
+
static #hasCompactIcSourceOriginOffset(placement, component, identityText) {
|
|
252
|
+
const distance = AltiumScene3dRepeatedModelOwnerRepair.#distance(
|
|
253
|
+
placement?.bodyPositionMil,
|
|
254
|
+
component
|
|
218
255
|
)
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
COMPACT_IC_BODY_PATTERN.test(identityText) &&
|
|
259
|
+
distance >
|
|
260
|
+
AltiumScene3dRepeatedModelOwnerRepair
|
|
261
|
+
.#MIN_PAD_FALLBACK_OWNER_OFFSET_MIL &&
|
|
262
|
+
distance <
|
|
263
|
+
AltiumScene3dRepeatedModelOwnerRepair.#MIN_OWNER_DISTANCE_MIL
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Builds searchable identity text for placement-owner policy checks.
|
|
269
|
+
* @param {object} placement Scene placement.
|
|
270
|
+
* @param {object} component PCB component.
|
|
271
|
+
* @returns {string}
|
|
272
|
+
*/
|
|
273
|
+
static #identityTextForPlacement(placement, component) {
|
|
274
|
+
return [
|
|
275
|
+
placement?.designator,
|
|
276
|
+
placement?.externalModel?.name,
|
|
277
|
+
placement?.externalModel?.relativePath,
|
|
278
|
+
placement?.externalModel?.sourceStream,
|
|
279
|
+
component?.pattern,
|
|
280
|
+
component?.source,
|
|
281
|
+
component?.description,
|
|
282
|
+
...Object.values(component?.parameters || {})
|
|
283
|
+
]
|
|
284
|
+
.map((value) => String(value || ''))
|
|
285
|
+
.join(' ')
|
|
219
286
|
}
|
|
220
287
|
|
|
221
288
|
/**
|
|
@@ -428,20 +495,32 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
428
495
|
* @param {object} component Resolved owner.
|
|
429
496
|
* @param {object} board Scene board metadata.
|
|
430
497
|
* @param {{ x: number, y: number }} offset Source-origin offset.
|
|
498
|
+
* @param {{ preserveRotation?: boolean }} [options] Repair options.
|
|
431
499
|
* @returns {object}
|
|
432
500
|
*/
|
|
433
|
-
static #withOwner(placement, component, board, offset) {
|
|
501
|
+
static #withOwner(placement, component, board, offset, options = {}) {
|
|
434
502
|
const mountSide =
|
|
435
503
|
AltiumScene3dRepeatedModelOwnerRepair.#mountSide(component) ||
|
|
436
504
|
placement.mountSide
|
|
505
|
+
const rotationDeg = options?.preserveRotation
|
|
506
|
+
? AltiumScene3dRepeatedModelOwnerRepair.#normalizeAngle(
|
|
507
|
+
placement?.rotationDeg
|
|
508
|
+
)
|
|
509
|
+
: AltiumScene3dRepeatedModelOwnerRepair.#normalizeAngle(
|
|
510
|
+
component?.rotation
|
|
511
|
+
)
|
|
512
|
+
const renderableOffset =
|
|
513
|
+
AltiumScene3dRepeatedModelOwnerRepair.#renderableOwnerAnchorOffset(
|
|
514
|
+
{ mountSide, rotationDeg },
|
|
515
|
+
offset,
|
|
516
|
+
placement?.modelTransform
|
|
517
|
+
)
|
|
437
518
|
|
|
438
519
|
return {
|
|
439
520
|
...placement,
|
|
440
521
|
designator: String(component?.designator || placement.designator),
|
|
441
522
|
mountSide,
|
|
442
|
-
rotationDeg
|
|
443
|
-
component?.rotation
|
|
444
|
-
),
|
|
523
|
+
rotationDeg,
|
|
445
524
|
positionMil: {
|
|
446
525
|
...placement.positionMil,
|
|
447
526
|
x: Number(component?.x || 0) - Number(board?.centerX || 0),
|
|
@@ -453,6 +532,7 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
453
532
|
},
|
|
454
533
|
modelTransform: {
|
|
455
534
|
...(placement.modelTransform || {}),
|
|
535
|
+
offsetMil: renderableOffset,
|
|
456
536
|
ownerAnchorOffsetMil: {
|
|
457
537
|
x: Number(offset?.x || 0),
|
|
458
538
|
y: Number(offset?.y || 0)
|
|
@@ -461,6 +541,52 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
461
541
|
}
|
|
462
542
|
}
|
|
463
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Converts a board-space source-origin offset into mount-rig local XY.
|
|
546
|
+
* @param {{ mountSide?: string, rotationDeg?: number }} placement Resolved placement fields.
|
|
547
|
+
* @param {{ x: number, y: number }} offset Board-space owner offset.
|
|
548
|
+
* @param {object | undefined} modelTransform Existing model transform.
|
|
549
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
550
|
+
*/
|
|
551
|
+
static #renderableOwnerAnchorOffset(placement, offset, modelTransform) {
|
|
552
|
+
const rotationRad =
|
|
553
|
+
(-AltiumScene3dRepeatedModelOwnerRepair.#normalizeAngle(
|
|
554
|
+
Number(placement?.rotationDeg || 0)
|
|
555
|
+
) *
|
|
556
|
+
Math.PI) /
|
|
557
|
+
180
|
|
558
|
+
const cos = Math.cos(rotationRad)
|
|
559
|
+
const sin = Math.sin(rotationRad)
|
|
560
|
+
const x = Number(offset?.x || 0) * cos - Number(offset?.y || 0) * sin
|
|
561
|
+
const y = Number(offset?.x || 0) * sin + Number(offset?.y || 0) * cos
|
|
562
|
+
const z = Number(
|
|
563
|
+
modelTransform?.offsetMil?.z ?? modelTransform?.dzMil ?? 0
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
x: Math.abs(x) < Number.EPSILON ? 0 : Number(x.toFixed(10)),
|
|
568
|
+
y: AltiumScene3dRepeatedModelOwnerRepair.#isBottomPlacement(
|
|
569
|
+
placement
|
|
570
|
+
)
|
|
571
|
+
? Math.abs(y) < Number.EPSILON
|
|
572
|
+
? 0
|
|
573
|
+
: Number((-y).toFixed(10))
|
|
574
|
+
: Math.abs(y) < Number.EPSILON
|
|
575
|
+
? 0
|
|
576
|
+
: Number(y.toFixed(10)),
|
|
577
|
+
z: Number.isFinite(z) ? z : 0
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Checks whether one placement mounts on the board bottom face.
|
|
583
|
+
* @param {{ mountSide?: string } | null | undefined} placement Placement.
|
|
584
|
+
* @returns {boolean}
|
|
585
|
+
*/
|
|
586
|
+
static #isBottomPlacement(placement) {
|
|
587
|
+
return String(placement?.mountSide || '').toLowerCase() === 'bottom'
|
|
588
|
+
}
|
|
589
|
+
|
|
464
590
|
/**
|
|
465
591
|
* Scores connector-like identity tokens on one component.
|
|
466
592
|
* @param {object} component PCB component.
|
|
@@ -26,10 +26,16 @@ export class PcbScene3dBuilder {
|
|
|
26
26
|
static #DENSE_OVERLAY_MIN_TRACK_COUNT = 250
|
|
27
27
|
static #DENSE_OVERLAY_KNOCKOUT_COLOR = 0x2f6a2c
|
|
28
28
|
static #PRECISE_BODY_MATCH_TOLERANCE_MIL = 20
|
|
29
|
+
static #EXACT_BODY_MISMATCH_TOLERANCE_MIL = 1
|
|
30
|
+
static #NEAR_PACKAGE_AFFINITY_DISTANCE_MIL = 100
|
|
29
31
|
static #UNMATCHED_BODY_OVERHANG_RATIO = 0.25
|
|
30
32
|
static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
|
|
31
33
|
static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
|
|
32
34
|
static #TRUETYPE_TEXT_WIDTH_RATIO = 0.55
|
|
35
|
+
static #AUTHORED_BODY_IDENTITY_PATTERN =
|
|
36
|
+
/(?:^|[^a-z0-9])(?:antenna|coax|conn|connector|edge|flex|fpc|frame|hardware|header|jack|mechanical|module|mount|shield|sma|socket|usb)(?:$|[^a-z0-9])/i
|
|
37
|
+
static #COMPONENT_PACKAGE_BODY_PATTERN =
|
|
38
|
+
/(?:^|[^a-z0-9])(?:[a-z0-9]*dfn|[a-z0-9]*qfn|bga|cap|capacitor|crystal|diode|ferrite|ind|inductor|lga|lqg[a-z0-9]*|lqw[a-z0-9]*|osc|qfp|res|resistor|sot|transistor|xtal)(?:$|[^a-z0-9])/i
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
41
|
* Builds a scene description for host 3D renderers.
|
|
@@ -81,10 +87,16 @@ export class PcbScene3dBuilder {
|
|
|
81
87
|
: []
|
|
82
88
|
}
|
|
83
89
|
const componentBodyModels = componentBodies.map((componentBody) =>
|
|
84
|
-
PcbScene3dBuilder.#
|
|
90
|
+
PcbScene3dBuilder.#shouldSuppressLayerlessBodyPlaceholder(
|
|
85
91
|
componentBody,
|
|
86
|
-
|
|
92
|
+
componentBodies,
|
|
93
|
+
board
|
|
87
94
|
)
|
|
95
|
+
? null
|
|
96
|
+
: PcbScene3dBuilder.#resolveComponentBodyModel(
|
|
97
|
+
componentBody,
|
|
98
|
+
modelRegistry
|
|
99
|
+
)
|
|
88
100
|
)
|
|
89
101
|
const bodyMatches = PcbScene3dBuilder.#resolveComponentBodyMatches(
|
|
90
102
|
componentBodies,
|
|
@@ -193,12 +205,12 @@ export class PcbScene3dBuilder {
|
|
|
193
205
|
|
|
194
206
|
/**
|
|
195
207
|
* Builds one procedural component scene entry.
|
|
196
|
-
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null, source?: string, modelPath?: string }} component
|
|
208
|
+
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null, source?: string, description?: string, parameters?: Record<string, unknown>, modelPath?: string }} component
|
|
197
209
|
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
|
|
198
210
|
* @param {{ centerX: number, centerY: number }} board
|
|
199
211
|
* @param {number} thicknessMil
|
|
200
212
|
* @param {{ resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null } | null} modelRegistry
|
|
201
|
-
* @returns {{ designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }}
|
|
213
|
+
* @returns {{ designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, description: string, parameters: Record<string, unknown>, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }}
|
|
202
214
|
*/
|
|
203
215
|
static #buildComponent(
|
|
204
216
|
component,
|
|
@@ -233,6 +245,13 @@ export class PcbScene3dBuilder {
|
|
|
233
245
|
},
|
|
234
246
|
pattern: String(component.pattern || ''),
|
|
235
247
|
source: String(component.source || ''),
|
|
248
|
+
description: String(component.description || ''),
|
|
249
|
+
parameters:
|
|
250
|
+
component.parameters &&
|
|
251
|
+
typeof component.parameters === 'object' &&
|
|
252
|
+
!Array.isArray(component.parameters)
|
|
253
|
+
? { ...component.parameters }
|
|
254
|
+
: {},
|
|
236
255
|
body,
|
|
237
256
|
externalModel: modelRegistry
|
|
238
257
|
? modelRegistry.resolveComponentModel(component)
|
|
@@ -275,6 +294,16 @@ export class PcbScene3dBuilder {
|
|
|
275
294
|
return null
|
|
276
295
|
}
|
|
277
296
|
|
|
297
|
+
if (
|
|
298
|
+
!matchedComponent &&
|
|
299
|
+
PcbScene3dBuilder.#shouldDropUnmatchedPackageBody(
|
|
300
|
+
componentBody,
|
|
301
|
+
components
|
|
302
|
+
)
|
|
303
|
+
) {
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
278
307
|
if (
|
|
279
308
|
!matchedComponent &&
|
|
280
309
|
!PcbScene3dBuilder.#isBodyPositionNearBoard(componentBody, board)
|
|
@@ -540,6 +569,12 @@ export class PcbScene3dBuilder {
|
|
|
540
569
|
bodyIndex,
|
|
541
570
|
componentIndex,
|
|
542
571
|
affinityScore,
|
|
572
|
+
nearPackageAffinityScore:
|
|
573
|
+
PcbScene3dBuilder.#nearPackageAffinityScore(
|
|
574
|
+
componentBody,
|
|
575
|
+
affinityScore,
|
|
576
|
+
distance
|
|
577
|
+
),
|
|
543
578
|
preciseOwnerScore:
|
|
544
579
|
precise && (sideCompatible || affinityScore > 0)
|
|
545
580
|
? 1
|
|
@@ -555,6 +590,8 @@ export class PcbScene3dBuilder {
|
|
|
555
590
|
closeCandidates
|
|
556
591
|
.sort(
|
|
557
592
|
(left, right) =>
|
|
593
|
+
right.nearPackageAffinityScore -
|
|
594
|
+
left.nearPackageAffinityScore ||
|
|
558
595
|
right.preciseOwnerScore - left.preciseOwnerScore ||
|
|
559
596
|
right.sideAffinityScore - left.sideAffinityScore ||
|
|
560
597
|
right.affinityScore - left.affinityScore ||
|
|
@@ -804,7 +841,11 @@ export class PcbScene3dBuilder {
|
|
|
804
841
|
distanceMil
|
|
805
842
|
) {
|
|
806
843
|
if (PcbScene3dBuilder.#isPreciseBodyComponentDistance(distanceMil)) {
|
|
807
|
-
return
|
|
844
|
+
return !PcbScene3dBuilder.#isIncompatiblePackageBodyMatch(
|
|
845
|
+
componentBody,
|
|
846
|
+
component,
|
|
847
|
+
distanceMil
|
|
848
|
+
)
|
|
808
849
|
}
|
|
809
850
|
|
|
810
851
|
if (
|
|
@@ -825,6 +866,242 @@ export class PcbScene3dBuilder {
|
|
|
825
866
|
return bodyCount > 0 && bodyCount <= candidateCount
|
|
826
867
|
}
|
|
827
868
|
|
|
869
|
+
/**
|
|
870
|
+
* Scores short-range package metadata matches above no-affinity anchors.
|
|
871
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
872
|
+
* @param {number} affinityScore Shared body/component identity score.
|
|
873
|
+
* @param {number} distanceMil Body/component anchor distance.
|
|
874
|
+
* @returns {number}
|
|
875
|
+
*/
|
|
876
|
+
static #nearPackageAffinityScore(
|
|
877
|
+
componentBody,
|
|
878
|
+
affinityScore,
|
|
879
|
+
distanceMil
|
|
880
|
+
) {
|
|
881
|
+
return PcbScene3dBuilder.#isComponentPackageBody(componentBody) &&
|
|
882
|
+
Number(affinityScore || 0) > 0 &&
|
|
883
|
+
Number(distanceMil || 0) <=
|
|
884
|
+
PcbScene3dBuilder.#NEAR_PACKAGE_AFFINITY_DISTANCE_MIL
|
|
885
|
+
? 1
|
|
886
|
+
: 0
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Checks whether an unmatched package-like body should be suppressed
|
|
891
|
+
* because it sits on an exact but incompatible component anchor.
|
|
892
|
+
* @param {{ name?: string, identifier?: string, positionMil?: { x?: number, y?: number } }} componentBody Component-body record.
|
|
893
|
+
* @param {{ x: number, y: number, pattern?: string, source?: string, modelPath?: string }[]} components PCB components.
|
|
894
|
+
* @returns {boolean}
|
|
895
|
+
*/
|
|
896
|
+
static #shouldDropUnmatchedPackageBody(componentBody, components) {
|
|
897
|
+
if (
|
|
898
|
+
!PcbScene3dBuilder.#isComponentPackageBody(componentBody) ||
|
|
899
|
+
PcbScene3dBuilder.#isAuthoredBodyIdentity(componentBody)
|
|
900
|
+
) {
|
|
901
|
+
return false
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const exactComponents = (
|
|
905
|
+
Array.isArray(components) ? components : []
|
|
906
|
+
).filter(
|
|
907
|
+
(component) =>
|
|
908
|
+
PcbScene3dBuilder.#distanceBetweenBodyAndComponent(
|
|
909
|
+
componentBody,
|
|
910
|
+
component
|
|
911
|
+
) <= PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
return (
|
|
915
|
+
exactComponents.length > 0 &&
|
|
916
|
+
exactComponents.every(
|
|
917
|
+
(component) =>
|
|
918
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
919
|
+
componentBody,
|
|
920
|
+
component
|
|
921
|
+
) <= 0
|
|
922
|
+
)
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Checks whether a precise body/component pair is a package-family mismatch.
|
|
928
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
929
|
+
* @param {{ pattern?: string, source?: string, modelPath?: string }} component PCB component.
|
|
930
|
+
* @param {number} distanceMil Body/component anchor distance.
|
|
931
|
+
* @returns {boolean}
|
|
932
|
+
*/
|
|
933
|
+
static #isIncompatiblePackageBodyMatch(
|
|
934
|
+
componentBody,
|
|
935
|
+
component,
|
|
936
|
+
distanceMil
|
|
937
|
+
) {
|
|
938
|
+
return (
|
|
939
|
+
Number(distanceMil || 0) <=
|
|
940
|
+
PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL &&
|
|
941
|
+
PcbScene3dBuilder.#isComponentPackageBody(componentBody) &&
|
|
942
|
+
!PcbScene3dBuilder.#isAuthoredBodyIdentity(componentBody) &&
|
|
943
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
944
|
+
componentBody,
|
|
945
|
+
component
|
|
946
|
+
) <= 0
|
|
947
|
+
)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Checks whether a component body names a package model rather than an
|
|
952
|
+
* authored board mechanical.
|
|
953
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
954
|
+
* @returns {boolean}
|
|
955
|
+
*/
|
|
956
|
+
static #isComponentPackageBody(componentBody) {
|
|
957
|
+
return PcbScene3dBuilder.#COMPONENT_PACKAGE_BODY_PATTERN.test(
|
|
958
|
+
PcbScene3dBuilder.#componentBodyIdentityText(componentBody)
|
|
959
|
+
)
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Checks whether a component body identity describes an authored hardware
|
|
964
|
+
* or connector anchor that can legitimately sit away from a footprint
|
|
965
|
+
* center.
|
|
966
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
967
|
+
* @returns {boolean}
|
|
968
|
+
*/
|
|
969
|
+
static #isAuthoredBodyIdentity(componentBody) {
|
|
970
|
+
return PcbScene3dBuilder.#AUTHORED_BODY_IDENTITY_PATTERN.test(
|
|
971
|
+
PcbScene3dBuilder.#componentBodyIdentityText(componentBody)
|
|
972
|
+
)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Checks whether a layerless component body is a footprint-library
|
|
977
|
+
* placeholder rather than a board-side placement.
|
|
978
|
+
* @param {{ layer?: string, name?: string, identifier?: string, modelId?: string, checksum?: number | null, positionMil?: { x?: number, y?: number } }} componentBody Component body record.
|
|
979
|
+
* @param {object[]} componentBodies All component body records.
|
|
980
|
+
* @param {{ minX: number, minY: number, widthMil: number, heightMil: number }} board Board envelope.
|
|
981
|
+
* @returns {boolean}
|
|
982
|
+
*/
|
|
983
|
+
static #shouldSuppressLayerlessBodyPlaceholder(
|
|
984
|
+
componentBody,
|
|
985
|
+
componentBodies,
|
|
986
|
+
board
|
|
987
|
+
) {
|
|
988
|
+
if (
|
|
989
|
+
!PcbScene3dBuilder.#isLayerlessComponentBody(componentBody) ||
|
|
990
|
+
PcbScene3dBuilder.#isAuthoredBodyIdentity(componentBody)
|
|
991
|
+
) {
|
|
992
|
+
return false
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return (
|
|
996
|
+
!PcbScene3dBuilder.#isBodyPositionNearBoard(componentBody, board) ||
|
|
997
|
+
PcbScene3dBuilder.#hasLayeredEquivalentPackageBody(
|
|
998
|
+
componentBody,
|
|
999
|
+
componentBodies
|
|
1000
|
+
)
|
|
1001
|
+
)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Checks whether a body lacks an authored mechanical layer.
|
|
1006
|
+
* @param {{ layer?: string }} componentBody Component body record.
|
|
1007
|
+
* @returns {boolean}
|
|
1008
|
+
*/
|
|
1009
|
+
static #isLayerlessComponentBody(componentBody) {
|
|
1010
|
+
return String(componentBody?.layer || '').trim() === ''
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Checks whether a layerless body duplicates a real board-side body.
|
|
1015
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Candidate layerless body.
|
|
1016
|
+
* @param {object[]} componentBodies All component body records.
|
|
1017
|
+
* @returns {boolean}
|
|
1018
|
+
*/
|
|
1019
|
+
static #hasLayeredEquivalentPackageBody(componentBody, componentBodies) {
|
|
1020
|
+
return (Array.isArray(componentBodies) ? componentBodies : []).some(
|
|
1021
|
+
(candidate) =>
|
|
1022
|
+
candidate !== componentBody &&
|
|
1023
|
+
!PcbScene3dBuilder.#isLayerlessComponentBody(candidate) &&
|
|
1024
|
+
PcbScene3dBuilder.#componentBodiesShareModelIdentity(
|
|
1025
|
+
componentBody,
|
|
1026
|
+
candidate
|
|
1027
|
+
) &&
|
|
1028
|
+
PcbScene3dBuilder.#distanceBetweenBodies(
|
|
1029
|
+
componentBody,
|
|
1030
|
+
candidate
|
|
1031
|
+
) <= PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL
|
|
1032
|
+
)
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Checks whether two component bodies refer to the same external package.
|
|
1037
|
+
* @param {{ name?: string, identifier?: string, modelId?: string, checksum?: number | null }} left First body.
|
|
1038
|
+
* @param {{ name?: string, identifier?: string, modelId?: string, checksum?: number | null }} right Second body.
|
|
1039
|
+
* @returns {boolean}
|
|
1040
|
+
*/
|
|
1041
|
+
static #componentBodiesShareModelIdentity(left, right) {
|
|
1042
|
+
const leftModelId = PcbScene3dBuilder.#normalizeIdentityToken(
|
|
1043
|
+
left?.modelId
|
|
1044
|
+
)
|
|
1045
|
+
const rightModelId = PcbScene3dBuilder.#normalizeIdentityToken(
|
|
1046
|
+
right?.modelId
|
|
1047
|
+
)
|
|
1048
|
+
if (leftModelId && rightModelId && leftModelId === rightModelId) {
|
|
1049
|
+
return true
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const leftChecksum = Number(left?.checksum)
|
|
1053
|
+
const rightChecksum = Number(right?.checksum)
|
|
1054
|
+
if (
|
|
1055
|
+
Number.isFinite(leftChecksum) &&
|
|
1056
|
+
Number.isFinite(rightChecksum) &&
|
|
1057
|
+
leftChecksum === rightChecksum
|
|
1058
|
+
) {
|
|
1059
|
+
return true
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const leftName = PcbScene3dBuilder.#normalizeBodyNameToken(left)
|
|
1063
|
+
const rightName = PcbScene3dBuilder.#normalizeBodyNameToken(right)
|
|
1064
|
+
|
|
1065
|
+
return Boolean(leftName && rightName && leftName === rightName)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Normalizes a body model name or identifier for duplicate detection.
|
|
1070
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component body.
|
|
1071
|
+
* @returns {string}
|
|
1072
|
+
*/
|
|
1073
|
+
static #normalizeBodyNameToken(componentBody) {
|
|
1074
|
+
return PcbScene3dBuilder.#normalizeIdentityToken(
|
|
1075
|
+
componentBody?.name || componentBody?.identifier
|
|
1076
|
+
).replace(/\.[a-z0-9]+$/i, '')
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Normalizes an identity token for case-insensitive comparisons.
|
|
1081
|
+
* @param {unknown} value Raw token.
|
|
1082
|
+
* @returns {string}
|
|
1083
|
+
*/
|
|
1084
|
+
static #normalizeIdentityToken(value) {
|
|
1085
|
+
return String(value || '')
|
|
1086
|
+
.trim()
|
|
1087
|
+
.toLowerCase()
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Builds searchable identity text for one component body.
|
|
1092
|
+
* @param {{ name?: string, identifier?: string, modelId?: string }} componentBody Component-body record.
|
|
1093
|
+
* @returns {string}
|
|
1094
|
+
*/
|
|
1095
|
+
static #componentBodyIdentityText(componentBody) {
|
|
1096
|
+
return [
|
|
1097
|
+
componentBody?.identifier,
|
|
1098
|
+
componentBody?.name,
|
|
1099
|
+
componentBody?.modelId
|
|
1100
|
+
]
|
|
1101
|
+
.map((value) => String(value || ''))
|
|
1102
|
+
.join(' ')
|
|
1103
|
+
}
|
|
1104
|
+
|
|
828
1105
|
/**
|
|
829
1106
|
* Pairs one unresolved repeated body group with a repeated component group
|
|
830
1107
|
* by preserving the dominant ordering axis and choosing the pairing that
|
|
@@ -1541,6 +1818,21 @@ export class PcbScene3dBuilder {
|
|
|
1541
1818
|
)
|
|
1542
1819
|
}
|
|
1543
1820
|
|
|
1821
|
+
/**
|
|
1822
|
+
* Returns the euclidean distance between two component body anchors.
|
|
1823
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} left First body.
|
|
1824
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} right Second body.
|
|
1825
|
+
* @returns {number}
|
|
1826
|
+
*/
|
|
1827
|
+
static #distanceBetweenBodies(left, right) {
|
|
1828
|
+
return Math.hypot(
|
|
1829
|
+
Number(left?.positionMil?.x || 0) -
|
|
1830
|
+
Number(right?.positionMil?.x || 0),
|
|
1831
|
+
Number(left?.positionMil?.y || 0) -
|
|
1832
|
+
Number(right?.positionMil?.y || 0)
|
|
1833
|
+
)
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1544
1836
|
/**
|
|
1545
1837
|
* Returns true when one unresolved body anchor still lies close enough to
|
|
1546
1838
|
* the board envelope to be renderable without a component match.
|