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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Assigns selectable keys to static 3D body placements.
|
|
7
7
|
*/
|
|
8
8
|
export class PcbScene3dStaticBodySelectionKeyBuilder {
|
|
9
|
-
static #STATIC_SELECTION_CLUSTER_TOLERANCE_MIL =
|
|
9
|
+
static #STATIC_SELECTION_CLUSTER_TOLERANCE_MIL = 2
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Assigns stable selectable keys while preserving display designators.
|
|
@@ -37,7 +37,7 @@ export class PcbScene3dStaticBodySelectionKeyBuilder {
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
sourceGroups.forEach((indexes) => {
|
|
40
|
-
|
|
40
|
+
let clusters =
|
|
41
41
|
PcbScene3dStaticBodySelectionKeyBuilder.#clusterPlacementIndexes(
|
|
42
42
|
placements,
|
|
43
43
|
indexes
|
|
@@ -56,7 +56,20 @@ export class PcbScene3dStaticBodySelectionKeyBuilder {
|
|
|
56
56
|
return
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
clusters = clusters.flatMap((cluster) =>
|
|
60
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#displaySubclusters(
|
|
61
|
+
placements,
|
|
62
|
+
placementRows,
|
|
63
|
+
cluster
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
59
67
|
const usedKeys = new Set()
|
|
68
|
+
const displayDesignatorCounts =
|
|
69
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#clusterDisplayDesignatorCounts(
|
|
70
|
+
placements,
|
|
71
|
+
clusters
|
|
72
|
+
)
|
|
60
73
|
clusters
|
|
61
74
|
.sort((left, right) =>
|
|
62
75
|
PcbScene3dStaticBodySelectionKeyBuilder.#compareClusterCenters(
|
|
@@ -70,8 +83,17 @@ export class PcbScene3dStaticBodySelectionKeyBuilder {
|
|
|
70
83
|
placementRows,
|
|
71
84
|
cluster.indexes
|
|
72
85
|
)
|
|
86
|
+
const displayDesignator =
|
|
87
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#uniquePlacementDesignator(
|
|
88
|
+
placements,
|
|
89
|
+
cluster.indexes
|
|
90
|
+
)
|
|
73
91
|
const selectionKey =
|
|
74
92
|
matchedDesignator ||
|
|
93
|
+
(displayDesignator &&
|
|
94
|
+
displayDesignatorCounts.get(displayDesignator) === 1
|
|
95
|
+
? displayDesignator
|
|
96
|
+
: '') ||
|
|
75
97
|
PcbScene3dStaticBodySelectionKeyBuilder.#uniqueSelectionKey(
|
|
76
98
|
placements[cluster.indexes[0]],
|
|
77
99
|
cluster,
|
|
@@ -83,9 +105,68 @@ export class PcbScene3dStaticBodySelectionKeyBuilder {
|
|
|
83
105
|
})
|
|
84
106
|
})
|
|
85
107
|
|
|
108
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#promoteStrictTouchingOwners(
|
|
109
|
+
placements,
|
|
110
|
+
placementRows
|
|
111
|
+
)
|
|
112
|
+
|
|
86
113
|
return placements
|
|
87
114
|
}
|
|
88
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Promotes one clear matched owner across physically touching fragments
|
|
118
|
+
* even when the fragments came from different source body identities.
|
|
119
|
+
* @param {{ mountSide?: string, selectionKey?: string }[]} placements Mutable placements.
|
|
120
|
+
* @param {{ matchedComponent?: { designator?: string } | null }[]} placementRows Placement rows.
|
|
121
|
+
*/
|
|
122
|
+
static #promoteStrictTouchingOwners(placements, placementRows) {
|
|
123
|
+
const sideGroups = new Map()
|
|
124
|
+
|
|
125
|
+
placements.forEach((placement, index) => {
|
|
126
|
+
const groupKey =
|
|
127
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#placementSideGroupKey(
|
|
128
|
+
placement
|
|
129
|
+
)
|
|
130
|
+
if (!sideGroups.has(groupKey)) {
|
|
131
|
+
sideGroups.set(groupKey, [])
|
|
132
|
+
}
|
|
133
|
+
sideGroups.get(groupKey)?.push(index)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
sideGroups.forEach((indexes) => {
|
|
137
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#clusterPlacementIndexes(
|
|
138
|
+
placements,
|
|
139
|
+
indexes
|
|
140
|
+
).forEach((cluster) => {
|
|
141
|
+
if (cluster.indexes.length <= 1) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const matchedDesignator =
|
|
146
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#uniqueMatchedDesignator(
|
|
147
|
+
placementRows,
|
|
148
|
+
cluster.indexes
|
|
149
|
+
)
|
|
150
|
+
if (!matchedDesignator) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
cluster.indexes.forEach((index) => {
|
|
155
|
+
placements[index].selectionKey = matchedDesignator
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Builds the grouping key used for strict same-side physical clusters.
|
|
163
|
+
* @param {{ mountSide?: string }} placement Static placement.
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
static #placementSideGroupKey(placement) {
|
|
167
|
+
return String(placement?.mountSide || '').trim()
|
|
168
|
+
}
|
|
169
|
+
|
|
89
170
|
/**
|
|
90
171
|
* Builds the grouping key used to connect sibling static body shapes.
|
|
91
172
|
* @param {{ designator?: string, sourceIdentityKey?: string, mountSide?: string }} placement Static placement.
|
|
@@ -135,6 +216,122 @@ export class PcbScene3dStaticBodySelectionKeyBuilder {
|
|
|
135
216
|
return matchedDesignators.size === 1 ? [...matchedDesignators][0] : ''
|
|
136
217
|
}
|
|
137
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Counts unique display designators across source-body clusters.
|
|
221
|
+
* @param {{ designator?: string }[]} placements Static placements.
|
|
222
|
+
* @param {{ indexes: number[] }[]} clusters Static body clusters.
|
|
223
|
+
* @returns {Map<string, number>}
|
|
224
|
+
*/
|
|
225
|
+
static #clusterDisplayDesignatorCounts(placements, clusters) {
|
|
226
|
+
const counts = new Map()
|
|
227
|
+
|
|
228
|
+
clusters.forEach((cluster) => {
|
|
229
|
+
const designator =
|
|
230
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#uniquePlacementDesignator(
|
|
231
|
+
placements,
|
|
232
|
+
cluster.indexes
|
|
233
|
+
)
|
|
234
|
+
if (!designator) {
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
counts.set(designator, (counts.get(designator) || 0) + 1)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
return counts
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolves one unambiguous display designator for a cluster.
|
|
246
|
+
* @param {{ designator?: string }[]} placements Static placements.
|
|
247
|
+
* @param {number[]} indexes Cluster indexes.
|
|
248
|
+
* @returns {string}
|
|
249
|
+
*/
|
|
250
|
+
static #uniquePlacementDesignator(placements, indexes) {
|
|
251
|
+
const designators = new Set(
|
|
252
|
+
indexes
|
|
253
|
+
.map((index) =>
|
|
254
|
+
String(placements[index]?.designator || '').trim()
|
|
255
|
+
)
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
return designators.size === 1 ? [...designators][0] : ''
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Splits an ambiguous physical cluster by display designator.
|
|
264
|
+
* @param {{ designator?: string }[]} placements Static placements.
|
|
265
|
+
* @param {{ matchedComponent?: { designator?: string } | null }[]} placementRows Placement rows.
|
|
266
|
+
* @param {{ indexes: number[], bounds: object, center: object }} cluster Static body cluster.
|
|
267
|
+
* @returns {{ indexes: number[], bounds: object, center: object }[]}
|
|
268
|
+
*/
|
|
269
|
+
static #displaySubclusters(placements, placementRows, cluster) {
|
|
270
|
+
if (
|
|
271
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#uniqueMatchedDesignator(
|
|
272
|
+
placementRows,
|
|
273
|
+
cluster.indexes
|
|
274
|
+
)
|
|
275
|
+
) {
|
|
276
|
+
return [cluster]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const groups = new Map()
|
|
280
|
+
cluster.indexes.forEach((index) => {
|
|
281
|
+
const designator = String(
|
|
282
|
+
placements[index]?.designator || ''
|
|
283
|
+
).trim()
|
|
284
|
+
if (!designator) {
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!groups.has(designator)) {
|
|
289
|
+
groups.set(designator, [])
|
|
290
|
+
}
|
|
291
|
+
groups.get(designator)?.push(index)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
return groups.size > 1
|
|
295
|
+
? [...groups.values()].map((groupIndexes) =>
|
|
296
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#clusterForIndexes(
|
|
297
|
+
placements,
|
|
298
|
+
groupIndexes
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
: [cluster]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Builds a cluster record for explicit placement indexes.
|
|
306
|
+
* @param {object[]} placements Static placements.
|
|
307
|
+
* @param {number[]} indexes Placement indexes.
|
|
308
|
+
* @returns {{ indexes: number[], bounds: object, center: object }}
|
|
309
|
+
*/
|
|
310
|
+
static #clusterForIndexes(placements, indexes) {
|
|
311
|
+
const bounds = indexes
|
|
312
|
+
.map((index) =>
|
|
313
|
+
PcbScene3dStaticBodySelectionKeyBuilder.#placementBounds(
|
|
314
|
+
placements[index]
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
.reduce((mergedBounds, boundsCandidate) =>
|
|
318
|
+
mergedBounds
|
|
319
|
+
? PcbScene3dStaticBodySelectionKeyBuilder.#mergeBounds(
|
|
320
|
+
mergedBounds,
|
|
321
|
+
boundsCandidate
|
|
322
|
+
)
|
|
323
|
+
: boundsCandidate
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
indexes,
|
|
328
|
+
bounds,
|
|
329
|
+
center: PcbScene3dStaticBodySelectionKeyBuilder.#boundsCenter(
|
|
330
|
+
bounds
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
138
335
|
/**
|
|
139
336
|
* Clusters static body placements whose horizontal geometry touches.
|
|
140
337
|
* @param {object[]} placements Static body placements.
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Promotes complete symmetric source-coordinate chip-body fragments.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbScene3dStaticBodySymmetricOwnerPromotion {
|
|
9
|
+
static #CENTER_TOLERANCE_MIL = 2
|
|
10
|
+
static #MAX_DISTANCE_MIL = 18
|
|
11
|
+
static #MIN_PROJECTION_MIL = 4
|
|
12
|
+
static #PROJECTION_TOLERANCE_MIL = 2.5
|
|
13
|
+
static #PERPENDICULAR_TOLERANCE_MIL = 4
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Reassigns complete symmetric source-coordinate fragments as one body.
|
|
17
|
+
* @param {{ placement: object, matchedComponent: object | null }[]} placementRows Mutable placement rows.
|
|
18
|
+
* @param {{ designator?: string, x?: number, y?: number, rotation?: number, layer?: string }[]} components PCB components.
|
|
19
|
+
*/
|
|
20
|
+
static promote(placementRows, components = []) {
|
|
21
|
+
const claims = (Array.isArray(components) ? components : [])
|
|
22
|
+
.map((component) =>
|
|
23
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#ownerClaim(
|
|
24
|
+
placementRows,
|
|
25
|
+
component
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.sort((left, right) => left.score - right.score)
|
|
30
|
+
const assignedIndexes = new Set()
|
|
31
|
+
|
|
32
|
+
claims.forEach((claim) => {
|
|
33
|
+
if (claim.indexes.some((index) => assignedIndexes.has(index))) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
claim.indexes.forEach((index) => {
|
|
38
|
+
assignedIndexes.add(index)
|
|
39
|
+
placementRows[index].matchedComponent = claim.component
|
|
40
|
+
placementRows[index].placement =
|
|
41
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#withOwner(
|
|
42
|
+
placementRows[index].placement,
|
|
43
|
+
claim.component
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Finds one centered source-coordinate owner claim for a component.
|
|
51
|
+
* @param {{ placement: object }[]} placementRows Placement rows.
|
|
52
|
+
* @param {{ designator?: string, x?: number, y?: number, rotation?: number, layer?: string }} component PCB component.
|
|
53
|
+
* @returns {{ component: object, indexes: number[], score: number } | null}
|
|
54
|
+
*/
|
|
55
|
+
static #ownerClaim(placementRows, component) {
|
|
56
|
+
const center =
|
|
57
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#componentPosition(
|
|
58
|
+
component
|
|
59
|
+
)
|
|
60
|
+
if (!center || !String(component?.designator || '').trim()) {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const axis =
|
|
65
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#componentAxis(
|
|
66
|
+
component
|
|
67
|
+
)
|
|
68
|
+
const ownerSide =
|
|
69
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#componentMountSide(
|
|
70
|
+
component
|
|
71
|
+
)
|
|
72
|
+
const candidates =
|
|
73
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#sourceCandidates(
|
|
74
|
+
placementRows
|
|
75
|
+
)
|
|
76
|
+
.filter(
|
|
77
|
+
(candidate) =>
|
|
78
|
+
!candidate.row?.placement?.mountSideLocked ||
|
|
79
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#normalizeMountSide(
|
|
80
|
+
candidate.row?.placement?.mountSide
|
|
81
|
+
) === ownerSide
|
|
82
|
+
)
|
|
83
|
+
.map((candidate) => ({
|
|
84
|
+
...candidate,
|
|
85
|
+
distance: Math.hypot(
|
|
86
|
+
candidate.position.x - center.x,
|
|
87
|
+
candidate.position.y - center.y
|
|
88
|
+
)
|
|
89
|
+
}))
|
|
90
|
+
const centerCandidates = candidates
|
|
91
|
+
.filter(
|
|
92
|
+
(candidate) =>
|
|
93
|
+
candidate.distance <=
|
|
94
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion
|
|
95
|
+
.#CENTER_TOLERANCE_MIL
|
|
96
|
+
)
|
|
97
|
+
.sort((left, right) => left.distance - right.distance)
|
|
98
|
+
|
|
99
|
+
return centerCandidates
|
|
100
|
+
.map((centerCandidate) =>
|
|
101
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#bestPairClaim(
|
|
102
|
+
component,
|
|
103
|
+
center,
|
|
104
|
+
axis,
|
|
105
|
+
centerCandidate,
|
|
106
|
+
candidates
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.sort((left, right) => left.score - right.score)[0]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Finds the best opposite matching fragment pair for one center fragment.
|
|
115
|
+
* @param {object} component Owner component.
|
|
116
|
+
* @param {{ x: number, y: number }} center Component center.
|
|
117
|
+
* @param {{ x: number, y: number }} axis Component axis unit vector.
|
|
118
|
+
* @param {{ index: number, distance: number }} centerCandidate Center fragment.
|
|
119
|
+
* @param {object[]} candidates Candidate fragments.
|
|
120
|
+
* @returns {{ component: object, indexes: number[], score: number } | null}
|
|
121
|
+
*/
|
|
122
|
+
static #bestPairClaim(
|
|
123
|
+
component,
|
|
124
|
+
center,
|
|
125
|
+
axis,
|
|
126
|
+
centerCandidate,
|
|
127
|
+
candidates
|
|
128
|
+
) {
|
|
129
|
+
const sideCandidates = candidates
|
|
130
|
+
.filter((candidate) => candidate.index !== centerCandidate.index)
|
|
131
|
+
.map((candidate) => ({
|
|
132
|
+
...candidate,
|
|
133
|
+
offset: PcbScene3dStaticBodySymmetricOwnerPromotion.#axisOffset(
|
|
134
|
+
candidate.position,
|
|
135
|
+
center,
|
|
136
|
+
axis
|
|
137
|
+
)
|
|
138
|
+
}))
|
|
139
|
+
.filter((candidate) =>
|
|
140
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#isSideCandidate(
|
|
141
|
+
candidate
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
let bestClaim = null
|
|
145
|
+
|
|
146
|
+
sideCandidates.forEach((negative) => {
|
|
147
|
+
if (negative.offset.projection >= 0) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
sideCandidates
|
|
152
|
+
.filter(
|
|
153
|
+
(positive) =>
|
|
154
|
+
positive.offset.projection > 0 &&
|
|
155
|
+
positive.identityKey === negative.identityKey
|
|
156
|
+
)
|
|
157
|
+
.forEach((positive) => {
|
|
158
|
+
const symmetry = Math.abs(
|
|
159
|
+
Math.abs(negative.offset.projection) -
|
|
160
|
+
Math.abs(positive.offset.projection)
|
|
161
|
+
)
|
|
162
|
+
if (
|
|
163
|
+
symmetry >
|
|
164
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion
|
|
165
|
+
.#PROJECTION_TOLERANCE_MIL
|
|
166
|
+
) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const score =
|
|
171
|
+
centerCandidate.distance +
|
|
172
|
+
symmetry +
|
|
173
|
+
negative.offset.perpendicular +
|
|
174
|
+
positive.offset.perpendicular
|
|
175
|
+
if (!bestClaim || score < bestClaim.score) {
|
|
176
|
+
bestClaim = {
|
|
177
|
+
component,
|
|
178
|
+
indexes: [
|
|
179
|
+
centerCandidate.index,
|
|
180
|
+
negative.index,
|
|
181
|
+
positive.index
|
|
182
|
+
],
|
|
183
|
+
score
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
return bestClaim
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Builds compact source-coordinate placement candidates.
|
|
194
|
+
* @param {{ placement: object }[]} placementRows Placement rows.
|
|
195
|
+
* @returns {object[]}
|
|
196
|
+
*/
|
|
197
|
+
static #sourceCandidates(placementRows) {
|
|
198
|
+
return placementRows
|
|
199
|
+
.map((row, index) => ({
|
|
200
|
+
index,
|
|
201
|
+
row,
|
|
202
|
+
position:
|
|
203
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#sourcePosition(
|
|
204
|
+
row?.placement
|
|
205
|
+
),
|
|
206
|
+
identityKey: String(row?.placement?.sourceIdentityKey || '')
|
|
207
|
+
}))
|
|
208
|
+
.filter(
|
|
209
|
+
(candidate) =>
|
|
210
|
+
candidate.position &&
|
|
211
|
+
candidate.row?.placement?.sourceCoordinateFrame &&
|
|
212
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#isCompactRow(
|
|
213
|
+
candidate.row
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Checks whether a projected fragment can be one side of a chip body.
|
|
220
|
+
* @param {{ offset: { projection: number, perpendicular: number } }} candidate Candidate fragment.
|
|
221
|
+
* @returns {boolean}
|
|
222
|
+
*/
|
|
223
|
+
static #isSideCandidate(candidate) {
|
|
224
|
+
return (
|
|
225
|
+
Math.abs(candidate.offset.projection) >=
|
|
226
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion
|
|
227
|
+
.#MIN_PROJECTION_MIL &&
|
|
228
|
+
Math.abs(candidate.offset.projection) <=
|
|
229
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#MAX_DISTANCE_MIL &&
|
|
230
|
+
candidate.offset.perpendicular <=
|
|
231
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion
|
|
232
|
+
.#PERPENDICULAR_TOLERANCE_MIL
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Checks whether one row is compact enough for symmetric ownership.
|
|
238
|
+
* @param {{ placement: object }} row Placement row.
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
static #isCompactRow(row) {
|
|
242
|
+
return (
|
|
243
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#maxSpan(
|
|
244
|
+
row.placement?.geometry
|
|
245
|
+
) <= 40
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Resolves the source-space body anchor used for owner proximity.
|
|
251
|
+
* @param {{ bodyPositionMil?: { x?: number, y?: number } }} placement Static placement.
|
|
252
|
+
* @returns {{ x: number, y: number } | null}
|
|
253
|
+
*/
|
|
254
|
+
static #sourcePosition(placement) {
|
|
255
|
+
const x = Number(placement?.bodyPositionMil?.x)
|
|
256
|
+
const y = Number(placement?.bodyPositionMil?.y)
|
|
257
|
+
|
|
258
|
+
return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Resolves a component source-space center.
|
|
263
|
+
* @param {{ x?: number, y?: number }} component PCB component.
|
|
264
|
+
* @returns {{ x: number, y: number } | null}
|
|
265
|
+
*/
|
|
266
|
+
static #componentPosition(component) {
|
|
267
|
+
const x = Number(component?.x)
|
|
268
|
+
const y = Number(component?.y)
|
|
269
|
+
|
|
270
|
+
return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Resolves a component axis unit vector.
|
|
275
|
+
* @param {{ rotation?: number }} component PCB component.
|
|
276
|
+
* @returns {{ x: number, y: number }}
|
|
277
|
+
*/
|
|
278
|
+
static #componentAxis(component) {
|
|
279
|
+
const angle = (Number(component?.rotation || 0) * Math.PI) / 180
|
|
280
|
+
|
|
281
|
+
return { x: Math.cos(angle), y: Math.sin(angle) }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Projects a source-space point onto a component axis.
|
|
286
|
+
* @param {{ x: number, y: number }} position Fragment source position.
|
|
287
|
+
* @param {{ x: number, y: number }} center Component source position.
|
|
288
|
+
* @param {{ x: number, y: number }} axis Component axis unit vector.
|
|
289
|
+
* @returns {{ projection: number, perpendicular: number }}
|
|
290
|
+
*/
|
|
291
|
+
static #axisOffset(position, center, axis) {
|
|
292
|
+
const dx = position.x - center.x
|
|
293
|
+
const dy = position.y - center.y
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
projection: dx * axis.x + dy * axis.y,
|
|
297
|
+
perpendicular: Math.abs(-dx * axis.y + dy * axis.x)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Applies a component owner to a placement.
|
|
303
|
+
* @param {object} placement Static placement.
|
|
304
|
+
* @param {{ designator?: string, layer?: string }} owner Owner component.
|
|
305
|
+
* @returns {object}
|
|
306
|
+
*/
|
|
307
|
+
static #withOwner(placement, owner) {
|
|
308
|
+
return {
|
|
309
|
+
...PcbScene3dStaticBodySymmetricOwnerPromotion.#withMountSide(
|
|
310
|
+
placement,
|
|
311
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#componentMountSide(
|
|
312
|
+
owner
|
|
313
|
+
)
|
|
314
|
+
),
|
|
315
|
+
designator:
|
|
316
|
+
String(owner?.designator || '').trim() || placement?.designator
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Applies a mount side while preserving the placement's XY anchor.
|
|
322
|
+
* @param {object} placement Static placement.
|
|
323
|
+
* @param {string | undefined} mountSide Owner mount side.
|
|
324
|
+
* @returns {object}
|
|
325
|
+
*/
|
|
326
|
+
static #withMountSide(placement, mountSide) {
|
|
327
|
+
const currentSide =
|
|
328
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#normalizeMountSide(
|
|
329
|
+
placement?.mountSide
|
|
330
|
+
)
|
|
331
|
+
const side =
|
|
332
|
+
PcbScene3dStaticBodySymmetricOwnerPromotion.#normalizeMountSide(
|
|
333
|
+
mountSide || currentSide
|
|
334
|
+
)
|
|
335
|
+
const z = Math.abs(Number(placement?.positionMil?.z || 0))
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
...placement,
|
|
339
|
+
mountSide: side,
|
|
340
|
+
positionMil: {
|
|
341
|
+
...(placement?.positionMil || {}),
|
|
342
|
+
z: side === 'bottom' ? -z : z
|
|
343
|
+
},
|
|
344
|
+
geometry:
|
|
345
|
+
currentSide === side
|
|
346
|
+
? placement?.geometry
|
|
347
|
+
: PcbScene3dStaticBodySymmetricOwnerPromotion.#mirrorGeometry(
|
|
348
|
+
placement
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Resolves the board side from one component layer.
|
|
355
|
+
* @param {{ layer?: string }} component PCB component.
|
|
356
|
+
* @returns {'top' | 'bottom'}
|
|
357
|
+
*/
|
|
358
|
+
static #componentMountSide(component) {
|
|
359
|
+
return String(component?.layer || '')
|
|
360
|
+
.trim()
|
|
361
|
+
.toLowerCase()
|
|
362
|
+
.includes('bottom')
|
|
363
|
+
? 'bottom'
|
|
364
|
+
: 'top'
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Normalizes a mount side token.
|
|
369
|
+
* @param {string | undefined} mountSide Candidate mount side.
|
|
370
|
+
* @returns {'top' | 'bottom'}
|
|
371
|
+
*/
|
|
372
|
+
static #normalizeMountSide(mountSide) {
|
|
373
|
+
return String(mountSide || 'top')
|
|
374
|
+
.trim()
|
|
375
|
+
.toLowerCase() === 'bottom'
|
|
376
|
+
? 'bottom'
|
|
377
|
+
: 'top'
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Mirrors source-coordinate geometry into the opposite mount-local frame.
|
|
382
|
+
* @param {{ sourceCoordinateFrame?: boolean, geometry?: object }} placement Static placement.
|
|
383
|
+
* @returns {object | undefined}
|
|
384
|
+
*/
|
|
385
|
+
static #mirrorGeometry(placement) {
|
|
386
|
+
const geometry = placement?.geometry
|
|
387
|
+
if (
|
|
388
|
+
!placement?.sourceCoordinateFrame ||
|
|
389
|
+
!Array.isArray(geometry?.verticesMil)
|
|
390
|
+
) {
|
|
391
|
+
return geometry
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
...geometry,
|
|
396
|
+
verticesMil: geometry.verticesMil.map((vertex) => ({
|
|
397
|
+
x: PcbScene3dStaticBodySymmetricOwnerPromotion.#roundMil(
|
|
398
|
+
Number(vertex?.x || 0)
|
|
399
|
+
),
|
|
400
|
+
y: PcbScene3dStaticBodySymmetricOwnerPromotion.#roundMil(
|
|
401
|
+
-Number(vertex?.y || 0)
|
|
402
|
+
)
|
|
403
|
+
}))
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Resolves the largest horizontal geometry span.
|
|
409
|
+
* @param {object | undefined} geometry Static geometry.
|
|
410
|
+
* @returns {number}
|
|
411
|
+
*/
|
|
412
|
+
static #maxSpan(geometry) {
|
|
413
|
+
const vertices = Array.isArray(geometry?.verticesMil)
|
|
414
|
+
? geometry.verticesMil
|
|
415
|
+
: []
|
|
416
|
+
if (!vertices.length) {
|
|
417
|
+
return 0
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const xs = vertices.map((vertex) => Number(vertex?.x || 0))
|
|
421
|
+
const ys = vertices.map((vertex) => Number(vertex?.y || 0))
|
|
422
|
+
|
|
423
|
+
return Math.max(
|
|
424
|
+
Math.max(...xs) - Math.min(...xs),
|
|
425
|
+
Math.max(...ys) - Math.min(...ys)
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Rounds one mil value for stable promoted geometry output.
|
|
431
|
+
* @param {number} value Candidate value.
|
|
432
|
+
* @returns {number}
|
|
433
|
+
*/
|
|
434
|
+
static #roundMil(value) {
|
|
435
|
+
const rounded = Math.round(Number(value) * 10000) / 10000
|
|
436
|
+
return Object.is(rounded, -0) ? 0 : rounded
|
|
437
|
+
}
|
|
438
|
+
}
|