altium-toolkit 1.4.15 → 1.4.17
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/README.md +5 -0
- package/docs/api.md +17 -0
- package/docs/migration.md +13 -3
- package/docs/release-notes-v1.4.16.md +27 -0
- package/docs/testing.md +17 -5
- package/package.json +2 -2
- package/spec/altium-3d-late-owner-recovery.md +10 -10
- package/spec/feature-preservation.json +1 -1
- package/spec/library-scope.md +4 -3
- package/src/convergence/AltiumWorkerClient.mjs +3 -12
- package/src/ui/AltiumScene3dBodyPlacementIndex.mjs +95 -0
- package/src/ui/AltiumScene3dComponentBodyAdapter.mjs +12 -32
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +5 -61
- package/src/ui/AltiumScene3dGeometricOwnerRecovery.mjs +11 -118
- package/src/ui/PcbInteractionIndex.mjs +59 -78
- package/src/ui/PcbInteractionLayerModel.mjs +2 -32
- package/src/ui/PcbInteractionSourceMetadata.mjs +122 -0
- package/src/ui/PcbScene3dBuilder.mjs +38 -267
- package/src/ui/PcbScene3dComponentGeometryResolver.mjs +249 -0
- package/src/ui/PcbScene3dCopperRegionDetailBuilder.mjs +36 -2
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { PcbInteractionGeometry } from './PcbInteractionGeometry.mjs'
|
|
6
6
|
import { PcbInteractionItemRegistry } from './PcbInteractionItemRegistry.mjs'
|
|
7
|
+
import { PcbInteractionSourceMetadata } from './PcbInteractionSourceMetadata.mjs'
|
|
7
8
|
|
|
8
9
|
const TYPE_PRIORITY = {
|
|
9
10
|
track: 100,
|
|
@@ -67,15 +68,18 @@ export class PcbInteractionIndex {
|
|
|
67
68
|
* @returns {object[]}
|
|
68
69
|
*/
|
|
69
70
|
static hitTestItems(items, point, options = {}) {
|
|
71
|
+
const visibility = PcbInteractionIndex.#visibilityContext(options)
|
|
72
|
+
const tolerance = Number(options?.tolerance) || 0
|
|
73
|
+
|
|
70
74
|
return (Array.isArray(items) ? items : [])
|
|
71
75
|
.filter((item) =>
|
|
72
|
-
PcbInteractionIndex.#isVisibleCandidate(item,
|
|
76
|
+
PcbInteractionIndex.#isVisibleCandidate(item, visibility)
|
|
73
77
|
)
|
|
74
78
|
.filter((item) =>
|
|
75
79
|
PcbInteractionGeometry.containsPoint(
|
|
76
80
|
item.geometry,
|
|
77
81
|
point,
|
|
78
|
-
|
|
82
|
+
tolerance
|
|
79
83
|
)
|
|
80
84
|
)
|
|
81
85
|
.sort(PcbInteractionIndex.#compareCandidates)
|
|
@@ -101,9 +105,23 @@ export class PcbInteractionIndex {
|
|
|
101
105
|
* @returns {object}
|
|
102
106
|
*/
|
|
103
107
|
static #context(pcb) {
|
|
108
|
+
const components = Array.isArray(pcb.components) ? pcb.components : []
|
|
109
|
+
const componentsById = new Map()
|
|
110
|
+
for (const component of components) {
|
|
111
|
+
const componentIndex = Number(component?.componentIndex)
|
|
112
|
+
if (
|
|
113
|
+
Number.isInteger(componentIndex) &&
|
|
114
|
+
!componentsById.has(componentIndex)
|
|
115
|
+
) {
|
|
116
|
+
// Explicit identities take precedence, retaining the first row.
|
|
117
|
+
componentsById.set(componentIndex, component)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
104
121
|
return {
|
|
105
|
-
components
|
|
106
|
-
|
|
122
|
+
components,
|
|
123
|
+
componentsById,
|
|
124
|
+
layerNameFor: PcbInteractionSourceMetadata.layerNameResolver(pcb)
|
|
107
125
|
}
|
|
108
126
|
}
|
|
109
127
|
|
|
@@ -129,13 +147,7 @@ export class PcbInteractionIndex {
|
|
|
129
147
|
*/
|
|
130
148
|
static #extractZones(documentModel, context) {
|
|
131
149
|
const pcb = documentModel?.pcb || {}
|
|
132
|
-
const zones =
|
|
133
|
-
...(Array.isArray(pcb.regions) ? pcb.regions : []),
|
|
134
|
-
...(Array.isArray(pcb.shapeBasedRegions)
|
|
135
|
-
? pcb.shapeBasedRegions
|
|
136
|
-
: []),
|
|
137
|
-
...(Array.isArray(pcb.polygons) ? pcb.polygons : [])
|
|
138
|
-
]
|
|
150
|
+
const zones = PcbInteractionSourceMetadata.zoneSources(pcb)
|
|
139
151
|
|
|
140
152
|
return zones
|
|
141
153
|
.map((zone, index) => {
|
|
@@ -326,10 +338,11 @@ export class PcbInteractionIndex {
|
|
|
326
338
|
* @returns {object | null}
|
|
327
339
|
*/
|
|
328
340
|
static #zoneGeometry(zone) {
|
|
329
|
-
|
|
341
|
+
const kind = PcbInteractionSourceMetadata.zoneGeometryKind(zone)
|
|
342
|
+
if (kind === 'points') {
|
|
330
343
|
return PcbInteractionGeometry.polygon(zone.points)
|
|
331
344
|
}
|
|
332
|
-
if (
|
|
345
|
+
if (kind === 'segments') {
|
|
333
346
|
return PcbInteractionGeometry.polygon(
|
|
334
347
|
zone.segments.map((segment) => ({
|
|
335
348
|
x: segment.x1,
|
|
@@ -337,12 +350,7 @@ export class PcbInteractionIndex {
|
|
|
337
350
|
}))
|
|
338
351
|
)
|
|
339
352
|
}
|
|
340
|
-
if (
|
|
341
|
-
Number.isFinite(Number(zone?.x1)) &&
|
|
342
|
-
Number.isFinite(Number(zone?.y1)) &&
|
|
343
|
-
Number.isFinite(Number(zone?.x2)) &&
|
|
344
|
-
Number.isFinite(Number(zone?.y2))
|
|
345
|
-
) {
|
|
353
|
+
if (kind === 'bounds') {
|
|
346
354
|
return PcbInteractionGeometry.bounds({
|
|
347
355
|
minX: Math.min(Number(zone.x1), Number(zone.x2)),
|
|
348
356
|
minY: Math.min(Number(zone.y1), Number(zone.y2)),
|
|
@@ -372,21 +380,38 @@ export class PcbInteractionIndex {
|
|
|
372
380
|
}
|
|
373
381
|
|
|
374
382
|
/**
|
|
375
|
-
*
|
|
376
|
-
* @param {object} item Interaction item.
|
|
383
|
+
* Normalizes visibility filters once for the current hit test.
|
|
377
384
|
* @param {object} options Hit-test options.
|
|
385
|
+
* @returns {object}
|
|
386
|
+
*/
|
|
387
|
+
static #visibilityContext(options) {
|
|
388
|
+
return {
|
|
389
|
+
side: PcbInteractionIndex.#normalizeSide(options?.side),
|
|
390
|
+
hiddenObjects: new Set(
|
|
391
|
+
(Array.isArray(options?.hiddenObjects)
|
|
392
|
+
? options.hiddenObjects
|
|
393
|
+
: []
|
|
394
|
+
).map(String)
|
|
395
|
+
),
|
|
396
|
+
hiddenLayers: new Set(
|
|
397
|
+
(Array.isArray(options?.hiddenLayers)
|
|
398
|
+
? options.hiddenLayers
|
|
399
|
+
: []
|
|
400
|
+
).map(String)
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Returns whether an item is visible under normalized hit-test filters.
|
|
407
|
+
* @param {object} item Interaction item.
|
|
408
|
+
* @param {object} visibility Normalized visibility filters.
|
|
378
409
|
* @returns {boolean}
|
|
379
410
|
*/
|
|
380
|
-
static #isVisibleCandidate(item,
|
|
381
|
-
const side =
|
|
411
|
+
static #isVisibleCandidate(item, visibility) {
|
|
412
|
+
const { side, hiddenObjects, hiddenLayers } = visibility
|
|
382
413
|
if (item.side !== 'both' && item.side !== side) return false
|
|
383
414
|
|
|
384
|
-
const hiddenObjects = new Set(
|
|
385
|
-
(Array.isArray(options?.hiddenObjects)
|
|
386
|
-
? options.hiddenObjects
|
|
387
|
-
: []
|
|
388
|
-
).map(String)
|
|
389
|
-
)
|
|
390
415
|
if (
|
|
391
416
|
hiddenObjects.has(item.objectKey) ||
|
|
392
417
|
hiddenObjects.has(PLURAL_TYPE_KEYS[item.type] || item.type)
|
|
@@ -394,12 +419,6 @@ export class PcbInteractionIndex {
|
|
|
394
419
|
return false
|
|
395
420
|
}
|
|
396
421
|
|
|
397
|
-
const hiddenLayers = new Set(
|
|
398
|
-
(Array.isArray(options?.hiddenLayers)
|
|
399
|
-
? options.hiddenLayers
|
|
400
|
-
: []
|
|
401
|
-
).map(String)
|
|
402
|
-
)
|
|
403
422
|
return (
|
|
404
423
|
!item.layerKeys?.length ||
|
|
405
424
|
item.layerKeys.some((layerKey) => !hiddenLayers.has(layerKey))
|
|
@@ -416,39 +435,6 @@ export class PcbInteractionIndex {
|
|
|
416
435
|
return second.priority - first.priority || first.order - second.order
|
|
417
436
|
}
|
|
418
437
|
|
|
419
|
-
/**
|
|
420
|
-
* Creates a layer-name resolver for layer-id based primitives.
|
|
421
|
-
* @param {object} pcb PCB model.
|
|
422
|
-
* @returns {(item: object) => string[]}
|
|
423
|
-
*/
|
|
424
|
-
static #layerNameResolver(pcb) {
|
|
425
|
-
const byId = new Map()
|
|
426
|
-
const layers = [
|
|
427
|
-
...(Array.isArray(pcb?.layers) ? pcb.layers : []),
|
|
428
|
-
...(Array.isArray(pcb?.primitiveLayers) ? pcb.primitiveLayers : [])
|
|
429
|
-
]
|
|
430
|
-
|
|
431
|
-
for (const layer of layers) {
|
|
432
|
-
const layerId = Number(layer?.layerId)
|
|
433
|
-
const name = String(layer?.name || '').trim()
|
|
434
|
-
if (Number.isInteger(layerId) && name) {
|
|
435
|
-
byId.set(layerId, name)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return (item) => {
|
|
440
|
-
const directLayer = String(item?.layer || '').trim()
|
|
441
|
-
if (directLayer) return [directLayer]
|
|
442
|
-
|
|
443
|
-
const layerId = Number(item?.layerId ?? item?.layerCode)
|
|
444
|
-
if (Number.isInteger(layerId) && byId.has(layerId)) {
|
|
445
|
-
return [byId.get(layerId)]
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return []
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
438
|
/**
|
|
453
439
|
* Resolves physical layer keys for an item.
|
|
454
440
|
* @param {object} item Source item.
|
|
@@ -484,18 +470,13 @@ export class PcbInteractionIndex {
|
|
|
484
470
|
const componentIndex = Number(primitive?.componentIndex)
|
|
485
471
|
if (!Number.isInteger(componentIndex)) return null
|
|
486
472
|
|
|
487
|
-
const explicitMatch =
|
|
488
|
-
context.components.find(
|
|
489
|
-
(component) =>
|
|
490
|
-
Number(component?.componentIndex) === componentIndex
|
|
491
|
-
) || null
|
|
473
|
+
const explicitMatch = context.componentsById.get(componentIndex)
|
|
492
474
|
if (explicitMatch) return explicitMatch
|
|
493
475
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
)
|
|
476
|
+
if (componentIndex < 0 || componentIndex >= context.components.length) {
|
|
477
|
+
return null
|
|
478
|
+
}
|
|
479
|
+
return context.components[componentIndex] || null
|
|
499
480
|
}
|
|
500
481
|
|
|
501
482
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { PcbInteractionSourceMetadata } from './PcbInteractionSourceMetadata.mjs'
|
|
6
6
|
|
|
7
7
|
const VIRTUAL_LAYER_DEFINITIONS = [
|
|
8
8
|
{ key: 'tracks', label: 'Tracks' },
|
|
@@ -25,8 +25,7 @@ export class PcbInteractionLayerModel {
|
|
|
25
25
|
static resolve(documentModel) {
|
|
26
26
|
const pcb = documentModel?.pcb || {}
|
|
27
27
|
const physicalLayers = PcbInteractionLayerModel.#physicalLayers(pcb)
|
|
28
|
-
const
|
|
29
|
-
const layersByObject = PcbInteractionLayerModel.#layersByObject(items)
|
|
28
|
+
const layersByObject = PcbInteractionSourceMetadata.layersByObject(pcb)
|
|
30
29
|
|
|
31
30
|
return {
|
|
32
31
|
physicalLayers,
|
|
@@ -71,33 +70,4 @@ export class PcbInteractionLayerModel {
|
|
|
71
70
|
|
|
72
71
|
return layers
|
|
73
72
|
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Collects referenced physical layer keys by virtual object key.
|
|
77
|
-
* @param {object[]} items Interaction items.
|
|
78
|
-
* @returns {Map<string, Set<string>>}
|
|
79
|
-
*/
|
|
80
|
-
static #layersByObject(items) {
|
|
81
|
-
const layersByObject = new Map()
|
|
82
|
-
|
|
83
|
-
for (const item of items) {
|
|
84
|
-
if (!layersByObject.has(item.objectKey)) {
|
|
85
|
-
layersByObject.set(item.objectKey, new Set())
|
|
86
|
-
}
|
|
87
|
-
const layerSet = layersByObject.get(item.objectKey)
|
|
88
|
-
for (const layerKey of item.layerKeys || []) {
|
|
89
|
-
layerSet.add(layerKey)
|
|
90
|
-
}
|
|
91
|
-
if (item.type === 'pad' || item.type === 'via') {
|
|
92
|
-
if (!layersByObject.has('holes')) {
|
|
93
|
-
layersByObject.set('holes', new Set())
|
|
94
|
-
}
|
|
95
|
-
for (const layerKey of item.layerKeys || []) {
|
|
96
|
-
layersByObject.get('holes').add(layerKey)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return layersByObject
|
|
102
|
-
}
|
|
103
73
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shares primitive eligibility and physical layer metadata across PCB views.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbInteractionSourceMetadata {
|
|
9
|
+
/**
|
|
10
|
+
* Returns zone sources in selectable-item extraction order.
|
|
11
|
+
* @param {object} pcb PCB model.
|
|
12
|
+
* @returns {object[]}
|
|
13
|
+
*/
|
|
14
|
+
static zoneSources(pcb) {
|
|
15
|
+
return [
|
|
16
|
+
...(Array.isArray(pcb?.regions) ? pcb.regions : []),
|
|
17
|
+
...(Array.isArray(pcb?.shapeBasedRegions)
|
|
18
|
+
? pcb.shapeBasedRegions
|
|
19
|
+
: []),
|
|
20
|
+
...(Array.isArray(pcb?.polygons) ? pcb.polygons : [])
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Identifies selectable zone geometry without constructing its points.
|
|
26
|
+
* @param {object} zone Zone-like source.
|
|
27
|
+
* @returns {'points' | 'segments' | 'bounds' | null}
|
|
28
|
+
*/
|
|
29
|
+
static zoneGeometryKind(zone) {
|
|
30
|
+
if (Array.isArray(zone?.points) && zone.points.length >= 3) {
|
|
31
|
+
return 'points'
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(zone?.segments) && zone.segments.length >= 3) {
|
|
34
|
+
return 'segments'
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
Number.isFinite(Number(zone?.x1)) &&
|
|
38
|
+
Number.isFinite(Number(zone?.y1)) &&
|
|
39
|
+
Number.isFinite(Number(zone?.x2)) &&
|
|
40
|
+
Number.isFinite(Number(zone?.y2))
|
|
41
|
+
) {
|
|
42
|
+
return 'bounds'
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a layer-name resolver for layer-id based primitives.
|
|
49
|
+
* @param {object} pcb PCB model.
|
|
50
|
+
* @returns {(item: object) => string[]}
|
|
51
|
+
*/
|
|
52
|
+
static layerNameResolver(pcb) {
|
|
53
|
+
const byId = new Map()
|
|
54
|
+
const layers = [
|
|
55
|
+
...(Array.isArray(pcb?.layers) ? pcb.layers : []),
|
|
56
|
+
...(Array.isArray(pcb?.primitiveLayers) ? pcb.primitiveLayers : [])
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
for (const layer of layers) {
|
|
60
|
+
const layerId = Number(layer?.layerId)
|
|
61
|
+
const name = String(layer?.name || '').trim()
|
|
62
|
+
if (Number.isInteger(layerId) && name) {
|
|
63
|
+
byId.set(layerId, name)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (item) => {
|
|
68
|
+
const directLayer = String(item?.layer || '').trim()
|
|
69
|
+
if (directLayer) return [directLayer]
|
|
70
|
+
|
|
71
|
+
const layerId = Number(item?.layerId ?? item?.layerCode)
|
|
72
|
+
if (Number.isInteger(layerId) && byId.has(layerId)) {
|
|
73
|
+
return [byId.get(layerId)]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Collects ordered physical layer keys without building selectable geometry.
|
|
82
|
+
* @param {object} pcb PCB model.
|
|
83
|
+
* @returns {Map<string, Set<string>>}
|
|
84
|
+
*/
|
|
85
|
+
static layersByObject(pcb) {
|
|
86
|
+
const layerNameFor = PcbInteractionSourceMetadata.layerNameResolver(pcb)
|
|
87
|
+
const layersByObject = new Map()
|
|
88
|
+
const groups = [
|
|
89
|
+
['zones', PcbInteractionSourceMetadata.zoneSources(pcb)],
|
|
90
|
+
['tracks', Array.isArray(pcb?.tracks) ? pcb.tracks : []],
|
|
91
|
+
['pads', Array.isArray(pcb?.pads) ? pcb.pads : []],
|
|
92
|
+
['footprint-text', Array.isArray(pcb?.texts) ? pcb.texts : []]
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
// Vias and component bodies have no physical layer keys in the index.
|
|
96
|
+
for (const [objectKey, sources] of groups) {
|
|
97
|
+
const layerSet = new Set()
|
|
98
|
+
layersByObject.set(objectKey, layerSet)
|
|
99
|
+
if (objectKey === 'pads') layersByObject.set('holes', layerSet)
|
|
100
|
+
|
|
101
|
+
for (const source of sources) {
|
|
102
|
+
if (
|
|
103
|
+
objectKey === 'zones' &&
|
|
104
|
+
!PcbInteractionSourceMetadata.zoneGeometryKind(source)
|
|
105
|
+
) {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
if (
|
|
109
|
+
objectKey === 'footprint-text' &&
|
|
110
|
+
source?.visible === false
|
|
111
|
+
) {
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
for (const layerKey of layerNameFor(source)) {
|
|
115
|
+
layerSet.add(layerKey)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return layersByObject
|
|
121
|
+
}
|
|
122
|
+
}
|