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
@@ -2,6 +2,10 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
+ const MILS_PER_METER = 39370.07874015748
6
+ const MILS_PER_MILLIMETER = 1000 / 25.4
7
+ const MILS_PER_INCH = 1000
8
+
5
9
  /**
6
10
  * Indexes session companion assets for 3D model lookup.
7
11
  */
@@ -9,12 +13,12 @@ export class PcbScene3dModelRegistry {
9
13
  /** @type {{ file?: File | Blob | null, name: string, relativePath: string, format: string, source: string, normalizedPath: string, normalizedBaseName: string }[]} */
10
14
  #modelFiles
11
15
 
12
- /** @type {{ id: string, checksum: number | null, name: string, format: string, payloadText: string, sourceStream: string, normalizedId: string, normalizedBaseName: string }[]} */
16
+ /** @type {{ id: string, checksum: number | null, name: string, format: string, payloadText: string, sourceStream: string, normalizedId: string, normalizedBaseName: string, boundsMil?: { width: number, depth: number, height: number }, transform?: { rotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number } }[]} */
13
17
  #embeddedModels
14
18
 
15
19
  /**
16
20
  * @param {{ file?: File | Blob | null, name: string, relativePath: string, format: string, source: string, normalizedPath: string, normalizedBaseName: string }[]} modelFiles
17
- * @param {{ id: string, checksum: number | null, name: string, format: string, payloadText: string, sourceStream: string, normalizedId: string, normalizedBaseName: string }[]} embeddedModels
21
+ * @param {{ id: string, checksum: number | null, name: string, format: string, payloadText: string, sourceStream: string, normalizedId: string, normalizedBaseName: string, boundsMil?: { width: number, depth: number, height: number }, transform?: { rotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number } }[]} embeddedModels
18
22
  */
19
23
  constructor(modelFiles, embeddedModels) {
20
24
  this.#modelFiles = modelFiles
@@ -24,7 +28,7 @@ export class PcbScene3dModelRegistry {
24
28
  /**
25
29
  * Creates one model registry from session files.
26
30
  * @param {{ name?: string, relativePath?: string, source?: string }[]} sessionFiles
27
- * @param {{ id?: string, checksum?: number | null, name?: string, format?: string, payloadText?: string, sourceStream?: string }[]} [embeddedModels]
31
+ * @param {{ id?: string, checksum?: number | null, name?: string, format?: string, payloadText?: string, sourceStream?: string, transform?: object }[]} [embeddedModels]
28
32
  * @returns {PcbScene3dModelRegistry}
29
33
  */
30
34
  static create(sessionFiles, embeddedModels = []) {
@@ -223,11 +227,16 @@ export class PcbScene3dModelRegistry {
223
227
  * Resolves the best available model for one normalized component-body
224
228
  * placement.
225
229
  * @param {{ modelId?: string, checksum?: number | null, name?: string }} componentBody
226
- * @returns {{ origin: 'embedded' | 'session', file?: File | Blob | null, name: string, relativePath?: string, format: string, payloadText?: string, sourceStream?: string } | null}
230
+ * @returns {{ origin: 'embedded' | 'session', file?: File | Blob | null, name: string, relativePath?: string, format: string, payloadText?: string, sourceStream?: string, boundsMil?: { width: number, depth: number, height: number }, transform?: { rotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number } } | null}
227
231
  */
228
232
  resolveComponentBodyModel(componentBody) {
229
233
  const embeddedMatch = this.#resolveEmbeddedMatch(componentBody)
230
- if (embeddedMatch) {
234
+ if (
235
+ embeddedMatch &&
236
+ PcbScene3dModelRegistry.#isRenderableEmbeddedFormat(
237
+ embeddedMatch.format
238
+ )
239
+ ) {
231
240
  return embeddedMatch
232
241
  }
233
242
 
@@ -279,8 +288,8 @@ export class PcbScene3dModelRegistry {
279
288
 
280
289
  /**
281
290
  * Normalizes one embedded payload for registry lookup.
282
- * @param {{ id?: string, checksum?: number | null, name?: string, format?: string, payloadText?: string, sourceStream?: string }} model
283
- * @returns {{ id: string, checksum: number | null, name: string, format: string, payloadText: string, sourceStream: string, normalizedId: string, normalizedBaseName: string } | null}
291
+ * @param {{ id?: string, checksum?: number | null, name?: string, format?: string, payloadText?: string, sourceStream?: string, transform?: object }} model
292
+ * @returns {{ id: string, checksum: number | null, name: string, format: string, payloadText: string, sourceStream: string, normalizedId: string, normalizedBaseName: string, boundsMil?: { width: number, depth: number, height: number }, transform?: { rotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number } } | null}
284
293
  */
285
294
  static #normalizeEmbeddedModel(model) {
286
295
  const id = String(model?.id || '').trim()
@@ -293,6 +302,14 @@ export class PcbScene3dModelRegistry {
293
302
  return null
294
303
  }
295
304
 
305
+ const boundsMil = PcbScene3dModelRegistry.#resolveEmbeddedBoundsMil(
306
+ format,
307
+ payloadText
308
+ )
309
+ const transform = PcbScene3dModelRegistry.#normalizeModelTransform(
310
+ model?.transform
311
+ )
312
+
296
313
  return {
297
314
  id,
298
315
  checksum: Number.isFinite(Number(model?.checksum))
@@ -305,14 +322,16 @@ export class PcbScene3dModelRegistry {
305
322
  normalizedId: PcbScene3dModelRegistry.#normalizeToken(id),
306
323
  normalizedBaseName: PcbScene3dModelRegistry.#normalizeToken(
307
324
  name.replace(/\.[^.]+$/, '')
308
- )
325
+ ),
326
+ ...(boundsMil ? { boundsMil } : {}),
327
+ ...(transform ? { transform } : {})
309
328
  }
310
329
  }
311
330
 
312
331
  /**
313
332
  * Resolves one embedded model match from authored model metadata.
314
333
  * @param {{ modelId?: string, checksum?: number | null, name?: string }} componentBody
315
- * @returns {{ origin: 'embedded', name: string, format: string, payloadText: string, sourceStream: string } | null}
334
+ * @returns {{ origin: 'embedded', name: string, format: string, payloadText: string, sourceStream: string, boundsMil?: { width: number, depth: number, height: number }, transform?: { rotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number } } | null}
316
335
  */
317
336
  #resolveEmbeddedMatch(componentBody) {
318
337
  const normalizedId = PcbScene3dModelRegistry.#normalizeToken(
@@ -351,7 +370,189 @@ export class PcbScene3dModelRegistry {
351
370
  name: embeddedMatch.name,
352
371
  format: embeddedMatch.format,
353
372
  payloadText: embeddedMatch.payloadText,
354
- sourceStream: embeddedMatch.sourceStream
373
+ sourceStream: embeddedMatch.sourceStream,
374
+ ...(embeddedMatch.boundsMil
375
+ ? { boundsMil: embeddedMatch.boundsMil }
376
+ : {}),
377
+ ...(embeddedMatch.transform
378
+ ? { transform: embeddedMatch.transform }
379
+ : {})
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Normalizes optional embedded model transform metadata.
385
+ * @param {object | null | undefined} transform Source transform metadata.
386
+ * @returns {{ rotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number } | null}
387
+ */
388
+ static #normalizeModelTransform(transform) {
389
+ if (!transform || typeof transform !== 'object') {
390
+ return null
391
+ }
392
+
393
+ const normalized = {}
394
+ const sourceRotation = transform.rotationDeg || {}
395
+ const rotationDeg = {}
396
+ for (const axis of ['x', 'y', 'z']) {
397
+ const value = Number(sourceRotation?.[axis])
398
+ if (Number.isFinite(value)) {
399
+ rotationDeg[axis] = value
400
+ }
401
+ }
402
+ if (Object.keys(rotationDeg).length) {
403
+ normalized.rotationDeg = rotationDeg
404
+ }
405
+
406
+ const dzMil = Number(transform.dzMil)
407
+ if (Number.isFinite(dzMil)) {
408
+ normalized.dzMil = dzMil
409
+ }
410
+
411
+ return Object.keys(normalized).length ? normalized : null
412
+ }
413
+
414
+ /**
415
+ * Resolves an embedded model envelope in mils when the payload format is
416
+ * inspectable without a geometry engine.
417
+ * @param {string} format Embedded model format.
418
+ * @param {string} payloadText Embedded model text payload.
419
+ * @returns {{ width: number, depth: number, height: number } | null}
420
+ */
421
+ static #resolveEmbeddedBoundsMil(format, payloadText) {
422
+ const normalizedFormat = String(format || '').toLowerCase()
423
+ if (normalizedFormat !== 'step' && normalizedFormat !== 'stp') {
424
+ return null
425
+ }
426
+
427
+ return PcbScene3dModelRegistry.#resolveStepBoundsMil(payloadText)
428
+ }
429
+
430
+ /**
431
+ * Resolves a STEP payload envelope from authored Cartesian points.
432
+ * @param {string} payloadText STEP text payload.
433
+ * @returns {{ width: number, depth: number, height: number } | null}
434
+ */
435
+ static #resolveStepBoundsMil(payloadText) {
436
+ const text = String(payloadText || '')
437
+ const points = []
438
+ const pointPattern =
439
+ /CARTESIAN_POINT\s*\(\s*(?:'[^']*'|[^,]*),\s*\(([^)]*)\)\s*\)/giu
440
+ let match = pointPattern.exec(text)
441
+
442
+ while (match) {
443
+ const coordinates = String(match[1] || '')
444
+ .split(',')
445
+ .slice(0, 3)
446
+ .map((value) => Number(value.trim()))
447
+ if (
448
+ coordinates.length === 3 &&
449
+ coordinates.every((value) => Number.isFinite(value))
450
+ ) {
451
+ points.push(coordinates)
452
+ }
453
+
454
+ match = pointPattern.exec(text)
455
+ }
456
+
457
+ if (points.length < 2) {
458
+ return null
459
+ }
460
+
461
+ const scale = PcbScene3dModelRegistry.#resolveStepMilScale(text)
462
+ const [firstPoint] = points
463
+ const bounds = {
464
+ minX: firstPoint[0],
465
+ maxX: firstPoint[0],
466
+ minY: firstPoint[1],
467
+ maxY: firstPoint[1],
468
+ minZ: firstPoint[2],
469
+ maxZ: firstPoint[2]
470
+ }
471
+
472
+ points.slice(1).forEach(([x, y, z]) => {
473
+ bounds.minX = Math.min(bounds.minX, x)
474
+ bounds.maxX = Math.max(bounds.maxX, x)
475
+ bounds.minY = Math.min(bounds.minY, y)
476
+ bounds.maxY = Math.max(bounds.maxY, y)
477
+ bounds.minZ = Math.min(bounds.minZ, z)
478
+ bounds.maxZ = Math.max(bounds.maxZ, z)
479
+ })
480
+
481
+ return {
482
+ width: (bounds.maxX - bounds.minX) * scale,
483
+ depth: (bounds.maxY - bounds.minY) * scale,
484
+ height: (bounds.maxZ - bounds.minZ) * scale
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Resolves the STEP length-unit scale to mils.
490
+ * @param {string} payloadText STEP text payload.
491
+ * @returns {number}
492
+ */
493
+ static #resolveStepMilScale(payloadText) {
494
+ const text = String(payloadText || '').toUpperCase()
495
+ if (/\bINCH\b|\.INCH\./u.test(text)) {
496
+ return MILS_PER_INCH
497
+ }
498
+
499
+ const siUnitMatch = text.match(
500
+ /SI_UNIT\s*\(\s*(\.[A-Z]+\.|\$)\s*,\s*\.METRE\.\s*\)/u
501
+ )
502
+ if (siUnitMatch) {
503
+ return (
504
+ PcbScene3dModelRegistry.#metricPrefixMeterScale(
505
+ siUnitMatch[1]
506
+ ) * MILS_PER_METER
507
+ )
508
+ }
509
+
510
+ return MILS_PER_MILLIMETER
511
+ }
512
+
513
+ /**
514
+ * Resolves one SI metric prefix into metres per STEP coordinate unit.
515
+ * @param {string} prefix STEP SI prefix token.
516
+ * @returns {number}
517
+ */
518
+ static #metricPrefixMeterScale(prefix) {
519
+ switch (String(prefix || '').toUpperCase()) {
520
+ case '.EXA.':
521
+ return 1e18
522
+ case '.PETA.':
523
+ return 1e15
524
+ case '.TERA.':
525
+ return 1e12
526
+ case '.GIGA.':
527
+ return 1e9
528
+ case '.MEGA.':
529
+ return 1e6
530
+ case '.KILO.':
531
+ return 1e3
532
+ case '.HECTO.':
533
+ return 1e2
534
+ case '.DECA.':
535
+ return 1e1
536
+ case '$':
537
+ return 1
538
+ case '.DECI.':
539
+ return 1e-1
540
+ case '.CENTI.':
541
+ return 1e-2
542
+ case '.MILLI.':
543
+ return 1e-3
544
+ case '.MICRO.':
545
+ return 1e-6
546
+ case '.NANO.':
547
+ return 1e-9
548
+ case '.PICO.':
549
+ return 1e-12
550
+ case '.FEMTO.':
551
+ return 1e-15
552
+ case '.ATTO.':
553
+ return 1e-18
554
+ default:
555
+ return 1e-3
355
556
  }
356
557
  }
357
558
 
@@ -364,6 +565,17 @@ export class PcbScene3dModelRegistry {
364
565
  return format === 'wrl' ? 0 : 1
365
566
  }
366
567
 
568
+ /**
569
+ * Checks whether one embedded payload format can be loaded inline.
570
+ * @param {string} format Embedded payload format.
571
+ * @returns {boolean}
572
+ */
573
+ static #isRenderableEmbeddedFormat(format) {
574
+ return ['step', 'stp', 'wrl'].includes(
575
+ String(format || '').toLowerCase()
576
+ )
577
+ }
578
+
367
579
  /**
368
580
  * Checks whether one model path is in a conventional board model folder.
369
581
  * @param {string | undefined} relativePath
@@ -54,7 +54,7 @@ export class PcbScene3dPackages {
54
54
  static #resolveFamily(pattern) {
55
55
  const normalized = String(pattern || '').toUpperCase()
56
56
 
57
- if (/(0402|0603|0805|1206|C0805|C0603)/.test(normalized)) {
57
+ if (PcbScene3dPackages.#hasChipPackageCode(normalized)) {
58
58
  return 'chip'
59
59
  }
60
60
 
@@ -97,6 +97,15 @@ export class PcbScene3dPackages {
97
97
  return 'generic'
98
98
  }
99
99
 
100
+ /**
101
+ * Checks whether a footprint pattern contains a compact chip package code.
102
+ * @param {string} normalized Uppercase footprint pattern.
103
+ * @returns {boolean}
104
+ */
105
+ static #hasChipPackageCode(normalized) {
106
+ return /(01005|0201|0402|0603|0805|1206|C0805|C0603)/.test(normalized)
107
+ }
108
+
100
109
  /**
101
110
  * Resolves one default body size for the chosen family.
102
111
  * @param {string} family
@@ -107,6 +116,12 @@ export class PcbScene3dPackages {
107
116
  const normalized = String(pattern || '').toUpperCase()
108
117
 
109
118
  if (family === 'chip') {
119
+ if (normalized.includes('01005')) {
120
+ return { width: 16, depth: 8, height: 8 }
121
+ }
122
+ if (normalized.includes('0201')) {
123
+ return { width: 24, depth: 12, height: 14 }
124
+ }
110
125
  if (normalized.includes('0402')) {
111
126
  return { width: 24, depth: 12, height: 14 }
112
127
  }