altium-toolkit 1.1.35 → 1.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -2
- package/package.json +1 -1
- package/src/core/altium/PrintableTextDecoder.mjs +20 -1
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +288 -7
- package/src/core/altium/SchematicTextParser.mjs +18 -253
- package/src/core/altium/SchematicTitleBlockParser.mjs +410 -0
- package/src/core/altium/SelectedPartAltiumExportAdapter.mjs +215 -0
- package/src/parser.mjs +1 -0
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +245 -12
- package/src/ui/AltiumScene3dAuthoredConnectorYawPolicy.mjs +176 -0
- package/src/ui/AltiumScene3dBottomSourceHalfTurnPolicy.mjs +93 -0
- package/src/ui/AltiumScene3dDisplayModuleYawPolicy.mjs +284 -0
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +521 -23
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +766 -19
- package/src/ui/AltiumScene3dQfnFootprintDetector.mjs +307 -0
- package/src/ui/AltiumScene3dRepeatedFullFootprintBodyCollapse.mjs +518 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +121 -17
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +286 -135
- package/src/ui/AltiumScene3dShapeStackOwnerConflictPolicy.mjs +299 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +22 -0
- package/src/ui/PcbScene3dBuilder.mjs +613 -117
- package/src/ui/PcbScene3dModelRegistry.mjs +222 -10
- package/src/ui/PcbScene3dPackages.mjs +16 -1
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +262 -22
- package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +984 -0
- package/src/ui/PcbScene3dStaticBodyPadOwnerPromotion.mjs +378 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +169 -23
- package/src/ui/PcbScene3dStaticBodyPrototypeRecovery.mjs +445 -0
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +7 -1
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +199 -2
- package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +439 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +23 -2
- package/src/ui/SchematicColorResolver.mjs +44 -0
- package/src/ui/SchematicLineColorResolver.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +13 -13
|
@@ -0,0 +1,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(
|