altium-toolkit 1.1.26 → 1.1.31
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/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +34 -1
- package/src/core/altium/AltiumSchematicArcAngleNormalizer.mjs +82 -0
- package/src/core/altium/AltiumSchematicFreeGraphicStrokeNormalizer.mjs +105 -0
- package/src/core/altium/AltiumSchematicHiddenDesignatorResolver.mjs +191 -0
- package/src/core/altium/AltiumSchematicPackedImageResolver.mjs +433 -0
- package/src/core/altium/AltiumSchematicSheetBoundsNormalizer.mjs +287 -0
- package/src/parser.mjs +5 -0
- package/src/renderers.mjs +2 -0
- package/src/scene3d.mjs +1 -0
- package/src/ui/AltiumPcbBottomViewMirror.mjs +394 -0
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +252 -0
- package/src/ui/AltiumScene3dBottomPadRotationAdapter.mjs +127 -0
- package/src/ui/AltiumScene3dComponentBodyAdapter.mjs +153 -0
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +919 -0
- package/src/ui/AltiumScene3dIdentityTokens.mjs +105 -0
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +357 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +576 -0
- package/src/ui/PcbFootprintPadAxisNormalizer.mjs +609 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +54 -11
- package/src/ui/PcbScene3dBuilder.mjs +76 -33
- package/src/ui/PcbScene3dPadLocalSpanResolver.mjs +109 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirrors Altium PCB render models into the same bottom-view frame used by
|
|
3
|
+
* the 3D bottom preset.
|
|
4
|
+
*/
|
|
5
|
+
export class AltiumPcbBottomViewMirror {
|
|
6
|
+
/**
|
|
7
|
+
* Mirrors a side-resolved Altium PCB model horizontally around the board.
|
|
8
|
+
* @param {object | null} documentModel Side-resolved PCB document model.
|
|
9
|
+
* @returns {object | null}
|
|
10
|
+
*/
|
|
11
|
+
static apply(documentModel) {
|
|
12
|
+
const pcb = documentModel?.pcb
|
|
13
|
+
const outline = pcb?.boardOutline
|
|
14
|
+
if (!pcb || !outline) {
|
|
15
|
+
return documentModel || null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const mirrorX = AltiumPcbBottomViewMirror.#buildMirrorX(outline)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
...documentModel,
|
|
22
|
+
pcb: {
|
|
23
|
+
...pcb,
|
|
24
|
+
boardOutline: AltiumPcbBottomViewMirror.#mirrorOutline(
|
|
25
|
+
outline,
|
|
26
|
+
mirrorX
|
|
27
|
+
),
|
|
28
|
+
polygons: AltiumPcbBottomViewMirror.#mirrorPolygons(
|
|
29
|
+
pcb.polygons,
|
|
30
|
+
mirrorX
|
|
31
|
+
),
|
|
32
|
+
fills: AltiumPcbBottomViewMirror.#mirrorFills(
|
|
33
|
+
pcb.fills,
|
|
34
|
+
mirrorX
|
|
35
|
+
),
|
|
36
|
+
tracks: AltiumPcbBottomViewMirror.#mirrorTracks(
|
|
37
|
+
pcb.tracks,
|
|
38
|
+
mirrorX
|
|
39
|
+
),
|
|
40
|
+
arcs: AltiumPcbBottomViewMirror.#mirrorArcs(pcb.arcs, mirrorX),
|
|
41
|
+
regions: AltiumPcbBottomViewMirror.#mirrorRegions(
|
|
42
|
+
pcb.regions,
|
|
43
|
+
mirrorX
|
|
44
|
+
),
|
|
45
|
+
shapeBasedRegions: AltiumPcbBottomViewMirror.#mirrorRegions(
|
|
46
|
+
pcb.shapeBasedRegions,
|
|
47
|
+
mirrorX
|
|
48
|
+
),
|
|
49
|
+
boardRegions: AltiumPcbBottomViewMirror.#mirrorRegions(
|
|
50
|
+
pcb.boardRegions,
|
|
51
|
+
mirrorX
|
|
52
|
+
),
|
|
53
|
+
vias: AltiumPcbBottomViewMirror.#mirrorVias(pcb.vias, mirrorX),
|
|
54
|
+
pads: AltiumPcbBottomViewMirror.#mirrorPads(pcb.pads, mirrorX),
|
|
55
|
+
texts: AltiumPcbBottomViewMirror.#mirrorTexts(pcb.texts),
|
|
56
|
+
textGroupTransform:
|
|
57
|
+
AltiumPcbBottomViewMirror.#buildTextGroupTransform(outline),
|
|
58
|
+
components: AltiumPcbBottomViewMirror.#mirrorComponents(
|
|
59
|
+
pcb.components,
|
|
60
|
+
mirrorX
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds the board-space X-axis mirror function.
|
|
68
|
+
* @param {{ minX?: number, widthMil?: number }} outline Board outline.
|
|
69
|
+
* @returns {(value: unknown) => number}
|
|
70
|
+
*/
|
|
71
|
+
static #buildMirrorX(outline) {
|
|
72
|
+
const minX = Number(outline?.minX || 0)
|
|
73
|
+
const maxX = minX + Number(outline?.widthMil || 0)
|
|
74
|
+
|
|
75
|
+
return (value) => minX + maxX - Number(value || 0)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Builds the SVG text-layer mirror used by the bottom 3D preset.
|
|
80
|
+
* @param {{ minX?: number, widthMil?: number }} outline Board outline.
|
|
81
|
+
* @returns {{ translateX: number, translateY: number, scaleX: number, scaleY: number }}
|
|
82
|
+
*/
|
|
83
|
+
static #buildTextGroupTransform(outline) {
|
|
84
|
+
const minX = Number(outline?.minX || 0)
|
|
85
|
+
const maxX = minX + Number(outline?.widthMil || 0)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
translateX: minX + maxX,
|
|
89
|
+
translateY: 0,
|
|
90
|
+
scaleX: -1,
|
|
91
|
+
scaleY: 1
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Mirrors the board outline segment coordinates.
|
|
97
|
+
* @param {object} outline Board outline.
|
|
98
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
99
|
+
* @returns {object}
|
|
100
|
+
*/
|
|
101
|
+
static #mirrorOutline(outline, mirrorX) {
|
|
102
|
+
return {
|
|
103
|
+
...outline,
|
|
104
|
+
segments: AltiumPcbBottomViewMirror.#mirrorSegments(
|
|
105
|
+
outline?.segments,
|
|
106
|
+
mirrorX
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Mirrors polygon segment coordinates.
|
|
113
|
+
* @param {readonly object[] | undefined} polygons Polygon primitives.
|
|
114
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
115
|
+
* @returns {object[]}
|
|
116
|
+
*/
|
|
117
|
+
static #mirrorPolygons(polygons, mirrorX) {
|
|
118
|
+
return AltiumPcbBottomViewMirror.#array(polygons).map((polygon) => ({
|
|
119
|
+
...polygon,
|
|
120
|
+
segments: AltiumPcbBottomViewMirror.#mirrorSegments(
|
|
121
|
+
polygon?.segments,
|
|
122
|
+
mirrorX
|
|
123
|
+
)
|
|
124
|
+
}))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Mirrors path segment X coordinates.
|
|
129
|
+
* @param {readonly object[] | undefined} segments Path segments.
|
|
130
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
131
|
+
* @returns {object[]}
|
|
132
|
+
*/
|
|
133
|
+
static #mirrorSegments(segments, mirrorX) {
|
|
134
|
+
return AltiumPcbBottomViewMirror.#array(segments).map((segment) => ({
|
|
135
|
+
...segment,
|
|
136
|
+
x1: mirrorX(segment?.x1),
|
|
137
|
+
x2: mirrorX(segment?.x2),
|
|
138
|
+
...(segment?.cx === null || segment?.cx === undefined
|
|
139
|
+
? {}
|
|
140
|
+
: { cx: mirrorX(segment.cx) })
|
|
141
|
+
}))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Mirrors rectangular fill extents.
|
|
146
|
+
* @param {readonly object[] | undefined} fills Fill primitives.
|
|
147
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
148
|
+
* @returns {object[]}
|
|
149
|
+
*/
|
|
150
|
+
static #mirrorFills(fills, mirrorX) {
|
|
151
|
+
return AltiumPcbBottomViewMirror.#array(fills).map((fill) => ({
|
|
152
|
+
...fill,
|
|
153
|
+
x1: mirrorX(fill?.x1),
|
|
154
|
+
x2: mirrorX(fill?.x2),
|
|
155
|
+
points: AltiumPcbBottomViewMirror.#mirrorPointList(
|
|
156
|
+
fill?.points,
|
|
157
|
+
mirrorX
|
|
158
|
+
),
|
|
159
|
+
holes: AltiumPcbBottomViewMirror.#mirrorHoleLists(
|
|
160
|
+
fill?.holes,
|
|
161
|
+
mirrorX
|
|
162
|
+
)
|
|
163
|
+
}))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Mirrors line track endpoints.
|
|
168
|
+
* @param {readonly object[] | undefined} tracks Track primitives.
|
|
169
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
170
|
+
* @returns {object[]}
|
|
171
|
+
*/
|
|
172
|
+
static #mirrorTracks(tracks, mirrorX) {
|
|
173
|
+
return AltiumPcbBottomViewMirror.#array(tracks).map((track) => ({
|
|
174
|
+
...track,
|
|
175
|
+
x1: mirrorX(track?.x1),
|
|
176
|
+
x2: mirrorX(track?.x2)
|
|
177
|
+
}))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Mirrors arc centers and angular spans.
|
|
182
|
+
* @param {readonly object[] | undefined} arcs Arc primitives.
|
|
183
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
184
|
+
* @returns {object[]}
|
|
185
|
+
*/
|
|
186
|
+
static #mirrorArcs(arcs, mirrorX) {
|
|
187
|
+
return AltiumPcbBottomViewMirror.#array(arcs).map((arc) => ({
|
|
188
|
+
...arc,
|
|
189
|
+
x: mirrorX(arc?.x),
|
|
190
|
+
startAngle: AltiumPcbBottomViewMirror.#mirrorAngleX(
|
|
191
|
+
arc?.startAngle
|
|
192
|
+
),
|
|
193
|
+
endAngle: AltiumPcbBottomViewMirror.#mirrorAngleX(arc?.endAngle)
|
|
194
|
+
}))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Mirrors filled region contours and holes.
|
|
199
|
+
* @param {readonly object[] | undefined} regions Region primitives.
|
|
200
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
201
|
+
* @returns {object[]}
|
|
202
|
+
*/
|
|
203
|
+
static #mirrorRegions(regions, mirrorX) {
|
|
204
|
+
return AltiumPcbBottomViewMirror.#array(regions).map((region) => ({
|
|
205
|
+
...region,
|
|
206
|
+
points: AltiumPcbBottomViewMirror.#mirrorPointList(
|
|
207
|
+
region?.points,
|
|
208
|
+
mirrorX
|
|
209
|
+
),
|
|
210
|
+
holes: AltiumPcbBottomViewMirror.#mirrorHoleLists(
|
|
211
|
+
region?.holes,
|
|
212
|
+
mirrorX
|
|
213
|
+
),
|
|
214
|
+
...(Array.isArray(region?.bendingLines)
|
|
215
|
+
? {
|
|
216
|
+
bendingLines:
|
|
217
|
+
AltiumPcbBottomViewMirror.#mirrorBendingLines(
|
|
218
|
+
region.bendingLines,
|
|
219
|
+
mirrorX
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
: {})
|
|
223
|
+
}))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Mirrors via centers.
|
|
228
|
+
* @param {readonly object[] | undefined} vias Via primitives.
|
|
229
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
230
|
+
* @returns {object[]}
|
|
231
|
+
*/
|
|
232
|
+
static #mirrorVias(vias, mirrorX) {
|
|
233
|
+
return AltiumPcbBottomViewMirror.#array(vias).map((via) => ({
|
|
234
|
+
...via,
|
|
235
|
+
x: mirrorX(via?.x)
|
|
236
|
+
}))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Mirrors pad centers and X-axis local offsets.
|
|
241
|
+
* @param {readonly object[] | undefined} pads Pad primitives.
|
|
242
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
243
|
+
* @returns {object[]}
|
|
244
|
+
*/
|
|
245
|
+
static #mirrorPads(pads, mirrorX) {
|
|
246
|
+
return AltiumPcbBottomViewMirror.#array(pads).map((pad) => ({
|
|
247
|
+
...pad,
|
|
248
|
+
x: mirrorX(pad?.x),
|
|
249
|
+
rotation: AltiumPcbBottomViewMirror.#mirrorRotation(pad?.rotation),
|
|
250
|
+
holeRotation:
|
|
251
|
+
pad?.holeRotation === null || pad?.holeRotation === undefined
|
|
252
|
+
? (pad?.holeRotation ?? null)
|
|
253
|
+
: AltiumPcbBottomViewMirror.#mirrorRotation(
|
|
254
|
+
pad.holeRotation
|
|
255
|
+
),
|
|
256
|
+
...(pad?.offsetTopX === null || pad?.offsetTopX === undefined
|
|
257
|
+
? {}
|
|
258
|
+
: { offsetTopX: -Number(pad.offsetTopX || 0) })
|
|
259
|
+
}))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Preserves PCB text insertion points for the mirrored text layer.
|
|
264
|
+
* @param {readonly object[] | undefined} texts Text primitives.
|
|
265
|
+
* @returns {object[]}
|
|
266
|
+
*/
|
|
267
|
+
static #mirrorTexts(texts) {
|
|
268
|
+
return AltiumPcbBottomViewMirror.#array(texts).map((text) => ({
|
|
269
|
+
...text
|
|
270
|
+
}))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Mirrors component origins and package rotations.
|
|
275
|
+
* @param {readonly object[] | undefined} components Component records.
|
|
276
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
277
|
+
* @returns {object[]}
|
|
278
|
+
*/
|
|
279
|
+
static #mirrorComponents(components, mirrorX) {
|
|
280
|
+
return AltiumPcbBottomViewMirror.#array(components).map(
|
|
281
|
+
(component) => ({
|
|
282
|
+
...component,
|
|
283
|
+
x: mirrorX(component?.x),
|
|
284
|
+
rotation: AltiumPcbBottomViewMirror.#mirrorRotation(
|
|
285
|
+
component?.rotation
|
|
286
|
+
)
|
|
287
|
+
})
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Mirrors a list of point objects.
|
|
293
|
+
* @param {readonly object[] | undefined} points Point list.
|
|
294
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
295
|
+
* @returns {object[]}
|
|
296
|
+
*/
|
|
297
|
+
static #mirrorPointList(points, mirrorX) {
|
|
298
|
+
return AltiumPcbBottomViewMirror.#array(points).map((point) => ({
|
|
299
|
+
...point,
|
|
300
|
+
x: mirrorX(point?.x),
|
|
301
|
+
...(point?.centerX === null || point?.centerX === undefined
|
|
302
|
+
? {}
|
|
303
|
+
: { centerX: mirrorX(point.centerX) }),
|
|
304
|
+
...(point?.startAngle === null || point?.startAngle === undefined
|
|
305
|
+
? {}
|
|
306
|
+
: {
|
|
307
|
+
startAngle: AltiumPcbBottomViewMirror.#mirrorAngleX(
|
|
308
|
+
point.startAngle
|
|
309
|
+
)
|
|
310
|
+
}),
|
|
311
|
+
...(point?.endAngle === null || point?.endAngle === undefined
|
|
312
|
+
? {}
|
|
313
|
+
: {
|
|
314
|
+
endAngle: AltiumPcbBottomViewMirror.#mirrorAngleX(
|
|
315
|
+
point.endAngle
|
|
316
|
+
)
|
|
317
|
+
})
|
|
318
|
+
}))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Mirrors nested hole point lists.
|
|
323
|
+
* @param {readonly object[][] | undefined} holes Hole point lists.
|
|
324
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
325
|
+
* @returns {object[][]}
|
|
326
|
+
*/
|
|
327
|
+
static #mirrorHoleLists(holes, mirrorX) {
|
|
328
|
+
return AltiumPcbBottomViewMirror.#array(holes).map((hole) =>
|
|
329
|
+
AltiumPcbBottomViewMirror.#mirrorPointList(hole, mirrorX)
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Mirrors board-region bending-line X endpoints.
|
|
335
|
+
* @param {readonly object[] | undefined} bendingLines Bending lines.
|
|
336
|
+
* @param {(value: unknown) => number} mirrorX X-axis mirror function.
|
|
337
|
+
* @returns {object[]}
|
|
338
|
+
*/
|
|
339
|
+
static #mirrorBendingLines(bendingLines, mirrorX) {
|
|
340
|
+
return AltiumPcbBottomViewMirror.#array(bendingLines).map((line) => ({
|
|
341
|
+
...line,
|
|
342
|
+
x1:
|
|
343
|
+
line?.x1 === null || line?.x1 === undefined
|
|
344
|
+
? (line?.x1 ?? null)
|
|
345
|
+
: mirrorX(line.x1),
|
|
346
|
+
x2:
|
|
347
|
+
line?.x2 === null || line?.x2 === undefined
|
|
348
|
+
? (line?.x2 ?? null)
|
|
349
|
+
: mirrorX(line.x2)
|
|
350
|
+
}))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Mirrors an object rotation across the board X axis.
|
|
355
|
+
* @param {unknown} angle Rotation angle in degrees.
|
|
356
|
+
* @returns {number}
|
|
357
|
+
*/
|
|
358
|
+
static #mirrorRotation(angle) {
|
|
359
|
+
return AltiumPcbBottomViewMirror.#normalizeAngle(
|
|
360
|
+
180 - Number(angle || 0)
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Mirrors a polar angle across the board X axis.
|
|
366
|
+
* @param {unknown} angle Angle in degrees.
|
|
367
|
+
* @returns {number}
|
|
368
|
+
*/
|
|
369
|
+
static #mirrorAngleX(angle) {
|
|
370
|
+
return AltiumPcbBottomViewMirror.#normalizeAngle(
|
|
371
|
+
180 - Number(angle || 0)
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Normalizes one angle into the [0, 360) range.
|
|
377
|
+
* @param {number} angle Angle in degrees.
|
|
378
|
+
* @returns {number}
|
|
379
|
+
*/
|
|
380
|
+
static #normalizeAngle(angle) {
|
|
381
|
+
const normalized = Number(angle || 0) % 360
|
|
382
|
+
|
|
383
|
+
return normalized < 0 ? normalized + 360 : normalized
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Returns an array copy for transform operations.
|
|
388
|
+
* @param {readonly object[] | undefined} value Input collection.
|
|
389
|
+
* @returns {object[]}
|
|
390
|
+
*/
|
|
391
|
+
static #array(value) {
|
|
392
|
+
return Array.isArray(value) ? Array.from(value) : []
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preserves Altium component-body anchors that intentionally differ from the
|
|
3
|
+
* resolved footprint owner origin.
|
|
4
|
+
*/
|
|
5
|
+
export class AltiumScene3dAuthoredBodyAnchorAdapter {
|
|
6
|
+
static #MIN_OWNER_OFFSET_MIL = 25
|
|
7
|
+
static #BODY_ANCHOR_TOLERANCE_MIL = 5
|
|
8
|
+
static #AUTHORED_SOURCE = 'authored-body-anchor'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Marks off-anchor explicit Altium body placements so the runtime does not
|
|
12
|
+
* recenter them by loaded model bounds.
|
|
13
|
+
* @param {object} sceneDescription Built scene description.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static apply(sceneDescription) {
|
|
17
|
+
if (
|
|
18
|
+
String(sceneDescription?.sourceFormat || '').toLowerCase() !==
|
|
19
|
+
'altium' ||
|
|
20
|
+
!Array.isArray(sceneDescription?.externalPlacements)
|
|
21
|
+
) {
|
|
22
|
+
return sceneDescription
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const componentByDesignator =
|
|
26
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#componentByDesignator(
|
|
27
|
+
sceneDescription?.components
|
|
28
|
+
)
|
|
29
|
+
if (!componentByDesignator.size) {
|
|
30
|
+
return sceneDescription
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let changed = false
|
|
34
|
+
const externalPlacements = sceneDescription.externalPlacements.map(
|
|
35
|
+
(placement) => {
|
|
36
|
+
const component = componentByDesignator.get(
|
|
37
|
+
String(placement?.designator || '')
|
|
38
|
+
)
|
|
39
|
+
if (
|
|
40
|
+
!AltiumScene3dAuthoredBodyAnchorAdapter.#shouldMarkPlacement(
|
|
41
|
+
placement,
|
|
42
|
+
component,
|
|
43
|
+
sceneDescription?.board
|
|
44
|
+
)
|
|
45
|
+
) {
|
|
46
|
+
return placement
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
changed = true
|
|
50
|
+
return AltiumScene3dAuthoredBodyAnchorAdapter.#markPlacement(
|
|
51
|
+
placement,
|
|
52
|
+
component,
|
|
53
|
+
sceneDescription?.board
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return changed
|
|
59
|
+
? {
|
|
60
|
+
...sceneDescription,
|
|
61
|
+
externalPlacements
|
|
62
|
+
}
|
|
63
|
+
: sceneDescription
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds a designator lookup for scene components.
|
|
68
|
+
* @param {object[] | undefined} components Scene components.
|
|
69
|
+
* @returns {Map<string, object>}
|
|
70
|
+
*/
|
|
71
|
+
static #componentByDesignator(components) {
|
|
72
|
+
return new Map(
|
|
73
|
+
(Array.isArray(components) ? components : [])
|
|
74
|
+
.map((component) => [
|
|
75
|
+
String(component?.designator || ''),
|
|
76
|
+
component
|
|
77
|
+
])
|
|
78
|
+
.filter(([designator]) => designator)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Checks whether one placement should bypass runtime pad-fallback
|
|
84
|
+
* recentering.
|
|
85
|
+
* @param {object} placement External placement.
|
|
86
|
+
* @param {object | undefined} component Matched scene component.
|
|
87
|
+
* @param {object | undefined} board Scene board.
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
static #shouldMarkPlacement(placement, component, board) {
|
|
91
|
+
if (
|
|
92
|
+
!component ||
|
|
93
|
+
String(placement?.projection?.source || '').toLowerCase() !==
|
|
94
|
+
'pad-fallback' ||
|
|
95
|
+
!placement?.positionMil ||
|
|
96
|
+
!placement?.bodyPositionMil
|
|
97
|
+
) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const bodyPosition = AltiumScene3dAuthoredBodyAnchorAdapter.#point(
|
|
102
|
+
placement.bodyPositionMil
|
|
103
|
+
)
|
|
104
|
+
const placementPosition =
|
|
105
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#absolutePlacementPosition(
|
|
106
|
+
placement,
|
|
107
|
+
board
|
|
108
|
+
)
|
|
109
|
+
const ownerPosition =
|
|
110
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#ownerPosition(
|
|
111
|
+
component,
|
|
112
|
+
board
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#distance(
|
|
117
|
+
bodyPosition,
|
|
118
|
+
ownerPosition
|
|
119
|
+
) < AltiumScene3dAuthoredBodyAnchorAdapter.#MIN_OWNER_OFFSET_MIL
|
|
120
|
+
) {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#distance(
|
|
126
|
+
placementPosition,
|
|
127
|
+
bodyPosition
|
|
128
|
+
) <=
|
|
129
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#BODY_ANCHOR_TOLERANCE_MIL
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Marks one placement as authored-anchor based.
|
|
135
|
+
* @param {object} placement External placement.
|
|
136
|
+
* @param {object} component Matched scene component.
|
|
137
|
+
* @param {object | undefined} board Scene board.
|
|
138
|
+
* @returns {object}
|
|
139
|
+
*/
|
|
140
|
+
static #markPlacement(placement, component, board) {
|
|
141
|
+
return {
|
|
142
|
+
...placement,
|
|
143
|
+
projection: {
|
|
144
|
+
...(placement.projection || {}),
|
|
145
|
+
source: AltiumScene3dAuthoredBodyAnchorAdapter.#AUTHORED_SOURCE,
|
|
146
|
+
reason: 'Altium component body uses an authored model-origin anchor offset from the owner footprint.'
|
|
147
|
+
},
|
|
148
|
+
modelTransform: {
|
|
149
|
+
...(placement.modelTransform || {}),
|
|
150
|
+
ownerAnchorOffsetMil:
|
|
151
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#ownerAnchorOffset(
|
|
152
|
+
placement,
|
|
153
|
+
component,
|
|
154
|
+
board
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolves the source body offset from its owner footprint anchor.
|
|
162
|
+
* @param {object} placement External placement.
|
|
163
|
+
* @param {object} component Matched scene component.
|
|
164
|
+
* @param {object | undefined} board Scene board.
|
|
165
|
+
* @returns {{ x: number, y: number }}
|
|
166
|
+
*/
|
|
167
|
+
static #ownerAnchorOffset(placement, component, board) {
|
|
168
|
+
const bodyPosition = AltiumScene3dAuthoredBodyAnchorAdapter.#point(
|
|
169
|
+
placement.bodyPositionMil
|
|
170
|
+
)
|
|
171
|
+
const ownerPosition =
|
|
172
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#ownerPosition(
|
|
173
|
+
component,
|
|
174
|
+
board
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
x: bodyPosition.x - ownerPosition.x,
|
|
179
|
+
y: bodyPosition.y - ownerPosition.y
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolves a placement position in board coordinates.
|
|
185
|
+
* @param {object} placement External placement.
|
|
186
|
+
* @param {object | undefined} board Scene board.
|
|
187
|
+
* @returns {{ x: number, y: number }}
|
|
188
|
+
*/
|
|
189
|
+
static #absolutePlacementPosition(placement, board) {
|
|
190
|
+
return {
|
|
191
|
+
x:
|
|
192
|
+
Number(placement?.positionMil?.x || 0) +
|
|
193
|
+
Number(board?.centerX || 0),
|
|
194
|
+
y:
|
|
195
|
+
Number(placement?.positionMil?.y || 0) +
|
|
196
|
+
Number(board?.centerY || 0)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Resolves a scene component position in board coordinates.
|
|
202
|
+
* @param {object} component Scene component.
|
|
203
|
+
* @param {object | undefined} board Scene board.
|
|
204
|
+
* @returns {{ x: number, y: number }}
|
|
205
|
+
*/
|
|
206
|
+
static #ownerPosition(component, board) {
|
|
207
|
+
const boardPosition = component?.boardPositionMil
|
|
208
|
+
if (
|
|
209
|
+
Number.isFinite(Number(boardPosition?.x)) &&
|
|
210
|
+
Number.isFinite(Number(boardPosition?.y))
|
|
211
|
+
) {
|
|
212
|
+
return {
|
|
213
|
+
x: Number(boardPosition.x),
|
|
214
|
+
y: Number(boardPosition.y)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
x:
|
|
220
|
+
Number(component?.positionMil?.x || 0) +
|
|
221
|
+
Number(board?.centerX || 0),
|
|
222
|
+
y:
|
|
223
|
+
Number(component?.positionMil?.y || 0) +
|
|
224
|
+
Number(board?.centerY || 0)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Normalizes a partial point.
|
|
230
|
+
* @param {object | undefined} point Source point.
|
|
231
|
+
* @returns {{ x: number, y: number }}
|
|
232
|
+
*/
|
|
233
|
+
static #point(point) {
|
|
234
|
+
return {
|
|
235
|
+
x: Number(point?.x || 0),
|
|
236
|
+
y: Number(point?.y || 0)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Measures XY distance between two points.
|
|
242
|
+
* @param {{ x: number, y: number }} first First point.
|
|
243
|
+
* @param {{ x: number, y: number }} second Second point.
|
|
244
|
+
* @returns {number}
|
|
245
|
+
*/
|
|
246
|
+
static #distance(first, second) {
|
|
247
|
+
return Math.hypot(
|
|
248
|
+
Number(first?.x || 0) - Number(second?.x || 0),
|
|
249
|
+
Number(first?.y || 0) - Number(second?.y || 0)
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
}
|