altium-toolkit 1.1.35 → 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/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/AltiumScene3dShapeStackOwnerAdapter.mjs +149 -20
- package/src/ui/PcbScene3dPackages.mjs +16 -1
- package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +899 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +138 -8
- 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 +438 -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
|
@@ -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',
|
|
@@ -66,6 +68,12 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
66
68
|
)
|
|
67
69
|
.filter(Boolean)
|
|
68
70
|
|
|
71
|
+
PcbScene3dStaticBodyOwnerPromotion.promote(
|
|
72
|
+
placementRows,
|
|
73
|
+
components,
|
|
74
|
+
board
|
|
75
|
+
)
|
|
76
|
+
|
|
69
77
|
return PcbScene3dStaticBodySelectionKeyBuilder.assign(placementRows)
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -183,6 +191,12 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
183
191
|
Number(thicknessMil || 0) / 2 +
|
|
184
192
|
(Number.isFinite(standoffMil) ? standoffMil : 0) +
|
|
185
193
|
heightMil / 2
|
|
194
|
+
const renderGeometry =
|
|
195
|
+
PcbScene3dStaticBodyPlacementBuilder.#placementGeometry(
|
|
196
|
+
geometry,
|
|
197
|
+
staticGeometry,
|
|
198
|
+
mountSide
|
|
199
|
+
)
|
|
186
200
|
|
|
187
201
|
return {
|
|
188
202
|
designator:
|
|
@@ -194,11 +208,21 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
194
208
|
PcbScene3dStaticBodyPlacementBuilder.#sourceIdentityKey(
|
|
195
209
|
componentBody
|
|
196
210
|
),
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
Number(componentBody.rotationDeg || 0) +
|
|
200
|
-
Number(matchedComponent?.rotation || 0)
|
|
211
|
+
sourceCoordinateFrame: Boolean(
|
|
212
|
+
staticGeometry.sourceCoordinateFrame
|
|
201
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
|
+
),
|
|
202
226
|
positionMil: {
|
|
203
227
|
x: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
|
|
204
228
|
Number(sourcePosition.x || 0) - Number(board.centerX || 0)
|
|
@@ -217,7 +241,107 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
217
241
|
...PcbScene3dStaticBodyPlacementBuilder.#displayMetadata(
|
|
218
242
|
componentBody
|
|
219
243
|
),
|
|
220
|
-
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
|
+
}))
|
|
221
345
|
}
|
|
222
346
|
}
|
|
223
347
|
|
|
@@ -226,19 +350,24 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
226
350
|
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
227
351
|
* @param {object | undefined} geometry Static geometry.
|
|
228
352
|
* @param {{ x?: number, y?: number } | null} matchedComponent Matched owner component.
|
|
229
|
-
* @returns {{ geometry: object | undefined, placementCenterMil?: { x: number, y: number } }}
|
|
353
|
+
* @returns {{ geometry: object | undefined, placementCenterMil?: { x: number, y: number }, sourceCoordinateFrame: boolean }}
|
|
230
354
|
*/
|
|
231
355
|
static #normalizeStaticGeometry(componentBody, geometry, matchedComponent) {
|
|
232
356
|
if (
|
|
233
357
|
String(geometry?.kind || '').toLowerCase() !== 'extruded-polygon' ||
|
|
234
358
|
!Array.isArray(geometry?.verticesMil) ||
|
|
235
|
-
geometry.verticesMil.length < 3
|
|
359
|
+
geometry.verticesMil.length < 3
|
|
360
|
+
) {
|
|
361
|
+
return { geometry, sourceCoordinateFrame: false }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (
|
|
236
365
|
!PcbScene3dStaticBodyPlacementBuilder.#usesSourceCoordinateFrame(
|
|
237
366
|
componentBody,
|
|
238
367
|
geometry.verticesMil
|
|
239
368
|
)
|
|
240
369
|
) {
|
|
241
|
-
return { geometry }
|
|
370
|
+
return { geometry, sourceCoordinateFrame: false }
|
|
242
371
|
}
|
|
243
372
|
|
|
244
373
|
const center =
|
|
@@ -255,6 +384,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
|
|
|
255
384
|
|
|
256
385
|
return {
|
|
257
386
|
placementCenterMil: placementCenter,
|
|
387
|
+
sourceCoordinateFrame: true,
|
|
258
388
|
geometry: {
|
|
259
389
|
...geometry,
|
|
260
390
|
verticesMil: geometry.verticesMil.map((vertex) => {
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Recovers missing anonymous static-body polygons from complete same-model rows.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbScene3dStaticBodyPrototypeRecovery {
|
|
9
|
+
/**
|
|
10
|
+
* Recovers repeated anonymous body rows that omit their vertex payload.
|
|
11
|
+
* @param {object[] | undefined} componentBodies Component bodies.
|
|
12
|
+
* @returns {object[]}
|
|
13
|
+
*/
|
|
14
|
+
static recover(componentBodies) {
|
|
15
|
+
const bodies = Array.isArray(componentBodies) ? componentBodies : []
|
|
16
|
+
const prototypes =
|
|
17
|
+
PcbScene3dStaticBodyPrototypeRecovery.#prototypeMap(bodies)
|
|
18
|
+
|
|
19
|
+
if (!prototypes.size) {
|
|
20
|
+
return bodies
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return bodies.map((componentBody) =>
|
|
24
|
+
PcbScene3dStaticBodyPrototypeRecovery.#recoverBody(
|
|
25
|
+
componentBody,
|
|
26
|
+
prototypes
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Builds reusable complete-geometry prototypes by exact model identity.
|
|
33
|
+
* @param {object[]} componentBodies Component bodies.
|
|
34
|
+
* @returns {Map<string, object[]>}
|
|
35
|
+
*/
|
|
36
|
+
static #prototypeMap(componentBodies) {
|
|
37
|
+
const prototypes = new Map()
|
|
38
|
+
|
|
39
|
+
componentBodies
|
|
40
|
+
.filter((componentBody) =>
|
|
41
|
+
PcbScene3dStaticBodyPrototypeRecovery.#isPrototype(
|
|
42
|
+
componentBody
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
.forEach((componentBody) => {
|
|
46
|
+
const key =
|
|
47
|
+
PcbScene3dStaticBodyPrototypeRecovery.#bodyFamilyKey(
|
|
48
|
+
componentBody
|
|
49
|
+
)
|
|
50
|
+
if (!key) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!prototypes.has(key)) {
|
|
55
|
+
prototypes.set(key, [])
|
|
56
|
+
}
|
|
57
|
+
prototypes.get(key)?.push(componentBody)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return prototypes
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Recovers one missing body from the closest compatible prototype.
|
|
65
|
+
* @param {object} componentBody Target body.
|
|
66
|
+
* @param {Map<string, object[]>} prototypes Complete prototype rows.
|
|
67
|
+
* @returns {object}
|
|
68
|
+
*/
|
|
69
|
+
static #recoverBody(componentBody, prototypes) {
|
|
70
|
+
if (
|
|
71
|
+
!PcbScene3dStaticBodyPrototypeRecovery.#isRecoverableTarget(
|
|
72
|
+
componentBody
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
75
|
+
return componentBody
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const key =
|
|
79
|
+
PcbScene3dStaticBodyPrototypeRecovery.#bodyFamilyKey(componentBody)
|
|
80
|
+
const prototype =
|
|
81
|
+
PcbScene3dStaticBodyPrototypeRecovery.#nearestPrototype(
|
|
82
|
+
componentBody,
|
|
83
|
+
prototypes.get(key)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return prototype
|
|
87
|
+
? PcbScene3dStaticBodyPrototypeRecovery.#withPrototypeGeometry(
|
|
88
|
+
componentBody,
|
|
89
|
+
prototype
|
|
90
|
+
)
|
|
91
|
+
: componentBody
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Checks whether a row can provide reusable static geometry.
|
|
96
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
static #isPrototype(componentBody) {
|
|
100
|
+
return (
|
|
101
|
+
PcbScene3dStaticBodyPrototypeRecovery.#isAnonymous(componentBody) &&
|
|
102
|
+
PcbScene3dStaticBodyPrototypeRecovery.#isCompleteExtrudedPolygon(
|
|
103
|
+
componentBody
|
|
104
|
+
) &&
|
|
105
|
+
Boolean(
|
|
106
|
+
PcbScene3dStaticBodyPrototypeRecovery.#bodyFamilyKey(
|
|
107
|
+
componentBody
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Checks whether a row is missing renderable static polygon vertices.
|
|
115
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
static #isRecoverableTarget(componentBody) {
|
|
119
|
+
return (
|
|
120
|
+
PcbScene3dStaticBodyPrototypeRecovery.#isAnonymous(componentBody) &&
|
|
121
|
+
PcbScene3dStaticBodyPrototypeRecovery.#isExtrudedPolygonBody(
|
|
122
|
+
componentBody
|
|
123
|
+
) &&
|
|
124
|
+
!PcbScene3dStaticBodyPrototypeRecovery.#isCompleteExtrudedPolygon(
|
|
125
|
+
componentBody
|
|
126
|
+
) &&
|
|
127
|
+
PcbScene3dStaticBodyPrototypeRecovery.#hasRecoverablePosition(
|
|
128
|
+
componentBody
|
|
129
|
+
) &&
|
|
130
|
+
PcbScene3dStaticBodyPrototypeRecovery.#heightMil(componentBody) > 0
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Checks whether the body row has no authored identity text.
|
|
136
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
137
|
+
* @returns {boolean}
|
|
138
|
+
*/
|
|
139
|
+
static #isAnonymous(componentBody) {
|
|
140
|
+
return ![componentBody?.identifier, componentBody?.name]
|
|
141
|
+
.map((value) => String(value || '').trim())
|
|
142
|
+
.some(Boolean)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Checks whether a body row describes an extruded polygon model.
|
|
147
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
*/
|
|
150
|
+
static #isExtrudedPolygonBody(componentBody) {
|
|
151
|
+
return (
|
|
152
|
+
PcbScene3dStaticBodyPrototypeRecovery.#geometryKind(
|
|
153
|
+
componentBody
|
|
154
|
+
) === 'extruded-polygon'
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Checks whether a row already has complete extruded polygon vertices.
|
|
160
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
161
|
+
* @returns {boolean}
|
|
162
|
+
*/
|
|
163
|
+
static #isCompleteExtrudedPolygon(componentBody) {
|
|
164
|
+
const geometry = componentBody?.staticGeometry
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
PcbScene3dStaticBodyPrototypeRecovery.#isExtrudedPolygonBody(
|
|
168
|
+
componentBody
|
|
169
|
+
) &&
|
|
170
|
+
String(geometry?.status || '').toLowerCase() === 'complete' &&
|
|
171
|
+
Array.isArray(geometry?.verticesMil) &&
|
|
172
|
+
geometry.verticesMil.length >= 3
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Builds the exact model-family key used for prototype reuse.
|
|
178
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
179
|
+
* @returns {string}
|
|
180
|
+
*/
|
|
181
|
+
static #bodyFamilyKey(componentBody) {
|
|
182
|
+
const identity = [
|
|
183
|
+
componentBody?.modelId,
|
|
184
|
+
componentBody?.checksum,
|
|
185
|
+
PcbScene3dStaticBodyPrototypeRecovery.#geometryKind(componentBody),
|
|
186
|
+
PcbScene3dStaticBodyPrototypeRecovery.#numberKey(
|
|
187
|
+
PcbScene3dStaticBodyPrototypeRecovery.#heightMil(componentBody)
|
|
188
|
+
),
|
|
189
|
+
PcbScene3dStaticBodyPrototypeRecovery.#numberKey(
|
|
190
|
+
PcbScene3dStaticBodyPrototypeRecovery.#standoffMil(
|
|
191
|
+
componentBody
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
]
|
|
195
|
+
.map((value) =>
|
|
196
|
+
String(value ?? '')
|
|
197
|
+
.trim()
|
|
198
|
+
.toLowerCase()
|
|
199
|
+
)
|
|
200
|
+
.filter(Boolean)
|
|
201
|
+
|
|
202
|
+
return identity.length >= 4 ? identity.join('|') : ''
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Chooses the closest layer-compatible prototype for one target row.
|
|
207
|
+
* @param {object} componentBody Target body.
|
|
208
|
+
* @param {object[] | undefined} candidates Candidate prototypes.
|
|
209
|
+
* @returns {object | null}
|
|
210
|
+
*/
|
|
211
|
+
static #nearestPrototype(componentBody, candidates) {
|
|
212
|
+
const normalizedCandidates = Array.isArray(candidates) ? candidates : []
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
normalizedCandidates
|
|
216
|
+
.filter((candidate) =>
|
|
217
|
+
PcbScene3dStaticBodyPrototypeRecovery.#compatibleLayers(
|
|
218
|
+
componentBody,
|
|
219
|
+
candidate
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
.map((candidate) => ({
|
|
223
|
+
candidate,
|
|
224
|
+
distance:
|
|
225
|
+
PcbScene3dStaticBodyPrototypeRecovery.#distanceBetweenBodies(
|
|
226
|
+
componentBody,
|
|
227
|
+
candidate
|
|
228
|
+
)
|
|
229
|
+
}))
|
|
230
|
+
.sort((left, right) => left.distance - right.distance)[0]
|
|
231
|
+
?.candidate || null
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns a cloned target row with translated prototype vertices.
|
|
237
|
+
* @param {object} componentBody Target body.
|
|
238
|
+
* @param {object} prototype Complete prototype body.
|
|
239
|
+
* @returns {object}
|
|
240
|
+
*/
|
|
241
|
+
static #withPrototypeGeometry(componentBody, prototype) {
|
|
242
|
+
const geometry = componentBody?.staticGeometry || {}
|
|
243
|
+
const prototypeGeometry = prototype?.staticGeometry || {}
|
|
244
|
+
const heightMil =
|
|
245
|
+
PcbScene3dStaticBodyPrototypeRecovery.#heightMil(componentBody) ||
|
|
246
|
+
PcbScene3dStaticBodyPrototypeRecovery.#heightMil(prototype)
|
|
247
|
+
const standoffHeightMil =
|
|
248
|
+
PcbScene3dStaticBodyPrototypeRecovery.#standoffMil(componentBody) ||
|
|
249
|
+
PcbScene3dStaticBodyPrototypeRecovery.#standoffMil(prototype)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
...componentBody,
|
|
253
|
+
staticGeometry: {
|
|
254
|
+
...prototypeGeometry,
|
|
255
|
+
...geometry,
|
|
256
|
+
kind: 'extruded-polygon',
|
|
257
|
+
status: 'complete',
|
|
258
|
+
units: geometry.units || prototypeGeometry.units || 'mil',
|
|
259
|
+
minZMil: Number(
|
|
260
|
+
geometry.minZMil ?? prototypeGeometry.minZMil ?? 0
|
|
261
|
+
),
|
|
262
|
+
maxZMil: Number(
|
|
263
|
+
geometry.maxZMil ??
|
|
264
|
+
prototypeGeometry.maxZMil ??
|
|
265
|
+
heightMil + standoffHeightMil
|
|
266
|
+
),
|
|
267
|
+
heightMil,
|
|
268
|
+
standoffHeightMil,
|
|
269
|
+
verticesMil:
|
|
270
|
+
PcbScene3dStaticBodyPrototypeRecovery.#prototypeVerticesForTarget(
|
|
271
|
+
componentBody,
|
|
272
|
+
prototype
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Translates prototype vertices into the target body's coordinate frame.
|
|
280
|
+
* @param {object} componentBody Target body.
|
|
281
|
+
* @param {object} prototype Complete prototype body.
|
|
282
|
+
* @returns {{ x: number, y: number }[]}
|
|
283
|
+
*/
|
|
284
|
+
static #prototypeVerticesForTarget(componentBody, prototype) {
|
|
285
|
+
const vertices = Array.isArray(prototype?.staticGeometry?.verticesMil)
|
|
286
|
+
? prototype.staticGeometry.verticesMil
|
|
287
|
+
: []
|
|
288
|
+
|
|
289
|
+
if (
|
|
290
|
+
!PcbScene3dStaticBodyPrototypeRecovery.#usesSourceCoordinateFrame(
|
|
291
|
+
prototype,
|
|
292
|
+
vertices
|
|
293
|
+
)
|
|
294
|
+
) {
|
|
295
|
+
return vertices.map((vertex) => ({
|
|
296
|
+
x: Number(vertex?.x || 0),
|
|
297
|
+
y: Number(vertex?.y || 0)
|
|
298
|
+
}))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const prototypeSource =
|
|
302
|
+
PcbScene3dStaticBodyPrototypeRecovery.#sourcePosition(prototype)
|
|
303
|
+
const targetSource =
|
|
304
|
+
PcbScene3dStaticBodyPrototypeRecovery.#sourcePosition(componentBody)
|
|
305
|
+
|
|
306
|
+
return vertices.map((vertex) => ({
|
|
307
|
+
x: targetSource.x + Number(vertex?.x || 0) - prototypeSource.x,
|
|
308
|
+
y: targetSource.y + Number(vertex?.y || 0) - prototypeSource.y
|
|
309
|
+
}))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Checks whether explicit mechanical layer values are compatible.
|
|
314
|
+
* @param {object | undefined} left Left body.
|
|
315
|
+
* @param {object | undefined} right Right body.
|
|
316
|
+
* @returns {boolean}
|
|
317
|
+
*/
|
|
318
|
+
static #compatibleLayers(left, right) {
|
|
319
|
+
const leftLayer = String(left?.layer || '')
|
|
320
|
+
.trim()
|
|
321
|
+
.toLowerCase()
|
|
322
|
+
const rightLayer = String(right?.layer || '')
|
|
323
|
+
.trim()
|
|
324
|
+
.toLowerCase()
|
|
325
|
+
|
|
326
|
+
return !leftLayer || !rightLayer || leftLayer === rightLayer
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Resolves the lower-case geometry kind for a body row.
|
|
331
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
332
|
+
* @returns {string}
|
|
333
|
+
*/
|
|
334
|
+
static #geometryKind(componentBody) {
|
|
335
|
+
return String(
|
|
336
|
+
componentBody?.staticGeometry?.kind ||
|
|
337
|
+
componentBody?.modelTypeName ||
|
|
338
|
+
''
|
|
339
|
+
)
|
|
340
|
+
.trim()
|
|
341
|
+
.toLowerCase()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Resolves the body extrusion height.
|
|
346
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
347
|
+
* @returns {number}
|
|
348
|
+
*/
|
|
349
|
+
static #heightMil(componentBody) {
|
|
350
|
+
const height = Number(
|
|
351
|
+
componentBody?.staticGeometry?.heightMil ??
|
|
352
|
+
componentBody?.overallHeightMil
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return Number.isFinite(height) ? height : 0
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Resolves the body standoff height.
|
|
360
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
361
|
+
* @returns {number}
|
|
362
|
+
*/
|
|
363
|
+
static #standoffMil(componentBody) {
|
|
364
|
+
const standoff = Number(
|
|
365
|
+
componentBody?.staticGeometry?.standoffHeightMil ??
|
|
366
|
+
componentBody?.standoffHeightMil
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return Number.isFinite(standoff) ? standoff : 0
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Checks whether the body row has a finite source anchor.
|
|
374
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
375
|
+
* @returns {boolean}
|
|
376
|
+
*/
|
|
377
|
+
static #hasRecoverablePosition(componentBody) {
|
|
378
|
+
const source =
|
|
379
|
+
PcbScene3dStaticBodyPrototypeRecovery.#sourcePosition(componentBody)
|
|
380
|
+
|
|
381
|
+
return Number.isFinite(source.x) && Number.isFinite(source.y)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Resolves a body source position.
|
|
386
|
+
* @param {object | undefined} componentBody Candidate body.
|
|
387
|
+
* @returns {{ x: number, y: number }}
|
|
388
|
+
*/
|
|
389
|
+
static #sourcePosition(componentBody) {
|
|
390
|
+
return {
|
|
391
|
+
x: Number(componentBody?.positionMil?.x),
|
|
392
|
+
y: Number(componentBody?.positionMil?.y)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Measures the source-anchor distance between two body rows.
|
|
398
|
+
* @param {object} left Left body.
|
|
399
|
+
* @param {object} right Right body.
|
|
400
|
+
* @returns {number}
|
|
401
|
+
*/
|
|
402
|
+
static #distanceBetweenBodies(left, right) {
|
|
403
|
+
const leftSource =
|
|
404
|
+
PcbScene3dStaticBodyPrototypeRecovery.#sourcePosition(left)
|
|
405
|
+
const rightSource =
|
|
406
|
+
PcbScene3dStaticBodyPrototypeRecovery.#sourcePosition(right)
|
|
407
|
+
|
|
408
|
+
return Math.hypot(
|
|
409
|
+
leftSource.x - rightSource.x,
|
|
410
|
+
leftSource.y - rightSource.y
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Checks whether polygon vertices use board/source coordinates.
|
|
416
|
+
* @param {object} componentBody Component body.
|
|
417
|
+
* @param {{ x?: number, y?: number }[]} vertices Vertices.
|
|
418
|
+
* @returns {boolean}
|
|
419
|
+
*/
|
|
420
|
+
static #usesSourceCoordinateFrame(componentBody, vertices) {
|
|
421
|
+
const source =
|
|
422
|
+
PcbScene3dStaticBodyPrototypeRecovery.#sourcePosition(componentBody)
|
|
423
|
+
const coordinates = (Array.isArray(vertices) ? vertices : []).flatMap(
|
|
424
|
+
(vertex) => [Number(vertex?.x || 0), Number(vertex?.y || 0)]
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
Math.max(Math.abs(source.x), Math.abs(source.y), ...coordinates) >
|
|
429
|
+
1000
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Converts a number into a stable key fragment.
|
|
435
|
+
* @param {number | string | undefined | null} value Candidate value.
|
|
436
|
+
* @returns {string}
|
|
437
|
+
*/
|
|
438
|
+
static #numberKey(value) {
|
|
439
|
+
const number = Number(value)
|
|
440
|
+
|
|
441
|
+
return Number.isFinite(number)
|
|
442
|
+
? String(Math.round(number * 10000) / 10000)
|
|
443
|
+
: ''
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { PcbScene3dStaticBodySymmetryRecovery } from './PcbScene3dStaticBodySymmetryRecovery.mjs'
|
|
6
|
+
import { PcbScene3dStaticBodyPrototypeRecovery } from './PcbScene3dStaticBodyPrototypeRecovery.mjs'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Recovers renderable static-body geometry and display metadata.
|
|
@@ -38,8 +39,13 @@ export class PcbScene3dStaticBodyRecovery {
|
|
|
38
39
|
*/
|
|
39
40
|
static recover(componentBodies, bodyMatches = []) {
|
|
40
41
|
const bodies = Array.isArray(componentBodies) ? componentBodies : []
|
|
42
|
+
const prototypeRecoveredBodies =
|
|
43
|
+
PcbScene3dStaticBodyPrototypeRecovery.recover(bodies)
|
|
41
44
|
const symmetricRecoveredBodies =
|
|
42
|
-
PcbScene3dStaticBodySymmetryRecovery.recover(
|
|
45
|
+
PcbScene3dStaticBodySymmetryRecovery.recover(
|
|
46
|
+
prototypeRecoveredBodies,
|
|
47
|
+
bodyMatches
|
|
48
|
+
)
|
|
43
49
|
const topRecoveredBodies = symmetricRecoveredBodies.map(
|
|
44
50
|
(componentBody) =>
|
|
45
51
|
PcbScene3dStaticBodyRecovery.#recoverCoverTopGeometry(
|