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
|
@@ -9,10 +9,21 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
9
9
|
static #NEARBY_SIDE_HINT_MAX_DISTANCE_MIL = 600
|
|
10
10
|
static #NEGATIVE_STANDOFF_SIDE_RATIO = 0.3
|
|
11
11
|
static #MIN_NEGATIVE_STANDOFF_SIDE_MIL = 20
|
|
12
|
+
static #IGNORED_IDENTITY_TOKENS = new Set([
|
|
13
|
+
'con',
|
|
14
|
+
'step',
|
|
15
|
+
'stp',
|
|
16
|
+
'model',
|
|
17
|
+
'default',
|
|
18
|
+
'black'
|
|
19
|
+
])
|
|
20
|
+
static #BODY_TOKEN_CACHE = new WeakMap()
|
|
21
|
+
static #COMPONENT_TOKEN_CACHE = new WeakMap()
|
|
22
|
+
static #AFFINITY_SCORE_CACHE = new WeakMap()
|
|
12
23
|
|
|
13
24
|
/**
|
|
14
25
|
* Resolves which board side one explicit model should mount on.
|
|
15
|
-
* @param {{ layer?: string, positionMil?: { x?: number, y?: number }, standoffHeightMil?: number | null, overallHeightMil?: number | null }} componentBody
|
|
26
|
+
* @param {{ layer?: string, positionMil?: { x?: number, y?: number }, dzMil?: number | null, standoffHeightMil?: number | null, overallHeightMil?: number | null }} componentBody
|
|
16
27
|
* @param {{ layer?: string } | null} matchedComponent
|
|
17
28
|
* @param {{ layer?: string, pattern?: string, source?: string, modelPath?: string, x?: number, y?: number }[]} components
|
|
18
29
|
* @param {{ minX?: number, minY?: number, widthMil?: number, heightMil?: number } | null} board
|
|
@@ -34,14 +45,21 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
34
45
|
|
|
35
46
|
const standoffSide =
|
|
36
47
|
PcbScene3dPlacementSideResolver.#resolveStandoffSide(componentBody)
|
|
48
|
+
const trustedStandoffSide =
|
|
49
|
+
PcbScene3dPlacementSideResolver.#shouldTrustStandoffSide(
|
|
50
|
+
componentBody,
|
|
51
|
+
standoffSide
|
|
52
|
+
)
|
|
53
|
+
? standoffSide
|
|
54
|
+
: null
|
|
37
55
|
if (
|
|
38
|
-
|
|
56
|
+
trustedStandoffSide &&
|
|
39
57
|
PcbScene3dPlacementSideResolver.#isBodyAnchorInsideBoard(
|
|
40
58
|
componentBody,
|
|
41
59
|
board
|
|
42
60
|
)
|
|
43
61
|
) {
|
|
44
|
-
return
|
|
62
|
+
return trustedStandoffSide
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
const nearbySide =
|
|
@@ -61,14 +79,14 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
61
79
|
return mechanicalSide
|
|
62
80
|
}
|
|
63
81
|
|
|
64
|
-
return
|
|
82
|
+
return trustedStandoffSide || 'top'
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
/**
|
|
68
86
|
* Resolves which board side one authored static shape body should mount on.
|
|
69
87
|
* Shape bodies carry explicit mechanical-layer intent, so that side wins
|
|
70
88
|
* over loose nearby-package identity unless the body was directly matched.
|
|
71
|
-
* @param {{ layer?: string, positionMil?: { x?: number, y?: number }, standoffHeightMil?: number | null, overallHeightMil?: number | null }} componentBody
|
|
89
|
+
* @param {{ layer?: string, positionMil?: { x?: number, y?: number }, dzMil?: number | null, standoffHeightMil?: number | null, overallHeightMil?: number | null }} componentBody
|
|
72
90
|
* @param {{ layer?: string } | null} matchedComponent
|
|
73
91
|
* @param {{ layer?: string, pattern?: string, source?: string, modelPath?: string, x?: number, y?: number }[]} components
|
|
74
92
|
* @param {{ minX?: number, minY?: number, widthMil?: number, heightMil?: number } | null} board
|
|
@@ -90,14 +108,21 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
90
108
|
|
|
91
109
|
const standoffSide =
|
|
92
110
|
PcbScene3dPlacementSideResolver.#resolveStandoffSide(componentBody)
|
|
111
|
+
const trustedStandoffSide =
|
|
112
|
+
PcbScene3dPlacementSideResolver.#shouldTrustStandoffSide(
|
|
113
|
+
componentBody,
|
|
114
|
+
standoffSide
|
|
115
|
+
)
|
|
116
|
+
? standoffSide
|
|
117
|
+
: null
|
|
93
118
|
if (
|
|
94
|
-
|
|
119
|
+
trustedStandoffSide &&
|
|
95
120
|
PcbScene3dPlacementSideResolver.#isBodyAnchorInsideBoard(
|
|
96
121
|
componentBody,
|
|
97
122
|
board
|
|
98
123
|
)
|
|
99
124
|
) {
|
|
100
|
-
return
|
|
125
|
+
return trustedStandoffSide
|
|
101
126
|
}
|
|
102
127
|
|
|
103
128
|
const mechanicalSide =
|
|
@@ -117,7 +142,7 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
117
142
|
return nearbySide
|
|
118
143
|
}
|
|
119
144
|
|
|
120
|
-
return
|
|
145
|
+
return trustedStandoffSide || 'top'
|
|
121
146
|
}
|
|
122
147
|
|
|
123
148
|
/**
|
|
@@ -141,21 +166,39 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
141
166
|
* @returns {number}
|
|
142
167
|
*/
|
|
143
168
|
static scoreBodyComponentAffinity(componentBody, component) {
|
|
169
|
+
const bodyValues =
|
|
170
|
+
PcbScene3dPlacementSideResolver.#bodyAffinityValues(componentBody)
|
|
171
|
+
const componentValues =
|
|
172
|
+
PcbScene3dPlacementSideResolver.#componentAffinityValues(component)
|
|
173
|
+
const bodyKey =
|
|
174
|
+
PcbScene3dPlacementSideResolver.#cacheIdentityKey(bodyValues)
|
|
175
|
+
const componentKey =
|
|
176
|
+
PcbScene3dPlacementSideResolver.#cacheIdentityKey(componentValues)
|
|
177
|
+
const cachedScore =
|
|
178
|
+
PcbScene3dPlacementSideResolver.#cachedAffinityScore(
|
|
179
|
+
componentBody,
|
|
180
|
+
component,
|
|
181
|
+
bodyKey,
|
|
182
|
+
componentKey
|
|
183
|
+
)
|
|
184
|
+
if (cachedScore !== null) {
|
|
185
|
+
return cachedScore
|
|
186
|
+
}
|
|
187
|
+
|
|
144
188
|
const bodyTokens =
|
|
145
|
-
PcbScene3dPlacementSideResolver.#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
189
|
+
PcbScene3dPlacementSideResolver.#cachedMeaningfulTokens(
|
|
190
|
+
PcbScene3dPlacementSideResolver.#BODY_TOKEN_CACHE,
|
|
191
|
+
componentBody,
|
|
192
|
+
bodyKey,
|
|
193
|
+
bodyValues
|
|
194
|
+
)
|
|
149
195
|
const componentTokens =
|
|
150
|
-
PcbScene3dPlacementSideResolver.#
|
|
151
|
-
|
|
152
|
-
component
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
component
|
|
157
|
-
)
|
|
158
|
-
])
|
|
196
|
+
PcbScene3dPlacementSideResolver.#cachedMeaningfulTokens(
|
|
197
|
+
PcbScene3dPlacementSideResolver.#COMPONENT_TOKEN_CACHE,
|
|
198
|
+
component,
|
|
199
|
+
componentKey,
|
|
200
|
+
componentValues
|
|
201
|
+
)
|
|
159
202
|
let score = 0
|
|
160
203
|
|
|
161
204
|
bodyTokens.forEach((token) => {
|
|
@@ -164,9 +207,166 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
164
207
|
}
|
|
165
208
|
})
|
|
166
209
|
|
|
210
|
+
PcbScene3dPlacementSideResolver.#cacheAffinityScore(
|
|
211
|
+
componentBody,
|
|
212
|
+
component,
|
|
213
|
+
bodyKey,
|
|
214
|
+
componentKey,
|
|
215
|
+
score
|
|
216
|
+
)
|
|
217
|
+
|
|
167
218
|
return score
|
|
168
219
|
}
|
|
169
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Resolves body identity fields used for affinity scoring.
|
|
223
|
+
* @param {{ name?: string, identifier?: string } | null | undefined} componentBody Component-body record.
|
|
224
|
+
* @returns {(string | undefined)[]}
|
|
225
|
+
*/
|
|
226
|
+
static #bodyAffinityValues(componentBody) {
|
|
227
|
+
return [
|
|
228
|
+
componentBody?.identifier,
|
|
229
|
+
String(componentBody?.name || '').replace(/\.[^.]+$/, '')
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Resolves component identity fields used for affinity scoring.
|
|
235
|
+
* @param {{ pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object } | null | undefined} component Component record.
|
|
236
|
+
* @returns {(string | undefined)[]}
|
|
237
|
+
*/
|
|
238
|
+
static #componentAffinityValues(component) {
|
|
239
|
+
return [
|
|
240
|
+
component?.pattern,
|
|
241
|
+
component?.source,
|
|
242
|
+
component?.modelPath,
|
|
243
|
+
component?.description,
|
|
244
|
+
...PcbScene3dPlacementSideResolver.#componentPackageMetadata(
|
|
245
|
+
component
|
|
246
|
+
)
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Resolves a deterministic key for identity fields.
|
|
252
|
+
* @param {unknown[]} values Identity field values.
|
|
253
|
+
* @returns {string}
|
|
254
|
+
*/
|
|
255
|
+
static #cacheIdentityKey(values) {
|
|
256
|
+
return (Array.isArray(values) ? values : [])
|
|
257
|
+
.map((value) => String(value || ''))
|
|
258
|
+
.join('\u0000')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Returns a cached score when both input identity keys still match.
|
|
263
|
+
* @param {unknown} componentBody Component-body record.
|
|
264
|
+
* @param {unknown} component Component record.
|
|
265
|
+
* @param {string} bodyKey Current body identity key.
|
|
266
|
+
* @param {string} componentKey Current component identity key.
|
|
267
|
+
* @returns {number | null}
|
|
268
|
+
*/
|
|
269
|
+
static #cachedAffinityScore(
|
|
270
|
+
componentBody,
|
|
271
|
+
component,
|
|
272
|
+
bodyKey,
|
|
273
|
+
componentKey
|
|
274
|
+
) {
|
|
275
|
+
if (
|
|
276
|
+
!PcbScene3dPlacementSideResolver.#isObjectLike(componentBody) ||
|
|
277
|
+
!PcbScene3dPlacementSideResolver.#isObjectLike(component)
|
|
278
|
+
) {
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const componentScores =
|
|
283
|
+
PcbScene3dPlacementSideResolver.#AFFINITY_SCORE_CACHE.get(
|
|
284
|
+
componentBody
|
|
285
|
+
)
|
|
286
|
+
const cachedScore = componentScores?.get(component)
|
|
287
|
+
|
|
288
|
+
return cachedScore?.bodyKey === bodyKey &&
|
|
289
|
+
cachedScore?.componentKey === componentKey
|
|
290
|
+
? cachedScore.score
|
|
291
|
+
: null
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Caches one body/component affinity score.
|
|
296
|
+
* @param {unknown} componentBody Component-body record.
|
|
297
|
+
* @param {unknown} component Component record.
|
|
298
|
+
* @param {string} bodyKey Current body identity key.
|
|
299
|
+
* @param {string} componentKey Current component identity key.
|
|
300
|
+
* @param {number} score Affinity score.
|
|
301
|
+
* @returns {void}
|
|
302
|
+
*/
|
|
303
|
+
static #cacheAffinityScore(
|
|
304
|
+
componentBody,
|
|
305
|
+
component,
|
|
306
|
+
bodyKey,
|
|
307
|
+
componentKey,
|
|
308
|
+
score
|
|
309
|
+
) {
|
|
310
|
+
if (
|
|
311
|
+
!PcbScene3dPlacementSideResolver.#isObjectLike(componentBody) ||
|
|
312
|
+
!PcbScene3dPlacementSideResolver.#isObjectLike(component)
|
|
313
|
+
) {
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let componentScores =
|
|
318
|
+
PcbScene3dPlacementSideResolver.#AFFINITY_SCORE_CACHE.get(
|
|
319
|
+
componentBody
|
|
320
|
+
)
|
|
321
|
+
if (!componentScores) {
|
|
322
|
+
componentScores = new WeakMap()
|
|
323
|
+
PcbScene3dPlacementSideResolver.#AFFINITY_SCORE_CACHE.set(
|
|
324
|
+
componentBody,
|
|
325
|
+
componentScores
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
componentScores.set(component, { bodyKey, componentKey, score })
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Collects cached normalized identity tokens.
|
|
334
|
+
* @param {WeakMap<object, { key: string, tokens: Set<string> }>} cache Token cache.
|
|
335
|
+
* @param {unknown} owner Source object that owns the identity fields.
|
|
336
|
+
* @param {string} key Current identity key.
|
|
337
|
+
* @param {unknown[]} values Identity field values.
|
|
338
|
+
* @returns {Set<string>}
|
|
339
|
+
*/
|
|
340
|
+
static #cachedMeaningfulTokens(cache, owner, key, values) {
|
|
341
|
+
if (!PcbScene3dPlacementSideResolver.#isObjectLike(owner)) {
|
|
342
|
+
return PcbScene3dPlacementSideResolver.#collectMeaningfulTokens(
|
|
343
|
+
values
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const cached = cache.get(owner)
|
|
348
|
+
if (cached?.key === key) {
|
|
349
|
+
return cached.tokens
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const tokens =
|
|
353
|
+
PcbScene3dPlacementSideResolver.#collectMeaningfulTokens(values)
|
|
354
|
+
cache.set(owner, { key, tokens })
|
|
355
|
+
return tokens
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Returns true when a value can be used as a WeakMap key.
|
|
360
|
+
* @param {unknown} value Candidate value.
|
|
361
|
+
* @returns {boolean}
|
|
362
|
+
*/
|
|
363
|
+
static #isObjectLike(value) {
|
|
364
|
+
return (
|
|
365
|
+
(typeof value === 'object' && value !== null) ||
|
|
366
|
+
typeof value === 'function'
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
170
370
|
/**
|
|
171
371
|
* Resolves package-related component metadata that can identify generic
|
|
172
372
|
* embedded 3D bodies when pattern/source only name the electrical part.
|
|
@@ -248,6 +448,46 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
248
448
|
return Math.abs(standoff) >= threshold ? 'bottom' : null
|
|
249
449
|
}
|
|
250
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Checks whether negative standoff metadata is reliable enough to infer
|
|
453
|
+
* the board side before nearby package or mechanical-layer evidence.
|
|
454
|
+
* @param {{ dzMil?: number | null, standoffHeightMil?: number | null, overallHeightMil?: number | null } | null} componentBody Component body row.
|
|
455
|
+
* @param {'bottom' | null} standoffSide Side inferred from the standoff.
|
|
456
|
+
* @returns {boolean}
|
|
457
|
+
*/
|
|
458
|
+
static #shouldTrustStandoffSide(componentBody, standoffSide) {
|
|
459
|
+
if (!standoffSide) {
|
|
460
|
+
return false
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return !PcbScene3dPlacementSideResolver.#hasInEnvelopeDzAgainstOverlargeStandoff(
|
|
464
|
+
componentBody
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Returns true for source-origin artifacts where Altium's standoff exceeds
|
|
470
|
+
* the model envelope but the authored dz offset is still physically valid.
|
|
471
|
+
* @param {{ dzMil?: number | null, standoffHeightMil?: number | null, overallHeightMil?: number | null } | null} componentBody Component body row.
|
|
472
|
+
* @returns {boolean}
|
|
473
|
+
*/
|
|
474
|
+
static #hasInEnvelopeDzAgainstOverlargeStandoff(componentBody) {
|
|
475
|
+
const standoff = Number(componentBody?.standoffHeightMil)
|
|
476
|
+
const dz = Number(componentBody?.dzMil)
|
|
477
|
+
const overallHeight = Number(componentBody?.overallHeightMil)
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
Number.isFinite(standoff) &&
|
|
481
|
+
standoff < 0 &&
|
|
482
|
+
Number.isFinite(dz) &&
|
|
483
|
+
dz < 0 &&
|
|
484
|
+
Number.isFinite(overallHeight) &&
|
|
485
|
+
overallHeight > 0 &&
|
|
486
|
+
Math.abs(standoff) > overallHeight &&
|
|
487
|
+
Math.abs(dz) < overallHeight
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
251
491
|
/**
|
|
252
492
|
* Returns true when one body anchor sits inside the normalized board bounds.
|
|
253
493
|
* @param {{ positionMil?: { x?: number, y?: number } } | null} componentBody
|
|
@@ -383,7 +623,7 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
383
623
|
static #isMeaningfulToken(token) {
|
|
384
624
|
return (
|
|
385
625
|
String(token || '').length >= 2 &&
|
|
386
|
-
!
|
|
626
|
+
!PcbScene3dPlacementSideResolver.#IGNORED_IDENTITY_TOKENS.has(
|
|
387
627
|
String(token || '')
|
|
388
628
|
)
|
|
389
629
|
)
|