altium-toolkit 1.1.33 → 1.1.36
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 +2 -2
- package/docs/model-format.md +2 -3
- package/package.json +1 -1
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +255 -1
- package/src/core/altium/AltiumPcbLibExporter.mjs +6 -1
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +120 -15
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +14 -3
- package/src/core/altium/PcbShapeBasedBodyGeometryParser.mjs +85 -11
- 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/core/altium/SourceComponentBundleNormalizer.mjs +31 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/parser.mjs +1 -0
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +30 -2
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +6 -2
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +44 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +171 -6
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +225 -17
- package/src/ui/AltiumScene3dTwoRowFootprintDetector.mjs +90 -0
- package/src/ui/PcbScene3dBuilder.mjs +470 -20
- 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 +31 -4
- package/src/ui/PcbScene3dPadYawResolver.mjs +200 -0
- package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +899 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +471 -35
- package/src/ui/PcbScene3dStaticBodyPrototypeRecovery.mjs +445 -0
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +897 -0
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +555 -0
- package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +438 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +897 -0
- package/src/ui/SchematicColorResolver.mjs +44 -0
- package/src/ui/SchematicLineColorResolver.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +13 -13
|
@@ -3,6 +3,9 @@
|
|
|
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'
|
|
7
|
+
import { PcbScene3dStaticBodyRecovery } from './PcbScene3dStaticBodyRecovery.mjs'
|
|
8
|
+
import { PcbScene3dStaticBodySelectionKeyBuilder } from './PcbScene3dStaticBodySelectionKeyBuilder.mjs'
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Builds scene placements for static shape-based 3D bodies.
|
|
@@ -12,10 +15,14 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
12
15
|
static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
|
|
13
16
|
static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
|
|
14
17
|
static #OWNER_AFFINITY_DISTANCE_MIL = 600
|
|
18
|
+
static #OWNER_BOUNDS_TOLERANCE_MIL = 20
|
|
15
19
|
static #OWNER_EXACT_DISTANCE_MIL = 5
|
|
16
20
|
static #OWNER_EXACT_MAX_SPAN_MIL = 500
|
|
21
|
+
static #SOURCE_COORDINATE_MIRROR_TOLERANCE_MIL = 5
|
|
22
|
+
static #SOURCE_COORDINATE_MIRROR_MIN_OFFSET_MIL = 20
|
|
17
23
|
static #GENERIC_MECHANICAL_IDENTITY_TOKENS = new Set([
|
|
18
24
|
'can',
|
|
25
|
+
'clip',
|
|
19
26
|
'cover',
|
|
20
27
|
'enclosure',
|
|
21
28
|
'frame',
|
|
@@ -34,7 +41,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
34
41
|
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads Pads.
|
|
35
42
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
36
43
|
* @param {number} thicknessMil Board thickness.
|
|
37
|
-
* @returns {{ designator: 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 }, geometry: object }[]}
|
|
38
45
|
*/
|
|
39
46
|
static build(
|
|
40
47
|
componentBodies,
|
|
@@ -44,9 +51,14 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
44
51
|
board,
|
|
45
52
|
thicknessMil
|
|
46
53
|
) {
|
|
47
|
-
|
|
54
|
+
const recoveredBodies = PcbScene3dStaticBodyRecovery.recover(
|
|
55
|
+
componentBodies,
|
|
56
|
+
bodyMatches
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const placementRows = recoveredBodies
|
|
48
60
|
.map((componentBody, index) =>
|
|
49
|
-
PcbScene3dStaticBodyPlacementBuilder.#
|
|
61
|
+
PcbScene3dStaticBodyPlacementBuilder.#buildPlacementRow(
|
|
50
62
|
componentBody,
|
|
51
63
|
bodyMatches?.[index] || null,
|
|
52
64
|
components,
|
|
@@ -55,6 +67,14 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
55
67
|
)
|
|
56
68
|
)
|
|
57
69
|
.filter(Boolean)
|
|
70
|
+
|
|
71
|
+
PcbScene3dStaticBodyOwnerPromotion.promote(
|
|
72
|
+
placementRows,
|
|
73
|
+
components,
|
|
74
|
+
board
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return PcbScene3dStaticBodySelectionKeyBuilder.assign(placementRows)
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
/**
|
|
@@ -66,6 +86,38 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
66
86
|
return Boolean(geometry && geometry.status === 'complete')
|
|
67
87
|
}
|
|
68
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Builds one static body placement row with its matched owner context.
|
|
91
|
+
* @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.
|
|
92
|
+
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent Matched component.
|
|
93
|
+
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
94
|
+
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
95
|
+
* @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}
|
|
97
|
+
*/
|
|
98
|
+
static #buildPlacementRow(
|
|
99
|
+
componentBody,
|
|
100
|
+
matchedComponent,
|
|
101
|
+
components,
|
|
102
|
+
board,
|
|
103
|
+
thicknessMil
|
|
104
|
+
) {
|
|
105
|
+
const placement = PcbScene3dStaticBodyPlacementBuilder.#buildPlacement(
|
|
106
|
+
componentBody,
|
|
107
|
+
matchedComponent,
|
|
108
|
+
components,
|
|
109
|
+
board,
|
|
110
|
+
thicknessMil
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return placement
|
|
114
|
+
? {
|
|
115
|
+
placement,
|
|
116
|
+
matchedComponent
|
|
117
|
+
}
|
|
118
|
+
: null
|
|
119
|
+
}
|
|
120
|
+
|
|
69
121
|
/**
|
|
70
122
|
* Builds one static shape-body scene placement.
|
|
71
123
|
* @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.
|
|
@@ -73,7 +125,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
73
125
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
|
|
74
126
|
* @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
|
|
75
127
|
* @param {number} thicknessMil Board thickness.
|
|
76
|
-
* @returns {{ designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object } | null}
|
|
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}
|
|
77
129
|
*/
|
|
78
130
|
static #buildPlacement(
|
|
79
131
|
componentBody,
|
|
@@ -82,24 +134,30 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
82
134
|
board,
|
|
83
135
|
thicknessMil
|
|
84
136
|
) {
|
|
85
|
-
const
|
|
137
|
+
const staticGeometry =
|
|
86
138
|
PcbScene3dStaticBodyPlacementBuilder.#normalizeStaticGeometry(
|
|
87
139
|
componentBody,
|
|
88
|
-
componentBody?.staticGeometry
|
|
140
|
+
componentBody?.staticGeometry,
|
|
141
|
+
matchedComponent
|
|
89
142
|
)
|
|
143
|
+
const geometry = staticGeometry.geometry
|
|
90
144
|
|
|
91
145
|
if (
|
|
92
146
|
!PcbScene3dStaticBodyPlacementBuilder.#isCompleteGeometry(geometry)
|
|
93
147
|
) {
|
|
94
148
|
return null
|
|
95
149
|
}
|
|
150
|
+
const sourcePosition =
|
|
151
|
+
staticGeometry.placementCenterMil ||
|
|
152
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
|
|
96
153
|
|
|
97
154
|
if (
|
|
98
155
|
!matchedComponent &&
|
|
99
156
|
PcbScene3dStaticBodyPlacementBuilder.#shouldSuppressStaticBody(
|
|
100
157
|
componentBody,
|
|
101
158
|
geometry,
|
|
102
|
-
components
|
|
159
|
+
components,
|
|
160
|
+
sourcePosition
|
|
103
161
|
)
|
|
104
162
|
) {
|
|
105
163
|
return null
|
|
@@ -122,8 +180,6 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
122
180
|
components,
|
|
123
181
|
board
|
|
124
182
|
)
|
|
125
|
-
const sourcePosition =
|
|
126
|
-
PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
|
|
127
183
|
const heightMil =
|
|
128
184
|
PcbScene3dStaticBodyPlacementBuilder.#geometryHeight(geometry)
|
|
129
185
|
const standoffMil = Math.abs(
|
|
@@ -135,6 +191,12 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
135
191
|
Number(thicknessMil || 0) / 2 +
|
|
136
192
|
(Number.isFinite(standoffMil) ? standoffMil : 0) +
|
|
137
193
|
heightMil / 2
|
|
194
|
+
const renderGeometry =
|
|
195
|
+
PcbScene3dStaticBodyPlacementBuilder.#placementGeometry(
|
|
196
|
+
geometry,
|
|
197
|
+
staticGeometry,
|
|
198
|
+
mountSide
|
|
199
|
+
)
|
|
138
200
|
|
|
139
201
|
return {
|
|
140
202
|
designator:
|
|
@@ -142,11 +204,25 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
142
204
|
String(
|
|
143
205
|
componentBody.identifier || componentBody.name || '3D body'
|
|
144
206
|
),
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
207
|
+
sourceIdentityKey:
|
|
208
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourceIdentityKey(
|
|
209
|
+
componentBody
|
|
210
|
+
),
|
|
211
|
+
sourceCoordinateFrame: Boolean(
|
|
212
|
+
staticGeometry.sourceCoordinateFrame
|
|
149
213
|
),
|
|
214
|
+
mountSideLocked:
|
|
215
|
+
PcbScene3dStaticBodyPlacementBuilder.#hasExplicitMountSide(
|
|
216
|
+
componentBody,
|
|
217
|
+
matchedComponent
|
|
218
|
+
),
|
|
219
|
+
mountSide,
|
|
220
|
+
rotationDeg:
|
|
221
|
+
PcbScene3dStaticBodyPlacementBuilder.#placementRotationDeg(
|
|
222
|
+
componentBody,
|
|
223
|
+
matchedComponent,
|
|
224
|
+
staticGeometry
|
|
225
|
+
),
|
|
150
226
|
positionMil: {
|
|
151
227
|
x: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
152
228
|
Number(sourcePosition.x || 0) - Number(board.centerX || 0)
|
|
@@ -165,7 +241,107 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
165
241
|
...PcbScene3dStaticBodyPlacementBuilder.#displayMetadata(
|
|
166
242
|
componentBody
|
|
167
243
|
),
|
|
168
|
-
geometry
|
|
244
|
+
geometry: renderGeometry
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Checks whether one body carries explicit top/bottom side evidence.
|
|
250
|
+
* @param {{ componentIndex?: number | string, layer?: string, standoffHeightMil?: number | string | null }} componentBody Component body.
|
|
251
|
+
* @param {{ layer?: string } | null} matchedComponent Matched owner component.
|
|
252
|
+
* @returns {boolean}
|
|
253
|
+
*/
|
|
254
|
+
static #hasExplicitMountSide(componentBody, matchedComponent) {
|
|
255
|
+
if (
|
|
256
|
+
matchedComponent &&
|
|
257
|
+
Number.isInteger(Number(componentBody?.componentIndex))
|
|
258
|
+
) {
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const layer = String(componentBody?.layer || '').trim()
|
|
263
|
+
if (!layer) {
|
|
264
|
+
const standoffMil = Number(componentBody?.standoffHeightMil)
|
|
265
|
+
return Number.isFinite(standoffMil) && standoffMil < 0
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (
|
|
269
|
+
!PcbScene3dStaticBodyPlacementBuilder.#isAuthoredMechanicalBody(
|
|
270
|
+
componentBody
|
|
271
|
+
)
|
|
272
|
+
) {
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (/(^|[^a-z])(top|bottom)([^a-z]|$)/i.test(layer)) {
|
|
277
|
+
return true
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return /^MECHANICAL\s*\d+$/i.test(layer)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Checks whether one body identity looks like authored board mechanics.
|
|
285
|
+
* @param {{ identifier?: string, name?: string, modelTypeName?: string }} componentBody Component body.
|
|
286
|
+
* @returns {boolean}
|
|
287
|
+
*/
|
|
288
|
+
static #isAuthoredMechanicalBody(componentBody) {
|
|
289
|
+
return PcbScene3dStaticBodyPlacementBuilder.#identityTokens(
|
|
290
|
+
componentBody
|
|
291
|
+
).some((token) =>
|
|
292
|
+
PcbScene3dStaticBodyPlacementBuilder.#GENERIC_MECHANICAL_IDENTITY_TOKENS.has(
|
|
293
|
+
token
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Resolves the rendered rotation for one static shape body.
|
|
300
|
+
* @param {{ rotationDeg?: number }} componentBody Component body.
|
|
301
|
+
* @param {{ rotation?: number } | null} matchedComponent Matched owner component.
|
|
302
|
+
* @param {{ sourceCoordinateFrame?: boolean }} normalizedStaticGeometry Normalized static geometry context.
|
|
303
|
+
* @returns {number}
|
|
304
|
+
*/
|
|
305
|
+
static #placementRotationDeg(
|
|
306
|
+
componentBody,
|
|
307
|
+
matchedComponent,
|
|
308
|
+
normalizedStaticGeometry
|
|
309
|
+
) {
|
|
310
|
+
const ownerRotation = normalizedStaticGeometry?.sourceCoordinateFrame
|
|
311
|
+
? 0
|
|
312
|
+
: Number(matchedComponent?.rotation || 0)
|
|
313
|
+
|
|
314
|
+
return PcbScene3dStaticBodyPlacementBuilder.#normalizeAngle(
|
|
315
|
+
Number(componentBody.rotationDeg || 0) + ownerRotation
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Converts normalized static geometry into the renderer mount-local frame.
|
|
321
|
+
* @param {object} geometry Normalized render geometry.
|
|
322
|
+
* @param {{ sourceCoordinateFrame?: boolean }} normalizedStaticGeometry Normalized static geometry context.
|
|
323
|
+
* @param {string} mountSide Resolved mount side.
|
|
324
|
+
* @returns {object}
|
|
325
|
+
*/
|
|
326
|
+
static #placementGeometry(geometry, normalizedStaticGeometry, mountSide) {
|
|
327
|
+
if (
|
|
328
|
+
!normalizedStaticGeometry?.sourceCoordinateFrame ||
|
|
329
|
+
String(mountSide || 'top').toLowerCase() !== 'bottom' ||
|
|
330
|
+
!Array.isArray(geometry?.verticesMil)
|
|
331
|
+
) {
|
|
332
|
+
return geometry
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
...geometry,
|
|
337
|
+
verticesMil: geometry.verticesMil.map((vertex) => ({
|
|
338
|
+
x: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
339
|
+
Number(vertex?.x || 0)
|
|
340
|
+
),
|
|
341
|
+
y: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
342
|
+
-Number(vertex?.y || 0)
|
|
343
|
+
)
|
|
344
|
+
}))
|
|
169
345
|
}
|
|
170
346
|
}
|
|
171
347
|
|
|
@@ -173,36 +349,161 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
173
349
|
* Converts source-space polygon vertices into render-local geometry.
|
|
174
350
|
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
175
351
|
* @param {object | undefined} geometry Static geometry.
|
|
176
|
-
* @
|
|
352
|
+
* @param {{ x?: number, y?: number } | null} matchedComponent Matched owner component.
|
|
353
|
+
* @returns {{ geometry: object | undefined, placementCenterMil?: { x: number, y: number }, sourceCoordinateFrame: boolean }}
|
|
177
354
|
*/
|
|
178
|
-
static #normalizeStaticGeometry(componentBody, geometry) {
|
|
355
|
+
static #normalizeStaticGeometry(componentBody, geometry, matchedComponent) {
|
|
179
356
|
if (
|
|
180
357
|
String(geometry?.kind || '').toLowerCase() !== 'extruded-polygon' ||
|
|
181
358
|
!Array.isArray(geometry?.verticesMil) ||
|
|
182
|
-
geometry.verticesMil.length < 3
|
|
359
|
+
geometry.verticesMil.length < 3
|
|
360
|
+
) {
|
|
361
|
+
return { geometry, sourceCoordinateFrame: false }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (
|
|
183
365
|
!PcbScene3dStaticBodyPlacementBuilder.#usesSourceCoordinateFrame(
|
|
184
366
|
componentBody,
|
|
185
367
|
geometry.verticesMil
|
|
186
368
|
)
|
|
187
369
|
) {
|
|
188
|
-
return geometry
|
|
370
|
+
return { geometry, sourceCoordinateFrame: false }
|
|
189
371
|
}
|
|
190
372
|
|
|
191
373
|
const center =
|
|
192
374
|
PcbScene3dStaticBodyPlacementBuilder.#polygonBoundsCenter(
|
|
193
375
|
geometry.verticesMil
|
|
194
376
|
)
|
|
377
|
+
const mirror =
|
|
378
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourceCoordinateMirror(
|
|
379
|
+
componentBody,
|
|
380
|
+
center,
|
|
381
|
+
matchedComponent
|
|
382
|
+
)
|
|
383
|
+
const placementCenter = mirror?.placementCenterMil || center
|
|
195
384
|
|
|
196
385
|
return {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
386
|
+
placementCenterMil: placementCenter,
|
|
387
|
+
sourceCoordinateFrame: true,
|
|
388
|
+
geometry: {
|
|
389
|
+
...geometry,
|
|
390
|
+
verticesMil: geometry.verticesMil.map((vertex) => {
|
|
391
|
+
const normalizedVertex =
|
|
392
|
+
PcbScene3dStaticBodyPlacementBuilder.#normalizeVertex(
|
|
393
|
+
vertex,
|
|
394
|
+
mirror
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
x: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
399
|
+
normalizedVertex.x - placementCenter.x
|
|
400
|
+
),
|
|
401
|
+
y: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
402
|
+
normalizedVertex.y - placementCenter.y
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Resolves owner-axis mirror transforms for source-coordinate polygons
|
|
412
|
+
* whose body anchor is opposite their raw polygon bounds.
|
|
413
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
414
|
+
* @param {{ x: number, y: number }} boundsCenter Source-coordinate polygon bounds center.
|
|
415
|
+
* @param {{ x?: number, y?: number } | null} matchedComponent Matched owner component.
|
|
416
|
+
* @returns {{ mirrorAxisX?: number, mirrorAxisY?: number, placementCenterMil: { x: number, y: number } } | null}
|
|
417
|
+
*/
|
|
418
|
+
static #sourceCoordinateMirror(
|
|
419
|
+
componentBody,
|
|
420
|
+
boundsCenter,
|
|
421
|
+
matchedComponent
|
|
422
|
+
) {
|
|
423
|
+
const source =
|
|
424
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
|
|
425
|
+
const xDecision =
|
|
426
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourceCoordinateMirrorAxis(
|
|
427
|
+
boundsCenter.x,
|
|
428
|
+
source.x,
|
|
429
|
+
Number(matchedComponent?.x)
|
|
430
|
+
)
|
|
431
|
+
const yDecision =
|
|
432
|
+
PcbScene3dStaticBodyPlacementBuilder.#sourceCoordinateMirrorAxis(
|
|
433
|
+
boundsCenter.y,
|
|
434
|
+
source.y,
|
|
435
|
+
Number(matchedComponent?.y)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if (
|
|
439
|
+
!xDecision.valid ||
|
|
440
|
+
!yDecision.valid ||
|
|
441
|
+
(!xDecision.mirrored && !yDecision.mirrored)
|
|
442
|
+
) {
|
|
443
|
+
return null
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
mirrorAxisX: xDecision.mirrored
|
|
448
|
+
? Number(matchedComponent?.x)
|
|
449
|
+
: undefined,
|
|
450
|
+
mirrorAxisY: yDecision.mirrored
|
|
451
|
+
? Number(matchedComponent?.y)
|
|
452
|
+
: undefined,
|
|
453
|
+
placementCenterMil: {
|
|
454
|
+
x: source.x,
|
|
455
|
+
y: source.y
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Checks whether one source-coordinate axis is aligned or mirrored.
|
|
462
|
+
* @param {number} center Raw polygon center coordinate.
|
|
463
|
+
* @param {number} source Body source coordinate.
|
|
464
|
+
* @param {number} ownerAxis Owner coordinate.
|
|
465
|
+
* @returns {{ valid: boolean, mirrored: boolean }}
|
|
466
|
+
*/
|
|
467
|
+
static #sourceCoordinateMirrorAxis(center, source, ownerAxis) {
|
|
468
|
+
const tolerance =
|
|
469
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
470
|
+
.#SOURCE_COORDINATE_MIRROR_TOLERANCE_MIL
|
|
471
|
+
const offset = Math.abs(Number(center || 0) - Number(source || 0))
|
|
472
|
+
if (offset <= tolerance) {
|
|
473
|
+
return { valid: true, mirrored: false }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const mirroredCenter = 2 * Number(ownerAxis) - Number(center || 0)
|
|
477
|
+
const mirrorError = Math.abs(mirroredCenter - Number(source || 0))
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
valid:
|
|
481
|
+
Number.isFinite(ownerAxis) &&
|
|
482
|
+
offset >
|
|
483
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
484
|
+
.#SOURCE_COORDINATE_MIRROR_MIN_OFFSET_MIL &&
|
|
485
|
+
mirrorError <= tolerance,
|
|
486
|
+
mirrored: true
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Converts one source-space vertex into render source-space coordinates.
|
|
492
|
+
* @param {{ x?: number, y?: number }} vertex Source vertex.
|
|
493
|
+
* @param {{ mirrorAxisX?: number, mirrorAxisY?: number } | null} mirror Optional mirror transform.
|
|
494
|
+
* @returns {{ x: number, y: number }}
|
|
495
|
+
*/
|
|
496
|
+
static #normalizeVertex(vertex, mirror) {
|
|
497
|
+
const sourceX = Number(vertex?.x || 0)
|
|
498
|
+
const sourceY = Number(vertex?.y || 0)
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
x: Number.isFinite(mirror?.mirrorAxisX)
|
|
502
|
+
? 2 * mirror.mirrorAxisX - sourceX
|
|
503
|
+
: sourceX,
|
|
504
|
+
y: Number.isFinite(mirror?.mirrorAxisY)
|
|
505
|
+
? 2 * mirror.mirrorAxisY - sourceY
|
|
506
|
+
: sourceY
|
|
206
507
|
}
|
|
207
508
|
}
|
|
208
509
|
|
|
@@ -264,23 +565,45 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
264
565
|
metadata.bodyColor = componentBody.bodyColor
|
|
265
566
|
}
|
|
266
567
|
|
|
267
|
-
const opacity =
|
|
268
|
-
|
|
568
|
+
const opacity = PcbScene3dStaticBodyRecovery.renderableOpacity(
|
|
569
|
+
componentBody?.bodyOpacity
|
|
570
|
+
)
|
|
571
|
+
if (opacity !== undefined) {
|
|
269
572
|
metadata.bodyOpacity = opacity
|
|
270
573
|
}
|
|
271
574
|
|
|
272
575
|
return metadata
|
|
273
576
|
}
|
|
274
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Resolves the stable source identity shared by sibling static bodies.
|
|
580
|
+
* @param {{ identifier?: string, name?: string, modelId?: string }} componentBody Component body.
|
|
581
|
+
* @returns {string}
|
|
582
|
+
*/
|
|
583
|
+
static #sourceIdentityKey(componentBody) {
|
|
584
|
+
return String(
|
|
585
|
+
componentBody?.identifier ||
|
|
586
|
+
componentBody?.name ||
|
|
587
|
+
componentBody?.modelId ||
|
|
588
|
+
''
|
|
589
|
+
).trim()
|
|
590
|
+
}
|
|
591
|
+
|
|
275
592
|
/**
|
|
276
593
|
* Checks whether an unowned static body is likely a translucent mechanical
|
|
277
594
|
* overlay instead of a component package.
|
|
278
595
|
* @param {{ componentIndex?: number, embedded?: boolean, bodyOpacity?: number | string, identifier?: string, name?: string, positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
279
596
|
* @param {object} geometry Static geometry.
|
|
280
597
|
* @param {{ componentIndex?: number, x?: number, y?: number, pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object }[]} components Components.
|
|
598
|
+
* @param {{ x: number, y: number }} sourcePosition Source-space placement center.
|
|
281
599
|
* @returns {boolean}
|
|
282
600
|
*/
|
|
283
|
-
static #shouldSuppressStaticBody(
|
|
601
|
+
static #shouldSuppressStaticBody(
|
|
602
|
+
componentBody,
|
|
603
|
+
geometry,
|
|
604
|
+
components,
|
|
605
|
+
sourcePosition
|
|
606
|
+
) {
|
|
284
607
|
return (
|
|
285
608
|
PcbScene3dStaticBodyPlacementBuilder.#isTranslucentBody(
|
|
286
609
|
componentBody
|
|
@@ -288,7 +611,8 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
288
611
|
!PcbScene3dStaticBodyPlacementBuilder.#hasLikelyComponentOwner(
|
|
289
612
|
componentBody,
|
|
290
613
|
geometry,
|
|
291
|
-
components
|
|
614
|
+
components,
|
|
615
|
+
sourcePosition
|
|
292
616
|
)
|
|
293
617
|
)
|
|
294
618
|
}
|
|
@@ -299,9 +623,11 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
299
623
|
* @returns {boolean}
|
|
300
624
|
*/
|
|
301
625
|
static #isTranslucentBody(componentBody) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
626
|
+
return (
|
|
627
|
+
PcbScene3dStaticBodyRecovery.renderableOpacity(
|
|
628
|
+
componentBody?.bodyOpacity
|
|
629
|
+
) !== undefined
|
|
630
|
+
)
|
|
305
631
|
}
|
|
306
632
|
|
|
307
633
|
/**
|
|
@@ -310,9 +636,15 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
310
636
|
* @param {{ componentIndex?: number, embedded?: boolean, identifier?: string, name?: string, positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
311
637
|
* @param {object} geometry Static geometry.
|
|
312
638
|
* @param {{ componentIndex?: number, x?: number, y?: number, pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object }[]} components Components.
|
|
639
|
+
* @param {{ x: number, y: number }} sourcePosition Source-space placement center.
|
|
313
640
|
* @returns {boolean}
|
|
314
641
|
*/
|
|
315
|
-
static #hasLikelyComponentOwner(
|
|
642
|
+
static #hasLikelyComponentOwner(
|
|
643
|
+
componentBody,
|
|
644
|
+
geometry,
|
|
645
|
+
components,
|
|
646
|
+
sourcePosition
|
|
647
|
+
) {
|
|
316
648
|
if (componentBody?.embedded) {
|
|
317
649
|
return true
|
|
318
650
|
}
|
|
@@ -334,6 +666,11 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
334
666
|
PcbScene3dStaticBodyPlacementBuilder.#hasGenericMechanicalIdentity(
|
|
335
667
|
componentBody
|
|
336
668
|
)
|
|
669
|
+
const sourceBounds =
|
|
670
|
+
PcbScene3dStaticBodyPlacementBuilder.#geometrySourceBounds(
|
|
671
|
+
geometry,
|
|
672
|
+
sourcePosition
|
|
673
|
+
)
|
|
337
674
|
|
|
338
675
|
return (Array.isArray(components) ? components : []).some(
|
|
339
676
|
(component) => {
|
|
@@ -349,6 +686,19 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
349
686
|
)
|
|
350
687
|
|
|
351
688
|
return (
|
|
689
|
+
(genericMechanical &&
|
|
690
|
+
PcbScene3dStaticBodyPlacementBuilder.#isShieldOwner(
|
|
691
|
+
component
|
|
692
|
+
) &&
|
|
693
|
+
(distance <=
|
|
694
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
695
|
+
.#OWNER_AFFINITY_DISTANCE_MIL ||
|
|
696
|
+
PcbScene3dStaticBodyPlacementBuilder.#boundsContainPoint(
|
|
697
|
+
sourceBounds,
|
|
698
|
+
component,
|
|
699
|
+
PcbScene3dStaticBodyPlacementBuilder
|
|
700
|
+
.#OWNER_BOUNDS_TOLERANCE_MIL
|
|
701
|
+
))) ||
|
|
352
702
|
(!genericMechanical &&
|
|
353
703
|
affinityScore > 0 &&
|
|
354
704
|
distance <=
|
|
@@ -366,6 +716,91 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
366
716
|
)
|
|
367
717
|
}
|
|
368
718
|
|
|
719
|
+
/**
|
|
720
|
+
* Resolves source-space bounds for one static polygon geometry.
|
|
721
|
+
* @param {object} geometry Static geometry.
|
|
722
|
+
* @param {{ x?: number, y?: number }} sourcePosition Source-space placement center.
|
|
723
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
724
|
+
*/
|
|
725
|
+
static #geometrySourceBounds(geometry, sourcePosition) {
|
|
726
|
+
if (
|
|
727
|
+
!Array.isArray(geometry?.verticesMil) ||
|
|
728
|
+
!geometry.verticesMil.length
|
|
729
|
+
) {
|
|
730
|
+
return null
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const points = geometry.verticesMil
|
|
734
|
+
.map((vertex) => ({
|
|
735
|
+
x: Number(vertex?.x || 0) + Number(sourcePosition?.x || 0),
|
|
736
|
+
y: Number(vertex?.y || 0) + Number(sourcePosition?.y || 0)
|
|
737
|
+
}))
|
|
738
|
+
.filter(
|
|
739
|
+
(point) => Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
740
|
+
)
|
|
741
|
+
if (!points.length) {
|
|
742
|
+
return null
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const xs = points.map((point) => point.x)
|
|
746
|
+
const ys = points.map((point) => point.y)
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
minX: Math.min(...xs),
|
|
750
|
+
minY: Math.min(...ys),
|
|
751
|
+
maxX: Math.max(...xs),
|
|
752
|
+
maxY: Math.max(...ys)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Checks whether bounds contain a component point with tolerance.
|
|
758
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number } | null} bounds Bounds.
|
|
759
|
+
* @param {{ x?: number, y?: number }} point Candidate point.
|
|
760
|
+
* @param {number} toleranceMil Bounds tolerance.
|
|
761
|
+
* @returns {boolean}
|
|
762
|
+
*/
|
|
763
|
+
static #boundsContainPoint(bounds, point, toleranceMil) {
|
|
764
|
+
if (!bounds) {
|
|
765
|
+
return false
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const x = Number(point?.x || 0)
|
|
769
|
+
const y = Number(point?.y || 0)
|
|
770
|
+
const tolerance = Math.max(Number(toleranceMil || 0), 0)
|
|
771
|
+
|
|
772
|
+
return (
|
|
773
|
+
Number.isFinite(x) &&
|
|
774
|
+
Number.isFinite(y) &&
|
|
775
|
+
x >= bounds.minX - tolerance &&
|
|
776
|
+
x <= bounds.maxX + tolerance &&
|
|
777
|
+
y >= bounds.minY - tolerance &&
|
|
778
|
+
y <= bounds.maxY + tolerance
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Checks whether a component identity describes a shield owner strongly
|
|
784
|
+
* enough to keep generic translucent shield sub-bodies.
|
|
785
|
+
* @param {{ designator?: string, pattern?: string, source?: string, description?: string, parameters?: object, provenance?: object }} component Component row.
|
|
786
|
+
* @returns {boolean}
|
|
787
|
+
*/
|
|
788
|
+
static #isShieldOwner(component) {
|
|
789
|
+
const identityText = [
|
|
790
|
+
component?.pattern,
|
|
791
|
+
component?.source,
|
|
792
|
+
component?.description,
|
|
793
|
+
...Object.values(component?.parameters || {}),
|
|
794
|
+
...Object.values(component?.provenance || {})
|
|
795
|
+
]
|
|
796
|
+
.join(' ')
|
|
797
|
+
.toLowerCase()
|
|
798
|
+
|
|
799
|
+
return /(?:^|[^a-z0-9])(?:emi|rfi|shield)(?:$|[^a-z0-9])/u.test(
|
|
800
|
+
identityText
|
|
801
|
+
)
|
|
802
|
+
}
|
|
803
|
+
|
|
369
804
|
/**
|
|
370
805
|
* Checks for generic mechanical labels that are too weak to prove package
|
|
371
806
|
* ownership for translucent static bodies.
|
|
@@ -390,6 +825,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
390
825
|
static #identityTokens(componentBody) {
|
|
391
826
|
return [componentBody?.identifier, componentBody?.name]
|
|
392
827
|
.join(' ')
|
|
828
|
+
.replace(/([a-z])([A-Z])/gu, '$1 $2')
|
|
393
829
|
.toLowerCase()
|
|
394
830
|
.split(/[^a-z0-9]+/g)
|
|
395
831
|
.flatMap((fragment) => fragment.match(/[a-z]+|\d+/g) || [])
|