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.
- package/AGENTS.md +17 -2
- package/package.json +1 -1
- package/src/core/altium/PrintableTextDecoder.mjs +20 -1
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +288 -7
- package/src/core/altium/SchematicTextParser.mjs +18 -253
- package/src/core/altium/SchematicTitleBlockParser.mjs +410 -0
- package/src/core/altium/SelectedPartAltiumExportAdapter.mjs +215 -0
- package/src/parser.mjs +1 -0
- 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 +286 -135
- 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/PcbScene3dPackages.mjs +16 -1
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +262 -22
- package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +984 -0
- package/src/ui/PcbScene3dStaticBodyPadOwnerPromotion.mjs +378 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +169 -23
- package/src/ui/PcbScene3dStaticBodyPrototypeRecovery.mjs +445 -0
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +7 -1
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +199 -2
- package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +439 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +23 -2
- package/src/ui/SchematicColorResolver.mjs +44 -0
- package/src/ui/SchematicLineColorResolver.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +13 -13
|
@@ -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
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { PcbScene3dPlacementSideResolver } from './PcbScene3dPlacementSideResolver.mjs'
|
|
6
|
+
import { PcbScene3dStaticBodyOwnerPromotion } from './PcbScene3dStaticBodyOwnerPromotion.mjs'
|
|
6
7
|
import { PcbScene3dStaticBodyRecovery } from './PcbScene3dStaticBodyRecovery.mjs'
|
|
7
8
|
import { PcbScene3dStaticBodySelectionKeyBuilder } from './PcbScene3dStaticBodySelectionKeyBuilder.mjs'
|
|
8
9
|
|
|
@@ -21,6 +22,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
21
22
|
static #SOURCE_COORDINATE_MIRROR_MIN_OFFSET_MIL = 20
|
|
22
23
|
static #GENERIC_MECHANICAL_IDENTITY_TOKENS = new Set([
|
|
23
24
|
'can',
|
|
25
|
+
'clip',
|
|
24
26
|
'cover',
|
|
25
27
|
'enclosure',
|
|
26
28
|
'frame',
|
|
@@ -39,7 +41,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
39
41
|
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads Pads.
|
|
40
42
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
41
43
|
* @param {number} thicknessMil Board thickness.
|
|
42
|
-
* @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 }[]}
|
|
43
45
|
*/
|
|
44
46
|
static build(
|
|
45
47
|
componentBodies,
|
|
@@ -66,16 +68,14 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
66
68
|
)
|
|
67
69
|
.filter(Boolean)
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
PcbScene3dStaticBodyOwnerPromotion.promote(
|
|
72
|
+
placementRows,
|
|
73
|
+
components,
|
|
74
|
+
board,
|
|
75
|
+
pads
|
|
76
|
+
)
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
* Checks whether one static geometry is already complete.
|
|
74
|
-
* @param {object | undefined} geometry Static geometry.
|
|
75
|
-
* @returns {boolean}
|
|
76
|
-
*/
|
|
77
|
-
static #isCompleteGeometry(geometry) {
|
|
78
|
-
return Boolean(geometry && geometry.status === 'complete')
|
|
78
|
+
return PcbScene3dStaticBodySelectionKeyBuilder.assign(placementRows)
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
@@ -85,7 +85,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
85
85
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
86
86
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
87
87
|
* @param {number} thicknessMil Board thickness.
|
|
88
|
-
* @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}
|
|
89
89
|
*/
|
|
90
90
|
static #buildPlacementRow(
|
|
91
91
|
componentBody,
|
|
@@ -117,7 +117,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
117
117
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
118
118
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
119
119
|
* @param {number} thicknessMil Board thickness.
|
|
120
|
-
* @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}
|
|
121
121
|
*/
|
|
122
122
|
static #buildPlacement(
|
|
123
123
|
componentBody,
|
|
@@ -134,9 +134,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
134
134
|
)
|
|
135
135
|
const geometry = staticGeometry.geometry
|
|
136
136
|
|
|
137
|
-
if (
|
|
138
|
-
!PcbScene3dStaticBodyPlacementBuilder.#isCompleteGeometry(geometry)
|
|
139
|
-
) {
|
|
137
|
+
if (geometry?.status !== 'complete') {
|
|
140
138
|
return null
|
|
141
139
|
}
|
|
142
140
|
const sourcePosition =
|
|
@@ -183,6 +181,17 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
183
181
|
Number(thicknessMil || 0) / 2 +
|
|
184
182
|
(Number.isFinite(standoffMil) ? standoffMil : 0) +
|
|
185
183
|
heightMil / 2
|
|
184
|
+
const renderGeometry =
|
|
185
|
+
PcbScene3dStaticBodyPlacementBuilder.#placementGeometry(
|
|
186
|
+
geometry,
|
|
187
|
+
staticGeometry,
|
|
188
|
+
mountSide
|
|
189
|
+
)
|
|
190
|
+
const ownerLocked =
|
|
191
|
+
PcbScene3dStaticBodyPlacementBuilder.#hasLockedOwner(
|
|
192
|
+
componentBody,
|
|
193
|
+
matchedComponent
|
|
194
|
+
)
|
|
186
195
|
|
|
187
196
|
return {
|
|
188
197
|
designator:
|
|
@@ -194,11 +203,23 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
194
203
|
PcbScene3dStaticBodyPlacementBuilder.#sourceIdentityKey(
|
|
195
204
|
componentBody
|
|
196
205
|
),
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
Number(componentBody.rotationDeg || 0) +
|
|
200
|
-
Number(matchedComponent?.rotation || 0)
|
|
206
|
+
sourceCoordinateFrame: Boolean(
|
|
207
|
+
staticGeometry.sourceCoordinateFrame
|
|
201
208
|
),
|
|
209
|
+
ownerLocked,
|
|
210
|
+
mountSideLocked:
|
|
211
|
+
ownerLocked ||
|
|
212
|
+
PcbScene3dStaticBodyPlacementBuilder.#hasExplicitMountSide(
|
|
213
|
+
componentBody,
|
|
214
|
+
matchedComponent
|
|
215
|
+
),
|
|
216
|
+
mountSide,
|
|
217
|
+
rotationDeg:
|
|
218
|
+
PcbScene3dStaticBodyPlacementBuilder.#placementRotationDeg(
|
|
219
|
+
componentBody,
|
|
220
|
+
matchedComponent,
|
|
221
|
+
staticGeometry
|
|
222
|
+
),
|
|
202
223
|
positionMil: {
|
|
203
224
|
x: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
204
225
|
Number(sourcePosition.x || 0) - Number(board.centerX || 0)
|
|
@@ -217,7 +238,126 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
217
238
|
...PcbScene3dStaticBodyPlacementBuilder.#displayMetadata(
|
|
218
239
|
componentBody
|
|
219
240
|
),
|
|
220
|
-
geometry
|
|
241
|
+
geometry: renderGeometry
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Checks whether one body carries explicit top/bottom side evidence.
|
|
247
|
+
* @param {{ componentIndex?: number | string, layer?: string, standoffHeightMil?: number | string | null }} componentBody Component body.
|
|
248
|
+
* @param {{ layer?: string } | null} matchedComponent Matched owner component.
|
|
249
|
+
* @returns {boolean}
|
|
250
|
+
*/
|
|
251
|
+
static #hasExplicitMountSide(componentBody, matchedComponent) {
|
|
252
|
+
if (
|
|
253
|
+
matchedComponent &&
|
|
254
|
+
Number.isInteger(Number(componentBody?.componentIndex))
|
|
255
|
+
) {
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const layer = String(componentBody?.layer || '').trim()
|
|
260
|
+
if (!layer) {
|
|
261
|
+
const standoffMil = Number(componentBody?.standoffHeightMil)
|
|
262
|
+
return Number.isFinite(standoffMil) && standoffMil < 0
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
!PcbScene3dStaticBodyPlacementBuilder.#isAuthoredMechanicalBody(
|
|
267
|
+
componentBody
|
|
268
|
+
)
|
|
269
|
+
) {
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (/(^|[^a-z])(top|bottom)([^a-z]|$)/i.test(layer)) {
|
|
274
|
+
return true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return /^MECHANICAL\s*\d+$/i.test(layer)
|
|
278
|
+
}
|
|
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
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Checks whether one body identity looks like authored board mechanics.
|
|
301
|
+
* @param {{ identifier?: string, name?: string, modelTypeName?: string }} componentBody Component body.
|
|
302
|
+
* @returns {boolean}
|
|
303
|
+
*/
|
|
304
|
+
static #isAuthoredMechanicalBody(componentBody) {
|
|
305
|
+
return PcbScene3dStaticBodyPlacementBuilder.#identityTokens(
|
|
306
|
+
componentBody
|
|
307
|
+
).some((token) =>
|
|
308
|
+
PcbScene3dStaticBodyPlacementBuilder.#GENERIC_MECHANICAL_IDENTITY_TOKENS.has(
|
|
309
|
+
token
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Resolves the rendered rotation for one static shape body.
|
|
316
|
+
* @param {{ rotationDeg?: number }} componentBody Component body.
|
|
317
|
+
* @param {{ rotation?: number } | null} matchedComponent Matched owner component.
|
|
318
|
+
* @param {{ sourceCoordinateFrame?: boolean }} normalizedStaticGeometry Normalized static geometry context.
|
|
319
|
+
* @returns {number}
|
|
320
|
+
*/
|
|
321
|
+
static #placementRotationDeg(
|
|
322
|
+
componentBody,
|
|
323
|
+
matchedComponent,
|
|
324
|
+
normalizedStaticGeometry
|
|
325
|
+
) {
|
|
326
|
+
const ownerRotation = normalizedStaticGeometry?.sourceCoordinateFrame
|
|
327
|
+
? 0
|
|
328
|
+
: Number(matchedComponent?.rotation || 0)
|
|
329
|
+
|
|
330
|
+
return PcbScene3dStaticBodyPlacementBuilder.#normalizeAngle(
|
|
331
|
+
Number(componentBody.rotationDeg || 0) + ownerRotation
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Converts normalized static geometry into the renderer mount-local frame.
|
|
337
|
+
* @param {object} geometry Normalized render geometry.
|
|
338
|
+
* @param {{ sourceCoordinateFrame?: boolean }} normalizedStaticGeometry Normalized static geometry context.
|
|
339
|
+
* @param {string} mountSide Resolved mount side.
|
|
340
|
+
* @returns {object}
|
|
341
|
+
*/
|
|
342
|
+
static #placementGeometry(geometry, normalizedStaticGeometry, mountSide) {
|
|
343
|
+
if (
|
|
344
|
+
!normalizedStaticGeometry?.sourceCoordinateFrame ||
|
|
345
|
+
String(mountSide || 'top').toLowerCase() !== 'bottom' ||
|
|
346
|
+
!Array.isArray(geometry?.verticesMil)
|
|
347
|
+
) {
|
|
348
|
+
return geometry
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
...geometry,
|
|
353
|
+
verticesMil: geometry.verticesMil.map((vertex) => ({
|
|
354
|
+
x: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
355
|
+
Number(vertex?.x || 0)
|
|
356
|
+
),
|
|
357
|
+
y: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
358
|
+
-Number(vertex?.y || 0)
|
|
359
|
+
)
|
|
360
|
+
}))
|
|
221
361
|
}
|
|
222
362
|
}
|
|
223
363
|
|
|
@@ -226,19 +366,24 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
226
366
|
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
227
367
|
* @param {object | undefined} geometry Static geometry.
|
|
228
368
|
* @param {{ x?: number, y?: number } | null} matchedComponent Matched owner component.
|
|
229
|
-
* @returns {{ geometry: object | undefined, placementCenterMil?: { x: number, y: number } }}
|
|
369
|
+
* @returns {{ geometry: object | undefined, placementCenterMil?: { x: number, y: number }, sourceCoordinateFrame: boolean }}
|
|
230
370
|
*/
|
|
231
371
|
static #normalizeStaticGeometry(componentBody, geometry, matchedComponent) {
|
|
232
372
|
if (
|
|
233
373
|
String(geometry?.kind || '').toLowerCase() !== 'extruded-polygon' ||
|
|
234
374
|
!Array.isArray(geometry?.verticesMil) ||
|
|
235
|
-
geometry.verticesMil.length < 3
|
|
375
|
+
geometry.verticesMil.length < 3
|
|
376
|
+
) {
|
|
377
|
+
return { geometry, sourceCoordinateFrame: false }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (
|
|
236
381
|
!PcbScene3dStaticBodyPlacementBuilder.#usesSourceCoordinateFrame(
|
|
237
382
|
componentBody,
|
|
238
383
|
geometry.verticesMil
|
|
239
384
|
)
|
|
240
385
|
) {
|
|
241
|
-
return { geometry }
|
|
386
|
+
return { geometry, sourceCoordinateFrame: false }
|
|
242
387
|
}
|
|
243
388
|
|
|
244
389
|
const center =
|
|
@@ -255,6 +400,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
255
400
|
|
|
256
401
|
return {
|
|
257
402
|
placementCenterMil: placementCenter,
|
|
403
|
+
sourceCoordinateFrame: true,
|
|
258
404
|
geometry: {
|
|
259
405
|
...geometry,
|
|
260
406
|
verticesMil: geometry.verticesMil.map((vertex) => {
|