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.
@@ -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 = componentBody?.staticGeometry
85
+ const geometry =
86
+ PcbScene3dStaticBodyPlacementBuilder.#normalizeStaticGeometry(
87
+ componentBody,
88
+ componentBody?.staticGeometry
89
+ )
61
90
 
62
- if (!geometry || geometry.status !== 'complete') {
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 = PcbScene3dPlacementSideResolver.resolvePlacementSide(
77
- componentBody,
78
- matchedComponent,
79
- components,
80
- board
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.