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
|
@@ -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 (
|
|
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 (
|
|
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
|
}
|