altium-toolkit 1.1.30 → 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.
@@ -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.#withPassiveOwnerCenter(
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 generic passive body on its resolved owner when the body anchor
160
- * carries a moderate source-origin offset.
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 #withPassiveOwnerCenter(placement, component, board) {
173
+ static #withSingleOwnerCenter(placement, component, board) {
167
174
  if (
168
175
  !component ||
169
176
  String(placement?.projection?.source || '') !== 'pad-fallback' ||
170
- !AltiumScene3dRepeatedModelOwnerRepair.#isPassivePlacement(
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.#MIN_OWNER_DISTANCE_MIL
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 a generic passive.
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 #isPassivePlacement(placement, component) {
208
- return PASSIVE_BODY_PATTERN.test(
209
- [
210
- placement?.designator,
211
- placement?.externalModel?.name,
212
- component?.pattern,
213
- component?.source,
214
- component?.description
215
- ]
216
- .map((value) => String(value || ''))
217
- .join(' ')
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: AltiumScene3dRepeatedModelOwnerRepair.#normalizeAngle(
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.