altium-toolkit 1.1.35 → 1.1.37

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.
Files changed (35) hide show
  1. package/AGENTS.md +17 -2
  2. package/package.json +1 -1
  3. package/src/core/altium/PrintableTextDecoder.mjs +20 -1
  4. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +288 -7
  5. package/src/core/altium/SchematicTextParser.mjs +18 -253
  6. package/src/core/altium/SchematicTitleBlockParser.mjs +410 -0
  7. package/src/core/altium/SelectedPartAltiumExportAdapter.mjs +215 -0
  8. package/src/parser.mjs +1 -0
  9. package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +245 -12
  10. package/src/ui/AltiumScene3dAuthoredConnectorYawPolicy.mjs +176 -0
  11. package/src/ui/AltiumScene3dBottomSourceHalfTurnPolicy.mjs +93 -0
  12. package/src/ui/AltiumScene3dDisplayModuleYawPolicy.mjs +284 -0
  13. package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +521 -23
  14. package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +766 -19
  15. package/src/ui/AltiumScene3dQfnFootprintDetector.mjs +307 -0
  16. package/src/ui/AltiumScene3dRepeatedFullFootprintBodyCollapse.mjs +518 -0
  17. package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +121 -17
  18. package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +286 -135
  19. package/src/ui/AltiumScene3dShapeStackOwnerConflictPolicy.mjs +299 -0
  20. package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +22 -0
  21. package/src/ui/PcbScene3dBuilder.mjs +613 -117
  22. package/src/ui/PcbScene3dModelRegistry.mjs +222 -10
  23. package/src/ui/PcbScene3dPackages.mjs +16 -1
  24. package/src/ui/PcbScene3dPlacementSideResolver.mjs +262 -22
  25. package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +984 -0
  26. package/src/ui/PcbScene3dStaticBodyPadOwnerPromotion.mjs +378 -0
  27. package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +169 -23
  28. package/src/ui/PcbScene3dStaticBodyPrototypeRecovery.mjs +445 -0
  29. package/src/ui/PcbScene3dStaticBodyRecovery.mjs +7 -1
  30. package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +199 -2
  31. package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +439 -0
  32. package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +23 -2
  33. package/src/ui/SchematicColorResolver.mjs +44 -0
  34. package/src/ui/SchematicLineColorResolver.mjs +1 -1
  35. package/src/ui/SchematicShapeRenderer.mjs +13 -13
@@ -1,4 +1,5 @@
1
1
  import { PcbScene3dPackageDimensionResolver } from './PcbScene3dPackageDimensionResolver.mjs'
2
+ import { AltiumScene3dRepeatedFullFootprintBodyCollapse } from './AltiumScene3dRepeatedFullFootprintBodyCollapse.mjs'
2
3
 
3
4
  const CONNECTOR_TOKENS = new Set([
4
5
  'antenna',
@@ -12,6 +13,8 @@ const PASSIVE_BODY_PATTERN =
12
13
  /(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|lqw|lqg)(?:$|[^a-z0-9])/i
13
14
  const COMPACT_IC_BODY_PATTERN =
14
15
  /(?:^|[^a-z0-9])(?:u?qfn(?:[-_ ]?\d+)?|dfn(?:[-_ ]?\d+)?|qfp(?:[-_ ]?\d+)?|bga(?:[-_ ]?\d+)?|lga(?:[-_ ]?\d+)?)(?:$|[^a-z0-9])/i
16
+ const MODEL_BOUNDS_CORNER_ORIGIN_PACKAGE_PATTERN =
17
+ /(?:^|[^a-z0-9])(?:[a-z0-9]*qfn[a-z0-9]*|[a-z0-9]*dfn[a-z0-9]*|qfp|lqfp|tqfp|bga|lga)(?:[-_ ]?\d+)?(?:$|[^a-z0-9])/i
15
18
  const TIMING_PACKAGE_PATTERN =
16
19
  /(?:^|[^a-z0-9])(?:clock|crystal|osc|oscillator|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
17
20
  const TIMING_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
@@ -52,6 +55,9 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
52
55
  const components = Array.isArray(documentModel?.pcb?.components)
53
56
  ? documentModel.pcb.components
54
57
  : []
58
+ const pads = Array.isArray(documentModel?.pcb?.pads)
59
+ ? documentModel.pcb.pads
60
+ : []
55
61
  if (!components.length) {
56
62
  return sceneDescription
57
63
  }
@@ -62,15 +68,18 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
62
68
  component
63
69
  ])
64
70
  )
65
- const placements = sceneDescription.externalPlacements.map(
66
- (placement) =>
67
- AltiumScene3dRepeatedModelOwnerRepair.#withSingleOwnerCenter(
68
- placement,
69
- componentByDesignator.get(
70
- String(placement?.designator || '')
71
- ),
72
- sceneDescription.board
73
- )
71
+ const collapsedPlacements =
72
+ AltiumScene3dRepeatedFullFootprintBodyCollapse.apply(
73
+ sceneDescription.externalPlacements,
74
+ componentByDesignator,
75
+ pads
76
+ )
77
+ const placements = collapsedPlacements.map((placement) =>
78
+ AltiumScene3dRepeatedModelOwnerRepair.#withSingleOwnerCenter(
79
+ placement,
80
+ componentByDesignator.get(String(placement?.designator || '')),
81
+ sceneDescription.board
82
+ )
74
83
  )
75
84
 
76
85
  for (const group of AltiumScene3dRepeatedModelOwnerRepair.#groups(
@@ -178,9 +187,13 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
178
187
  * @returns {object}
179
188
  */
180
189
  static #withSingleOwnerCenter(placement, component, board) {
190
+ const projectionSource = String(
191
+ placement?.projection?.source || ''
192
+ ).toLowerCase()
181
193
  if (
182
194
  !component ||
183
- String(placement?.projection?.source || '') !== 'pad-fallback' ||
195
+ (projectionSource !== 'pad-fallback' &&
196
+ projectionSource !== 'model-bounds') ||
184
197
  AltiumScene3dRepeatedModelOwnerRepair.#isAuthoredAnchorPlacement(
185
198
  placement,
186
199
  component
@@ -204,13 +217,58 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
204
217
  ) {
205
218
  return placement
206
219
  }
220
+ if (
221
+ projectionSource === 'model-bounds' &&
222
+ !AltiumScene3dRepeatedModelOwnerRepair.#hasModelBoundsCornerSourceOriginOffset(
223
+ placement,
224
+ component,
225
+ offset
226
+ )
227
+ ) {
228
+ return placement
229
+ }
207
230
 
208
231
  return AltiumScene3dRepeatedModelOwnerRepair.#withOwner(
209
232
  placement,
210
233
  component,
211
234
  board,
212
235
  offset,
213
- { preserveRotation: true }
236
+ {
237
+ preserveRotation: true,
238
+ carryRenderableSourceOffset: projectionSource !== 'model-bounds'
239
+ }
240
+ )
241
+ }
242
+
243
+ /**
244
+ * Checks whether a model-bounds IC body uses a package-corner source
245
+ * origin rather than an authored mechanical anchor.
246
+ * @param {object} placement Scene placement.
247
+ * @param {object} component PCB component.
248
+ * @param {{ x: number, y: number }} offset Body anchor offset from owner.
249
+ * @returns {boolean}
250
+ */
251
+ static #hasModelBoundsCornerSourceOriginOffset(
252
+ placement,
253
+ component,
254
+ offset
255
+ ) {
256
+ const identityText =
257
+ AltiumScene3dRepeatedModelOwnerRepair.#identityTextForPlacement(
258
+ placement,
259
+ component
260
+ )
261
+ if (!MODEL_BOUNDS_CORNER_ORIGIN_PACKAGE_PATTERN.test(identityText)) {
262
+ return false
263
+ }
264
+
265
+ const offsetWidth = Math.abs(Number(offset?.x || 0)) * 2
266
+ const offsetDepth = Math.abs(Number(offset?.y || 0)) * 2
267
+
268
+ return AltiumScene3dRepeatedModelOwnerRepair.#matchesCornerOriginBounds(
269
+ offsetWidth,
270
+ offsetDepth,
271
+ placement?.projection?.boundsMil
214
272
  )
215
273
  }
216
274
 
@@ -387,6 +445,35 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
387
445
  )
388
446
  }
389
447
 
448
+ /**
449
+ * Checks whether doubled owner offsets match any planar pair from measured
450
+ * model bounds. Tilted STEP bodies often expose package height on `depth`
451
+ * and package width/depth on `width` plus `height`.
452
+ * @param {number} offsetWidth Doubled X offset.
453
+ * @param {number} offsetDepth Doubled Y offset.
454
+ * @param {{ width?: number, depth?: number, height?: number } | null | undefined} bounds Model bounds.
455
+ * @returns {boolean}
456
+ */
457
+ static #matchesCornerOriginBounds(offsetWidth, offsetDepth, bounds) {
458
+ const dimensions = [
459
+ Number(bounds?.width),
460
+ Number(bounds?.depth),
461
+ Number(bounds?.height)
462
+ ].filter((dimension) => Number.isFinite(dimension) && dimension > 0)
463
+
464
+ return dimensions.some((width, widthIndex) =>
465
+ dimensions.some(
466
+ (depth, depthIndex) =>
467
+ widthIndex !== depthIndex &&
468
+ AltiumScene3dRepeatedModelOwnerRepair.#matchesCornerOriginSize(
469
+ offsetWidth,
470
+ offsetDepth,
471
+ { width, depth }
472
+ )
473
+ )
474
+ )
475
+ }
476
+
390
477
  /**
391
478
  * Checks whether two dimensions are close enough for source-origin repair.
392
479
  * @param {number} actual Actual dimension.
@@ -656,7 +743,7 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
656
743
  * @param {object} component Resolved owner.
657
744
  * @param {object} board Scene board metadata.
658
745
  * @param {{ x: number, y: number }} offset Source-origin offset.
659
- * @param {{ preserveRotation?: boolean }} [options] Repair options.
746
+ * @param {{ preserveRotation?: boolean, carryRenderableSourceOffset?: boolean }} [options] Repair options.
660
747
  * @returns {object}
661
748
  */
662
749
  static #withOwner(placement, component, board, offset, options = {}) {
@@ -671,11 +758,15 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
671
758
  component?.rotation
672
759
  )
673
760
  const renderableOffset =
674
- AltiumScene3dRepeatedModelOwnerRepair.#renderableOwnerAnchorOffset(
675
- { mountSide, rotationDeg },
676
- offset,
677
- placement?.modelTransform
678
- )
761
+ options?.carryRenderableSourceOffset === false
762
+ ? AltiumScene3dRepeatedModelOwnerRepair.#renderableZOffset(
763
+ placement?.modelTransform
764
+ )
765
+ : AltiumScene3dRepeatedModelOwnerRepair.#renderableOwnerAnchorOffset(
766
+ { mountSide, rotationDeg },
767
+ offset,
768
+ placement?.modelTransform
769
+ )
679
770
 
680
771
  return {
681
772
  ...placement,
@@ -702,6 +793,19 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
702
793
  }
703
794
  }
704
795
 
796
+ /**
797
+ * Preserves authored model Z offset while suppressing lateral render shift.
798
+ * @param {object | undefined} modelTransform Existing model transform.
799
+ * @returns {{ x: number, y: number, z: number }}
800
+ */
801
+ static #renderableZOffset(modelTransform) {
802
+ const z = Number(
803
+ modelTransform?.offsetMil?.z ?? modelTransform?.dzMil ?? 0
804
+ )
805
+
806
+ return { x: 0, y: 0, z: Number.isFinite(z) ? z : 0 }
807
+ }
808
+
705
809
  /**
706
810
  * Converts a board-space source-origin offset into mount-rig local XY.
707
811
  * @param {{ mountSide?: string, rotationDeg?: number }} placement Resolved placement fields.