altium-toolkit 1.1.32 → 1.1.33
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/AltiumGeneratedLibraryRecordBuilder.mjs +551 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +31 -1
- package/src/core/altium/AltiumPcbLibExporter.mjs +3 -1
- package/src/core/altium/AltiumSchLibExporter.mjs +1 -1
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +38 -1
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +21 -0
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +703 -0
- package/src/ui/PcbScene3dBuilder.mjs +135 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +56 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +326 -10
|
@@ -9,6 +9,7 @@ import { AltiumScene3dExternalPlacementAdapter } from './AltiumScene3dExternalPl
|
|
|
9
9
|
import { AltiumScene3dBottomPadRotationAdapter } from './AltiumScene3dBottomPadRotationAdapter.mjs'
|
|
10
10
|
import { AltiumScene3dComponentBodyAdapter } from './AltiumScene3dComponentBodyAdapter.mjs'
|
|
11
11
|
import { AltiumScene3dAuthoredBodyAnchorAdapter } from './AltiumScene3dAuthoredBodyAnchorAdapter.mjs'
|
|
12
|
+
import { AltiumScene3dShapeStackOwnerAdapter } from './AltiumScene3dShapeStackOwnerAdapter.mjs'
|
|
12
13
|
import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
|
|
13
14
|
import { PcbScene3dPadLocalSpanResolver } from './PcbScene3dPadLocalSpanResolver.mjs'
|
|
14
15
|
import { PcbScene3dPackages } from './PcbScene3dPackages.mjs'
|
|
@@ -31,7 +32,10 @@ export class PcbScene3dBuilder {
|
|
|
31
32
|
static #UNMATCHED_BODY_OVERHANG_RATIO = 0.25
|
|
32
33
|
static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
|
|
33
34
|
static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
|
|
35
|
+
static #OVERSIZED_GENERIC_FALLBACK_MAX_MIL = 800
|
|
34
36
|
static #TRUETYPE_TEXT_WIDTH_RATIO = 0.55
|
|
37
|
+
static #LOW_CONFIDENCE_GENERIC_FOOTPRINT_PATTERN =
|
|
38
|
+
/(?:^|[^a-z0-9])(?:edge|finger|fingers|contact|contacts|mech|mechanical|jumper|jump)(?:$|[^a-z0-9])/i
|
|
35
39
|
static #AUTHORED_BODY_IDENTITY_PATTERN =
|
|
36
40
|
/(?:^|[^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
41
|
static #COMPONENT_PACKAGE_BODY_PATTERN =
|
|
@@ -141,15 +145,17 @@ export class PcbScene3dBuilder {
|
|
|
141
145
|
boardAssemblyModel:
|
|
142
146
|
modelRegistry?.resolveBoardAssemblyModel?.(documentModel) ||
|
|
143
147
|
null,
|
|
144
|
-
components: components
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
components: components
|
|
149
|
+
.map((component) =>
|
|
150
|
+
PcbScene3dBuilder.#buildComponent(
|
|
151
|
+
component,
|
|
152
|
+
pads,
|
|
153
|
+
board,
|
|
154
|
+
thicknessMil,
|
|
155
|
+
modelRegistry
|
|
156
|
+
)
|
|
151
157
|
)
|
|
152
|
-
|
|
158
|
+
.filter(Boolean),
|
|
153
159
|
externalPlacements: componentBodies
|
|
154
160
|
.map((componentBody, index) =>
|
|
155
161
|
PcbScene3dBuilder.#buildExternalPlacement(
|
|
@@ -167,6 +173,7 @@ export class PcbScene3dBuilder {
|
|
|
167
173
|
componentBodies,
|
|
168
174
|
bodyMatches,
|
|
169
175
|
components,
|
|
176
|
+
pads,
|
|
170
177
|
board,
|
|
171
178
|
thicknessMil
|
|
172
179
|
),
|
|
@@ -191,8 +198,11 @@ export class PcbScene3dBuilder {
|
|
|
191
198
|
AltiumScene3dBottomPadRotationAdapter.apply(
|
|
192
199
|
AltiumScene3dComponentBodyAdapter.apply(
|
|
193
200
|
AltiumScene3dExternalPlacementAdapter.apply(
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
AltiumScene3dShapeStackOwnerAdapter.apply(
|
|
202
|
+
PcbScene3dBoardOutlineRefiner.refine(
|
|
203
|
+
sceneDescription,
|
|
204
|
+
sceneDocumentModel
|
|
205
|
+
),
|
|
196
206
|
sceneDocumentModel
|
|
197
207
|
),
|
|
198
208
|
sceneDocumentModel
|
|
@@ -210,7 +220,7 @@ export class PcbScene3dBuilder {
|
|
|
210
220
|
* @param {{ centerX: number, centerY: number }} board
|
|
211
221
|
* @param {number} thicknessMil
|
|
212
222
|
* @param {{ resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null } | null} modelRegistry
|
|
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 }}
|
|
223
|
+
* @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 } | null}
|
|
214
224
|
*/
|
|
215
225
|
static #buildComponent(
|
|
216
226
|
component,
|
|
@@ -222,6 +232,19 @@ export class PcbScene3dBuilder {
|
|
|
222
232
|
const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
|
|
223
233
|
const padSpan = PcbScene3dBuilder.#resolvePadSpan(component, pads)
|
|
224
234
|
const body = PcbScene3dPackages.resolve(component, padSpan)
|
|
235
|
+
const externalModel = modelRegistry
|
|
236
|
+
? modelRegistry.resolveComponentModel(component)
|
|
237
|
+
: null
|
|
238
|
+
if (
|
|
239
|
+
PcbScene3dBuilder.#shouldSuppressProceduralComponent(
|
|
240
|
+
component,
|
|
241
|
+
body,
|
|
242
|
+
externalModel
|
|
243
|
+
)
|
|
244
|
+
) {
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
|
|
225
248
|
const halfBoardThickness = thicknessMil / 2
|
|
226
249
|
const halfBodyHeight = body.sizeMil.height / 2
|
|
227
250
|
const z =
|
|
@@ -253,12 +276,56 @@ export class PcbScene3dBuilder {
|
|
|
253
276
|
? { ...component.parameters }
|
|
254
277
|
: {},
|
|
255
278
|
body,
|
|
256
|
-
externalModel
|
|
257
|
-
? modelRegistry.resolveComponentModel(component)
|
|
258
|
-
: null
|
|
279
|
+
externalModel
|
|
259
280
|
}
|
|
260
281
|
}
|
|
261
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Checks whether one generated fallback body is too uncertain to render.
|
|
285
|
+
* @param {{ pattern?: string, source?: string, description?: string }} component Source component.
|
|
286
|
+
* @param {{ family?: string, sizeMil?: { width?: number, depth?: number } }} body Procedural body.
|
|
287
|
+
* @param {object | null} externalModel Resolved external model.
|
|
288
|
+
* @returns {boolean}
|
|
289
|
+
*/
|
|
290
|
+
static #shouldSuppressProceduralComponent(component, body, externalModel) {
|
|
291
|
+
return (
|
|
292
|
+
!externalModel &&
|
|
293
|
+
body?.family === 'generic' &&
|
|
294
|
+
PcbScene3dBuilder.#isOversizedGenericFallback(body) &&
|
|
295
|
+
PcbScene3dBuilder.#isLowConfidenceGenericFootprint(component)
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Checks whether one generic fallback body spans too much of the board.
|
|
301
|
+
* @param {{ sizeMil?: { width?: number, depth?: number } }} body Procedural body.
|
|
302
|
+
* @returns {boolean}
|
|
303
|
+
*/
|
|
304
|
+
static #isOversizedGenericFallback(body) {
|
|
305
|
+
return (
|
|
306
|
+
Math.max(
|
|
307
|
+
Number(body?.sizeMil?.width || 0),
|
|
308
|
+
Number(body?.sizeMil?.depth || 0)
|
|
309
|
+
) > PcbScene3dBuilder.#OVERSIZED_GENERIC_FALLBACK_MAX_MIL
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Checks for footprint identities that describe board features or
|
|
315
|
+
* mechanical placeholders more often than physical package bodies.
|
|
316
|
+
* @param {{ pattern?: string, source?: string, description?: string }} component Source component.
|
|
317
|
+
* @returns {boolean}
|
|
318
|
+
*/
|
|
319
|
+
static #isLowConfidenceGenericFootprint(component) {
|
|
320
|
+
return PcbScene3dBuilder.#LOW_CONFIDENCE_GENERIC_FOOTPRINT_PATTERN.test(
|
|
321
|
+
[
|
|
322
|
+
component?.pattern,
|
|
323
|
+
component?.source,
|
|
324
|
+
component?.description
|
|
325
|
+
].join(' ')
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
262
329
|
/**
|
|
263
330
|
* Resolves one component-body model through the active registry.
|
|
264
331
|
* @param {{ modelId?: string, checksum?: number | null, name?: string }} componentBody Component body metadata.
|
|
@@ -352,7 +419,8 @@ export class PcbScene3dBuilder {
|
|
|
352
419
|
modelTransform: {
|
|
353
420
|
rotationDeg: modelRotation,
|
|
354
421
|
dzMil: PcbScene3dBuilder.#resolveComponentBodyVerticalOffset(
|
|
355
|
-
componentBody
|
|
422
|
+
componentBody,
|
|
423
|
+
matchedComponent
|
|
356
424
|
)
|
|
357
425
|
},
|
|
358
426
|
projection: PcbScene3dBuilder.#resolveProjectionDiagnostics(
|
|
@@ -369,16 +437,65 @@ export class PcbScene3dBuilder {
|
|
|
369
437
|
* Resolves the vertical offset that should remain after the viewer seats
|
|
370
438
|
* raw model bounds on the board face.
|
|
371
439
|
* @param {{ dzMil?: number, standoffHeightMil?: number | null }} componentBody Component-body placement metadata.
|
|
440
|
+
* @param {object | null} matchedComponent Matched owner component.
|
|
372
441
|
* @returns {number}
|
|
373
442
|
*/
|
|
374
|
-
static #resolveComponentBodyVerticalOffset(
|
|
443
|
+
static #resolveComponentBodyVerticalOffset(
|
|
444
|
+
componentBody,
|
|
445
|
+
matchedComponent = null
|
|
446
|
+
) {
|
|
375
447
|
const standoffHeightMil = Number(componentBody?.standoffHeightMil)
|
|
376
448
|
if (Number.isFinite(standoffHeightMil)) {
|
|
377
|
-
return standoffHeightMil < 0
|
|
449
|
+
return standoffHeightMil < 0 ||
|
|
450
|
+
PcbScene3dBuilder.#shouldPreservePositiveBodyStandoff(
|
|
451
|
+
componentBody,
|
|
452
|
+
matchedComponent
|
|
453
|
+
)
|
|
454
|
+
? standoffHeightMil
|
|
455
|
+
: 0
|
|
378
456
|
}
|
|
379
457
|
|
|
380
458
|
const dzMil = Number(componentBody?.dzMil)
|
|
381
|
-
return Number.isFinite(dzMil) &&
|
|
459
|
+
return Number.isFinite(dzMil) &&
|
|
460
|
+
(dzMil < 0 ||
|
|
461
|
+
PcbScene3dBuilder.#shouldPreservePositiveBodyStandoff(
|
|
462
|
+
componentBody,
|
|
463
|
+
matchedComponent
|
|
464
|
+
))
|
|
465
|
+
? dzMil
|
|
466
|
+
: 0
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Checks whether a positive shape-body standoff is part of an authored
|
|
471
|
+
* stack instead of a model-origin quirk that should be seated on the board.
|
|
472
|
+
* @param {object | null | undefined} componentBody Component-body row.
|
|
473
|
+
* @param {object | null} matchedComponent Matched owner component.
|
|
474
|
+
* @returns {boolean}
|
|
475
|
+
*/
|
|
476
|
+
static #shouldPreservePositiveBodyStandoff(
|
|
477
|
+
componentBody,
|
|
478
|
+
matchedComponent
|
|
479
|
+
) {
|
|
480
|
+
return (
|
|
481
|
+
!matchedComponent &&
|
|
482
|
+
PcbScene3dBuilder.#isShapeBasedComponentBody(componentBody)
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Checks whether one body row came from shape-based 3D body metadata.
|
|
488
|
+
* @param {object | null | undefined} componentBody Component-body row.
|
|
489
|
+
* @returns {boolean}
|
|
490
|
+
*/
|
|
491
|
+
static #isShapeBasedComponentBody(componentBody) {
|
|
492
|
+
return (
|
|
493
|
+
String(componentBody?.sourceStream || '').includes(
|
|
494
|
+
'ShapeBasedComponentBodies'
|
|
495
|
+
) ||
|
|
496
|
+
Boolean(componentBody?.staticGeometry) ||
|
|
497
|
+
Boolean(componentBody?.modelTypeName)
|
|
498
|
+
)
|
|
382
499
|
}
|
|
383
500
|
|
|
384
501
|
/**
|
|
@@ -64,6 +64,62 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
64
64
|
return standoffSide || 'top'
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Resolves which board side one authored static shape body should mount on.
|
|
69
|
+
* Shape bodies carry explicit mechanical-layer intent, so that side wins
|
|
70
|
+
* over loose nearby-package identity unless the body was directly matched.
|
|
71
|
+
* @param {{ layer?: string, positionMil?: { x?: number, y?: number }, standoffHeightMil?: number | null, overallHeightMil?: number | null }} componentBody
|
|
72
|
+
* @param {{ layer?: string } | null} matchedComponent
|
|
73
|
+
* @param {{ layer?: string, pattern?: string, source?: string, modelPath?: string, x?: number, y?: number }[]} components
|
|
74
|
+
* @param {{ minX?: number, minY?: number, widthMil?: number, heightMil?: number } | null} board
|
|
75
|
+
* @returns {'top' | 'bottom'}
|
|
76
|
+
*/
|
|
77
|
+
static resolveStaticBodyPlacementSide(
|
|
78
|
+
componentBody,
|
|
79
|
+
matchedComponent,
|
|
80
|
+
components = [],
|
|
81
|
+
board = null
|
|
82
|
+
) {
|
|
83
|
+
const matchedSide =
|
|
84
|
+
PcbScene3dPlacementSideResolver.#resolveComponentLayerSide(
|
|
85
|
+
matchedComponent?.layer
|
|
86
|
+
)
|
|
87
|
+
if (matchedSide) {
|
|
88
|
+
return matchedSide
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const standoffSide =
|
|
92
|
+
PcbScene3dPlacementSideResolver.#resolveStandoffSide(componentBody)
|
|
93
|
+
if (
|
|
94
|
+
standoffSide &&
|
|
95
|
+
PcbScene3dPlacementSideResolver.#isBodyAnchorInsideBoard(
|
|
96
|
+
componentBody,
|
|
97
|
+
board
|
|
98
|
+
)
|
|
99
|
+
) {
|
|
100
|
+
return standoffSide
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const mechanicalSide =
|
|
104
|
+
PcbScene3dPlacementSideResolver.#resolveMechanicalLayerSide(
|
|
105
|
+
componentBody?.layer
|
|
106
|
+
)
|
|
107
|
+
if (mechanicalSide) {
|
|
108
|
+
return mechanicalSide
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const nearbySide =
|
|
112
|
+
PcbScene3dPlacementSideResolver.#resolveNearbyComponentSide(
|
|
113
|
+
componentBody,
|
|
114
|
+
components
|
|
115
|
+
)
|
|
116
|
+
if (nearbySide) {
|
|
117
|
+
return nearbySide
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return standoffSide || 'top'
|
|
121
|
+
}
|
|
122
|
+
|
|
67
123
|
/**
|
|
68
124
|
* Resolves the grouping key for repeated component-body matching.
|
|
69
125
|
* @param {{ modelId?: string, name?: string, identifier?: string }} componentBody
|
|
@@ -11,12 +11,27 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
11
11
|
static #UNMATCHED_BODY_OVERHANG_RATIO = 0.25
|
|
12
12
|
static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
|
|
13
13
|
static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
|
|
14
|
+
static #OWNER_AFFINITY_DISTANCE_MIL = 600
|
|
15
|
+
static #OWNER_EXACT_DISTANCE_MIL = 5
|
|
16
|
+
static #OWNER_EXACT_MAX_SPAN_MIL = 500
|
|
17
|
+
static #GENERIC_MECHANICAL_IDENTITY_TOKENS = new Set([
|
|
18
|
+
'can',
|
|
19
|
+
'cover',
|
|
20
|
+
'enclosure',
|
|
21
|
+
'frame',
|
|
22
|
+
'hardware',
|
|
23
|
+
'leg',
|
|
24
|
+
'mechanical',
|
|
25
|
+
'plate',
|
|
26
|
+
'shield'
|
|
27
|
+
])
|
|
14
28
|
|
|
15
29
|
/**
|
|
16
30
|
* Builds static shape-body scene placements.
|
|
17
|
-
* @param {{ identifier?: string, name?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, standoffHeightMil?: number | null, overallHeightMil?: number | null, staticGeometry?: object }[]} componentBodies Component bodies.
|
|
31
|
+
* @param {{ componentIndex?: number, identifier?: string, name?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, standoffHeightMil?: number | null, overallHeightMil?: number | null, bodyOpacity?: number | string, staticGeometry?: object }[]} componentBodies Component bodies.
|
|
18
32
|
* @param {({ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null)[]} bodyMatches Matched components.
|
|
19
33
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
34
|
+
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads Pads.
|
|
20
35
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
21
36
|
* @param {number} thicknessMil Board thickness.
|
|
22
37
|
* @returns {{ designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object }[]}
|
|
@@ -25,6 +40,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
25
40
|
componentBodies,
|
|
26
41
|
bodyMatches,
|
|
27
42
|
components,
|
|
43
|
+
pads,
|
|
28
44
|
board,
|
|
29
45
|
thicknessMil
|
|
30
46
|
) {
|
|
@@ -41,9 +57,18 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
41
57
|
.filter(Boolean)
|
|
42
58
|
}
|
|
43
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Checks whether one static geometry is already complete.
|
|
62
|
+
* @param {object | undefined} geometry Static geometry.
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
static #isCompleteGeometry(geometry) {
|
|
66
|
+
return Boolean(geometry && geometry.status === 'complete')
|
|
67
|
+
}
|
|
68
|
+
|
|
44
69
|
/**
|
|
45
70
|
* Builds one static shape-body scene placement.
|
|
46
|
-
* @param {{ identifier?: string, name?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, standoffHeightMil?: number | null, overallHeightMil?: number | null, staticGeometry?: object }} componentBody Component body.
|
|
71
|
+
* @param {{ componentIndex?: number, embedded?: boolean, identifier?: string, name?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, standoffHeightMil?: number | null, overallHeightMil?: number | null, bodyOpacity?: number | string, staticGeometry?: object }} componentBody Component body.
|
|
47
72
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent Matched component.
|
|
48
73
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
49
74
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
@@ -57,9 +82,26 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
57
82
|
board,
|
|
58
83
|
thicknessMil
|
|
59
84
|
) {
|
|
60
|
-
const geometry =
|
|
85
|
+
const geometry =
|
|
86
|
+
PcbScene3dStaticBodyPlacementBuilder.#normalizeStaticGeometry(
|
|
87
|
+
componentBody,
|
|
88
|
+
componentBody?.staticGeometry
|
|
89
|
+
)
|
|
61
90
|
|
|
62
|
-
if (
|
|
91
|
+
if (
|
|
92
|
+
!PcbScene3dStaticBodyPlacementBuilder.#isCompleteGeometry(geometry)
|
|
93
|
+
) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
!matchedComponent &&
|
|
99
|
+
PcbScene3dStaticBodyPlacementBuilder.#shouldSuppressStaticBody(
|
|
100
|
+
componentBody,
|
|
101
|
+
geometry,
|
|
102
|
+
components
|
|
103
|
+
)
|
|
104
|
+
) {
|
|
63
105
|
return null
|
|
64
106
|
}
|
|
65
107
|
|
|
@@ -73,12 +115,13 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
73
115
|
return null
|
|
74
116
|
}
|
|
75
117
|
|
|
76
|
-
const mountSide =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
118
|
+
const mountSide =
|
|
119
|
+
PcbScene3dPlacementSideResolver.resolveStaticBodyPlacementSide(
|
|
120
|
+
componentBody,
|
|
121
|
+
matchedComponent,
|
|
122
|
+
components,
|
|
123
|
+
board
|
|
124
|
+
)
|
|
82
125
|
const sourcePosition =
|
|
83
126
|
PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
|
|
84
127
|
const heightMil =
|
|
@@ -119,10 +162,283 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
119
162
|
x: Number(componentBody.positionMil?.x || 0),
|
|
120
163
|
y: Number(componentBody.positionMil?.y || 0)
|
|
121
164
|
},
|
|
165
|
+
...PcbScene3dStaticBodyPlacementBuilder.#displayMetadata(
|
|
166
|
+
componentBody
|
|
167
|
+
),
|
|
122
168
|
geometry
|
|
123
169
|
}
|
|
124
170
|
}
|
|
125
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Converts source-space polygon vertices into render-local geometry.
|
|
174
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
175
|
+
* @param {object | undefined} geometry Static geometry.
|
|
176
|
+
* @returns {object | undefined}
|
|
177
|
+
*/
|
|
178
|
+
static #normalizeStaticGeometry(componentBody, geometry) {
|
|
179
|
+
if (
|
|
180
|
+
String(geometry?.kind || '').toLowerCase() !== 'extruded-polygon' ||
|
|
181
|
+
!Array.isArray(geometry?.verticesMil) ||
|
|
182
|
+
geometry.verticesMil.length < 3 ||
|
|
183
|
+
!PcbScene3dStaticBodyPlacementBuilder.#usesSourceCoordinateFrame(
|
|
184
|
+
componentBody,
|
|
185
|
+
geometry.verticesMil
|
|
186
|
+
)
|
|
187
|
+
) {
|
|
188
|
+
return geometry
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const center =
|
|
192
|
+
PcbScene3dStaticBodyPlacementBuilder.#polygonBoundsCenter(
|
|
193
|
+
geometry.verticesMil
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
...geometry,
|
|
198
|
+
verticesMil: geometry.verticesMil.map((vertex) => ({
|
|
199
|
+
x: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
200
|
+
Number(vertex?.x || 0) - center.x
|
|
201
|
+
),
|
|
202
|
+
y: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
203
|
+
Number(vertex?.y || 0) - center.y
|
|
204
|
+
)
|
|
205
|
+
}))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Checks whether polygon vertices use board/source coordinates instead of
|
|
211
|
+
* small body-local coordinates.
|
|
212
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
213
|
+
* @param {{ x?: number, y?: number }[]} vertices Vertices.
|
|
214
|
+
* @returns {boolean}
|
|
215
|
+
*/
|
|
216
|
+
static #usesSourceCoordinateFrame(componentBody, vertices) {
|
|
217
|
+
const center =
|
|
218
|
+
PcbScene3dStaticBodyPlacementBuilder.#polygonBoundsCenter(vertices)
|
|
219
|
+
const source =
|
|
220
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
Math.max(
|
|
224
|
+
Math.abs(center.x),
|
|
225
|
+
Math.abs(center.y),
|
|
226
|
+
Math.abs(source.x),
|
|
227
|
+
Math.abs(source.y)
|
|
228
|
+
) > 1000
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Resolves the axis-aligned polygon bounds center.
|
|
234
|
+
* @param {{ x?: number, y?: number }[]} vertices Vertices.
|
|
235
|
+
* @returns {{ x: number, y: number }}
|
|
236
|
+
*/
|
|
237
|
+
static #polygonBoundsCenter(vertices) {
|
|
238
|
+
const points = (Array.isArray(vertices) ? vertices : []).map(
|
|
239
|
+
(vertex) => ({
|
|
240
|
+
x: Number(vertex?.x || 0),
|
|
241
|
+
y: Number(vertex?.y || 0)
|
|
242
|
+
})
|
|
243
|
+
)
|
|
244
|
+
const xs = points.map((point) => point.x)
|
|
245
|
+
const ys = points.map((point) => point.y)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
x: (Math.min(...xs) + Math.max(...xs)) / 2,
|
|
249
|
+
y: (Math.min(...ys) + Math.max(...ys)) / 2
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Resolves optional display metadata for static body rendering.
|
|
255
|
+
* @param {{ bodyColor?: object, bodyOpacity?: number | string }} componentBody Component body.
|
|
256
|
+
* @returns {{ bodyColor?: object, bodyOpacity?: number }}
|
|
257
|
+
*/
|
|
258
|
+
static #displayMetadata(componentBody) {
|
|
259
|
+
const metadata = {}
|
|
260
|
+
if (
|
|
261
|
+
componentBody?.bodyColor &&
|
|
262
|
+
typeof componentBody.bodyColor === 'object'
|
|
263
|
+
) {
|
|
264
|
+
metadata.bodyColor = componentBody.bodyColor
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const opacity = Number(componentBody?.bodyOpacity)
|
|
268
|
+
if (Number.isFinite(opacity)) {
|
|
269
|
+
metadata.bodyOpacity = opacity
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return metadata
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Checks whether an unowned static body is likely a translucent mechanical
|
|
277
|
+
* overlay instead of a component package.
|
|
278
|
+
* @param {{ componentIndex?: number, embedded?: boolean, bodyOpacity?: number | string, identifier?: string, name?: string, positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
279
|
+
* @param {object} geometry Static geometry.
|
|
280
|
+
* @param {{ componentIndex?: number, x?: number, y?: number, pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object }[]} components Components.
|
|
281
|
+
* @returns {boolean}
|
|
282
|
+
*/
|
|
283
|
+
static #shouldSuppressStaticBody(componentBody, geometry, components) {
|
|
284
|
+
return (
|
|
285
|
+
PcbScene3dStaticBodyPlacementBuilder.#isTranslucentBody(
|
|
286
|
+
componentBody
|
|
287
|
+
) &&
|
|
288
|
+
!PcbScene3dStaticBodyPlacementBuilder.#hasLikelyComponentOwner(
|
|
289
|
+
componentBody,
|
|
290
|
+
geometry,
|
|
291
|
+
components
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Checks whether a body was authored with visible partial transparency.
|
|
298
|
+
* @param {{ bodyOpacity?: number | string }} componentBody Component body.
|
|
299
|
+
* @returns {boolean}
|
|
300
|
+
*/
|
|
301
|
+
static #isTranslucentBody(componentBody) {
|
|
302
|
+
const opacity = Number(componentBody?.bodyOpacity)
|
|
303
|
+
|
|
304
|
+
return Number.isFinite(opacity) && opacity >= 0 && opacity < 1
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Checks whether an otherwise unmatched body still has a plausible owning
|
|
309
|
+
* component.
|
|
310
|
+
* @param {{ componentIndex?: number, embedded?: boolean, identifier?: string, name?: string, positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
311
|
+
* @param {object} geometry Static geometry.
|
|
312
|
+
* @param {{ componentIndex?: number, x?: number, y?: number, pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object }[]} components Components.
|
|
313
|
+
* @returns {boolean}
|
|
314
|
+
*/
|
|
315
|
+
static #hasLikelyComponentOwner(componentBody, geometry, components) {
|
|
316
|
+
if (componentBody?.embedded) {
|
|
317
|
+
return true
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const componentIndex = Number(componentBody?.componentIndex)
|
|
321
|
+
if (
|
|
322
|
+
Number.isInteger(componentIndex) &&
|
|
323
|
+
(Array.isArray(components) ? components : []).some(
|
|
324
|
+
(component) =>
|
|
325
|
+
Number(component?.componentIndex) === componentIndex
|
|
326
|
+
)
|
|
327
|
+
) {
|
|
328
|
+
return true
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const maxSpanMil =
|
|
332
|
+
PcbScene3dStaticBodyPlacementBuilder.#geometryMaxSpan(geometry)
|
|
333
|
+
const genericMechanical =
|
|
334
|
+
PcbScene3dStaticBodyPlacementBuilder.#hasGenericMechanicalIdentity(
|
|
335
|
+
componentBody
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return (Array.isArray(components) ? components : []).some(
|
|
339
|
+
(component) => {
|
|
340
|
+
const distance =
|
|
341
|
+
PcbScene3dStaticBodyPlacementBuilder.#distanceBetweenBodyAndComponent(
|
|
342
|
+
componentBody,
|
|
343
|
+
component
|
|
344
|
+
)
|
|
345
|
+
const affinityScore =
|
|
346
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
347
|
+
componentBody,
|
|
348
|
+
component
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
(!genericMechanical &&
|
|
353
|
+
affinityScore > 0 &&
|
|
354
|
+
distance <=
|
|
355
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
356
|
+
.#OWNER_AFFINITY_DISTANCE_MIL) ||
|
|
357
|
+
(!genericMechanical &&
|
|
358
|
+
distance <=
|
|
359
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
360
|
+
.#OWNER_EXACT_DISTANCE_MIL &&
|
|
361
|
+
maxSpanMil <=
|
|
362
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
363
|
+
.#OWNER_EXACT_MAX_SPAN_MIL)
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Checks for generic mechanical labels that are too weak to prove package
|
|
371
|
+
* ownership for translucent static bodies.
|
|
372
|
+
* @param {{ identifier?: string, name?: string }} componentBody Component body.
|
|
373
|
+
* @returns {boolean}
|
|
374
|
+
*/
|
|
375
|
+
static #hasGenericMechanicalIdentity(componentBody) {
|
|
376
|
+
return PcbScene3dStaticBodyPlacementBuilder.#identityTokens(
|
|
377
|
+
componentBody
|
|
378
|
+
).some((token) =>
|
|
379
|
+
PcbScene3dStaticBodyPlacementBuilder.#GENERIC_MECHANICAL_IDENTITY_TOKENS.has(
|
|
380
|
+
token
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Collects normalized identity tokens from one body row.
|
|
387
|
+
* @param {{ identifier?: string, name?: string }} componentBody Component body.
|
|
388
|
+
* @returns {string[]}
|
|
389
|
+
*/
|
|
390
|
+
static #identityTokens(componentBody) {
|
|
391
|
+
return [componentBody?.identifier, componentBody?.name]
|
|
392
|
+
.join(' ')
|
|
393
|
+
.toLowerCase()
|
|
394
|
+
.split(/[^a-z0-9]+/g)
|
|
395
|
+
.flatMap((fragment) => fragment.match(/[a-z]+|\d+/g) || [])
|
|
396
|
+
.filter(Boolean)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Resolves the largest horizontal span of a static body.
|
|
401
|
+
* @param {object} geometry Static geometry.
|
|
402
|
+
* @returns {number}
|
|
403
|
+
*/
|
|
404
|
+
static #geometryMaxSpan(geometry) {
|
|
405
|
+
if (
|
|
406
|
+
Array.isArray(geometry?.verticesMil) &&
|
|
407
|
+
geometry.verticesMil.length
|
|
408
|
+
) {
|
|
409
|
+
const points = geometry.verticesMil.map((vertex) => ({
|
|
410
|
+
x: Number(vertex?.x || 0),
|
|
411
|
+
y: Number(vertex?.y || 0)
|
|
412
|
+
}))
|
|
413
|
+
const xs = points.map((point) => point.x)
|
|
414
|
+
const ys = points.map((point) => point.y)
|
|
415
|
+
|
|
416
|
+
return Math.max(
|
|
417
|
+
Math.max(...xs) - Math.min(...xs),
|
|
418
|
+
Math.max(...ys) - Math.min(...ys)
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const radius = Number(geometry?.radiusMil)
|
|
423
|
+
|
|
424
|
+
return Number.isFinite(radius) && radius > 0 ? radius * 2 : 0
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Returns the euclidean distance between body and component anchors.
|
|
429
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
430
|
+
* @param {{ x?: number, y?: number }} component Component.
|
|
431
|
+
* @returns {number}
|
|
432
|
+
*/
|
|
433
|
+
static #distanceBetweenBodyAndComponent(componentBody, component) {
|
|
434
|
+
return Math.hypot(
|
|
435
|
+
Number(component?.x || 0) -
|
|
436
|
+
Number(componentBody?.positionMil?.x || 0),
|
|
437
|
+
Number(component?.y || 0) -
|
|
438
|
+
Number(componentBody?.positionMil?.y || 0)
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
126
442
|
/**
|
|
127
443
|
* Returns the native body anchor.
|
|
128
444
|
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|