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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const TIMING_BODY_IDENTITY_PATTERN =
|
|
2
|
+
/(?:^|[^a-z0-9])(?:clock|crystal|osc|oscillator|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
|
|
3
|
+
const STACK_DETAIL_BODY_IDENTITY_PATTERN =
|
|
4
|
+
/(?:^|[^a-z0-9])(?:cap|capacitor|clock|crystal|ferrite|ind|inductor|osc|oscillator|res|resistor|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
|
|
5
|
+
const TIMING_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
|
|
6
|
+
const TIMING_PARAMETER_NAME_PATTERN =
|
|
7
|
+
/(?:^|[^a-z0-9])(?:comment|description|device|function|part|type|value)(?:$|[^a-z0-9])/i
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves ambiguous ownership around authored Altium shape stacks.
|
|
11
|
+
*/
|
|
12
|
+
export class AltiumScene3dShapeStackOwnerConflictPolicy {
|
|
13
|
+
/**
|
|
14
|
+
* Checks whether a component is a timing-package owner.
|
|
15
|
+
* @param {object} component Source component.
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
static isTimingComponent(component) {
|
|
19
|
+
const designator = String(component?.designator || '').trim()
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
TIMING_DESIGNATOR_PATTERN.test(designator) ||
|
|
23
|
+
TIMING_BODY_IDENTITY_PATTERN.test(
|
|
24
|
+
AltiumScene3dShapeStackOwnerConflictPolicy.#timingIdentityText(
|
|
25
|
+
component
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Scores how strongly a component looks like the fitted stack owner.
|
|
33
|
+
* @param {object} component Source component.
|
|
34
|
+
* @returns {number}
|
|
35
|
+
*/
|
|
36
|
+
static ownerRank(component) {
|
|
37
|
+
const parameters = Object.values(component?.parameters || {}).filter(
|
|
38
|
+
(value) => String(value || '').trim()
|
|
39
|
+
)
|
|
40
|
+
return (
|
|
41
|
+
parameters.length * 10 +
|
|
42
|
+
(TIMING_DESIGNATOR_PATTERN.test(component?.designator) ? 5 : 0) +
|
|
43
|
+
(TIMING_BODY_IDENTITY_PATTERN.test(
|
|
44
|
+
[
|
|
45
|
+
component?.pattern,
|
|
46
|
+
component?.source,
|
|
47
|
+
component?.description,
|
|
48
|
+
component?.provenance?.footprintDescription
|
|
49
|
+
]
|
|
50
|
+
.map((value) => String(value || ''))
|
|
51
|
+
.join(' ')
|
|
52
|
+
)
|
|
53
|
+
? 3
|
|
54
|
+
: 0)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Keeps carrier stacks on the side of a clearly nearest timing owner.
|
|
60
|
+
* @param {{ x?: number, y?: number }} anchor Carrier base anchor.
|
|
61
|
+
* @param {object[]} owners Nearby timing owner candidates.
|
|
62
|
+
* @param {number} oppositeSideMarginMil Required distance margin.
|
|
63
|
+
* @returns {object[]}
|
|
64
|
+
*/
|
|
65
|
+
static preferredSideOwners(anchor, owners, oppositeSideMarginMil) {
|
|
66
|
+
const ownerRows = (Array.isArray(owners) ? owners : [])
|
|
67
|
+
.map((owner) => ({
|
|
68
|
+
owner,
|
|
69
|
+
side: AltiumScene3dShapeStackOwnerConflictPolicy.#mountSide(
|
|
70
|
+
owner
|
|
71
|
+
),
|
|
72
|
+
distance: AltiumScene3dShapeStackOwnerConflictPolicy.#distance(
|
|
73
|
+
anchor,
|
|
74
|
+
owner
|
|
75
|
+
)
|
|
76
|
+
}))
|
|
77
|
+
.filter((row) => Number.isFinite(row.distance))
|
|
78
|
+
.sort((left, right) => left.distance - right.distance)
|
|
79
|
+
const nearest = ownerRows[0]
|
|
80
|
+
if (!nearest) {
|
|
81
|
+
return []
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const oppositeSideNearest = ownerRows.find(
|
|
85
|
+
(row) => row.side !== nearest.side
|
|
86
|
+
)
|
|
87
|
+
if (
|
|
88
|
+
!oppositeSideNearest ||
|
|
89
|
+
oppositeSideNearest.distance - nearest.distance <=
|
|
90
|
+
Number(oppositeSideMarginMil || 0)
|
|
91
|
+
) {
|
|
92
|
+
return owners
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return owners.filter(
|
|
96
|
+
(owner) =>
|
|
97
|
+
AltiumScene3dShapeStackOwnerConflictPolicy.#mountSide(owner) ===
|
|
98
|
+
nearest.side
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Checks whether a raised body is better explained by a distinct
|
|
104
|
+
* opposite-side component than by the candidate stack owner.
|
|
105
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Source component-body row.
|
|
106
|
+
* @param {object} stackOwner Candidate stack owner.
|
|
107
|
+
* @param {object[]} components Source components.
|
|
108
|
+
* @param {number} marginMil Required closer-owner distance margin.
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
static hasConflictingComponentOwner(
|
|
112
|
+
componentBody,
|
|
113
|
+
stackOwner,
|
|
114
|
+
components,
|
|
115
|
+
marginMil
|
|
116
|
+
) {
|
|
117
|
+
const bodyPosition = AltiumScene3dShapeStackOwnerConflictPolicy.#point(
|
|
118
|
+
componentBody?.positionMil
|
|
119
|
+
)
|
|
120
|
+
const stackDistance =
|
|
121
|
+
AltiumScene3dShapeStackOwnerConflictPolicy.#distance(
|
|
122
|
+
bodyPosition,
|
|
123
|
+
stackOwner
|
|
124
|
+
)
|
|
125
|
+
if (!Number.isFinite(stackDistance)) {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (Array.isArray(components) ? components : []).some(
|
|
130
|
+
(component) => {
|
|
131
|
+
if (
|
|
132
|
+
String(component?.designator || '') ===
|
|
133
|
+
String(stackOwner?.designator || '')
|
|
134
|
+
) {
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
if (
|
|
138
|
+
AltiumScene3dShapeStackOwnerConflictPolicy.#mountSide(
|
|
139
|
+
component
|
|
140
|
+
) ===
|
|
141
|
+
AltiumScene3dShapeStackOwnerConflictPolicy.#mountSide(
|
|
142
|
+
stackOwner
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const componentDistance =
|
|
149
|
+
AltiumScene3dShapeStackOwnerConflictPolicy.#distance(
|
|
150
|
+
bodyPosition,
|
|
151
|
+
component
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
Number.isFinite(componentDistance) &&
|
|
156
|
+
componentDistance + Number(marginMil || 0) < stackDistance
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Checks whether the raised body is exactly anchored on an opposite-side
|
|
164
|
+
* component and should therefore remain as that component's placement.
|
|
165
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Source component-body row.
|
|
166
|
+
* @param {object} stackOwner Candidate stack owner.
|
|
167
|
+
* @param {object[]} components Source components.
|
|
168
|
+
* @param {number} toleranceMil Exact-owner tolerance.
|
|
169
|
+
* @returns {boolean}
|
|
170
|
+
*/
|
|
171
|
+
static hasExactOppositeSideOwner(
|
|
172
|
+
componentBody,
|
|
173
|
+
stackOwner,
|
|
174
|
+
components,
|
|
175
|
+
toleranceMil
|
|
176
|
+
) {
|
|
177
|
+
return (Array.isArray(components) ? components : []).some(
|
|
178
|
+
(component) =>
|
|
179
|
+
AltiumScene3dShapeStackOwnerConflictPolicy.#mountSide(
|
|
180
|
+
component
|
|
181
|
+
) !==
|
|
182
|
+
AltiumScene3dShapeStackOwnerConflictPolicy.#mountSide(
|
|
183
|
+
stackOwner
|
|
184
|
+
) &&
|
|
185
|
+
AltiumScene3dShapeStackOwnerConflictPolicy.#distance(
|
|
186
|
+
componentBody?.positionMil,
|
|
187
|
+
component
|
|
188
|
+
) <= Number(toleranceMil || 0)
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Checks whether one raised body identifies itself as a timing-package
|
|
194
|
+
* detail rather than a generic nearby package body.
|
|
195
|
+
* @param {{ name?: string, identifier?: string }} componentBody Source body.
|
|
196
|
+
* @returns {boolean}
|
|
197
|
+
*/
|
|
198
|
+
static hasTimingBodyIdentity(componentBody) {
|
|
199
|
+
return TIMING_BODY_IDENTITY_PATTERN.test(
|
|
200
|
+
[
|
|
201
|
+
componentBody?.name,
|
|
202
|
+
componentBody?.identifier,
|
|
203
|
+
componentBody?.modelTypeName
|
|
204
|
+
]
|
|
205
|
+
.map((value) => String(value || ''))
|
|
206
|
+
.join(' ')
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Checks whether one raised body identifies itself as a stack detail that
|
|
212
|
+
* may belong to an authored carrier even when a nearby opposite-side owner
|
|
213
|
+
* is closer in XY.
|
|
214
|
+
* @param {{ name?: string, identifier?: string, modelTypeName?: string }} componentBody Source body.
|
|
215
|
+
* @returns {boolean}
|
|
216
|
+
*/
|
|
217
|
+
static hasStackDetailBodyIdentity(componentBody) {
|
|
218
|
+
return STACK_DETAIL_BODY_IDENTITY_PATTERN.test(
|
|
219
|
+
[
|
|
220
|
+
componentBody?.name,
|
|
221
|
+
componentBody?.identifier,
|
|
222
|
+
componentBody?.modelTypeName
|
|
223
|
+
]
|
|
224
|
+
.map((value) => String(value || ''))
|
|
225
|
+
.join(' ')
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Builds timing-relevant component identity text.
|
|
231
|
+
* @param {object} component Source component.
|
|
232
|
+
* @returns {string}
|
|
233
|
+
*/
|
|
234
|
+
static #timingIdentityText(component) {
|
|
235
|
+
return [
|
|
236
|
+
component?.pattern,
|
|
237
|
+
component?.source,
|
|
238
|
+
component?.description,
|
|
239
|
+
component?.provenance?.footprintDescription,
|
|
240
|
+
...AltiumScene3dShapeStackOwnerConflictPolicy.#timingParameterValues(
|
|
241
|
+
component?.parameters
|
|
242
|
+
)
|
|
243
|
+
]
|
|
244
|
+
.map((value) => String(value || ''))
|
|
245
|
+
.join(' ')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Returns parameter values whose names describe component identity.
|
|
250
|
+
* @param {object | undefined} parameters Source component parameters.
|
|
251
|
+
* @returns {unknown[]}
|
|
252
|
+
*/
|
|
253
|
+
static #timingParameterValues(parameters) {
|
|
254
|
+
if (!parameters || typeof parameters !== 'object') {
|
|
255
|
+
return []
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return Object.entries(parameters)
|
|
259
|
+
.filter(([key]) =>
|
|
260
|
+
TIMING_PARAMETER_NAME_PATTERN.test(String(key || ''))
|
|
261
|
+
)
|
|
262
|
+
.map(([, value]) => value)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Resolves one component's mount side.
|
|
267
|
+
* @param {object} component Source component.
|
|
268
|
+
* @returns {'top' | 'bottom'}
|
|
269
|
+
*/
|
|
270
|
+
static #mountSide(component) {
|
|
271
|
+
const layer = String(component?.layer || '').toUpperCase()
|
|
272
|
+
return layer.includes('BOTTOM') || layer === 'BOT' ? 'bottom' : 'top'
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Measures the XY distance between two board points.
|
|
277
|
+
* @param {object | undefined} first First point.
|
|
278
|
+
* @param {object | undefined} second Second point.
|
|
279
|
+
* @returns {number}
|
|
280
|
+
*/
|
|
281
|
+
static #distance(first, second) {
|
|
282
|
+
return Math.hypot(
|
|
283
|
+
Number(first?.x || 0) - Number(second?.x || 0),
|
|
284
|
+
Number(first?.y || 0) - Number(second?.y || 0)
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Normalizes one point.
|
|
290
|
+
* @param {object | undefined} point Source point.
|
|
291
|
+
* @returns {{ x: number, y: number }}
|
|
292
|
+
*/
|
|
293
|
+
static #point(point) {
|
|
294
|
+
return {
|
|
295
|
+
x: Number(point?.x || 0),
|
|
296
|
+
y: Number(point?.y || 0)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -254,6 +254,10 @@ export class PcbScene3dBoardOutlineRefiner {
|
|
|
254
254
|
return false
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
if (PcbScene3dBoardOutlineRefiner.#isLayerStackBodyRegion(region)) {
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
257
261
|
const candidateBounds =
|
|
258
262
|
PcbScene3dBoardOutlineRefiner.#resolveOutlineBounds(candidate)
|
|
259
263
|
if (!candidateBounds) {
|
|
@@ -275,6 +279,24 @@ export class PcbScene3dBoardOutlineRefiner {
|
|
|
275
279
|
return insideEnvelope && !touchesOuterEdge
|
|
276
280
|
}
|
|
277
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Returns true when a board region describes a layer-stack body contour
|
|
284
|
+
* rather than a separate internal cutout aperture.
|
|
285
|
+
* @param {object} region Source board region.
|
|
286
|
+
* @returns {boolean}
|
|
287
|
+
*/
|
|
288
|
+
static #isLayerStackBodyRegion(region) {
|
|
289
|
+
if (region?.objectKind !== 'BoardRegion') {
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
region?.isRigidRegion === true ||
|
|
295
|
+
region?.isFlexRegion === true ||
|
|
296
|
+
String(region?.layerStackId || '').trim().length > 0
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
278
300
|
/**
|
|
279
301
|
* Returns true when a region can represent the board body boundary.
|
|
280
302
|
* @param {object} region Source board region.
|