altium-toolkit 1.4.16 → 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/docs/api.md +17 -0
- package/docs/migration.md +13 -3
- package/docs/testing.md +17 -5
- package/package.json +2 -2
- 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/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
package/spec/library-scope.md
CHANGED
|
@@ -7,8 +7,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|
|
7
7
|
# Library Scope
|
|
8
8
|
|
|
9
9
|
Altium Toolkit provides reusable native Altium parsing behind the common ECAD
|
|
10
|
-
toolkit API. Canonical results use CircuitJSON; source-only facts and
|
|
11
|
-
|
|
10
|
+
toolkit API. Canonical results use CircuitJSON; source-only facts and historical
|
|
11
|
+
1.1.41 APIs remain explicit Altium extensions with maintained implementations.
|
|
12
12
|
|
|
13
13
|
## In Scope
|
|
14
14
|
|
|
@@ -18,7 +18,8 @@ historical 1.1.41 contracts remain explicit Altium extensions.
|
|
|
18
18
|
reusable document contexts
|
|
19
19
|
- Shared CircuitJSON render, interaction, query, manufacturing, simulation,
|
|
20
20
|
and data-only 3D scene services
|
|
21
|
-
-
|
|
21
|
+
- Preservation of audited Altium 1.1.41 `/extensions` API signatures and public
|
|
22
|
+
asset entrypoints and targets, with tested bug fixes and performance improvements
|
|
22
23
|
- `.SchDoc`, `.PcbDoc`, `.PCBDwf`, `.SchLib`, `.PcbLib`, `.PrjPcb`, `.PrjScr`,
|
|
23
24
|
and `.IntLib` parsing from `ArrayBuffer`
|
|
24
25
|
- OLE and binary stream helpers needed by parser recovery
|
|
@@ -74,18 +74,9 @@ export class AltiumWorkerClient {
|
|
|
74
74
|
*/
|
|
75
75
|
static #client() {
|
|
76
76
|
if (!client) {
|
|
77
|
-
client =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return Reflect.construct(WorkerConstructor, [
|
|
81
|
-
new URL(
|
|
82
|
-
'../workers/parser.worker.mjs',
|
|
83
|
-
import.meta.url
|
|
84
|
-
),
|
|
85
|
-
{ type: 'module' }
|
|
86
|
-
])
|
|
87
|
-
}
|
|
88
|
-
})
|
|
77
|
+
client = ParserWorkerClient.fromWorkerUrl(
|
|
78
|
+
new URL('../workers/parser.worker.mjs', import.meta.url)
|
|
79
|
+
)
|
|
89
80
|
}
|
|
90
81
|
return client
|
|
91
82
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
/** Matches placements to authored bodies within one adapter invocation. */
|
|
5
|
+
export class AltiumScene3dBodyPlacementIndex {
|
|
6
|
+
#cells = new Map()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Groups authored body positions into one-mil cells in source order.
|
|
10
|
+
* @param {object[]} componentBodies Source component bodies.
|
|
11
|
+
*/
|
|
12
|
+
constructor(componentBodies) {
|
|
13
|
+
componentBodies.forEach((componentBody, order) => {
|
|
14
|
+
const x = Number(componentBody?.positionMil?.x || 0)
|
|
15
|
+
const y = Number(componentBody?.positionMil?.y || 0)
|
|
16
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
|
17
|
+
const key = `${Math.floor(x)}:${Math.floor(y)}`
|
|
18
|
+
const cell = this.#cells.get(key) || []
|
|
19
|
+
cell.push({ componentBody, x, y, order })
|
|
20
|
+
this.#cells.set(key, cell)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Selects by identity affinity, distance, then original source order.
|
|
26
|
+
* @param {object} placement Built external placement.
|
|
27
|
+
* @returns {object | null}
|
|
28
|
+
*/
|
|
29
|
+
resolve(placement) {
|
|
30
|
+
const x = Number(placement?.bodyPositionMil?.x || 0)
|
|
31
|
+
const y = Number(placement?.bodyPositionMil?.y || 0)
|
|
32
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
33
|
+
const placementText = AltiumScene3dBodyPlacementIndex.#identityText([
|
|
34
|
+
placement?.designator,
|
|
35
|
+
placement?.externalModel?.name
|
|
36
|
+
])
|
|
37
|
+
let best = null
|
|
38
|
+
let bestScore = -1
|
|
39
|
+
let bestDistance = Infinity
|
|
40
|
+
for (const dx of [-1, 0, 1]) {
|
|
41
|
+
for (const dy of [-1, 0, 1]) {
|
|
42
|
+
const cell =
|
|
43
|
+
this.#cells.get(
|
|
44
|
+
`${Math.floor(x) + dx}:${Math.floor(y) + dy}`
|
|
45
|
+
) || []
|
|
46
|
+
for (const candidate of cell) {
|
|
47
|
+
const distance = Math.hypot(
|
|
48
|
+
x - candidate.x,
|
|
49
|
+
y - candidate.y
|
|
50
|
+
)
|
|
51
|
+
if (!(distance <= 0.01)) continue
|
|
52
|
+
const bodyText =
|
|
53
|
+
AltiumScene3dBodyPlacementIndex.#identityText([
|
|
54
|
+
candidate.componentBody?.identifier,
|
|
55
|
+
candidate.componentBody?.name
|
|
56
|
+
])
|
|
57
|
+
const score =
|
|
58
|
+
placementText &&
|
|
59
|
+
bodyText &&
|
|
60
|
+
placementText.includes(bodyText)
|
|
61
|
+
? bodyText.length
|
|
62
|
+
: 0
|
|
63
|
+
if (
|
|
64
|
+
best &&
|
|
65
|
+
!(
|
|
66
|
+
score > bestScore ||
|
|
67
|
+
(score === bestScore &&
|
|
68
|
+
(distance < bestDistance ||
|
|
69
|
+
(distance === bestDistance &&
|
|
70
|
+
candidate.order < best.order)))
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
continue
|
|
74
|
+
best = candidate
|
|
75
|
+
bestScore = score
|
|
76
|
+
bestDistance = distance
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return best?.componentBody || null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalizes metadata with the placement adapter's identity convention.
|
|
85
|
+
* @param {unknown[]} values Source identity fields.
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
static #identityText(values) {
|
|
89
|
+
return values
|
|
90
|
+
.map((value) => String(value || '').toLowerCase())
|
|
91
|
+
.join(' ')
|
|
92
|
+
.replace(/\.[a-z0-9]+\\b/g, '')
|
|
93
|
+
.replace(/[^a-z0-9]+/g, '')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PcbScene3dComponentGeometryResolver } from './PcbScene3dComponentGeometryResolver.mjs'
|
|
1
2
|
import { PcbScene3dPadLocalSpanResolver } from './PcbScene3dPadLocalSpanResolver.mjs'
|
|
2
3
|
|
|
3
4
|
const REFINABLE_FAMILIES = new Set(['chip', 'diode', 'generic', 'ic', 'sot'])
|
|
@@ -33,6 +34,10 @@ export class AltiumScene3dComponentBodyAdapter {
|
|
|
33
34
|
return sceneDescription
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
const componentGeometry = new PcbScene3dComponentGeometryResolver(
|
|
38
|
+
sourceComponents,
|
|
39
|
+
pads
|
|
40
|
+
)
|
|
36
41
|
const sourceByDesignator = new Map(
|
|
37
42
|
sourceComponents.map((component) => [
|
|
38
43
|
String(component?.designator || ''),
|
|
@@ -46,7 +51,7 @@ export class AltiumScene3dComponentBodyAdapter {
|
|
|
46
51
|
AltiumScene3dComponentBodyAdapter.#refineComponent(
|
|
47
52
|
component,
|
|
48
53
|
sourceByDesignator.get(String(component?.designator || '')),
|
|
49
|
-
|
|
54
|
+
componentGeometry
|
|
50
55
|
)
|
|
51
56
|
)
|
|
52
57
|
}
|
|
@@ -56,10 +61,10 @@ export class AltiumScene3dComponentBodyAdapter {
|
|
|
56
61
|
* Refines one procedural component body when nearby pads overinflated it.
|
|
57
62
|
* @param {object} component Scene component.
|
|
58
63
|
* @param {object | undefined} sourceComponent Source PCB component.
|
|
59
|
-
* @param {
|
|
64
|
+
* @param {PcbScene3dComponentGeometryResolver} componentGeometry Build-scoped geometry.
|
|
60
65
|
* @returns {object}
|
|
61
66
|
*/
|
|
62
|
-
static #refineComponent(component, sourceComponent,
|
|
67
|
+
static #refineComponent(component, sourceComponent, componentGeometry) {
|
|
63
68
|
const family = String(component?.body?.family || '')
|
|
64
69
|
if (
|
|
65
70
|
component?.externalModel ||
|
|
@@ -72,7 +77,7 @@ export class AltiumScene3dComponentBodyAdapter {
|
|
|
72
77
|
const span = AltiumScene3dComponentBodyAdapter.#ownedPadSpan(
|
|
73
78
|
sourceComponent,
|
|
74
79
|
component.mountSide,
|
|
75
|
-
|
|
80
|
+
componentGeometry
|
|
76
81
|
)
|
|
77
82
|
const size = component?.body?.sizeMil || {}
|
|
78
83
|
if (
|
|
@@ -99,42 +104,17 @@ export class AltiumScene3dComponentBodyAdapter {
|
|
|
99
104
|
* Resolves the owned surface-pad span for one source component.
|
|
100
105
|
* @param {object} component Source component.
|
|
101
106
|
* @param {string} mountSide Component mount side.
|
|
102
|
-
* @param {
|
|
107
|
+
* @param {PcbScene3dComponentGeometryResolver} componentGeometry Build-scoped geometry.
|
|
103
108
|
* @returns {{ width: number, depth: number } | null}
|
|
104
109
|
*/
|
|
105
|
-
static #ownedPadSpan(component, mountSide,
|
|
106
|
-
const componentIndex = Number(component?.componentIndex)
|
|
107
|
-
if (!Number.isFinite(componentIndex)) {
|
|
108
|
-
return null
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const ownedPads = pads.filter(
|
|
112
|
-
(pad) => Number(pad?.componentIndex) === componentIndex
|
|
113
|
-
)
|
|
114
|
-
const surfacePads = ownedPads.filter((pad) =>
|
|
115
|
-
AltiumScene3dComponentBodyAdapter.#isSurfacePad(pad, mountSide)
|
|
116
|
-
)
|
|
117
|
-
const spanPads = surfacePads.length ? surfacePads : ownedPads
|
|
118
|
-
|
|
110
|
+
static #ownedPadSpan(component, mountSide, componentGeometry) {
|
|
119
111
|
return PcbScene3dPadLocalSpanResolver.resolve(
|
|
120
112
|
component,
|
|
121
|
-
|
|
113
|
+
componentGeometry.componentPads(component, mountSide),
|
|
122
114
|
mountSide
|
|
123
115
|
)
|
|
124
116
|
}
|
|
125
117
|
|
|
126
|
-
/**
|
|
127
|
-
* Checks whether one pad belongs to the component's mounted surface.
|
|
128
|
-
* @param {object} pad Source pad.
|
|
129
|
-
* @param {string} mountSide Component mount side.
|
|
130
|
-
* @returns {boolean}
|
|
131
|
-
*/
|
|
132
|
-
static #isSurfacePad(pad, mountSide) {
|
|
133
|
-
return String(mountSide || '').toLowerCase() === 'bottom'
|
|
134
|
-
? Boolean(pad?.hasBottomPasteMaskOpening)
|
|
135
|
-
: Boolean(pad?.hasTopPasteMaskOpening)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
118
|
/**
|
|
139
119
|
* Checks whether the current body is clearly larger than owned pads.
|
|
140
120
|
* @param {object} size Current body size.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AltiumScene3dBodyPlacementIndex } from './AltiumScene3dBodyPlacementIndex.mjs'
|
|
1
2
|
import { AltiumScene3dIdentityTokens } from './AltiumScene3dIdentityTokens.mjs'
|
|
2
3
|
import { AltiumScene3dAuthoredConnectorYawPolicy } from './AltiumScene3dAuthoredConnectorYawPolicy.mjs'
|
|
3
4
|
import { AltiumScene3dPlacementRotationPolicy } from './AltiumScene3dPlacementRotationPolicy.mjs'
|
|
@@ -59,6 +60,7 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
59
60
|
])
|
|
60
61
|
)
|
|
61
62
|
|
|
63
|
+
const bodyIndex = new AltiumScene3dBodyPlacementIndex(componentBodies)
|
|
62
64
|
const repairedScene = {
|
|
63
65
|
...sceneDescription,
|
|
64
66
|
externalPlacements: sceneDescription.externalPlacements
|
|
@@ -67,7 +69,7 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
67
69
|
placement,
|
|
68
70
|
components,
|
|
69
71
|
componentByDesignator,
|
|
70
|
-
|
|
72
|
+
bodyIndex,
|
|
71
73
|
pads,
|
|
72
74
|
sceneDescription?.board
|
|
73
75
|
)
|
|
@@ -86,7 +88,7 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
86
88
|
placement,
|
|
87
89
|
components,
|
|
88
90
|
componentByDesignator,
|
|
89
|
-
|
|
91
|
+
bodyIndex,
|
|
90
92
|
pads,
|
|
91
93
|
board
|
|
92
94
|
) {
|
|
@@ -101,11 +103,7 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
101
103
|
return placement
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
const componentBody =
|
|
105
|
-
AltiumScene3dExternalPlacementAdapter.#resolveComponentBody(
|
|
106
|
-
placement,
|
|
107
|
-
componentBodies
|
|
108
|
-
)
|
|
106
|
+
const componentBody = bodyIndex.resolve(placement)
|
|
109
107
|
const currentComponent = componentByDesignator.get(
|
|
110
108
|
String(placement?.designator || '')
|
|
111
109
|
)
|
|
@@ -1542,60 +1540,6 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
1542
1540
|
]
|
|
1543
1541
|
}
|
|
1544
1542
|
|
|
1545
|
-
/**
|
|
1546
|
-
* Resolves the source component body row for one placement.
|
|
1547
|
-
* @param {object} placement External model placement.
|
|
1548
|
-
* @param {object[]} componentBodies Source component body rows.
|
|
1549
|
-
* @returns {object | null}
|
|
1550
|
-
*/
|
|
1551
|
-
static #resolveComponentBody(placement, componentBodies) {
|
|
1552
|
-
const candidates = componentBodies
|
|
1553
|
-
.map((componentBody) => ({
|
|
1554
|
-
componentBody,
|
|
1555
|
-
distance:
|
|
1556
|
-
AltiumScene3dExternalPlacementAdapter.#distanceBetweenPoints(
|
|
1557
|
-
placement?.bodyPositionMil,
|
|
1558
|
-
componentBody?.positionMil
|
|
1559
|
-
),
|
|
1560
|
-
identityScore:
|
|
1561
|
-
AltiumScene3dExternalPlacementAdapter.#bodyPlacementIdentityScore(
|
|
1562
|
-
placement,
|
|
1563
|
-
componentBody
|
|
1564
|
-
)
|
|
1565
|
-
}))
|
|
1566
|
-
.filter((candidate) => candidate.distance <= 0.01)
|
|
1567
|
-
.sort(
|
|
1568
|
-
(left, right) =>
|
|
1569
|
-
right.identityScore - left.identityScore ||
|
|
1570
|
-
left.distance - right.distance
|
|
1571
|
-
)
|
|
1572
|
-
|
|
1573
|
-
return candidates[0]?.componentBody || null
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
/**
|
|
1577
|
-
* Scores whether a source body row belongs to one placement.
|
|
1578
|
-
* @param {object} placement External model placement.
|
|
1579
|
-
* @param {object} componentBody Source component body.
|
|
1580
|
-
* @returns {number}
|
|
1581
|
-
*/
|
|
1582
|
-
static #bodyPlacementIdentityScore(placement, componentBody) {
|
|
1583
|
-
const placementText =
|
|
1584
|
-
AltiumScene3dExternalPlacementAdapter.#normalizeIdentityText([
|
|
1585
|
-
placement?.designator,
|
|
1586
|
-
placement?.externalModel?.name
|
|
1587
|
-
])
|
|
1588
|
-
const bodyText =
|
|
1589
|
-
AltiumScene3dExternalPlacementAdapter.#normalizeIdentityText([
|
|
1590
|
-
componentBody?.identifier,
|
|
1591
|
-
componentBody?.name
|
|
1592
|
-
])
|
|
1593
|
-
|
|
1594
|
-
return placementText && bodyText && placementText.includes(bodyText)
|
|
1595
|
-
? bodyText.length
|
|
1596
|
-
: 0
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
1543
|
/**
|
|
1600
1544
|
* Repairs model-local Altium rotation signs for embedded body transforms.
|
|
1601
1545
|
* @param {object | null | undefined} modelTransform Placement transform.
|
|
@@ -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
|
}
|