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.
Files changed (35) hide show
  1. package/AGENTS.md +17 -2
  2. package/package.json +1 -1
  3. package/src/core/altium/PrintableTextDecoder.mjs +20 -1
  4. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +288 -7
  5. package/src/core/altium/SchematicTextParser.mjs +18 -253
  6. package/src/core/altium/SchematicTitleBlockParser.mjs +410 -0
  7. package/src/core/altium/SelectedPartAltiumExportAdapter.mjs +215 -0
  8. package/src/parser.mjs +1 -0
  9. package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +245 -12
  10. package/src/ui/AltiumScene3dAuthoredConnectorYawPolicy.mjs +176 -0
  11. package/src/ui/AltiumScene3dBottomSourceHalfTurnPolicy.mjs +93 -0
  12. package/src/ui/AltiumScene3dDisplayModuleYawPolicy.mjs +284 -0
  13. package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +521 -23
  14. package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +766 -19
  15. package/src/ui/AltiumScene3dQfnFootprintDetector.mjs +307 -0
  16. package/src/ui/AltiumScene3dRepeatedFullFootprintBodyCollapse.mjs +518 -0
  17. package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +121 -17
  18. package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +286 -135
  19. package/src/ui/AltiumScene3dShapeStackOwnerConflictPolicy.mjs +299 -0
  20. package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +22 -0
  21. package/src/ui/PcbScene3dBuilder.mjs +613 -117
  22. package/src/ui/PcbScene3dModelRegistry.mjs +222 -10
  23. package/src/ui/PcbScene3dPackages.mjs +16 -1
  24. package/src/ui/PcbScene3dPlacementSideResolver.mjs +262 -22
  25. package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +984 -0
  26. package/src/ui/PcbScene3dStaticBodyPadOwnerPromotion.mjs +378 -0
  27. package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +169 -23
  28. package/src/ui/PcbScene3dStaticBodyPrototypeRecovery.mjs +445 -0
  29. package/src/ui/PcbScene3dStaticBodyRecovery.mjs +7 -1
  30. package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +199 -2
  31. package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +439 -0
  32. package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +23 -2
  33. package/src/ui/SchematicColorResolver.mjs +44 -0
  34. package/src/ui/SchematicLineColorResolver.mjs +1 -1
  35. 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
- standoffSide &&
56
+ trustedStandoffSide &&
39
57
  PcbScene3dPlacementSideResolver.#isBodyAnchorInsideBoard(
40
58
  componentBody,
41
59
  board
42
60
  )
43
61
  ) {
44
- return standoffSide
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 standoffSide || 'top'
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
- standoffSide &&
119
+ trustedStandoffSide &&
95
120
  PcbScene3dPlacementSideResolver.#isBodyAnchorInsideBoard(
96
121
  componentBody,
97
122
  board
98
123
  )
99
124
  ) {
100
- return standoffSide
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 standoffSide || 'top'
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.#collectMeaningfulTokens([
146
- componentBody?.identifier,
147
- String(componentBody?.name || '').replace(/\.[^.]+$/, '')
148
- ])
189
+ PcbScene3dPlacementSideResolver.#cachedMeaningfulTokens(
190
+ PcbScene3dPlacementSideResolver.#BODY_TOKEN_CACHE,
191
+ componentBody,
192
+ bodyKey,
193
+ bodyValues
194
+ )
149
195
  const componentTokens =
150
- PcbScene3dPlacementSideResolver.#collectMeaningfulTokens([
151
- component?.pattern,
152
- component?.source,
153
- component?.modelPath,
154
- component?.description,
155
- ...PcbScene3dPlacementSideResolver.#componentPackageMetadata(
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
- !new Set(['con', 'step', 'stp', 'model', 'default', 'black']).has(
626
+ !PcbScene3dPlacementSideResolver.#IGNORED_IDENTITY_TOKENS.has(
387
627
  String(token || '')
388
628
  )
389
629
  )