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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
const DISPLAY_MODULE_PATTERN =
|
|
2
|
+
/(?:^|[^a-z0-9])(?:display|lcd|oled|screen|tft)(?:$|[^a-z0-9])/i
|
|
3
|
+
const MIN_DIRECTIONAL_COSINE = 0.25
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects display modules whose footprint yaw is more reliable than the
|
|
7
|
+
* embedded body yaw.
|
|
8
|
+
*/
|
|
9
|
+
export class AltiumScene3dDisplayModuleYawPolicy {
|
|
10
|
+
/**
|
|
11
|
+
* Checks whether one placement should use the component yaw.
|
|
12
|
+
* @param {{ placement?: object, component?: object | null, pads?: object[], identityText?: string }} context Placement context.
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
static shouldUseComponentYaw(context) {
|
|
16
|
+
const { placement, component, pads, identityText } = context || {}
|
|
17
|
+
if (
|
|
18
|
+
!component ||
|
|
19
|
+
String(placement?.mountSide || '').toLowerCase() !== 'top' ||
|
|
20
|
+
String(placement?.projection?.source || '').toLowerCase() !==
|
|
21
|
+
'pad-fallback' ||
|
|
22
|
+
String(placement?.externalModel?.origin || '').toLowerCase() !==
|
|
23
|
+
'embedded' ||
|
|
24
|
+
!DISPLAY_MODULE_PATTERN.test(String(identityText || ''))
|
|
25
|
+
) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return AltiumScene3dDisplayModuleYawPolicy.#hasOffsetSingleRowPads(
|
|
30
|
+
component,
|
|
31
|
+
pads
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks whether a model-bounds display source points away from its edge
|
|
37
|
+
* pad row and needs a half-turn around the board normal.
|
|
38
|
+
* @param {{ placement?: object, component?: object | null, pads?: object[], identityText?: string }} context Placement context.
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
static shouldReverseModelBoundsYaw(context) {
|
|
42
|
+
const { placement, component, pads, identityText } = context || {}
|
|
43
|
+
if (
|
|
44
|
+
!component ||
|
|
45
|
+
String(placement?.mountSide || '').toLowerCase() !== 'top' ||
|
|
46
|
+
String(placement?.projection?.source || '').toLowerCase() !==
|
|
47
|
+
'model-bounds' ||
|
|
48
|
+
String(placement?.externalModel?.origin || '').toLowerCase() !==
|
|
49
|
+
'embedded' ||
|
|
50
|
+
!DISPLAY_MODULE_PATTERN.test(String(identityText || ''))
|
|
51
|
+
) {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const padOffset =
|
|
56
|
+
AltiumScene3dDisplayModuleYawPolicy.#offsetSingleRowPadVector(
|
|
57
|
+
component,
|
|
58
|
+
pads
|
|
59
|
+
)
|
|
60
|
+
const sourceOffset =
|
|
61
|
+
AltiumScene3dDisplayModuleYawPolicy.#modelBoundsSourceVector(
|
|
62
|
+
placement
|
|
63
|
+
)
|
|
64
|
+
if (!padOffset || !sourceOffset) {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const alignment = AltiumScene3dDisplayModuleYawPolicy.#cosineSimilarity(
|
|
69
|
+
padOffset,
|
|
70
|
+
sourceOffset
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
Number.isFinite(alignment) && alignment <= -MIN_DIRECTIONAL_COSINE
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Checks whether the footprint has one edge-mounted contact row.
|
|
80
|
+
* @param {object} component PCB component.
|
|
81
|
+
* @param {object[] | undefined} pads Source PCB pads.
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
static #hasOffsetSingleRowPads(component, pads) {
|
|
85
|
+
return Boolean(
|
|
86
|
+
AltiumScene3dDisplayModuleYawPolicy.#offsetSingleRowPadVector(
|
|
87
|
+
component,
|
|
88
|
+
pads
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolves the component-to-pad-row vector for an edge-mounted display
|
|
95
|
+
* connector.
|
|
96
|
+
* @param {object} component PCB component.
|
|
97
|
+
* @param {object[] | undefined} pads Source PCB pads.
|
|
98
|
+
* @returns {{ x: number, y: number } | null}
|
|
99
|
+
*/
|
|
100
|
+
static #offsetSingleRowPadVector(component, pads) {
|
|
101
|
+
const measurablePads = AltiumScene3dDisplayModuleYawPolicy.#surfacePads(
|
|
102
|
+
component,
|
|
103
|
+
pads
|
|
104
|
+
).filter((pad) =>
|
|
105
|
+
AltiumScene3dDisplayModuleYawPolicy.#isMeasurablePad(pad)
|
|
106
|
+
)
|
|
107
|
+
if (measurablePads.length < 6) {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const xValues = measurablePads.map((pad) => Number(pad?.x || 0))
|
|
112
|
+
const yValues = measurablePads.map((pad) => Number(pad?.y || 0))
|
|
113
|
+
const spreadX = Math.max(...xValues) - Math.min(...xValues)
|
|
114
|
+
const spreadY = Math.max(...yValues) - Math.min(...yValues)
|
|
115
|
+
const rowAxis = spreadX >= spreadY ? 'x' : 'y'
|
|
116
|
+
const crossAxis = rowAxis === 'x' ? 'y' : 'x'
|
|
117
|
+
const majorSpread = Math.max(spreadX, spreadY)
|
|
118
|
+
const minorSpread = Math.min(spreadX, spreadY)
|
|
119
|
+
const maxPadSpan = Math.max(
|
|
120
|
+
...measurablePads.map((pad) =>
|
|
121
|
+
AltiumScene3dDisplayModuleYawPolicy.#maxPadSpan(pad)
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
const componentCross = Number(component?.[crossAxis])
|
|
125
|
+
const rowCross =
|
|
126
|
+
measurablePads.reduce(
|
|
127
|
+
(sum, pad) => sum + Number(pad?.[crossAxis] || 0),
|
|
128
|
+
0
|
|
129
|
+
) / measurablePads.length
|
|
130
|
+
const componentX = Number(component?.x)
|
|
131
|
+
const componentY = Number(component?.y)
|
|
132
|
+
const rowX =
|
|
133
|
+
measurablePads.reduce((sum, pad) => sum + Number(pad?.x || 0), 0) /
|
|
134
|
+
measurablePads.length
|
|
135
|
+
const rowY =
|
|
136
|
+
measurablePads.reduce((sum, pad) => sum + Number(pad?.y || 0), 0) /
|
|
137
|
+
measurablePads.length
|
|
138
|
+
if (
|
|
139
|
+
!Number.isFinite(componentCross) ||
|
|
140
|
+
!Number.isFinite(rowCross) ||
|
|
141
|
+
!Number.isFinite(componentX) ||
|
|
142
|
+
!Number.isFinite(componentY) ||
|
|
143
|
+
!Number.isFinite(rowX) ||
|
|
144
|
+
!Number.isFinite(rowY) ||
|
|
145
|
+
majorSpread < Math.max(150, maxPadSpan * 2) ||
|
|
146
|
+
minorSpread > Math.max(10, maxPadSpan * 0.25)
|
|
147
|
+
) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
Math.abs(rowCross - componentCross) <
|
|
153
|
+
Math.max(100, maxPadSpan * 1.5)
|
|
154
|
+
) {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { x: rowX - componentX, y: rowY - componentY }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Estimates the scene-space vector from a model-bounds source origin to
|
|
163
|
+
* the dominant display body center.
|
|
164
|
+
* @param {object | null | undefined} placement External placement.
|
|
165
|
+
* @returns {{ x: number, y: number } | null}
|
|
166
|
+
*/
|
|
167
|
+
static #modelBoundsSourceVector(placement) {
|
|
168
|
+
const bounds = placement?.projection?.boundsMil || {}
|
|
169
|
+
const width = Math.abs(Number(bounds?.width || 0))
|
|
170
|
+
const depth = Math.abs(Number(bounds?.depth || 0))
|
|
171
|
+
if (
|
|
172
|
+
!Number.isFinite(width) ||
|
|
173
|
+
!Number.isFinite(depth) ||
|
|
174
|
+
width <= 0 ||
|
|
175
|
+
depth <= 0
|
|
176
|
+
) {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const rotationRad =
|
|
181
|
+
(AltiumScene3dDisplayModuleYawPolicy.#normalizeAngle(
|
|
182
|
+
Number(placement?.rotationDeg || 0)
|
|
183
|
+
) *
|
|
184
|
+
Math.PI) /
|
|
185
|
+
180
|
|
186
|
+
const cos = Math.cos(rotationRad)
|
|
187
|
+
const sin = Math.sin(rotationRad)
|
|
188
|
+
|
|
189
|
+
return depth >= width
|
|
190
|
+
? { x: -sin * (depth / 2), y: cos * (depth / 2) }
|
|
191
|
+
: { x: cos * (width / 2), y: sin * (width / 2) }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Computes the cosine similarity between two planar vectors.
|
|
196
|
+
* @param {{ x?: number, y?: number }} left First vector.
|
|
197
|
+
* @param {{ x?: number, y?: number }} right Second vector.
|
|
198
|
+
* @returns {number}
|
|
199
|
+
*/
|
|
200
|
+
static #cosineSimilarity(left, right) {
|
|
201
|
+
const leftX = Number(left?.x || 0)
|
|
202
|
+
const leftY = Number(left?.y || 0)
|
|
203
|
+
const rightX = Number(right?.x || 0)
|
|
204
|
+
const rightY = Number(right?.y || 0)
|
|
205
|
+
const leftMagnitude = Math.hypot(leftX, leftY)
|
|
206
|
+
const rightMagnitude = Math.hypot(rightX, rightY)
|
|
207
|
+
if (leftMagnitude <= 0 || rightMagnitude <= 0) {
|
|
208
|
+
return Number.NaN
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
(leftX * rightX + leftY * rightY) / leftMagnitude / rightMagnitude
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Normalizes an angle into the renderer's [0, 360) degree range.
|
|
218
|
+
* @param {number} value Raw angle.
|
|
219
|
+
* @returns {number}
|
|
220
|
+
*/
|
|
221
|
+
static #normalizeAngle(value) {
|
|
222
|
+
const angle = Number(value || 0) % 360
|
|
223
|
+
|
|
224
|
+
return angle < 0 ? angle + 360 : angle
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Collects surface pads owned by one component.
|
|
229
|
+
* @param {object} component PCB component.
|
|
230
|
+
* @param {object[] | undefined} pads Source PCB pads.
|
|
231
|
+
* @returns {object[]}
|
|
232
|
+
*/
|
|
233
|
+
static #surfacePads(component, pads) {
|
|
234
|
+
const componentIndex = Number(component?.componentIndex)
|
|
235
|
+
if (!Number.isFinite(componentIndex)) {
|
|
236
|
+
return []
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const ownedPads = (Array.isArray(pads) ? pads : []).filter(
|
|
240
|
+
(pad) => Number(pad?.componentIndex) === componentIndex
|
|
241
|
+
)
|
|
242
|
+
const bottom =
|
|
243
|
+
String(component?.layer || '')
|
|
244
|
+
.toUpperCase()
|
|
245
|
+
.includes('BOTTOM') ||
|
|
246
|
+
String(component?.layer || '').toUpperCase() === 'BOT'
|
|
247
|
+
const surfacePads = ownedPads.filter((pad) =>
|
|
248
|
+
bottom
|
|
249
|
+
? Boolean(pad?.hasBottomPasteMaskOpening)
|
|
250
|
+
: Boolean(pad?.hasTopPasteMaskOpening)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return surfacePads.length ? surfacePads : ownedPads
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Checks whether one pad has finite coordinates and non-zero dimensions.
|
|
258
|
+
* @param {object} pad Source PCB pad.
|
|
259
|
+
* @returns {boolean}
|
|
260
|
+
*/
|
|
261
|
+
static #isMeasurablePad(pad) {
|
|
262
|
+
return (
|
|
263
|
+
Number.isFinite(Number(pad?.x)) &&
|
|
264
|
+
Number.isFinite(Number(pad?.y)) &&
|
|
265
|
+
AltiumScene3dDisplayModuleYawPolicy.#maxPadSpan(pad) > 0
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Measures the largest available pad dimension.
|
|
271
|
+
* @param {object} pad Source PCB pad.
|
|
272
|
+
* @returns {number}
|
|
273
|
+
*/
|
|
274
|
+
static #maxPadSpan(pad) {
|
|
275
|
+
return Math.max(
|
|
276
|
+
Number(pad?.sizeTopX || 0),
|
|
277
|
+
Number(pad?.sizeTopY || 0),
|
|
278
|
+
Number(pad?.sizeMidX || 0),
|
|
279
|
+
Number(pad?.sizeMidY || 0),
|
|
280
|
+
Number(pad?.sizeBottomX || 0),
|
|
281
|
+
Number(pad?.sizeBottomY || 0)
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
}
|