altium-toolkit 1.1.31 → 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/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +54 -6
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +385 -22
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +568 -4
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +149 -23
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +703 -0
- package/src/ui/PcbScene3dBuilder.mjs +431 -22
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +90 -2
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +326 -10
|
@@ -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
|
|
@@ -81,7 +137,7 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
81
137
|
* Scores how strongly one component record appears to belong to one body
|
|
82
138
|
* record based on shared model/footprint tokens.
|
|
83
139
|
* @param {{ name?: string, identifier?: string }} componentBody
|
|
84
|
-
* @param {{ pattern?: string, source?: string, modelPath?: string }} component
|
|
140
|
+
* @param {{ pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object }} component
|
|
85
141
|
* @returns {number}
|
|
86
142
|
*/
|
|
87
143
|
static scoreBodyComponentAffinity(componentBody, component) {
|
|
@@ -94,7 +150,11 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
94
150
|
PcbScene3dPlacementSideResolver.#collectMeaningfulTokens([
|
|
95
151
|
component?.pattern,
|
|
96
152
|
component?.source,
|
|
97
|
-
component?.modelPath
|
|
153
|
+
component?.modelPath,
|
|
154
|
+
component?.description,
|
|
155
|
+
...PcbScene3dPlacementSideResolver.#componentPackageMetadata(
|
|
156
|
+
component
|
|
157
|
+
)
|
|
98
158
|
])
|
|
99
159
|
let score = 0
|
|
100
160
|
|
|
@@ -107,6 +167,34 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
107
167
|
return score
|
|
108
168
|
}
|
|
109
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Resolves package-related component metadata that can identify generic
|
|
172
|
+
* embedded 3D bodies when pattern/source only name the electrical part.
|
|
173
|
+
* @param {{ parameters?: object, provenance?: object } | null | undefined} component Component record.
|
|
174
|
+
* @returns {string[]}
|
|
175
|
+
*/
|
|
176
|
+
static #componentPackageMetadata(component) {
|
|
177
|
+
const parameters =
|
|
178
|
+
component?.parameters && typeof component.parameters === 'object'
|
|
179
|
+
? component.parameters
|
|
180
|
+
: {}
|
|
181
|
+
const provenance =
|
|
182
|
+
component?.provenance && typeof component.provenance === 'object'
|
|
183
|
+
? component.provenance
|
|
184
|
+
: {}
|
|
185
|
+
|
|
186
|
+
return [
|
|
187
|
+
parameters['Package / Case'],
|
|
188
|
+
parameters['Supplier Device Package'],
|
|
189
|
+
parameters['Part Description'],
|
|
190
|
+
parameters.Package,
|
|
191
|
+
provenance.footprintDescription,
|
|
192
|
+
provenance.sourceLibReference,
|
|
193
|
+
provenance.sourceFootprintLibrary,
|
|
194
|
+
provenance.sourceFootprintLibraryName
|
|
195
|
+
].map((value) => String(value || ''))
|
|
196
|
+
}
|
|
197
|
+
|
|
110
198
|
/**
|
|
111
199
|
* Resolves side from the normalized component layer.
|
|
112
200
|
* @param {string | undefined} layer
|
|
@@ -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.
|