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