altium-toolkit 1.1.25 → 1.1.26
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 -3
- package/docs/api.md +9 -4
- package/docs/model-format.md +15 -4
- package/docs/schemas/altium_toolkit/library_compatibility_a1.schema.json +116 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +3 -0
- package/package.json +1 -1
- package/spec/library-compatibility.md +65 -0
- package/src/core/altium/LibraryCompatibilityGeometry.mjs +618 -0
- package/src/core/altium/LibraryCompatibilityModelHintBuilder.mjs +296 -0
- package/src/core/altium/LibraryCompatibilityReportBuilder.mjs +930 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +29 -1
- package/src/core/altium/PcbLibModelParser.mjs +24 -2
- package/src/core/altium/PcbLibStorageNameResolver.mjs +171 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +15 -93
- package/src/parser.mjs +1 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
const COURTYARD_MARGIN_MIL = 2
|
|
6
|
+
const SYMBOL_FIELD_MARGIN_MIL = 10
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Computes deterministic footprint geometry used by compatibility reports.
|
|
10
|
+
*/
|
|
11
|
+
export class LibraryCompatibilityGeometry {
|
|
12
|
+
/**
|
|
13
|
+
* Computes merged footprint bounds.
|
|
14
|
+
* @param {object} footprint Footprint row.
|
|
15
|
+
* @returns {object | null}
|
|
16
|
+
*/
|
|
17
|
+
static footprintBounds(footprint) {
|
|
18
|
+
return LibraryCompatibilityGeometry.#normalizeBounds(
|
|
19
|
+
[
|
|
20
|
+
...(footprint?.pads || []).map((pad) =>
|
|
21
|
+
LibraryCompatibilityGeometry.#padBounds(pad)
|
|
22
|
+
),
|
|
23
|
+
...(footprint?.tracks || []).map((track) =>
|
|
24
|
+
LibraryCompatibilityGeometry.#trackBounds(track)
|
|
25
|
+
),
|
|
26
|
+
...(footprint?.fills || []).map((fill) =>
|
|
27
|
+
LibraryCompatibilityGeometry.#boxBounds(fill)
|
|
28
|
+
),
|
|
29
|
+
...(footprint?.arcs || []).map((arc) =>
|
|
30
|
+
LibraryCompatibilityGeometry.#arcBounds(arc)
|
|
31
|
+
),
|
|
32
|
+
...(footprint?.regions || []).map((region) =>
|
|
33
|
+
LibraryCompatibilityGeometry.#regionBounds(region)
|
|
34
|
+
),
|
|
35
|
+
...(footprint?.texts || []).map((text) =>
|
|
36
|
+
LibraryCompatibilityGeometry.#textBounds(text)
|
|
37
|
+
)
|
|
38
|
+
].filter(Boolean)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Builds a padded footprint courtyard bound.
|
|
44
|
+
* @param {object} bounds Footprint bounds.
|
|
45
|
+
* @returns {object}
|
|
46
|
+
*/
|
|
47
|
+
static courtyard(bounds) {
|
|
48
|
+
return {
|
|
49
|
+
...LibraryCompatibilityGeometry.#completeBounds({
|
|
50
|
+
minX: bounds.minX - COURTYARD_MARGIN_MIL,
|
|
51
|
+
minY: bounds.minY - COURTYARD_MARGIN_MIL,
|
|
52
|
+
maxX: bounds.maxX + COURTYARD_MARGIN_MIL,
|
|
53
|
+
maxY: bounds.maxY + COURTYARD_MARGIN_MIL
|
|
54
|
+
}),
|
|
55
|
+
marginMil: COURTYARD_MARGIN_MIL
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Counts footprint primitive families used by bounds.
|
|
61
|
+
* @param {object} footprint Footprint row.
|
|
62
|
+
* @returns {object}
|
|
63
|
+
*/
|
|
64
|
+
static sourceCounts(footprint) {
|
|
65
|
+
return {
|
|
66
|
+
pads: (footprint?.pads || []).length,
|
|
67
|
+
tracks: (footprint?.tracks || []).length,
|
|
68
|
+
arcs: (footprint?.arcs || []).length,
|
|
69
|
+
fills: (footprint?.fills || []).length,
|
|
70
|
+
regions: (footprint?.regions || []).length,
|
|
71
|
+
texts: (footprint?.texts || []).length
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Computes schematic symbol bounds from pins and drawn body primitives.
|
|
77
|
+
* @param {object} symbol Schematic library symbol row.
|
|
78
|
+
* @returns {object | null}
|
|
79
|
+
*/
|
|
80
|
+
static symbolBounds(symbol) {
|
|
81
|
+
const bodyBounds = LibraryCompatibilityGeometry.#normalizeBounds(
|
|
82
|
+
LibraryCompatibilityGeometry.#symbolBodyBounds(symbol)
|
|
83
|
+
)
|
|
84
|
+
const pinBounds = LibraryCompatibilityGeometry.#normalizeBounds(
|
|
85
|
+
(symbol?.pins || [])
|
|
86
|
+
.map((pin) =>
|
|
87
|
+
LibraryCompatibilityGeometry.#schematicPinBounds(pin)
|
|
88
|
+
)
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
)
|
|
91
|
+
const bounds = LibraryCompatibilityGeometry.#normalizeBounds(
|
|
92
|
+
[bodyBounds, pinBounds].filter(Boolean)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if (!bounds) return null
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
bounds,
|
|
99
|
+
...(bodyBounds ? { bodyBounds } : {}),
|
|
100
|
+
...(pinBounds ? { pinBounds } : {}),
|
|
101
|
+
fieldAnchors:
|
|
102
|
+
LibraryCompatibilityGeometry.#symbolFieldAnchors(bounds),
|
|
103
|
+
sourceCounts: {
|
|
104
|
+
pins: (symbol?.pins || []).length,
|
|
105
|
+
bodyPrimitives:
|
|
106
|
+
LibraryCompatibilityGeometry.#symbolBodyPrimitiveCount(
|
|
107
|
+
symbol
|
|
108
|
+
),
|
|
109
|
+
texts: (symbol?.texts || []).length
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Computes bounds for one custom-pad shape layer.
|
|
116
|
+
* @param {object} layer Custom-shape layer row.
|
|
117
|
+
* @returns {object | null}
|
|
118
|
+
*/
|
|
119
|
+
static customShapeLayerBounds(layer) {
|
|
120
|
+
return LibraryCompatibilityGeometry.#normalizeBounds(
|
|
121
|
+
[
|
|
122
|
+
...(layer?.regions || []).map((region) =>
|
|
123
|
+
LibraryCompatibilityGeometry.#regionBounds(region)
|
|
124
|
+
),
|
|
125
|
+
...(layer?.shapeRegions || []).map((region) =>
|
|
126
|
+
LibraryCompatibilityGeometry.#regionBounds(region)
|
|
127
|
+
),
|
|
128
|
+
...(layer?.tracks || []).map((track) =>
|
|
129
|
+
LibraryCompatibilityGeometry.#trackBounds(track)
|
|
130
|
+
),
|
|
131
|
+
...(layer?.arcs || []).map((arc) =>
|
|
132
|
+
LibraryCompatibilityGeometry.#arcBounds(arc)
|
|
133
|
+
),
|
|
134
|
+
...(layer?.fills || []).map((fill) =>
|
|
135
|
+
LibraryCompatibilityGeometry.#boxBounds(fill)
|
|
136
|
+
)
|
|
137
|
+
].filter(Boolean)
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns true when a normalized bounds row has no area.
|
|
143
|
+
* @param {object | null} bounds Bounds row.
|
|
144
|
+
* @returns {boolean}
|
|
145
|
+
*/
|
|
146
|
+
static hasZeroArea(bounds) {
|
|
147
|
+
return Boolean(
|
|
148
|
+
bounds &&
|
|
149
|
+
(Number(bounds.width || 0) === 0 ||
|
|
150
|
+
Number(bounds.height || 0) === 0)
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns whether two bounds are equivalent after report rounding.
|
|
156
|
+
* @param {object | null} left First bounds row.
|
|
157
|
+
* @param {object | null} right Second bounds row.
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
static sameBounds(left, right) {
|
|
161
|
+
if (!left || !right) return false
|
|
162
|
+
|
|
163
|
+
return ['minX', 'minY', 'maxX', 'maxY', 'width', 'height'].every(
|
|
164
|
+
(key) => Number(left[key]) === Number(right[key])
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Computes axis-aligned bounds for one pad.
|
|
170
|
+
* @param {object} pad Pad row.
|
|
171
|
+
* @returns {object | null}
|
|
172
|
+
*/
|
|
173
|
+
static #padBounds(pad) {
|
|
174
|
+
const x = LibraryCompatibilityGeometry.#finiteNumber(pad?.x)
|
|
175
|
+
const y = LibraryCompatibilityGeometry.#finiteNumber(pad?.y)
|
|
176
|
+
const width = LibraryCompatibilityGeometry.#padWidth(pad)
|
|
177
|
+
const height = LibraryCompatibilityGeometry.#padHeight(pad)
|
|
178
|
+
|
|
179
|
+
if (x === null || y === null || width === null || height === null) {
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return LibraryCompatibilityGeometry.#rotatedBoxBounds(
|
|
184
|
+
x,
|
|
185
|
+
y,
|
|
186
|
+
width,
|
|
187
|
+
height,
|
|
188
|
+
Number(pad?.rotation || 0)
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Computes one rotated rectangle bound.
|
|
194
|
+
* @param {number} centerX Center x.
|
|
195
|
+
* @param {number} centerY Center y.
|
|
196
|
+
* @param {number} width Rectangle width.
|
|
197
|
+
* @param {number} height Rectangle height.
|
|
198
|
+
* @param {number} rotationDeg Rotation in degrees.
|
|
199
|
+
* @returns {object}
|
|
200
|
+
*/
|
|
201
|
+
static #rotatedBoxBounds(centerX, centerY, width, height, rotationDeg) {
|
|
202
|
+
const halfWidth = Math.abs(width) / 2
|
|
203
|
+
const halfHeight = Math.abs(height) / 2
|
|
204
|
+
const radians = (Number(rotationDeg) || 0) * (Math.PI / 180)
|
|
205
|
+
const cos = Math.cos(radians)
|
|
206
|
+
const sin = Math.sin(radians)
|
|
207
|
+
const corners = [
|
|
208
|
+
[-halfWidth, -halfHeight],
|
|
209
|
+
[halfWidth, -halfHeight],
|
|
210
|
+
[halfWidth, halfHeight],
|
|
211
|
+
[-halfWidth, halfHeight]
|
|
212
|
+
].map(([x, y]) => ({
|
|
213
|
+
x: centerX + x * cos - y * sin,
|
|
214
|
+
y: centerY + x * sin + y * cos
|
|
215
|
+
}))
|
|
216
|
+
|
|
217
|
+
return LibraryCompatibilityGeometry.#pointsBounds(corners)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Computes one track line bound.
|
|
222
|
+
* @param {object} track Track row.
|
|
223
|
+
* @returns {object | null}
|
|
224
|
+
*/
|
|
225
|
+
static #trackBounds(track) {
|
|
226
|
+
const x1 = LibraryCompatibilityGeometry.#finiteNumber(track?.x1)
|
|
227
|
+
const y1 = LibraryCompatibilityGeometry.#finiteNumber(track?.y1)
|
|
228
|
+
const x2 = LibraryCompatibilityGeometry.#finiteNumber(track?.x2)
|
|
229
|
+
const y2 = LibraryCompatibilityGeometry.#finiteNumber(track?.y2)
|
|
230
|
+
if (x1 === null || y1 === null || x2 === null || y2 === null) {
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const inflate = Math.abs(Number(track?.width || 0)) / 2
|
|
235
|
+
return {
|
|
236
|
+
minX: Math.min(x1, x2) - inflate,
|
|
237
|
+
minY: Math.min(y1, y2) - inflate,
|
|
238
|
+
maxX: Math.max(x1, x2) + inflate,
|
|
239
|
+
maxY: Math.max(y1, y2) + inflate
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Computes one box bound.
|
|
245
|
+
* @param {object} box Box-like primitive.
|
|
246
|
+
* @returns {object | null}
|
|
247
|
+
*/
|
|
248
|
+
static #boxBounds(box) {
|
|
249
|
+
const x1 = LibraryCompatibilityGeometry.#finiteNumber(box?.x1)
|
|
250
|
+
const y1 = LibraryCompatibilityGeometry.#finiteNumber(box?.y1)
|
|
251
|
+
const x2 = LibraryCompatibilityGeometry.#finiteNumber(box?.x2)
|
|
252
|
+
const y2 = LibraryCompatibilityGeometry.#finiteNumber(box?.y2)
|
|
253
|
+
if (x1 === null || y1 === null || x2 === null || y2 === null) {
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
minX: Math.min(x1, x2),
|
|
259
|
+
minY: Math.min(y1, y2),
|
|
260
|
+
maxX: Math.max(x1, x2),
|
|
261
|
+
maxY: Math.max(y1, y2)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Computes one full-circle arc bound.
|
|
267
|
+
* @param {object} arc Arc row.
|
|
268
|
+
* @returns {object | null}
|
|
269
|
+
*/
|
|
270
|
+
static #arcBounds(arc) {
|
|
271
|
+
const x = LibraryCompatibilityGeometry.#finiteNumber(arc?.x)
|
|
272
|
+
const y = LibraryCompatibilityGeometry.#finiteNumber(arc?.y)
|
|
273
|
+
const radius = LibraryCompatibilityGeometry.#finiteNumber(arc?.radius)
|
|
274
|
+
if (x === null || y === null || radius === null) return null
|
|
275
|
+
|
|
276
|
+
const inflate = Math.abs(Number(arc?.width || 0)) / 2
|
|
277
|
+
const extent = Math.abs(radius) + inflate
|
|
278
|
+
return {
|
|
279
|
+
minX: x - extent,
|
|
280
|
+
minY: y - extent,
|
|
281
|
+
maxX: x + extent,
|
|
282
|
+
maxY: y + extent
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Computes one region point bound.
|
|
288
|
+
* @param {object} region Region row.
|
|
289
|
+
* @returns {object | null}
|
|
290
|
+
*/
|
|
291
|
+
static #regionBounds(region) {
|
|
292
|
+
const points = (region?.points || [])
|
|
293
|
+
.map((point) => ({
|
|
294
|
+
x: LibraryCompatibilityGeometry.#finiteNumber(point?.x),
|
|
295
|
+
y: LibraryCompatibilityGeometry.#finiteNumber(point?.y)
|
|
296
|
+
}))
|
|
297
|
+
.filter((point) => point.x !== null && point.y !== null)
|
|
298
|
+
|
|
299
|
+
return LibraryCompatibilityGeometry.#pointsBounds(points)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Computes an approximate text bound.
|
|
304
|
+
* @param {object} text Text row.
|
|
305
|
+
* @returns {object | null}
|
|
306
|
+
*/
|
|
307
|
+
static #textBounds(text) {
|
|
308
|
+
const x = LibraryCompatibilityGeometry.#finiteNumber(text?.x)
|
|
309
|
+
const y = LibraryCompatibilityGeometry.#finiteNumber(text?.y)
|
|
310
|
+
const height =
|
|
311
|
+
LibraryCompatibilityGeometry.#finiteNumber(text?.height) || null
|
|
312
|
+
if (x === null || y === null || height === null) return null
|
|
313
|
+
|
|
314
|
+
const width =
|
|
315
|
+
Math.max(String(text?.text || '').length, 1) * height * 0.6
|
|
316
|
+
return LibraryCompatibilityGeometry.#rotatedBoxBounds(
|
|
317
|
+
x,
|
|
318
|
+
y,
|
|
319
|
+
width,
|
|
320
|
+
height,
|
|
321
|
+
Number(text?.rotation || 0)
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Computes body primitive bounds for one schematic symbol.
|
|
327
|
+
* @param {object} symbol Schematic library symbol row.
|
|
328
|
+
* @returns {object[]}
|
|
329
|
+
*/
|
|
330
|
+
static #symbolBodyBounds(symbol) {
|
|
331
|
+
return [
|
|
332
|
+
...(symbol?.rectangles || []).map((box) =>
|
|
333
|
+
LibraryCompatibilityGeometry.#schematicBoxBounds(box)
|
|
334
|
+
),
|
|
335
|
+
...(symbol?.roundedRectangles || []).map((box) =>
|
|
336
|
+
LibraryCompatibilityGeometry.#schematicBoxBounds(box)
|
|
337
|
+
),
|
|
338
|
+
...(symbol?.lines || []).map((line) =>
|
|
339
|
+
LibraryCompatibilityGeometry.#trackBounds(line)
|
|
340
|
+
),
|
|
341
|
+
...(symbol?.polygons || []).map((polygon) =>
|
|
342
|
+
LibraryCompatibilityGeometry.#regionBounds(polygon)
|
|
343
|
+
),
|
|
344
|
+
...(symbol?.regions || []).map((region) =>
|
|
345
|
+
LibraryCompatibilityGeometry.#regionBounds(region)
|
|
346
|
+
),
|
|
347
|
+
...(symbol?.beziers || []).map((bezier) =>
|
|
348
|
+
LibraryCompatibilityGeometry.#regionBounds(bezier)
|
|
349
|
+
),
|
|
350
|
+
...(symbol?.ellipses || []).map((ellipse) =>
|
|
351
|
+
LibraryCompatibilityGeometry.#ellipseBounds(ellipse)
|
|
352
|
+
),
|
|
353
|
+
...(symbol?.arcs || []).map((arc) =>
|
|
354
|
+
LibraryCompatibilityGeometry.#arcBounds(arc)
|
|
355
|
+
),
|
|
356
|
+
...(symbol?.pies || []).map((pie) =>
|
|
357
|
+
LibraryCompatibilityGeometry.#arcBounds(pie)
|
|
358
|
+
)
|
|
359
|
+
].filter(Boolean)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Counts drawn symbol body primitives.
|
|
364
|
+
* @param {object} symbol Schematic library symbol row.
|
|
365
|
+
* @returns {number}
|
|
366
|
+
*/
|
|
367
|
+
static #symbolBodyPrimitiveCount(symbol) {
|
|
368
|
+
return [
|
|
369
|
+
'rectangles',
|
|
370
|
+
'roundedRectangles',
|
|
371
|
+
'lines',
|
|
372
|
+
'polygons',
|
|
373
|
+
'regions',
|
|
374
|
+
'beziers',
|
|
375
|
+
'ellipses',
|
|
376
|
+
'arcs',
|
|
377
|
+
'pies'
|
|
378
|
+
].reduce((sum, key) => sum + (symbol?.[key] || []).length, 0)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Computes bounds for one schematic pin.
|
|
383
|
+
* @param {object} pin Pin row.
|
|
384
|
+
* @returns {object | null}
|
|
385
|
+
*/
|
|
386
|
+
static #schematicPinBounds(pin) {
|
|
387
|
+
const x = LibraryCompatibilityGeometry.#finiteNumber(pin?.x)
|
|
388
|
+
const y = LibraryCompatibilityGeometry.#finiteNumber(pin?.y)
|
|
389
|
+
const length = LibraryCompatibilityGeometry.#finiteNumber(pin?.length)
|
|
390
|
+
|
|
391
|
+
if (x === null || y === null || length === null) return null
|
|
392
|
+
|
|
393
|
+
const endpoint =
|
|
394
|
+
{
|
|
395
|
+
left: { x: x - length, y },
|
|
396
|
+
right: { x: x + length, y },
|
|
397
|
+
top: { x, y: y + length },
|
|
398
|
+
bottom: { x, y: y - length }
|
|
399
|
+
}[String(pin?.orientation || '').toLowerCase()] || null
|
|
400
|
+
|
|
401
|
+
if (!endpoint) return null
|
|
402
|
+
|
|
403
|
+
return LibraryCompatibilityGeometry.#pointsBounds([{ x, y }, endpoint])
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Computes bounds for one schematic box primitive.
|
|
408
|
+
* @param {object} box Box-like primitive.
|
|
409
|
+
* @returns {object | null}
|
|
410
|
+
*/
|
|
411
|
+
static #schematicBoxBounds(box) {
|
|
412
|
+
const x = LibraryCompatibilityGeometry.#finiteNumber(box?.x ?? box?.x1)
|
|
413
|
+
const y = LibraryCompatibilityGeometry.#finiteNumber(box?.y ?? box?.y1)
|
|
414
|
+
const width = LibraryCompatibilityGeometry.#finiteNumber(box?.width)
|
|
415
|
+
const height = LibraryCompatibilityGeometry.#finiteNumber(box?.height)
|
|
416
|
+
const x2 = LibraryCompatibilityGeometry.#finiteNumber(
|
|
417
|
+
box?.x2 ?? box?.cornerX
|
|
418
|
+
)
|
|
419
|
+
const y2 = LibraryCompatibilityGeometry.#finiteNumber(
|
|
420
|
+
box?.y2 ?? box?.cornerY
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if (x !== null && y !== null && width !== null && height !== null) {
|
|
424
|
+
return LibraryCompatibilityGeometry.#pointsBounds([
|
|
425
|
+
{ x, y },
|
|
426
|
+
{ x: x + width, y: y + height }
|
|
427
|
+
])
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (x !== null && y !== null && x2 !== null && y2 !== null) {
|
|
431
|
+
return LibraryCompatibilityGeometry.#pointsBounds([
|
|
432
|
+
{ x, y },
|
|
433
|
+
{ x: x2, y: y2 }
|
|
434
|
+
])
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return null
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Computes bounds for one schematic ellipse primitive.
|
|
442
|
+
* @param {object} ellipse Ellipse row.
|
|
443
|
+
* @returns {object | null}
|
|
444
|
+
*/
|
|
445
|
+
static #ellipseBounds(ellipse) {
|
|
446
|
+
const x = LibraryCompatibilityGeometry.#finiteNumber(ellipse?.x)
|
|
447
|
+
const y = LibraryCompatibilityGeometry.#finiteNumber(ellipse?.y)
|
|
448
|
+
const radiusX =
|
|
449
|
+
LibraryCompatibilityGeometry.#finiteNumber(ellipse?.radiusX) ??
|
|
450
|
+
LibraryCompatibilityGeometry.#finiteNumber(ellipse?.radius)
|
|
451
|
+
const radiusY =
|
|
452
|
+
LibraryCompatibilityGeometry.#finiteNumber(ellipse?.radiusY) ??
|
|
453
|
+
radiusX
|
|
454
|
+
|
|
455
|
+
if (x === null || y === null || radiusX === null || radiusY === null) {
|
|
456
|
+
return null
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
minX: x - Math.abs(radiusX),
|
|
461
|
+
minY: y - Math.abs(radiusY),
|
|
462
|
+
maxX: x + Math.abs(radiusX),
|
|
463
|
+
maxY: y + Math.abs(radiusY)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Builds deterministic field anchor hints from symbol bounds.
|
|
469
|
+
* @param {object} bounds Normalized symbol bounds.
|
|
470
|
+
* @returns {object}
|
|
471
|
+
*/
|
|
472
|
+
static #symbolFieldAnchors(bounds) {
|
|
473
|
+
return {
|
|
474
|
+
designator: {
|
|
475
|
+
x: bounds.minX,
|
|
476
|
+
y: LibraryCompatibilityGeometry.#round(
|
|
477
|
+
bounds.maxY + SYMBOL_FIELD_MARGIN_MIL
|
|
478
|
+
),
|
|
479
|
+
horizontal: 'left',
|
|
480
|
+
vertical: 'bottom'
|
|
481
|
+
},
|
|
482
|
+
comment: {
|
|
483
|
+
x: bounds.minX,
|
|
484
|
+
y: LibraryCompatibilityGeometry.#round(
|
|
485
|
+
bounds.minY - SYMBOL_FIELD_MARGIN_MIL
|
|
486
|
+
),
|
|
487
|
+
horizontal: 'left',
|
|
488
|
+
vertical: 'top'
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Resolves the effective pad width.
|
|
495
|
+
* @param {object} pad Pad row.
|
|
496
|
+
* @returns {number | null}
|
|
497
|
+
*/
|
|
498
|
+
static #padWidth(pad) {
|
|
499
|
+
return LibraryCompatibilityGeometry.#maxPositive([
|
|
500
|
+
pad?.sizeTopX,
|
|
501
|
+
pad?.sizeMidX,
|
|
502
|
+
pad?.sizeBottomX,
|
|
503
|
+
...(pad?.padStack?.layers || []).map((layer) => layer?.width),
|
|
504
|
+
...(pad?.localPadStack?.layers || []).map((layer) => layer?.width)
|
|
505
|
+
])
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Resolves the effective pad height.
|
|
510
|
+
* @param {object} pad Pad row.
|
|
511
|
+
* @returns {number | null}
|
|
512
|
+
*/
|
|
513
|
+
static #padHeight(pad) {
|
|
514
|
+
return LibraryCompatibilityGeometry.#maxPositive([
|
|
515
|
+
pad?.sizeTopY,
|
|
516
|
+
pad?.sizeMidY,
|
|
517
|
+
pad?.sizeBottomY,
|
|
518
|
+
...(pad?.padStack?.layers || []).map((layer) => layer?.height),
|
|
519
|
+
...(pad?.localPadStack?.layers || []).map((layer) => layer?.height)
|
|
520
|
+
])
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Finds the largest finite absolute value from a list.
|
|
525
|
+
* @param {unknown[]} values Candidate values.
|
|
526
|
+
* @returns {number | null}
|
|
527
|
+
*/
|
|
528
|
+
static #maxPositive(values) {
|
|
529
|
+
const finiteValues = (values || [])
|
|
530
|
+
.map((value) => LibraryCompatibilityGeometry.#finiteNumber(value))
|
|
531
|
+
.filter((value) => value !== null)
|
|
532
|
+
.map((value) => Math.abs(value))
|
|
533
|
+
|
|
534
|
+
if (!finiteValues.length) return null
|
|
535
|
+
return Math.max(...finiteValues)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Builds bounds from points.
|
|
540
|
+
* @param {{ x: number | null, y: number | null }[]} points Point rows.
|
|
541
|
+
* @returns {object | null}
|
|
542
|
+
*/
|
|
543
|
+
static #pointsBounds(points) {
|
|
544
|
+
const finitePoints = (points || []).filter(
|
|
545
|
+
(point) => Number.isFinite(point?.x) && Number.isFinite(point?.y)
|
|
546
|
+
)
|
|
547
|
+
if (!finitePoints.length) return null
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
minX: Math.min(...finitePoints.map((point) => point.x)),
|
|
551
|
+
minY: Math.min(...finitePoints.map((point) => point.y)),
|
|
552
|
+
maxX: Math.max(...finitePoints.map((point) => point.x)),
|
|
553
|
+
maxY: Math.max(...finitePoints.map((point) => point.y))
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Merges and normalizes multiple bounds.
|
|
559
|
+
* @param {object[]} bounds Bounds rows.
|
|
560
|
+
* @returns {object | null}
|
|
561
|
+
*/
|
|
562
|
+
static #normalizeBounds(bounds) {
|
|
563
|
+
const finiteBounds = (bounds || []).filter(
|
|
564
|
+
(bound) =>
|
|
565
|
+
Number.isFinite(bound?.minX) &&
|
|
566
|
+
Number.isFinite(bound?.minY) &&
|
|
567
|
+
Number.isFinite(bound?.maxX) &&
|
|
568
|
+
Number.isFinite(bound?.maxY)
|
|
569
|
+
)
|
|
570
|
+
if (!finiteBounds.length) return null
|
|
571
|
+
|
|
572
|
+
return LibraryCompatibilityGeometry.#completeBounds({
|
|
573
|
+
minX: Math.min(...finiteBounds.map((bound) => bound.minX)),
|
|
574
|
+
minY: Math.min(...finiteBounds.map((bound) => bound.minY)),
|
|
575
|
+
maxX: Math.max(...finiteBounds.map((bound) => bound.maxX)),
|
|
576
|
+
maxY: Math.max(...finiteBounds.map((bound) => bound.maxY))
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Adds width and height fields to a bound.
|
|
582
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Raw bounds.
|
|
583
|
+
* @returns {object}
|
|
584
|
+
*/
|
|
585
|
+
static #completeBounds(bounds) {
|
|
586
|
+
return {
|
|
587
|
+
minX: LibraryCompatibilityGeometry.#round(bounds.minX),
|
|
588
|
+
minY: LibraryCompatibilityGeometry.#round(bounds.minY),
|
|
589
|
+
maxX: LibraryCompatibilityGeometry.#round(bounds.maxX),
|
|
590
|
+
maxY: LibraryCompatibilityGeometry.#round(bounds.maxY),
|
|
591
|
+
width: LibraryCompatibilityGeometry.#round(
|
|
592
|
+
bounds.maxX - bounds.minX
|
|
593
|
+
),
|
|
594
|
+
height: LibraryCompatibilityGeometry.#round(
|
|
595
|
+
bounds.maxY - bounds.minY
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Converts one value to a finite number.
|
|
602
|
+
* @param {unknown} value Candidate number.
|
|
603
|
+
* @returns {number | null}
|
|
604
|
+
*/
|
|
605
|
+
static #finiteNumber(value) {
|
|
606
|
+
const numeric = Number(value)
|
|
607
|
+
return Number.isFinite(numeric) ? numeric : null
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Rounds floating-point report values.
|
|
612
|
+
* @param {number} value Numeric value.
|
|
613
|
+
* @returns {number}
|
|
614
|
+
*/
|
|
615
|
+
static #round(value) {
|
|
616
|
+
return Number(Number(value).toFixed(6))
|
|
617
|
+
}
|
|
618
|
+
}
|