altium-toolkit 1.1.32 → 1.1.35
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/docs/model-format.md +2 -3
- package/package.json +1 -1
- package/src/core/altium/AltiumGeneratedLibraryRecordBuilder.mjs +551 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +286 -2
- package/src/core/altium/AltiumPcbLibExporter.mjs +9 -2
- package/src/core/altium/AltiumSchLibExporter.mjs +1 -1
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +120 -15
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +52 -4
- package/src/core/altium/PcbShapeBasedBodyGeometryParser.mjs +85 -11
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +31 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +30 -2
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +27 -2
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +44 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +171 -6
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +782 -0
- package/src/ui/AltiumScene3dTwoRowFootprintDetector.mjs +90 -0
- package/src/ui/PcbScene3dBuilder.mjs +595 -28
- package/src/ui/PcbScene3dCopperRegionDetailBuilder.mjs +280 -0
- package/src/ui/PcbScene3dModelRegistry.mjs +16 -0
- package/src/ui/PcbScene3dPackageDimensionResolver.mjs +107 -0
- package/src/ui/PcbScene3dPackages.mjs +15 -3
- package/src/ui/PcbScene3dPadYawResolver.mjs +200 -0
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +56 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +638 -16
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +891 -0
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +358 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +876 -0
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { PcbScene3dPlacementSideResolver } from './PcbScene3dPlacementSideResolver.mjs'
|
|
6
|
+
import { PcbScene3dStaticBodyRecovery } from './PcbScene3dStaticBodyRecovery.mjs'
|
|
7
|
+
import { PcbScene3dStaticBodySelectionKeyBuilder } from './PcbScene3dStaticBodySelectionKeyBuilder.mjs'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Builds scene placements for static shape-based 3D bodies.
|
|
@@ -11,26 +13,50 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
11
13
|
static #UNMATCHED_BODY_OVERHANG_RATIO = 0.25
|
|
12
14
|
static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
|
|
13
15
|
static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
|
|
16
|
+
static #OWNER_AFFINITY_DISTANCE_MIL = 600
|
|
17
|
+
static #OWNER_BOUNDS_TOLERANCE_MIL = 20
|
|
18
|
+
static #OWNER_EXACT_DISTANCE_MIL = 5
|
|
19
|
+
static #OWNER_EXACT_MAX_SPAN_MIL = 500
|
|
20
|
+
static #SOURCE_COORDINATE_MIRROR_TOLERANCE_MIL = 5
|
|
21
|
+
static #SOURCE_COORDINATE_MIRROR_MIN_OFFSET_MIL = 20
|
|
22
|
+
static #GENERIC_MECHANICAL_IDENTITY_TOKENS = new Set([
|
|
23
|
+
'can',
|
|
24
|
+
'cover',
|
|
25
|
+
'enclosure',
|
|
26
|
+
'frame',
|
|
27
|
+
'hardware',
|
|
28
|
+
'leg',
|
|
29
|
+
'mechanical',
|
|
30
|
+
'plate',
|
|
31
|
+
'shield'
|
|
32
|
+
])
|
|
14
33
|
|
|
15
34
|
/**
|
|
16
35
|
* Builds static shape-body scene placements.
|
|
17
|
-
* @param {{ identifier?: string, name?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, standoffHeightMil?: number | null, overallHeightMil?: number | null, staticGeometry?: object }[]} componentBodies Component bodies.
|
|
36
|
+
* @param {{ componentIndex?: number, identifier?: string, name?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, standoffHeightMil?: number | null, overallHeightMil?: number | null, bodyOpacity?: number | string, staticGeometry?: object }[]} componentBodies Component bodies.
|
|
18
37
|
* @param {({ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null)[]} bodyMatches Matched components.
|
|
19
38
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
39
|
+
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads Pads.
|
|
20
40
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
21
41
|
* @param {number} thicknessMil Board thickness.
|
|
22
|
-
* @returns {{ designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object }[]}
|
|
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 }[]}
|
|
23
43
|
*/
|
|
24
44
|
static build(
|
|
25
45
|
componentBodies,
|
|
26
46
|
bodyMatches,
|
|
27
47
|
components,
|
|
48
|
+
pads,
|
|
28
49
|
board,
|
|
29
50
|
thicknessMil
|
|
30
51
|
) {
|
|
31
|
-
|
|
52
|
+
const recoveredBodies = PcbScene3dStaticBodyRecovery.recover(
|
|
53
|
+
componentBodies,
|
|
54
|
+
bodyMatches
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const placementRows = recoveredBodies
|
|
32
58
|
.map((componentBody, index) =>
|
|
33
|
-
PcbScene3dStaticBodyPlacementBuilder.#
|
|
59
|
+
PcbScene3dStaticBodyPlacementBuilder.#buildPlacementRow(
|
|
34
60
|
componentBody,
|
|
35
61
|
bodyMatches?.[index] || null,
|
|
36
62
|
components,
|
|
@@ -39,16 +65,59 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
39
65
|
)
|
|
40
66
|
)
|
|
41
67
|
.filter(Boolean)
|
|
68
|
+
|
|
69
|
+
return PcbScene3dStaticBodySelectionKeyBuilder.assign(placementRows)
|
|
70
|
+
}
|
|
71
|
+
|
|
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')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Builds one static body placement row with its matched owner context.
|
|
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.
|
|
84
|
+
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent Matched component.
|
|
85
|
+
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
86
|
+
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
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}
|
|
89
|
+
*/
|
|
90
|
+
static #buildPlacementRow(
|
|
91
|
+
componentBody,
|
|
92
|
+
matchedComponent,
|
|
93
|
+
components,
|
|
94
|
+
board,
|
|
95
|
+
thicknessMil
|
|
96
|
+
) {
|
|
97
|
+
const placement = PcbScene3dStaticBodyPlacementBuilder.#buildPlacement(
|
|
98
|
+
componentBody,
|
|
99
|
+
matchedComponent,
|
|
100
|
+
components,
|
|
101
|
+
board,
|
|
102
|
+
thicknessMil
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return placement
|
|
106
|
+
? {
|
|
107
|
+
placement,
|
|
108
|
+
matchedComponent
|
|
109
|
+
}
|
|
110
|
+
: null
|
|
42
111
|
}
|
|
43
112
|
|
|
44
113
|
/**
|
|
45
114
|
* Builds one static shape-body scene placement.
|
|
46
|
-
* @param {{ identifier?: string, name?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, standoffHeightMil?: number | null, overallHeightMil?: number | null, staticGeometry?: object }} componentBody Component body.
|
|
115
|
+
* @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.
|
|
47
116
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent Matched component.
|
|
48
117
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
49
118
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
50
119
|
* @param {number} thicknessMil Board thickness.
|
|
51
|
-
* @returns {{ designator: 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 }, geometry: object } | null}
|
|
52
121
|
*/
|
|
53
122
|
static #buildPlacement(
|
|
54
123
|
componentBody,
|
|
@@ -57,9 +126,32 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
57
126
|
board,
|
|
58
127
|
thicknessMil
|
|
59
128
|
) {
|
|
60
|
-
const
|
|
129
|
+
const staticGeometry =
|
|
130
|
+
PcbScene3dStaticBodyPlacementBuilder.#normalizeStaticGeometry(
|
|
131
|
+
componentBody,
|
|
132
|
+
componentBody?.staticGeometry,
|
|
133
|
+
matchedComponent
|
|
134
|
+
)
|
|
135
|
+
const geometry = staticGeometry.geometry
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
!PcbScene3dStaticBodyPlacementBuilder.#isCompleteGeometry(geometry)
|
|
139
|
+
) {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
const sourcePosition =
|
|
143
|
+
staticGeometry.placementCenterMil ||
|
|
144
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
|
|
61
145
|
|
|
62
|
-
if (
|
|
146
|
+
if (
|
|
147
|
+
!matchedComponent &&
|
|
148
|
+
PcbScene3dStaticBodyPlacementBuilder.#shouldSuppressStaticBody(
|
|
149
|
+
componentBody,
|
|
150
|
+
geometry,
|
|
151
|
+
components,
|
|
152
|
+
sourcePosition
|
|
153
|
+
)
|
|
154
|
+
) {
|
|
63
155
|
return null
|
|
64
156
|
}
|
|
65
157
|
|
|
@@ -73,14 +165,13 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
73
165
|
return null
|
|
74
166
|
}
|
|
75
167
|
|
|
76
|
-
const mountSide =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
|
|
168
|
+
const mountSide =
|
|
169
|
+
PcbScene3dPlacementSideResolver.resolveStaticBodyPlacementSide(
|
|
170
|
+
componentBody,
|
|
171
|
+
matchedComponent,
|
|
172
|
+
components,
|
|
173
|
+
board
|
|
174
|
+
)
|
|
84
175
|
const heightMil =
|
|
85
176
|
PcbScene3dStaticBodyPlacementBuilder.#geometryHeight(geometry)
|
|
86
177
|
const standoffMil = Math.abs(
|
|
@@ -99,6 +190,10 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
99
190
|
String(
|
|
100
191
|
componentBody.identifier || componentBody.name || '3D body'
|
|
101
192
|
),
|
|
193
|
+
sourceIdentityKey:
|
|
194
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourceIdentityKey(
|
|
195
|
+
componentBody
|
|
196
|
+
),
|
|
102
197
|
mountSide,
|
|
103
198
|
rotationDeg: PcbScene3dStaticBodyPlacementBuilder.#normalizeAngle(
|
|
104
199
|
Number(componentBody.rotationDeg || 0) +
|
|
@@ -119,10 +214,537 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
119
214
|
x: Number(componentBody.positionMil?.x || 0),
|
|
120
215
|
y: Number(componentBody.positionMil?.y || 0)
|
|
121
216
|
},
|
|
217
|
+
...PcbScene3dStaticBodyPlacementBuilder.#displayMetadata(
|
|
218
|
+
componentBody
|
|
219
|
+
),
|
|
122
220
|
geometry
|
|
123
221
|
}
|
|
124
222
|
}
|
|
125
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Converts source-space polygon vertices into render-local geometry.
|
|
226
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
227
|
+
* @param {object | undefined} geometry Static geometry.
|
|
228
|
+
* @param {{ x?: number, y?: number } | null} matchedComponent Matched owner component.
|
|
229
|
+
* @returns {{ geometry: object | undefined, placementCenterMil?: { x: number, y: number } }}
|
|
230
|
+
*/
|
|
231
|
+
static #normalizeStaticGeometry(componentBody, geometry, matchedComponent) {
|
|
232
|
+
if (
|
|
233
|
+
String(geometry?.kind || '').toLowerCase() !== 'extruded-polygon' ||
|
|
234
|
+
!Array.isArray(geometry?.verticesMil) ||
|
|
235
|
+
geometry.verticesMil.length < 3 ||
|
|
236
|
+
!PcbScene3dStaticBodyPlacementBuilder.#usesSourceCoordinateFrame(
|
|
237
|
+
componentBody,
|
|
238
|
+
geometry.verticesMil
|
|
239
|
+
)
|
|
240
|
+
) {
|
|
241
|
+
return { geometry }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const center =
|
|
245
|
+
PcbScene3dStaticBodyPlacementBuilder.#polygonBoundsCenter(
|
|
246
|
+
geometry.verticesMil
|
|
247
|
+
)
|
|
248
|
+
const mirror =
|
|
249
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourceCoordinateMirror(
|
|
250
|
+
componentBody,
|
|
251
|
+
center,
|
|
252
|
+
matchedComponent
|
|
253
|
+
)
|
|
254
|
+
const placementCenter = mirror?.placementCenterMil || center
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
placementCenterMil: placementCenter,
|
|
258
|
+
geometry: {
|
|
259
|
+
...geometry,
|
|
260
|
+
verticesMil: geometry.verticesMil.map((vertex) => {
|
|
261
|
+
const normalizedVertex =
|
|
262
|
+
PcbScene3dStaticBodyPlacementBuilder.#normalizeVertex(
|
|
263
|
+
vertex,
|
|
264
|
+
mirror
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
x: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
269
|
+
normalizedVertex.x - placementCenter.x
|
|
270
|
+
),
|
|
271
|
+
y: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
272
|
+
normalizedVertex.y - placementCenter.y
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Resolves owner-axis mirror transforms for source-coordinate polygons
|
|
282
|
+
* whose body anchor is opposite their raw polygon bounds.
|
|
283
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
284
|
+
* @param {{ x: number, y: number }} boundsCenter Source-coordinate polygon bounds center.
|
|
285
|
+
* @param {{ x?: number, y?: number } | null} matchedComponent Matched owner component.
|
|
286
|
+
* @returns {{ mirrorAxisX?: number, mirrorAxisY?: number, placementCenterMil: { x: number, y: number } } | null}
|
|
287
|
+
*/
|
|
288
|
+
static #sourceCoordinateMirror(
|
|
289
|
+
componentBody,
|
|
290
|
+
boundsCenter,
|
|
291
|
+
matchedComponent
|
|
292
|
+
) {
|
|
293
|
+
const source =
|
|
294
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
|
|
295
|
+
const xDecision =
|
|
296
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourceCoordinateMirrorAxis(
|
|
297
|
+
boundsCenter.x,
|
|
298
|
+
source.x,
|
|
299
|
+
Number(matchedComponent?.x)
|
|
300
|
+
)
|
|
301
|
+
const yDecision =
|
|
302
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourceCoordinateMirrorAxis(
|
|
303
|
+
boundsCenter.y,
|
|
304
|
+
source.y,
|
|
305
|
+
Number(matchedComponent?.y)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if (
|
|
309
|
+
!xDecision.valid ||
|
|
310
|
+
!yDecision.valid ||
|
|
311
|
+
(!xDecision.mirrored && !yDecision.mirrored)
|
|
312
|
+
) {
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
mirrorAxisX: xDecision.mirrored
|
|
318
|
+
? Number(matchedComponent?.x)
|
|
319
|
+
: undefined,
|
|
320
|
+
mirrorAxisY: yDecision.mirrored
|
|
321
|
+
? Number(matchedComponent?.y)
|
|
322
|
+
: undefined,
|
|
323
|
+
placementCenterMil: {
|
|
324
|
+
x: source.x,
|
|
325
|
+
y: source.y
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Checks whether one source-coordinate axis is aligned or mirrored.
|
|
332
|
+
* @param {number} center Raw polygon center coordinate.
|
|
333
|
+
* @param {number} source Body source coordinate.
|
|
334
|
+
* @param {number} ownerAxis Owner coordinate.
|
|
335
|
+
* @returns {{ valid: boolean, mirrored: boolean }}
|
|
336
|
+
*/
|
|
337
|
+
static #sourceCoordinateMirrorAxis(center, source, ownerAxis) {
|
|
338
|
+
const tolerance =
|
|
339
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
340
|
+
.#SOURCE_COORDINATE_MIRROR_TOLERANCE_MIL
|
|
341
|
+
const offset = Math.abs(Number(center || 0) - Number(source || 0))
|
|
342
|
+
if (offset <= tolerance) {
|
|
343
|
+
return { valid: true, mirrored: false }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const mirroredCenter = 2 * Number(ownerAxis) - Number(center || 0)
|
|
347
|
+
const mirrorError = Math.abs(mirroredCenter - Number(source || 0))
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
valid:
|
|
351
|
+
Number.isFinite(ownerAxis) &&
|
|
352
|
+
offset >
|
|
353
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
354
|
+
.#SOURCE_COORDINATE_MIRROR_MIN_OFFSET_MIL &&
|
|
355
|
+
mirrorError <= tolerance,
|
|
356
|
+
mirrored: true
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Converts one source-space vertex into render source-space coordinates.
|
|
362
|
+
* @param {{ x?: number, y?: number }} vertex Source vertex.
|
|
363
|
+
* @param {{ mirrorAxisX?: number, mirrorAxisY?: number } | null} mirror Optional mirror transform.
|
|
364
|
+
* @returns {{ x: number, y: number }}
|
|
365
|
+
*/
|
|
366
|
+
static #normalizeVertex(vertex, mirror) {
|
|
367
|
+
const sourceX = Number(vertex?.x || 0)
|
|
368
|
+
const sourceY = Number(vertex?.y || 0)
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
x: Number.isFinite(mirror?.mirrorAxisX)
|
|
372
|
+
? 2 * mirror.mirrorAxisX - sourceX
|
|
373
|
+
: sourceX,
|
|
374
|
+
y: Number.isFinite(mirror?.mirrorAxisY)
|
|
375
|
+
? 2 * mirror.mirrorAxisY - sourceY
|
|
376
|
+
: sourceY
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Checks whether polygon vertices use board/source coordinates instead of
|
|
382
|
+
* small body-local coordinates.
|
|
383
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
384
|
+
* @param {{ x?: number, y?: number }[]} vertices Vertices.
|
|
385
|
+
* @returns {boolean}
|
|
386
|
+
*/
|
|
387
|
+
static #usesSourceCoordinateFrame(componentBody, vertices) {
|
|
388
|
+
const center =
|
|
389
|
+
PcbScene3dStaticBodyPlacementBuilder.#polygonBoundsCenter(vertices)
|
|
390
|
+
const source =
|
|
391
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
Math.max(
|
|
395
|
+
Math.abs(center.x),
|
|
396
|
+
Math.abs(center.y),
|
|
397
|
+
Math.abs(source.x),
|
|
398
|
+
Math.abs(source.y)
|
|
399
|
+
) > 1000
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Resolves the axis-aligned polygon bounds center.
|
|
405
|
+
* @param {{ x?: number, y?: number }[]} vertices Vertices.
|
|
406
|
+
* @returns {{ x: number, y: number }}
|
|
407
|
+
*/
|
|
408
|
+
static #polygonBoundsCenter(vertices) {
|
|
409
|
+
const points = (Array.isArray(vertices) ? vertices : []).map(
|
|
410
|
+
(vertex) => ({
|
|
411
|
+
x: Number(vertex?.x || 0),
|
|
412
|
+
y: Number(vertex?.y || 0)
|
|
413
|
+
})
|
|
414
|
+
)
|
|
415
|
+
const xs = points.map((point) => point.x)
|
|
416
|
+
const ys = points.map((point) => point.y)
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
x: (Math.min(...xs) + Math.max(...xs)) / 2,
|
|
420
|
+
y: (Math.min(...ys) + Math.max(...ys)) / 2
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Resolves optional display metadata for static body rendering.
|
|
426
|
+
* @param {{ bodyColor?: object, bodyOpacity?: number | string }} componentBody Component body.
|
|
427
|
+
* @returns {{ bodyColor?: object, bodyOpacity?: number }}
|
|
428
|
+
*/
|
|
429
|
+
static #displayMetadata(componentBody) {
|
|
430
|
+
const metadata = {}
|
|
431
|
+
if (
|
|
432
|
+
componentBody?.bodyColor &&
|
|
433
|
+
typeof componentBody.bodyColor === 'object'
|
|
434
|
+
) {
|
|
435
|
+
metadata.bodyColor = componentBody.bodyColor
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const opacity = PcbScene3dStaticBodyRecovery.renderableOpacity(
|
|
439
|
+
componentBody?.bodyOpacity
|
|
440
|
+
)
|
|
441
|
+
if (opacity !== undefined) {
|
|
442
|
+
metadata.bodyOpacity = opacity
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return metadata
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Resolves the stable source identity shared by sibling static bodies.
|
|
450
|
+
* @param {{ identifier?: string, name?: string, modelId?: string }} componentBody Component body.
|
|
451
|
+
* @returns {string}
|
|
452
|
+
*/
|
|
453
|
+
static #sourceIdentityKey(componentBody) {
|
|
454
|
+
return String(
|
|
455
|
+
componentBody?.identifier ||
|
|
456
|
+
componentBody?.name ||
|
|
457
|
+
componentBody?.modelId ||
|
|
458
|
+
''
|
|
459
|
+
).trim()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Checks whether an unowned static body is likely a translucent mechanical
|
|
464
|
+
* overlay instead of a component package.
|
|
465
|
+
* @param {{ componentIndex?: number, embedded?: boolean, bodyOpacity?: number | string, identifier?: string, name?: string, positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
466
|
+
* @param {object} geometry Static geometry.
|
|
467
|
+
* @param {{ componentIndex?: number, x?: number, y?: number, pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object }[]} components Components.
|
|
468
|
+
* @param {{ x: number, y: number }} sourcePosition Source-space placement center.
|
|
469
|
+
* @returns {boolean}
|
|
470
|
+
*/
|
|
471
|
+
static #shouldSuppressStaticBody(
|
|
472
|
+
componentBody,
|
|
473
|
+
geometry,
|
|
474
|
+
components,
|
|
475
|
+
sourcePosition
|
|
476
|
+
) {
|
|
477
|
+
return (
|
|
478
|
+
PcbScene3dStaticBodyPlacementBuilder.#isTranslucentBody(
|
|
479
|
+
componentBody
|
|
480
|
+
) &&
|
|
481
|
+
!PcbScene3dStaticBodyPlacementBuilder.#hasLikelyComponentOwner(
|
|
482
|
+
componentBody,
|
|
483
|
+
geometry,
|
|
484
|
+
components,
|
|
485
|
+
sourcePosition
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Checks whether a body was authored with visible partial transparency.
|
|
492
|
+
* @param {{ bodyOpacity?: number | string }} componentBody Component body.
|
|
493
|
+
* @returns {boolean}
|
|
494
|
+
*/
|
|
495
|
+
static #isTranslucentBody(componentBody) {
|
|
496
|
+
return (
|
|
497
|
+
PcbScene3dStaticBodyRecovery.renderableOpacity(
|
|
498
|
+
componentBody?.bodyOpacity
|
|
499
|
+
) !== undefined
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Checks whether an otherwise unmatched body still has a plausible owning
|
|
505
|
+
* component.
|
|
506
|
+
* @param {{ componentIndex?: number, embedded?: boolean, identifier?: string, name?: string, positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
507
|
+
* @param {object} geometry Static geometry.
|
|
508
|
+
* @param {{ componentIndex?: number, x?: number, y?: number, pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object }[]} components Components.
|
|
509
|
+
* @param {{ x: number, y: number }} sourcePosition Source-space placement center.
|
|
510
|
+
* @returns {boolean}
|
|
511
|
+
*/
|
|
512
|
+
static #hasLikelyComponentOwner(
|
|
513
|
+
componentBody,
|
|
514
|
+
geometry,
|
|
515
|
+
components,
|
|
516
|
+
sourcePosition
|
|
517
|
+
) {
|
|
518
|
+
if (componentBody?.embedded) {
|
|
519
|
+
return true
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const componentIndex = Number(componentBody?.componentIndex)
|
|
523
|
+
if (
|
|
524
|
+
Number.isInteger(componentIndex) &&
|
|
525
|
+
(Array.isArray(components) ? components : []).some(
|
|
526
|
+
(component) =>
|
|
527
|
+
Number(component?.componentIndex) === componentIndex
|
|
528
|
+
)
|
|
529
|
+
) {
|
|
530
|
+
return true
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const maxSpanMil =
|
|
534
|
+
PcbScene3dStaticBodyPlacementBuilder.#geometryMaxSpan(geometry)
|
|
535
|
+
const genericMechanical =
|
|
536
|
+
PcbScene3dStaticBodyPlacementBuilder.#hasGenericMechanicalIdentity(
|
|
537
|
+
componentBody
|
|
538
|
+
)
|
|
539
|
+
const sourceBounds =
|
|
540
|
+
PcbScene3dStaticBodyPlacementBuilder.#geometrySourceBounds(
|
|
541
|
+
geometry,
|
|
542
|
+
sourcePosition
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
return (Array.isArray(components) ? components : []).some(
|
|
546
|
+
(component) => {
|
|
547
|
+
const distance =
|
|
548
|
+
PcbScene3dStaticBodyPlacementBuilder.#distanceBetweenBodyAndComponent(
|
|
549
|
+
componentBody,
|
|
550
|
+
component
|
|
551
|
+
)
|
|
552
|
+
const affinityScore =
|
|
553
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
554
|
+
componentBody,
|
|
555
|
+
component
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
return (
|
|
559
|
+
(genericMechanical &&
|
|
560
|
+
PcbScene3dStaticBodyPlacementBuilder.#isShieldOwner(
|
|
561
|
+
component
|
|
562
|
+
) &&
|
|
563
|
+
(distance <=
|
|
564
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
565
|
+
.#OWNER_AFFINITY_DISTANCE_MIL ||
|
|
566
|
+
PcbScene3dStaticBodyPlacementBuilder.#boundsContainPoint(
|
|
567
|
+
sourceBounds,
|
|
568
|
+
component,
|
|
569
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
570
|
+
.#OWNER_BOUNDS_TOLERANCE_MIL
|
|
571
|
+
))) ||
|
|
572
|
+
(!genericMechanical &&
|
|
573
|
+
affinityScore > 0 &&
|
|
574
|
+
distance <=
|
|
575
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
576
|
+
.#OWNER_AFFINITY_DISTANCE_MIL) ||
|
|
577
|
+
(!genericMechanical &&
|
|
578
|
+
distance <=
|
|
579
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
580
|
+
.#OWNER_EXACT_DISTANCE_MIL &&
|
|
581
|
+
maxSpanMil <=
|
|
582
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
583
|
+
.#OWNER_EXACT_MAX_SPAN_MIL)
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Resolves source-space bounds for one static polygon geometry.
|
|
591
|
+
* @param {object} geometry Static geometry.
|
|
592
|
+
* @param {{ x?: number, y?: number }} sourcePosition Source-space placement center.
|
|
593
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
594
|
+
*/
|
|
595
|
+
static #geometrySourceBounds(geometry, sourcePosition) {
|
|
596
|
+
if (
|
|
597
|
+
!Array.isArray(geometry?.verticesMil) ||
|
|
598
|
+
!geometry.verticesMil.length
|
|
599
|
+
) {
|
|
600
|
+
return null
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const points = geometry.verticesMil
|
|
604
|
+
.map((vertex) => ({
|
|
605
|
+
x: Number(vertex?.x || 0) + Number(sourcePosition?.x || 0),
|
|
606
|
+
y: Number(vertex?.y || 0) + Number(sourcePosition?.y || 0)
|
|
607
|
+
}))
|
|
608
|
+
.filter(
|
|
609
|
+
(point) => Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
610
|
+
)
|
|
611
|
+
if (!points.length) {
|
|
612
|
+
return null
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const xs = points.map((point) => point.x)
|
|
616
|
+
const ys = points.map((point) => point.y)
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
minX: Math.min(...xs),
|
|
620
|
+
minY: Math.min(...ys),
|
|
621
|
+
maxX: Math.max(...xs),
|
|
622
|
+
maxY: Math.max(...ys)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Checks whether bounds contain a component point with tolerance.
|
|
628
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number } | null} bounds Bounds.
|
|
629
|
+
* @param {{ x?: number, y?: number }} point Candidate point.
|
|
630
|
+
* @param {number} toleranceMil Bounds tolerance.
|
|
631
|
+
* @returns {boolean}
|
|
632
|
+
*/
|
|
633
|
+
static #boundsContainPoint(bounds, point, toleranceMil) {
|
|
634
|
+
if (!bounds) {
|
|
635
|
+
return false
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const x = Number(point?.x || 0)
|
|
639
|
+
const y = Number(point?.y || 0)
|
|
640
|
+
const tolerance = Math.max(Number(toleranceMil || 0), 0)
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
Number.isFinite(x) &&
|
|
644
|
+
Number.isFinite(y) &&
|
|
645
|
+
x >= bounds.minX - tolerance &&
|
|
646
|
+
x <= bounds.maxX + tolerance &&
|
|
647
|
+
y >= bounds.minY - tolerance &&
|
|
648
|
+
y <= bounds.maxY + tolerance
|
|
649
|
+
)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Checks whether a component identity describes a shield owner strongly
|
|
654
|
+
* enough to keep generic translucent shield sub-bodies.
|
|
655
|
+
* @param {{ designator?: string, pattern?: string, source?: string, description?: string, parameters?: object, provenance?: object }} component Component row.
|
|
656
|
+
* @returns {boolean}
|
|
657
|
+
*/
|
|
658
|
+
static #isShieldOwner(component) {
|
|
659
|
+
const identityText = [
|
|
660
|
+
component?.pattern,
|
|
661
|
+
component?.source,
|
|
662
|
+
component?.description,
|
|
663
|
+
...Object.values(component?.parameters || {}),
|
|
664
|
+
...Object.values(component?.provenance || {})
|
|
665
|
+
]
|
|
666
|
+
.join(' ')
|
|
667
|
+
.toLowerCase()
|
|
668
|
+
|
|
669
|
+
return /(?:^|[^a-z0-9])(?:emi|rfi|shield)(?:$|[^a-z0-9])/u.test(
|
|
670
|
+
identityText
|
|
671
|
+
)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Checks for generic mechanical labels that are too weak to prove package
|
|
676
|
+
* ownership for translucent static bodies.
|
|
677
|
+
* @param {{ identifier?: string, name?: string }} componentBody Component body.
|
|
678
|
+
* @returns {boolean}
|
|
679
|
+
*/
|
|
680
|
+
static #hasGenericMechanicalIdentity(componentBody) {
|
|
681
|
+
return PcbScene3dStaticBodyPlacementBuilder.#identityTokens(
|
|
682
|
+
componentBody
|
|
683
|
+
).some((token) =>
|
|
684
|
+
PcbScene3dStaticBodyPlacementBuilder.#GENERIC_MECHANICAL_IDENTITY_TOKENS.has(
|
|
685
|
+
token
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Collects normalized identity tokens from one body row.
|
|
692
|
+
* @param {{ identifier?: string, name?: string }} componentBody Component body.
|
|
693
|
+
* @returns {string[]}
|
|
694
|
+
*/
|
|
695
|
+
static #identityTokens(componentBody) {
|
|
696
|
+
return [componentBody?.identifier, componentBody?.name]
|
|
697
|
+
.join(' ')
|
|
698
|
+
.replace(/([a-z])([A-Z])/gu, '$1 $2')
|
|
699
|
+
.toLowerCase()
|
|
700
|
+
.split(/[^a-z0-9]+/g)
|
|
701
|
+
.flatMap((fragment) => fragment.match(/[a-z]+|\d+/g) || [])
|
|
702
|
+
.filter(Boolean)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Resolves the largest horizontal span of a static body.
|
|
707
|
+
* @param {object} geometry Static geometry.
|
|
708
|
+
* @returns {number}
|
|
709
|
+
*/
|
|
710
|
+
static #geometryMaxSpan(geometry) {
|
|
711
|
+
if (
|
|
712
|
+
Array.isArray(geometry?.verticesMil) &&
|
|
713
|
+
geometry.verticesMil.length
|
|
714
|
+
) {
|
|
715
|
+
const points = geometry.verticesMil.map((vertex) => ({
|
|
716
|
+
x: Number(vertex?.x || 0),
|
|
717
|
+
y: Number(vertex?.y || 0)
|
|
718
|
+
}))
|
|
719
|
+
const xs = points.map((point) => point.x)
|
|
720
|
+
const ys = points.map((point) => point.y)
|
|
721
|
+
|
|
722
|
+
return Math.max(
|
|
723
|
+
Math.max(...xs) - Math.min(...xs),
|
|
724
|
+
Math.max(...ys) - Math.min(...ys)
|
|
725
|
+
)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const radius = Number(geometry?.radiusMil)
|
|
729
|
+
|
|
730
|
+
return Number.isFinite(radius) && radius > 0 ? radius * 2 : 0
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Returns the euclidean distance between body and component anchors.
|
|
735
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
736
|
+
* @param {{ x?: number, y?: number }} component Component.
|
|
737
|
+
* @returns {number}
|
|
738
|
+
*/
|
|
739
|
+
static #distanceBetweenBodyAndComponent(componentBody, component) {
|
|
740
|
+
return Math.hypot(
|
|
741
|
+
Number(component?.x || 0) -
|
|
742
|
+
Number(componentBody?.positionMil?.x || 0),
|
|
743
|
+
Number(component?.y || 0) -
|
|
744
|
+
Number(componentBody?.positionMil?.y || 0)
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
|
|
126
748
|
/**
|
|
127
749
|
* Returns the native body anchor.
|
|
128
750
|
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|