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
@@ -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.