altium-toolkit 1.4.15 → 1.4.16

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/README.md CHANGED
@@ -57,6 +57,10 @@ extension graphs retain their proven ownership instead of repeating defensive
57
57
  binary classification. See the
58
58
  [1.4.2 release notes](docs/release-notes-v1.4.2.md).
59
59
 
60
+ Version 1.4.16 preserves validated authored STEP anchors during late geometric
61
+ owner recovery and keeps board-space component yaw unchanged. See the
62
+ [1.4.16 release notes](docs/release-notes-v1.4.16.md).
63
+
60
64
  Default `extensions: 'canonical'` keeps compact Altium summary metadata.
61
65
  Request the complete native read model with `extensions: 'full'`,
62
66
  `preserveRaw: true`, or `extensions: ['altium.native-model']`. Project batches
@@ -224,6 +228,7 @@ const legacyCircuitJson = AltiumParser.parseArrayBuffer(file.name, arrayBuffer)
224
228
  - [API](docs/api.md)
225
229
  - [Capabilities](docs/capabilities.md)
226
230
  - [Migration from 1.1.41](docs/migration.md)
231
+ - [1.4.16 release notes](docs/release-notes-v1.4.16.md)
227
232
  - [1.4.2 release notes](docs/release-notes-v1.4.2.md)
228
233
  - [1.4.1 release notes](docs/release-notes-v1.4.1.md)
229
234
  - [1.4.0 release notes](docs/release-notes-v1.4.0.md)
@@ -0,0 +1,27 @@
1
+ # altium-toolkit 1.4.16
2
+
3
+ Version 1.4.16 preserves authored STEP anchors during late geometric owner
4
+ recovery and leaves model-local orientation normalization to the 3D runtime.
5
+
6
+ ## 3D placement fidelity
7
+
8
+ - Near-centroid surface-mount bodies retain their authored positions when the
9
+ recovered owner is geometrically unambiguous.
10
+ - Height-backed owner recovery marks valid source anchors explicitly instead
11
+ of converting them into component-center offsets.
12
+ - Geometric owner recovery preserves the component's authored board-space yaw;
13
+ it no longer infers package-specific half-turns from labels or pad topology.
14
+
15
+ ## Compatibility
16
+
17
+ - Placement recovery remains based on generic body, pad, and footprint
18
+ geometry without project names, filenames, designators, or library strings.
19
+ - Existing schematic colors, canvas borders, and public renderer signatures
20
+ remain unchanged.
21
+
22
+ ## Verification
23
+
24
+ - Repository-owned synthetic tests cover near-centroid connectors,
25
+ height-backed authored anchors, and preserved switch yaw.
26
+ - The complete package suite, formatting check, and npm package dry run are
27
+ required for release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altium-toolkit",
3
- "version": "1.4.15",
3
+ "version": "1.4.16",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -18,23 +18,23 @@ vendor names, or library identifiers.
18
18
  span.
19
19
  - Use component height agreement only as supporting evidence for nearby
20
20
  package ownership; height alone is insufficient.
21
- - Preserve a pad-centroid body position because it is already a valid authored
22
- package center.
21
+ - Preserve pad-centroid and nearby height-backed body positions because they
22
+ are valid authored source anchors, and mark height-backed anchors so the
23
+ runtime does not apply a second source-origin adjustment.
23
24
  - Center proven corner-origin and row-origin bodies on the component owner and
24
25
  retain the source offset as transform diagnostics.
25
26
  - Recompute component side and authored vertical standoff after late owner
26
27
  recovery.
27
- - Correct the half-turn of a four-pad tactile-switch model when its exact owner,
28
- footprint topology, and tilted source frame jointly establish that package
29
- class.
28
+ - Preserve authored component yaw; model-local axis normalization belongs to
29
+ the runtime that has access to the imported mesh envelope.
30
30
  - Decline ambiguous matches.
31
31
 
32
32
  ## Verification
33
33
 
34
- - Focused synthetic tests use obfuscated package identities and cover pad
35
- centroid, corner origin, row origin, late standoff, ambiguity rejection, and
36
- tactile-switch yaw.
34
+ - Focused synthetic tests use obfuscated package identities and cover exact and
35
+ near pad centroids, corner origin, row origin, authored source anchors, late
36
+ standoff, ambiguity rejection, and preserved component yaw.
37
37
  - The full toolkit test suite and formatting check pass.
38
38
  - A read-only probe of the supplied PCB must show real component designators
39
- for the affected placements, owner-centered corner/row models, recovered
40
- top-side standoff, and corrected switch yaw.
39
+ for the affected placements, owner-centered corner/row models, preserved
40
+ authored anchors, recovered top-side standoff, and source-authored yaw.
@@ -13,6 +13,7 @@ export class AltiumScene3dGeometricOwnerRecovery {
13
13
  static #COMPONENT_INDEX_CELL_MIL = 500
14
14
  static #MINIMUM_SCORE = 12
15
15
  static #MINIMUM_SCORE_MARGIN = 4
16
+ static #PAD_CENTROID_TOLERANCE_MIL = 12
16
17
 
17
18
  /**
18
19
  * Applies geometry-backed owner recovery to final external placements.
@@ -108,14 +109,7 @@ export class AltiumScene3dGeometricOwnerRecovery {
108
109
  const currentOwner = context.componentsByDesignator.get(
109
110
  String(placement?.designator || '')
110
111
  )
111
- if (currentOwner) {
112
- return AltiumScene3dGeometricOwnerRecovery.#correctTactileYaw(
113
- placement,
114
- currentOwner,
115
- body,
116
- context.geometryByComponent.get(currentOwner)
117
- )
118
- }
112
+ if (currentOwner) return placement
119
113
 
120
114
  const match =
121
115
  AltiumScene3dGeometricOwnerRecovery.#resolveGeometricOwner(
@@ -125,18 +119,12 @@ export class AltiumScene3dGeometricOwnerRecovery {
125
119
  )
126
120
  if (!match) return placement
127
121
 
128
- const recovered = AltiumScene3dGeometricOwnerRecovery.#withOwner(
122
+ return AltiumScene3dGeometricOwnerRecovery.#withOwner(
129
123
  placement,
130
124
  body,
131
125
  match,
132
126
  context.board
133
127
  )
134
- return AltiumScene3dGeometricOwnerRecovery.#correctTactileYaw(
135
- recovered,
136
- match.component,
137
- body,
138
- context.geometryByComponent.get(match.component)
139
- )
140
128
  }
141
129
 
142
130
  /**
@@ -329,7 +317,7 @@ export class AltiumScene3dGeometricOwnerRecovery {
329
317
 
330
318
  if (
331
319
  centroidDistance <=
332
- AltiumScene3dGeometricOwnerRecovery.#ANCHOR_TOLERANCE_MIL
320
+ AltiumScene3dGeometricOwnerRecovery.#PAD_CENTROID_TOLERANCE_MIL
333
321
  ) {
334
322
  score = 24
335
323
  mode = 'pad-centroid'
@@ -570,7 +558,9 @@ export class AltiumScene3dGeometricOwnerRecovery {
570
558
  const component = match.component
571
559
  const mountSide =
572
560
  AltiumScene3dGeometricOwnerRecovery.#componentSide(component)
573
- const preserveAnchor = match.mode === 'pad-centroid'
561
+ const preserveAnchor =
562
+ match.mode === 'pad-centroid' ||
563
+ match.mode === 'height-backed-origin'
574
564
  const offset = {
575
565
  x:
576
566
  Number(placement?.bodyPositionMil?.x || 0) -
@@ -590,6 +580,10 @@ export class AltiumScene3dGeometricOwnerRecovery {
590
580
  dzMil: verticalOffset
591
581
  }
592
582
 
583
+ if (match.mode === 'height-backed-origin') {
584
+ modelTransform.preserveSourceAnchor = true
585
+ }
586
+
593
587
  if (!preserveAnchor) {
594
588
  modelTransform.ownerAnchorOffsetMil = offset
595
589
  modelTransform.offsetMil = { x: 0, y: 0, z: verticalOffset }
@@ -622,107 +616,6 @@ export class AltiumScene3dGeometricOwnerRecovery {
622
616
  }
623
617
  }
624
618
 
625
- /**
626
- * Corrects the source-frame half-turn for a four-pad tactile switch.
627
- * @param {object} placement External placement.
628
- * @param {object} component Resolved owner.
629
- * @param {object} body Source body.
630
- * @param {object | null} geometry Precomputed owner pad geometry.
631
- * @returns {object}
632
- */
633
- static #correctTactileYaw(placement, component, body, geometry) {
634
- const identity = [
635
- component?.designator,
636
- component?.description,
637
- component?.provenance?.footprintDescription
638
- ]
639
- .map((value) => String(value || ''))
640
- .join(' ')
641
- const sourceTilt = AltiumScene3dGeometricOwnerRecovery.#normalizeAngle(
642
- body?.modelRotationDeg?.x
643
- )
644
- const currentYaw = AltiumScene3dGeometricOwnerRecovery.#normalizeAngle(
645
- placement?.rotationDeg
646
- )
647
- const componentYaw =
648
- AltiumScene3dGeometricOwnerRecovery.#normalizeAngle(
649
- component?.rotation
650
- )
651
- const isTactileSwitch =
652
- /(?:^|[^a-z0-9])(?:tact|tactile|pushbutton)(?:$|[^a-z0-9])/i.test(
653
- identity
654
- ) &&
655
- AltiumScene3dGeometricOwnerRecovery.#hasTactileContactTopology(
656
- geometry
657
- )
658
-
659
- if (
660
- !isTactileSwitch ||
661
- (sourceTilt !== 90 && sourceTilt !== 270) ||
662
- currentYaw !== componentYaw
663
- ) {
664
- return placement
665
- }
666
-
667
- return {
668
- ...placement,
669
- rotationDeg: AltiumScene3dGeometricOwnerRecovery.#normalizeAngle(
670
- currentYaw + 180
671
- )
672
- }
673
- }
674
-
675
- /**
676
- * Detects a two-by-two tactile contact layout with two duplicated routed
677
- * contact pairs aligned along one footprint axis.
678
- * @param {object | null} geometry Precomputed owner pad geometry.
679
- * @returns {boolean}
680
- */
681
- static #hasTactileContactTopology(geometry) {
682
- if (
683
- geometry?.pads?.length !== 4 ||
684
- geometry.xCount !== 2 ||
685
- geometry.yCount !== 2
686
- ) {
687
- return false
688
- }
689
-
690
- const groups = new Map()
691
- for (const pad of geometry.localPads) {
692
- const sourcePad = pad.source
693
- const netName = String(sourcePad?.netName || '').trim()
694
- const netIndex = sourcePad?.netIndex
695
- const contactKey = netName
696
- ? `name:${netName}`
697
- : netIndex !== null &&
698
- netIndex !== undefined &&
699
- netIndex !== '' &&
700
- Number.isFinite(Number(netIndex))
701
- ? `index:${Number(netIndex)}`
702
- : ''
703
- if (!contactKey) return false
704
- groups.set(contactKey, [...(groups.get(contactKey) || []), pad])
705
- }
706
- if (
707
- groups.size !== 2 ||
708
- [...groups.values()].some((group) => group.length !== 2)
709
- ) {
710
- return false
711
- }
712
-
713
- return [...groups.values()].every((group) => {
714
- const sameX =
715
- AltiumScene3dGeometricOwnerRecovery.#distinctCoordinateCount(
716
- group.map((pad) => Number(pad?.x || 0))
717
- ) === 1
718
- const sameY =
719
- AltiumScene3dGeometricOwnerRecovery.#distinctCoordinateCount(
720
- group.map((pad) => Number(pad?.y || 0))
721
- ) === 1
722
- return sameX || sameY
723
- })
724
- }
725
-
726
619
  /**
727
620
  * Resolves a body vertical offset after a late owner recovery.
728
621
  * @param {object} body Source body.