altium-toolkit 1.1.36 → 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.
- package/AGENTS.md +15 -0
- package/package.json +1 -1
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +245 -12
- package/src/ui/AltiumScene3dAuthoredConnectorYawPolicy.mjs +176 -0
- package/src/ui/AltiumScene3dBottomSourceHalfTurnPolicy.mjs +93 -0
- package/src/ui/AltiumScene3dDisplayModuleYawPolicy.mjs +284 -0
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +521 -23
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +766 -19
- package/src/ui/AltiumScene3dQfnFootprintDetector.mjs +307 -0
- package/src/ui/AltiumScene3dRepeatedFullFootprintBodyCollapse.mjs +518 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +121 -17
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +180 -158
- package/src/ui/AltiumScene3dShapeStackOwnerConflictPolicy.mjs +299 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +22 -0
- package/src/ui/PcbScene3dBuilder.mjs +613 -117
- package/src/ui/PcbScene3dModelRegistry.mjs +222 -10
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +262 -22
- package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +87 -2
- package/src/ui/PcbScene3dStaticBodyPadOwnerPromotion.mjs +378 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +32 -16
- package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +1 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { PcbScene3dStaticBodySymmetricOwnerPromotion } from './PcbScene3dStaticBodySymmetricOwnerPromotion.mjs'
|
|
6
|
+
import { PcbScene3dStaticBodyPadOwnerPromotion } from './PcbScene3dStaticBodyPadOwnerPromotion.mjs'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Promotes compact static body fragments to one clear touching owner.
|
|
@@ -11,7 +12,20 @@ export class PcbScene3dStaticBodyOwnerPromotion {
|
|
|
11
12
|
static #NEAREST_OWNER_DISTANCE_MIL = 20
|
|
12
13
|
static #NEAREST_OWNER_AMBIGUITY_MARGIN_MIL = 2
|
|
13
14
|
static #CLUSTER_CENTER_MAX_SPAN_MIL = 45
|
|
15
|
+
static #CENTERED_PACKAGE_OWNER_MAX_SPAN_MIL = 180
|
|
14
16
|
static #GENERIC_SUBPART_OWNER_MAX_SPAN_MIL = 80
|
|
17
|
+
static #AUTHORED_MECHANICAL_TOKENS = new Set([
|
|
18
|
+
'can',
|
|
19
|
+
'clip',
|
|
20
|
+
'cover',
|
|
21
|
+
'enclosure',
|
|
22
|
+
'frame',
|
|
23
|
+
'hardware',
|
|
24
|
+
'leg',
|
|
25
|
+
'mechanical',
|
|
26
|
+
'plate',
|
|
27
|
+
'shield'
|
|
28
|
+
])
|
|
15
29
|
static #GENERIC_SUBPART_TOKENS = new Set(['plastic'])
|
|
16
30
|
|
|
17
31
|
/**
|
|
@@ -19,8 +33,9 @@ export class PcbScene3dStaticBodyOwnerPromotion {
|
|
|
19
33
|
* @param {{ placement: object, matchedComponent: object | null }[]} placementRows Mutable placement rows.
|
|
20
34
|
* @param {{ designator?: string, x?: number, y?: number, layer?: string }[]} [components] PCB components.
|
|
21
35
|
* @param {{ centerX?: number, centerY?: number } | null} [board] Board context.
|
|
36
|
+
* @param {{ componentIndex?: number, x?: number, y?: number, hasTopPasteMaskOpening?: boolean, hasBottomPasteMaskOpening?: boolean }[]} [pads] PCB pads.
|
|
22
37
|
*/
|
|
23
|
-
static promote(placementRows, components = [], board = null) {
|
|
38
|
+
static promote(placementRows, components = [], board = null, pads = []) {
|
|
24
39
|
PcbScene3dStaticBodyOwnerPromotion.#promoteNearestOwners(
|
|
25
40
|
placementRows,
|
|
26
41
|
components
|
|
@@ -31,6 +46,12 @@ export class PcbScene3dStaticBodyOwnerPromotion {
|
|
|
31
46
|
components
|
|
32
47
|
)
|
|
33
48
|
|
|
49
|
+
PcbScene3dStaticBodyPadOwnerPromotion.promote(
|
|
50
|
+
placementRows,
|
|
51
|
+
components,
|
|
52
|
+
pads
|
|
53
|
+
)
|
|
54
|
+
|
|
34
55
|
const clusters =
|
|
35
56
|
PcbScene3dStaticBodyOwnerPromotion.#clusters(placementRows)
|
|
36
57
|
|
|
@@ -91,6 +112,11 @@ export class PcbScene3dStaticBodyOwnerPromotion {
|
|
|
91
112
|
)
|
|
92
113
|
.forEach((cluster) => {
|
|
93
114
|
if (
|
|
115
|
+
cluster.indexes.some((index) =>
|
|
116
|
+
PcbScene3dStaticBodyOwnerPromotion.#hasLockedOwner(
|
|
117
|
+
placementRows[index]
|
|
118
|
+
)
|
|
119
|
+
) ||
|
|
94
120
|
cluster.indexes.length <= 1 ||
|
|
95
121
|
PcbScene3dStaticBodyOwnerPromotion.#boundsMaxSpan(
|
|
96
122
|
cluster.bounds
|
|
@@ -620,6 +646,10 @@ export class PcbScene3dStaticBodyOwnerPromotion {
|
|
|
620
646
|
* @returns {boolean}
|
|
621
647
|
*/
|
|
622
648
|
static #canPromote(row) {
|
|
649
|
+
if (PcbScene3dStaticBodyOwnerPromotion.#hasLockedOwner(row)) {
|
|
650
|
+
return false
|
|
651
|
+
}
|
|
652
|
+
|
|
623
653
|
if (row.matchedComponent) {
|
|
624
654
|
return true
|
|
625
655
|
}
|
|
@@ -640,9 +670,14 @@ export class PcbScene3dStaticBodyOwnerPromotion {
|
|
|
640
670
|
* @returns {boolean}
|
|
641
671
|
*/
|
|
642
672
|
static #canPromoteNearestOwner(row) {
|
|
673
|
+
if (PcbScene3dStaticBodyOwnerPromotion.#hasLockedOwner(row)) {
|
|
674
|
+
return false
|
|
675
|
+
}
|
|
676
|
+
|
|
643
677
|
if (
|
|
644
678
|
PcbScene3dStaticBodyOwnerPromotion.#canPromote(row) ||
|
|
645
|
-
PcbScene3dStaticBodyOwnerPromotion.#isCompactRow(row)
|
|
679
|
+
PcbScene3dStaticBodyOwnerPromotion.#isCompactRow(row) ||
|
|
680
|
+
PcbScene3dStaticBodyOwnerPromotion.#isCenteredPackageBody(row)
|
|
646
681
|
) {
|
|
647
682
|
return true
|
|
648
683
|
}
|
|
@@ -659,6 +694,15 @@ export class PcbScene3dStaticBodyOwnerPromotion {
|
|
|
659
694
|
)
|
|
660
695
|
}
|
|
661
696
|
|
|
697
|
+
/**
|
|
698
|
+
* Checks whether a placement already carries a deliberate static owner.
|
|
699
|
+
* @param {{ placement?: { ownerLocked?: boolean } } | null} row Placement row.
|
|
700
|
+
* @returns {boolean}
|
|
701
|
+
*/
|
|
702
|
+
static #hasLockedOwner(row) {
|
|
703
|
+
return Boolean(row?.placement?.ownerLocked)
|
|
704
|
+
}
|
|
705
|
+
|
|
662
706
|
/**
|
|
663
707
|
* Checks whether one row is compact enough for cluster-center ownership.
|
|
664
708
|
* @param {{ placement: object }} row Placement row.
|
|
@@ -672,6 +716,47 @@ export class PcbScene3dStaticBodyOwnerPromotion {
|
|
|
672
716
|
)
|
|
673
717
|
}
|
|
674
718
|
|
|
719
|
+
/**
|
|
720
|
+
* Checks whether one source-coordinate body looks like a complete package
|
|
721
|
+
* body centered on a component rather than authored board mechanics.
|
|
722
|
+
* @param {{ placement: object, matchedComponent: object | null }} row Placement row.
|
|
723
|
+
* @returns {boolean}
|
|
724
|
+
*/
|
|
725
|
+
static #isCenteredPackageBody(row) {
|
|
726
|
+
const placement = row?.placement
|
|
727
|
+
const span = PcbScene3dStaticBodyOwnerPromotion.#maxSpan(
|
|
728
|
+
placement?.geometry
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
!row?.matchedComponent &&
|
|
733
|
+
placement?.sourceCoordinateFrame &&
|
|
734
|
+
!placement?.mountSideLocked &&
|
|
735
|
+
span > 40 &&
|
|
736
|
+
span <=
|
|
737
|
+
PcbScene3dStaticBodyOwnerPromotion
|
|
738
|
+
.#CENTERED_PACKAGE_OWNER_MAX_SPAN_MIL &&
|
|
739
|
+
!PcbScene3dStaticBodyOwnerPromotion.#hasAuthoredMechanicalIdentity(
|
|
740
|
+
placement
|
|
741
|
+
)
|
|
742
|
+
)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Checks whether one placement identity names authored board mechanics.
|
|
747
|
+
* @param {{ designator?: string, sourceIdentityKey?: string } | null} placement Static placement.
|
|
748
|
+
* @returns {boolean}
|
|
749
|
+
*/
|
|
750
|
+
static #hasAuthoredMechanicalIdentity(placement) {
|
|
751
|
+
return PcbScene3dStaticBodyOwnerPromotion.#identityTokens(
|
|
752
|
+
placement
|
|
753
|
+
).some((token) =>
|
|
754
|
+
PcbScene3dStaticBodyOwnerPromotion.#AUTHORED_MECHANICAL_TOKENS.has(
|
|
755
|
+
token
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
}
|
|
759
|
+
|
|
675
760
|
/**
|
|
676
761
|
* Checks whether a placement uses only the generic static-body label.
|
|
677
762
|
* @param {{ designator?: string } | null} placement Static placement.
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Promotes compact static bodies that sit exactly on component-owned pads.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbScene3dStaticBodyPadOwnerPromotion {
|
|
9
|
+
static #PAD_OWNER_DISTANCE_MIL = 5
|
|
10
|
+
static #PAD_OWNER_AMBIGUITY_MARGIN_MIL = 0.25
|
|
11
|
+
static #COMPACT_BODY_MAX_SPAN_MIL = 40
|
|
12
|
+
static #AUTHORED_MECHANICAL_TOKENS = new Set([
|
|
13
|
+
'can',
|
|
14
|
+
'clip',
|
|
15
|
+
'cover',
|
|
16
|
+
'enclosure',
|
|
17
|
+
'frame',
|
|
18
|
+
'hardware',
|
|
19
|
+
'leg',
|
|
20
|
+
'mechanical',
|
|
21
|
+
'plate',
|
|
22
|
+
'shield'
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Applies exact pad ownership to mutable static placement rows.
|
|
27
|
+
* @param {{ placement: object, matchedComponent: object | null }[]} placementRows Mutable placement rows.
|
|
28
|
+
* @param {{ componentIndex?: number, designator?: string, layer?: string }[]} components PCB components.
|
|
29
|
+
* @param {{ componentIndex?: number, x?: number, y?: number, hasTopPasteMaskOpening?: boolean, hasBottomPasteMaskOpening?: boolean }[]} pads PCB pads.
|
|
30
|
+
* @returns {void}
|
|
31
|
+
*/
|
|
32
|
+
static promote(placementRows, components = [], pads = []) {
|
|
33
|
+
const componentByIndex =
|
|
34
|
+
PcbScene3dStaticBodyPadOwnerPromotion.#componentByIndex(components)
|
|
35
|
+
|
|
36
|
+
placementRows.forEach((row) => {
|
|
37
|
+
if (!PcbScene3dStaticBodyPadOwnerPromotion.#canPromoteRow(row)) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const owner =
|
|
42
|
+
PcbScene3dStaticBodyPadOwnerPromotion.#padOwnerForPlacement(
|
|
43
|
+
row.placement,
|
|
44
|
+
componentByIndex,
|
|
45
|
+
pads
|
|
46
|
+
)
|
|
47
|
+
if (!owner) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
row.matchedComponent = owner
|
|
52
|
+
row.placement = PcbScene3dStaticBodyPadOwnerPromotion.#withOwner(
|
|
53
|
+
row.placement,
|
|
54
|
+
owner
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Builds a component-index lookup.
|
|
61
|
+
* @param {object[]} components PCB components.
|
|
62
|
+
* @returns {Map<number, object>}
|
|
63
|
+
*/
|
|
64
|
+
static #componentByIndex(components) {
|
|
65
|
+
const componentByIndex = new Map()
|
|
66
|
+
|
|
67
|
+
;(Array.isArray(components) ? components : []).forEach((component) => {
|
|
68
|
+
const componentIndex = Number(component?.componentIndex)
|
|
69
|
+
if (
|
|
70
|
+
Number.isInteger(componentIndex) &&
|
|
71
|
+
String(component?.designator || '').trim()
|
|
72
|
+
) {
|
|
73
|
+
componentByIndex.set(componentIndex, component)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return componentByIndex
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Checks whether one static row is eligible for exact pad ownership.
|
|
82
|
+
* @param {{ placement?: object } | null} row Static placement row.
|
|
83
|
+
* @returns {boolean}
|
|
84
|
+
*/
|
|
85
|
+
static #canPromoteRow(row) {
|
|
86
|
+
const placement = row?.placement
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
placement &&
|
|
90
|
+
!placement.ownerLocked &&
|
|
91
|
+
!PcbScene3dStaticBodyPadOwnerPromotion.#hasAuthoredMechanicalIdentity(
|
|
92
|
+
placement
|
|
93
|
+
) &&
|
|
94
|
+
PcbScene3dStaticBodyPadOwnerPromotion.#maxSpan(
|
|
95
|
+
placement.geometry
|
|
96
|
+
) <=
|
|
97
|
+
PcbScene3dStaticBodyPadOwnerPromotion.#COMPACT_BODY_MAX_SPAN_MIL
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolves the unique component whose surface pad owns a body anchor.
|
|
103
|
+
* @param {{ bodyPositionMil?: { x?: number, y?: number } }} placement Static placement.
|
|
104
|
+
* @param {Map<number, object>} componentByIndex Component lookup.
|
|
105
|
+
* @param {object[]} pads PCB pads.
|
|
106
|
+
* @returns {object | null}
|
|
107
|
+
*/
|
|
108
|
+
static #padOwnerForPlacement(placement, componentByIndex, pads) {
|
|
109
|
+
const position =
|
|
110
|
+
PcbScene3dStaticBodyPadOwnerPromotion.#bodyPosition(placement)
|
|
111
|
+
if (!position) {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const candidates = (Array.isArray(pads) ? pads : [])
|
|
116
|
+
.map((pad) =>
|
|
117
|
+
PcbScene3dStaticBodyPadOwnerPromotion.#padOwnerCandidate(
|
|
118
|
+
pad,
|
|
119
|
+
position,
|
|
120
|
+
componentByIndex
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.sort((left, right) => left.distance - right.distance)
|
|
125
|
+
|
|
126
|
+
return PcbScene3dStaticBodyPadOwnerPromotion.#unambiguousOwner(
|
|
127
|
+
candidates
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Builds one exact pad-owner candidate.
|
|
133
|
+
* @param {object} pad PCB pad.
|
|
134
|
+
* @param {{ x: number, y: number }} position Body source position.
|
|
135
|
+
* @param {Map<number, object>} componentByIndex Component lookup.
|
|
136
|
+
* @returns {{ component: object, distance: number } | null}
|
|
137
|
+
*/
|
|
138
|
+
static #padOwnerCandidate(pad, position, componentByIndex) {
|
|
139
|
+
const component = componentByIndex.get(Number(pad?.componentIndex))
|
|
140
|
+
if (
|
|
141
|
+
!component ||
|
|
142
|
+
!PcbScene3dStaticBodyPadOwnerPromotion.#isSurfacePadForComponent(
|
|
143
|
+
pad,
|
|
144
|
+
component
|
|
145
|
+
)
|
|
146
|
+
) {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const distance = Math.hypot(
|
|
151
|
+
Number(pad?.x || 0) - position.x,
|
|
152
|
+
Number(pad?.y || 0) - position.y
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return Number.isFinite(distance) &&
|
|
156
|
+
distance <=
|
|
157
|
+
PcbScene3dStaticBodyPadOwnerPromotion.#PAD_OWNER_DISTANCE_MIL
|
|
158
|
+
? { component, distance }
|
|
159
|
+
: null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolves an unambiguous owner from pad-distance candidates.
|
|
164
|
+
* @param {{ component: object, distance: number }[]} candidates Candidates.
|
|
165
|
+
* @returns {object | null}
|
|
166
|
+
*/
|
|
167
|
+
static #unambiguousOwner(candidates) {
|
|
168
|
+
const nearest = candidates[0]
|
|
169
|
+
if (!nearest) {
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const nextDifferentOwner = candidates.find(
|
|
174
|
+
(candidate) =>
|
|
175
|
+
String(candidate.component?.designator || '') !==
|
|
176
|
+
String(nearest.component?.designator || '')
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return nextDifferentOwner &&
|
|
180
|
+
nextDifferentOwner.distance - nearest.distance <=
|
|
181
|
+
PcbScene3dStaticBodyPadOwnerPromotion
|
|
182
|
+
.#PAD_OWNER_AMBIGUITY_MARGIN_MIL
|
|
183
|
+
? null
|
|
184
|
+
: nearest.component
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolves the static body source position.
|
|
189
|
+
* @param {{ bodyPositionMil?: { x?: number, y?: number } }} placement Static placement.
|
|
190
|
+
* @returns {{ x: number, y: number } | null}
|
|
191
|
+
*/
|
|
192
|
+
static #bodyPosition(placement) {
|
|
193
|
+
const x = Number(placement?.bodyPositionMil?.x)
|
|
194
|
+
const y = Number(placement?.bodyPositionMil?.y)
|
|
195
|
+
|
|
196
|
+
return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Checks whether a pad belongs to the component's mounted PCB face.
|
|
201
|
+
* @param {object} pad PCB pad.
|
|
202
|
+
* @param {{ layer?: string }} component PCB component.
|
|
203
|
+
* @returns {boolean}
|
|
204
|
+
*/
|
|
205
|
+
static #isSurfacePadForComponent(pad, component) {
|
|
206
|
+
return PcbScene3dStaticBodyPadOwnerPromotion.#componentMountSide(
|
|
207
|
+
component
|
|
208
|
+
) === 'bottom'
|
|
209
|
+
? Boolean(pad?.hasBottomPasteMaskOpening)
|
|
210
|
+
: Boolean(pad?.hasTopPasteMaskOpening)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Applies a component owner to a placement.
|
|
215
|
+
* @param {object} placement Static placement.
|
|
216
|
+
* @param {{ designator?: string, layer?: string }} owner Owner component.
|
|
217
|
+
* @returns {object}
|
|
218
|
+
*/
|
|
219
|
+
static #withOwner(placement, owner) {
|
|
220
|
+
return {
|
|
221
|
+
...PcbScene3dStaticBodyPadOwnerPromotion.#withMountSide(
|
|
222
|
+
placement,
|
|
223
|
+
PcbScene3dStaticBodyPadOwnerPromotion.#componentMountSide(owner)
|
|
224
|
+
),
|
|
225
|
+
designator:
|
|
226
|
+
String(owner?.designator || '').trim() || placement?.designator
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Applies a mount side while preserving the placement's XY anchor.
|
|
232
|
+
* @param {object} placement Static placement.
|
|
233
|
+
* @param {string | undefined} mountSide Owner mount side.
|
|
234
|
+
* @returns {object}
|
|
235
|
+
*/
|
|
236
|
+
static #withMountSide(placement, mountSide) {
|
|
237
|
+
const currentSide =
|
|
238
|
+
PcbScene3dStaticBodyPadOwnerPromotion.#normalizeMountSide(
|
|
239
|
+
placement?.mountSide
|
|
240
|
+
)
|
|
241
|
+
const side = PcbScene3dStaticBodyPadOwnerPromotion.#normalizeMountSide(
|
|
242
|
+
mountSide || currentSide
|
|
243
|
+
)
|
|
244
|
+
const z = Math.abs(Number(placement?.positionMil?.z || 0))
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
...placement,
|
|
248
|
+
mountSide: side,
|
|
249
|
+
positionMil: {
|
|
250
|
+
...(placement?.positionMil || {}),
|
|
251
|
+
z: side === 'bottom' ? -z : z
|
|
252
|
+
},
|
|
253
|
+
geometry:
|
|
254
|
+
currentSide === side
|
|
255
|
+
? placement?.geometry
|
|
256
|
+
: PcbScene3dStaticBodyPadOwnerPromotion.#mirrorGeometry(
|
|
257
|
+
placement
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Resolves the board side from one component layer.
|
|
264
|
+
* @param {{ layer?: string }} component PCB component.
|
|
265
|
+
* @returns {'top' | 'bottom'}
|
|
266
|
+
*/
|
|
267
|
+
static #componentMountSide(component) {
|
|
268
|
+
return String(component?.layer || '')
|
|
269
|
+
.trim()
|
|
270
|
+
.toLowerCase()
|
|
271
|
+
.includes('bottom')
|
|
272
|
+
? 'bottom'
|
|
273
|
+
: 'top'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Normalizes a mount-side token.
|
|
278
|
+
* @param {string | undefined} mountSide Candidate side.
|
|
279
|
+
* @returns {'top' | 'bottom'}
|
|
280
|
+
*/
|
|
281
|
+
static #normalizeMountSide(mountSide) {
|
|
282
|
+
return String(mountSide || 'top')
|
|
283
|
+
.trim()
|
|
284
|
+
.toLowerCase() === 'bottom'
|
|
285
|
+
? 'bottom'
|
|
286
|
+
: 'top'
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Mirrors source-coordinate polygon geometry for a side change.
|
|
291
|
+
* @param {{ sourceCoordinateFrame?: boolean, geometry?: object }} placement Static placement.
|
|
292
|
+
* @returns {object | undefined}
|
|
293
|
+
*/
|
|
294
|
+
static #mirrorGeometry(placement) {
|
|
295
|
+
const geometry = placement?.geometry
|
|
296
|
+
if (
|
|
297
|
+
!placement?.sourceCoordinateFrame ||
|
|
298
|
+
!Array.isArray(geometry?.verticesMil)
|
|
299
|
+
) {
|
|
300
|
+
return geometry
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
...geometry,
|
|
305
|
+
verticesMil: geometry.verticesMil.map((vertex) => ({
|
|
306
|
+
x: PcbScene3dStaticBodyPadOwnerPromotion.#roundMil(
|
|
307
|
+
Number(vertex?.x || 0)
|
|
308
|
+
),
|
|
309
|
+
y: PcbScene3dStaticBodyPadOwnerPromotion.#roundMil(
|
|
310
|
+
-Number(vertex?.y || 0)
|
|
311
|
+
)
|
|
312
|
+
}))
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Checks whether a placement identity names authored board mechanics.
|
|
318
|
+
* @param {{ designator?: string, sourceIdentityKey?: string } | null} placement Static placement.
|
|
319
|
+
* @returns {boolean}
|
|
320
|
+
*/
|
|
321
|
+
static #hasAuthoredMechanicalIdentity(placement) {
|
|
322
|
+
return PcbScene3dStaticBodyPadOwnerPromotion.#identityTokens(
|
|
323
|
+
placement
|
|
324
|
+
).some((token) =>
|
|
325
|
+
PcbScene3dStaticBodyPadOwnerPromotion.#AUTHORED_MECHANICAL_TOKENS.has(
|
|
326
|
+
token
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Collects normalized identity tokens from one placement.
|
|
333
|
+
* @param {{ designator?: string, sourceIdentityKey?: string } | null} placement Static placement.
|
|
334
|
+
* @returns {string[]}
|
|
335
|
+
*/
|
|
336
|
+
static #identityTokens(placement) {
|
|
337
|
+
return [placement?.designator, placement?.sourceIdentityKey]
|
|
338
|
+
.join(' ')
|
|
339
|
+
.replace(/([a-z])([A-Z])/gu, '$1 $2')
|
|
340
|
+
.toLowerCase()
|
|
341
|
+
.split(/[^a-z0-9]+/g)
|
|
342
|
+
.flatMap((fragment) => fragment.match(/[a-z]+|\d+/g) || [])
|
|
343
|
+
.filter(Boolean)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Resolves the largest horizontal body span.
|
|
348
|
+
* @param {object | undefined} geometry Static geometry.
|
|
349
|
+
* @returns {number}
|
|
350
|
+
*/
|
|
351
|
+
static #maxSpan(geometry) {
|
|
352
|
+
const vertices = Array.isArray(geometry?.verticesMil)
|
|
353
|
+
? geometry.verticesMil
|
|
354
|
+
: []
|
|
355
|
+
if (vertices.length) {
|
|
356
|
+
const xs = vertices.map((vertex) => Number(vertex?.x || 0))
|
|
357
|
+
const ys = vertices.map((vertex) => Number(vertex?.y || 0))
|
|
358
|
+
|
|
359
|
+
return Math.max(
|
|
360
|
+
Math.max(...xs) - Math.min(...xs),
|
|
361
|
+
Math.max(...ys) - Math.min(...ys)
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const radius = Number(geometry?.radiusMil)
|
|
366
|
+
return Number.isFinite(radius) && radius > 0 ? radius * 2 : 0
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Rounds one mil value for stable promoted geometry output.
|
|
371
|
+
* @param {number} value Candidate value.
|
|
372
|
+
* @returns {number}
|
|
373
|
+
*/
|
|
374
|
+
static #roundMil(value) {
|
|
375
|
+
const rounded = Math.round(Number(value) * 10000) / 10000
|
|
376
|
+
return Object.is(rounded, -0) ? 0 : rounded
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -41,7 +41,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
41
41
|
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads Pads.
|
|
42
42
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
43
43
|
* @param {number} thicknessMil Board thickness.
|
|
44
|
-
* @returns {{ designator: string, selectionKey: string, sourceIdentityKey: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object }[]}
|
|
44
|
+
* @returns {{ designator: string, selectionKey: string, sourceIdentityKey: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, ownerLocked?: boolean, geometry: object }[]}
|
|
45
45
|
*/
|
|
46
46
|
static build(
|
|
47
47
|
componentBodies,
|
|
@@ -71,21 +71,13 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
71
71
|
PcbScene3dStaticBodyOwnerPromotion.promote(
|
|
72
72
|
placementRows,
|
|
73
73
|
components,
|
|
74
|
-
board
|
|
74
|
+
board,
|
|
75
|
+
pads
|
|
75
76
|
)
|
|
76
77
|
|
|
77
78
|
return PcbScene3dStaticBodySelectionKeyBuilder.assign(placementRows)
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
/**
|
|
81
|
-
* Checks whether one static geometry is already complete.
|
|
82
|
-
* @param {object | undefined} geometry Static geometry.
|
|
83
|
-
* @returns {boolean}
|
|
84
|
-
*/
|
|
85
|
-
static #isCompleteGeometry(geometry) {
|
|
86
|
-
return Boolean(geometry && geometry.status === 'complete')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
81
|
/**
|
|
90
82
|
* Builds one static body placement row with its matched owner context.
|
|
91
83
|
* @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.
|
|
@@ -93,7 +85,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
93
85
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
94
86
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
95
87
|
* @param {number} thicknessMil Board thickness.
|
|
96
|
-
* @returns {{ placement: { designator: string, sourceIdentityKey: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object }, matchedComponent: object | null } | null}
|
|
88
|
+
* @returns {{ placement: { designator: string, sourceIdentityKey: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, ownerLocked?: boolean, geometry: object }, matchedComponent: object | null } | null}
|
|
97
89
|
*/
|
|
98
90
|
static #buildPlacementRow(
|
|
99
91
|
componentBody,
|
|
@@ -125,7 +117,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
125
117
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
126
118
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
127
119
|
* @param {number} thicknessMil Board thickness.
|
|
128
|
-
* @returns {{ designator: string, sourceIdentityKey: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object } | null}
|
|
120
|
+
* @returns {{ designator: string, sourceIdentityKey: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, ownerLocked?: boolean, geometry: object } | null}
|
|
129
121
|
*/
|
|
130
122
|
static #buildPlacement(
|
|
131
123
|
componentBody,
|
|
@@ -142,9 +134,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
142
134
|
)
|
|
143
135
|
const geometry = staticGeometry.geometry
|
|
144
136
|
|
|
145
|
-
if (
|
|
146
|
-
!PcbScene3dStaticBodyPlacementBuilder.#isCompleteGeometry(geometry)
|
|
147
|
-
) {
|
|
137
|
+
if (geometry?.status !== 'complete') {
|
|
148
138
|
return null
|
|
149
139
|
}
|
|
150
140
|
const sourcePosition =
|
|
@@ -197,6 +187,11 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
197
187
|
staticGeometry,
|
|
198
188
|
mountSide
|
|
199
189
|
)
|
|
190
|
+
const ownerLocked =
|
|
191
|
+
PcbScene3dStaticBodyPlacementBuilder.#hasLockedOwner(
|
|
192
|
+
componentBody,
|
|
193
|
+
matchedComponent
|
|
194
|
+
)
|
|
200
195
|
|
|
201
196
|
return {
|
|
202
197
|
designator:
|
|
@@ -211,7 +206,9 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
211
206
|
sourceCoordinateFrame: Boolean(
|
|
212
207
|
staticGeometry.sourceCoordinateFrame
|
|
213
208
|
),
|
|
209
|
+
ownerLocked,
|
|
214
210
|
mountSideLocked:
|
|
211
|
+
ownerLocked ||
|
|
215
212
|
PcbScene3dStaticBodyPlacementBuilder.#hasExplicitMountSide(
|
|
216
213
|
componentBody,
|
|
217
214
|
matchedComponent
|
|
@@ -280,6 +277,25 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
280
277
|
return /^MECHANICAL\s*\d+$/i.test(layer)
|
|
281
278
|
}
|
|
282
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Checks whether a matched shield owner is deliberate enough to keep away
|
|
282
|
+
* from later nearest-neighbor fragment promotion.
|
|
283
|
+
* @param {{ identifier?: string, name?: string }} componentBody Component body.
|
|
284
|
+
* @param {{ pattern?: string, source?: string, description?: string, parameters?: object, provenance?: object } | null} matchedComponent Matched owner component.
|
|
285
|
+
* @returns {boolean}
|
|
286
|
+
*/
|
|
287
|
+
static #hasLockedOwner(componentBody, matchedComponent) {
|
|
288
|
+
return Boolean(
|
|
289
|
+
matchedComponent &&
|
|
290
|
+
PcbScene3dStaticBodyPlacementBuilder.#hasGenericMechanicalIdentity(
|
|
291
|
+
componentBody
|
|
292
|
+
) &&
|
|
293
|
+
PcbScene3dStaticBodyPlacementBuilder.#isShieldOwner(
|
|
294
|
+
matchedComponent
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
283
299
|
/**
|
|
284
300
|
* Checks whether one body identity looks like authored board mechanics.
|
|
285
301
|
* @param {{ identifier?: string, name?: string, modelTypeName?: string }} componentBody Component body.
|
|
@@ -208,6 +208,7 @@ export class PcbScene3dStaticBodySymmetricOwnerPromotion {
|
|
|
208
208
|
.filter(
|
|
209
209
|
(candidate) =>
|
|
210
210
|
candidate.position &&
|
|
211
|
+
!candidate.row?.placement?.ownerLocked &&
|
|
211
212
|
candidate.row?.placement?.sourceCoordinateFrame &&
|
|
212
213
|
PcbScene3dStaticBodySymmetricOwnerPromotion.#isCompactRow(
|
|
213
214
|
candidate.row
|