circuitjson-toolkit 1.0.3 → 1.0.16
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 +5 -3
- package/README.md +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +21 -3
- package/package.json +3 -2
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +143 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +990 -0
- package/src/core/CircuitJsonIndexer.mjs +773 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
- package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
- package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
- package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
- package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
- package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
- package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- package/src/core/PcbBoundsSelectionModel.mjs +250 -0
- package/src/core/PcbCandidateSelectionModel.mjs +77 -0
- package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
- package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
- package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
- package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
- package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
- package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
- package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
- package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
- package/src/core/spice/SpiceSimulationService.mjs +92 -0
- package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
- package/src/index.mjs +8 -0
- package/src/renderers.mjs +29 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
|
|
2
|
+
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
3
|
+
import { PcbInteractionPrimitiveModel } from './PcbInteractionPrimitiveModel.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves viewport focus targets for PCB diagnostic rows.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbDiagnosticFocusModel {
|
|
9
|
+
/**
|
|
10
|
+
* Builds a diagnostic-id to focus-target map.
|
|
11
|
+
* @param {object | object[]} documentModel Parsed PCB document.
|
|
12
|
+
* @returns {Map<string, object>}
|
|
13
|
+
*/
|
|
14
|
+
static build(documentModel) {
|
|
15
|
+
const model = PcbInteractionPrimitiveModel.build(documentModel)
|
|
16
|
+
const context = PcbDiagnosticFocusModel.#context(documentModel, model)
|
|
17
|
+
const rows = new Map()
|
|
18
|
+
|
|
19
|
+
for (const diagnostic of model.diagnostics || []) {
|
|
20
|
+
const row = PcbDiagnosticFocusModel.#focusRow(diagnostic, context)
|
|
21
|
+
if (row) rows.set(row.id, row)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return rows
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds a compact viewport target around a diagnostic center.
|
|
29
|
+
* @param {{ point?: object, bounds?: object } | null | undefined} focus Diagnostic focus row.
|
|
30
|
+
* @returns {{ x: number, y: number, width: number, height: number } | null}
|
|
31
|
+
*/
|
|
32
|
+
static viewportBounds(focus) {
|
|
33
|
+
const bounds = PcbDiagnosticFocusModel.#normalizeBounds(focus?.bounds)
|
|
34
|
+
if (!bounds) return null
|
|
35
|
+
const center =
|
|
36
|
+
CircuitJsonUnits.optionalPoint(focus?.point) ||
|
|
37
|
+
PcbDiagnosticFocusModel.#boundsCenter(bounds)
|
|
38
|
+
const width = Math.min(Math.max(bounds.width, 0.4), 1)
|
|
39
|
+
const height = Math.min(Math.max(bounds.height, 0.4), 0.6)
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
x: PcbDiagnosticFocusModel.#rounded(center.x - width / 2),
|
|
43
|
+
y: PcbDiagnosticFocusModel.#rounded(center.y - height / 2),
|
|
44
|
+
width: PcbDiagnosticFocusModel.#rounded(width),
|
|
45
|
+
height: PcbDiagnosticFocusModel.#rounded(height)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Builds lookup data for focus resolution.
|
|
51
|
+
* @param {object | object[]} documentModel Parsed document.
|
|
52
|
+
* @param {object} model Primitive model.
|
|
53
|
+
* @returns {object}
|
|
54
|
+
*/
|
|
55
|
+
static #context(documentModel, model) {
|
|
56
|
+
const elements = PcbDiagnosticFocusModel.#elements(documentModel)
|
|
57
|
+
const elementsById = new Map()
|
|
58
|
+
for (const element of elements) {
|
|
59
|
+
const id = CircuitJsonIndexer.getElementId(element)
|
|
60
|
+
if (id) elementsById.set(id, element)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
model,
|
|
65
|
+
elements,
|
|
66
|
+
elementsById,
|
|
67
|
+
sourcePortToPcbPortIds:
|
|
68
|
+
PcbDiagnosticFocusModel.#sourcePortToPcbPortIds(elements),
|
|
69
|
+
primitivesById: new Map(
|
|
70
|
+
(model.primitives || [])
|
|
71
|
+
.filter((primitive) => String(primitive.id || '').trim())
|
|
72
|
+
.map((primitive) => [String(primitive.id), primitive])
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Builds one diagnostic focus row.
|
|
79
|
+
* @param {object} diagnostic Diagnostic row.
|
|
80
|
+
* @param {object} context Focus context.
|
|
81
|
+
* @returns {object | null}
|
|
82
|
+
*/
|
|
83
|
+
static #focusRow(diagnostic, context) {
|
|
84
|
+
const id = String(diagnostic.id || '').trim()
|
|
85
|
+
if (!id) return null
|
|
86
|
+
|
|
87
|
+
const element = context.elementsById.get(id) || null
|
|
88
|
+
const relatedPrimitives = PcbDiagnosticFocusModel.#relatedPrimitives(
|
|
89
|
+
diagnostic,
|
|
90
|
+
element,
|
|
91
|
+
context
|
|
92
|
+
)
|
|
93
|
+
const relatedBounds =
|
|
94
|
+
PcbDiagnosticFocusModel.#mergeBounds(
|
|
95
|
+
relatedPrimitives.map((primitive) => primitive.bounds)
|
|
96
|
+
) ||
|
|
97
|
+
PcbDiagnosticFocusModel.#componentBounds(element, context.model)
|
|
98
|
+
const bounds =
|
|
99
|
+
relatedBounds ||
|
|
100
|
+
PcbDiagnosticFocusModel.#normalizeBounds(diagnostic.bounds) ||
|
|
101
|
+
PcbDiagnosticFocusModel.#pointBounds(diagnostic.point)
|
|
102
|
+
if (!bounds) return null
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
id,
|
|
106
|
+
point: PcbDiagnosticFocusModel.#roundPoint(
|
|
107
|
+
PcbDiagnosticFocusModel.#boundsCenter(bounds)
|
|
108
|
+
),
|
|
109
|
+
bounds: PcbDiagnosticFocusModel.#viewportBounds(bounds),
|
|
110
|
+
relatedPrimitiveIds: relatedPrimitives
|
|
111
|
+
.map((primitive) => String(primitive.id || '').trim())
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.sort()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolves primitives related to one diagnostic.
|
|
119
|
+
* @param {object} diagnostic Diagnostic row.
|
|
120
|
+
* @param {object | null} element Source diagnostic element.
|
|
121
|
+
* @param {object} context Focus context.
|
|
122
|
+
* @returns {object[]}
|
|
123
|
+
*/
|
|
124
|
+
static #relatedPrimitives(diagnostic, element, context) {
|
|
125
|
+
const ids = Array.isArray(diagnostic.relatedPrimitiveIds)
|
|
126
|
+
? diagnostic.relatedPrimitiveIds
|
|
127
|
+
: []
|
|
128
|
+
const direct = ids
|
|
129
|
+
.map((id) => context.primitivesById.get(String(id || '').trim()))
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
if (direct.length) return direct
|
|
132
|
+
|
|
133
|
+
const fields = PcbDiagnosticFocusModel.#relatedFields(element)
|
|
134
|
+
const fieldPrimitives = (context.model.primitives || []).filter(
|
|
135
|
+
(primitive) =>
|
|
136
|
+
fields.some(([field, value]) =>
|
|
137
|
+
PcbDiagnosticFocusModel.#primitiveMatches(
|
|
138
|
+
primitive,
|
|
139
|
+
field,
|
|
140
|
+
value
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
if (fieldPrimitives.length) return fieldPrimitives
|
|
145
|
+
|
|
146
|
+
const sourcePortId = String(element?.source_port_id || '').trim()
|
|
147
|
+
const pcbPortIds =
|
|
148
|
+
context.sourcePortToPcbPortIds.get(sourcePortId) || []
|
|
149
|
+
const sourcePortPrimitives = (context.model.primitives || []).filter(
|
|
150
|
+
(primitive) =>
|
|
151
|
+
pcbPortIds.some(
|
|
152
|
+
(id) =>
|
|
153
|
+
String(primitive.source?.pcb_port_id || '').trim() ===
|
|
154
|
+
id
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
if (sourcePortPrimitives.length) return sourcePortPrimitives
|
|
158
|
+
|
|
159
|
+
const sourceComponentId = String(
|
|
160
|
+
element?.source_component_id || ''
|
|
161
|
+
).trim()
|
|
162
|
+
if (!sourceComponentId) return []
|
|
163
|
+
|
|
164
|
+
return (context.model.primitives || []).filter(
|
|
165
|
+
(primitive) =>
|
|
166
|
+
String(primitive.sourceComponentId || '').trim() ===
|
|
167
|
+
sourceComponentId
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds source element fields that can identify related primitives.
|
|
173
|
+
* @param {object | null} element Source diagnostic element.
|
|
174
|
+
* @returns {Array<[string, string]>}
|
|
175
|
+
*/
|
|
176
|
+
static #relatedFields(element) {
|
|
177
|
+
return [
|
|
178
|
+
['pcb_trace_id', [element?.pcb_trace_id, element?.pcb_trace_ids]],
|
|
179
|
+
[
|
|
180
|
+
'pcb_smtpad_id',
|
|
181
|
+
[element?.pcb_smtpad_id, element?.pcb_smtpad_ids]
|
|
182
|
+
],
|
|
183
|
+
['pcb_via_id', [element?.pcb_via_id, element?.pcb_via_ids]],
|
|
184
|
+
[
|
|
185
|
+
'pcb_plated_hole_id',
|
|
186
|
+
[element?.pcb_plated_hole_id, element?.pcb_plated_hole_ids]
|
|
187
|
+
],
|
|
188
|
+
['pcb_hole_id', [element?.pcb_hole_id, element?.pcb_hole_ids]],
|
|
189
|
+
['pcb_port_id', [element?.pcb_port_id, element?.pcb_port_ids]],
|
|
190
|
+
[
|
|
191
|
+
'pcb_component_id',
|
|
192
|
+
[element?.pcb_component_id, element?.pcb_component_ids]
|
|
193
|
+
]
|
|
194
|
+
].flatMap(([field, values]) =>
|
|
195
|
+
PcbDiagnosticFocusModel.#idValues(values).map((value) => [
|
|
196
|
+
field,
|
|
197
|
+
value
|
|
198
|
+
])
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Normalizes scalar or array ID values.
|
|
204
|
+
* @param {unknown[]} values Candidate values.
|
|
205
|
+
* @returns {string[]}
|
|
206
|
+
*/
|
|
207
|
+
static #idValues(values) {
|
|
208
|
+
return [
|
|
209
|
+
...new Set(
|
|
210
|
+
values
|
|
211
|
+
.flatMap((value) =>
|
|
212
|
+
Array.isArray(value) ? value : [value]
|
|
213
|
+
)
|
|
214
|
+
.map((value) => String(value || '').trim())
|
|
215
|
+
.filter(Boolean)
|
|
216
|
+
)
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Returns true when a primitive is associated with a source field.
|
|
222
|
+
* @param {object} primitive Primitive row.
|
|
223
|
+
* @param {string} field Source id field.
|
|
224
|
+
* @param {string} value Source id value.
|
|
225
|
+
* @returns {boolean}
|
|
226
|
+
*/
|
|
227
|
+
static #primitiveMatches(primitive, field, value) {
|
|
228
|
+
if (field === 'pcb_trace_id') {
|
|
229
|
+
return String(primitive.source?.pcb_trace_id || '').trim() === value
|
|
230
|
+
}
|
|
231
|
+
if (field === 'pcb_component_id') {
|
|
232
|
+
return String(primitive.componentId || '').trim() === value
|
|
233
|
+
}
|
|
234
|
+
return String(primitive.source?.[field] || '').trim() === value
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Builds source-port to PCB-port id lookup data.
|
|
239
|
+
* @param {object[]} elements Element rows.
|
|
240
|
+
* @returns {Map<string, string[]>}
|
|
241
|
+
*/
|
|
242
|
+
static #sourcePortToPcbPortIds(elements) {
|
|
243
|
+
const map = new Map()
|
|
244
|
+
for (const element of elements) {
|
|
245
|
+
if (element?.type !== 'pcb_port') continue
|
|
246
|
+
const sourcePortId = String(element.source_port_id || '').trim()
|
|
247
|
+
const pcbPortId = String(element.pcb_port_id || '').trim()
|
|
248
|
+
if (!sourcePortId || !pcbPortId) continue
|
|
249
|
+
if (!map.has(sourcePortId)) map.set(sourcePortId, [])
|
|
250
|
+
map.get(sourcePortId).push(pcbPortId)
|
|
251
|
+
}
|
|
252
|
+
return map
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Resolves component bounds when no primitive bounds are available.
|
|
257
|
+
* @param {object | null} element Source diagnostic element.
|
|
258
|
+
* @param {object} model Primitive model.
|
|
259
|
+
* @returns {object | null}
|
|
260
|
+
*/
|
|
261
|
+
static #componentBounds(element, model) {
|
|
262
|
+
const componentId = String(element?.pcb_component_id || '').trim()
|
|
263
|
+
if (!componentId) return null
|
|
264
|
+
|
|
265
|
+
const component = (model.components || []).find(
|
|
266
|
+
(row) => String(row.pcbComponentId || '').trim() === componentId
|
|
267
|
+
)
|
|
268
|
+
if (!component) return null
|
|
269
|
+
|
|
270
|
+
return PcbDiagnosticFocusModel.#centerBounds(
|
|
271
|
+
{ x: component.x, y: component.y },
|
|
272
|
+
CircuitJsonUnits.length(component.width, 0.8),
|
|
273
|
+
CircuitJsonUnits.length(component.height, 0.8)
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Reads element rows from an array or wrapper object.
|
|
279
|
+
* @param {object | object[]} documentModel Parsed document.
|
|
280
|
+
* @returns {object[]}
|
|
281
|
+
*/
|
|
282
|
+
static #elements(documentModel) {
|
|
283
|
+
if (Array.isArray(documentModel)) return documentModel
|
|
284
|
+
if (Array.isArray(documentModel?.elements))
|
|
285
|
+
return documentModel.elements
|
|
286
|
+
if (Array.isArray(documentModel?.circuitJson)) {
|
|
287
|
+
return documentModel.circuitJson
|
|
288
|
+
}
|
|
289
|
+
return []
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Normalizes min/max bounds.
|
|
294
|
+
* @param {object | null | undefined} bounds Bounds candidate.
|
|
295
|
+
* @returns {object | null}
|
|
296
|
+
*/
|
|
297
|
+
static #normalizeBounds(bounds) {
|
|
298
|
+
if (!bounds) return null
|
|
299
|
+
const minX = Number(bounds.minX ?? bounds.x)
|
|
300
|
+
const minY = Number(bounds.minY ?? bounds.y)
|
|
301
|
+
const width = Number(bounds.width)
|
|
302
|
+
const height = Number(bounds.height)
|
|
303
|
+
const maxX = Number(bounds.maxX ?? minX + width)
|
|
304
|
+
const maxY = Number(bounds.maxY ?? minY + height)
|
|
305
|
+
if (
|
|
306
|
+
![minX, minY, maxX, maxY].every((value) => Number.isFinite(value))
|
|
307
|
+
) {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
minX,
|
|
312
|
+
minY,
|
|
313
|
+
maxX,
|
|
314
|
+
maxY,
|
|
315
|
+
width: maxX - minX,
|
|
316
|
+
height: maxY - minY
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Builds compact bounds around a diagnostic point.
|
|
322
|
+
* @param {object | null | undefined} point Point candidate.
|
|
323
|
+
* @returns {object | null}
|
|
324
|
+
*/
|
|
325
|
+
static #pointBounds(point) {
|
|
326
|
+
const normalized = CircuitJsonUnits.optionalPoint(point)
|
|
327
|
+
return normalized
|
|
328
|
+
? PcbDiagnosticFocusModel.#centerBounds(normalized, 0.8, 0.8)
|
|
329
|
+
: null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Builds center-size bounds.
|
|
334
|
+
* @param {{ x: number, y: number }} center Center point.
|
|
335
|
+
* @param {number} width Width.
|
|
336
|
+
* @param {number} height Height.
|
|
337
|
+
* @returns {object}
|
|
338
|
+
*/
|
|
339
|
+
static #centerBounds(center, width, height) {
|
|
340
|
+
const minX = Number(center.x) - width / 2
|
|
341
|
+
const minY = Number(center.y) - height / 2
|
|
342
|
+
return {
|
|
343
|
+
minX,
|
|
344
|
+
minY,
|
|
345
|
+
maxX: minX + width,
|
|
346
|
+
maxY: minY + height,
|
|
347
|
+
width,
|
|
348
|
+
height
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Merges min/max bounds rows.
|
|
354
|
+
* @param {object[]} boundsRows Bounds rows.
|
|
355
|
+
* @returns {object | null}
|
|
356
|
+
*/
|
|
357
|
+
static #mergeBounds(boundsRows) {
|
|
358
|
+
const rows = boundsRows
|
|
359
|
+
.map((bounds) => PcbDiagnosticFocusModel.#normalizeBounds(bounds))
|
|
360
|
+
.filter(Boolean)
|
|
361
|
+
if (!rows.length) return null
|
|
362
|
+
|
|
363
|
+
const minX = Math.min(...rows.map((bounds) => bounds.minX))
|
|
364
|
+
const minY = Math.min(...rows.map((bounds) => bounds.minY))
|
|
365
|
+
const maxX = Math.max(...rows.map((bounds) => bounds.maxX))
|
|
366
|
+
const maxY = Math.max(...rows.map((bounds) => bounds.maxY))
|
|
367
|
+
return {
|
|
368
|
+
minX,
|
|
369
|
+
minY,
|
|
370
|
+
maxX,
|
|
371
|
+
maxY,
|
|
372
|
+
width: maxX - minX,
|
|
373
|
+
height: maxY - minY
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Resolves a bounds center.
|
|
379
|
+
* @param {object} bounds Bounds row.
|
|
380
|
+
* @returns {{ x: number, y: number }}
|
|
381
|
+
*/
|
|
382
|
+
static #boundsCenter(bounds) {
|
|
383
|
+
return {
|
|
384
|
+
x: bounds.minX + bounds.width / 2,
|
|
385
|
+
y: bounds.minY + bounds.height / 2
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Formats bounds for viewport controllers.
|
|
391
|
+
* @param {object} bounds Bounds row.
|
|
392
|
+
* @returns {{ x: number, y: number, width: number, height: number }}
|
|
393
|
+
*/
|
|
394
|
+
static #viewportBounds(bounds) {
|
|
395
|
+
return {
|
|
396
|
+
x: PcbDiagnosticFocusModel.#rounded(bounds.minX),
|
|
397
|
+
y: PcbDiagnosticFocusModel.#rounded(bounds.minY),
|
|
398
|
+
width: PcbDiagnosticFocusModel.#rounded(bounds.width),
|
|
399
|
+
height: PcbDiagnosticFocusModel.#rounded(bounds.height)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Rounds one point.
|
|
405
|
+
* @param {{ x: number, y: number }} point Point row.
|
|
406
|
+
* @returns {{ x: number, y: number }}
|
|
407
|
+
*/
|
|
408
|
+
static #roundPoint(point) {
|
|
409
|
+
return {
|
|
410
|
+
x: PcbDiagnosticFocusModel.#rounded(point.x),
|
|
411
|
+
y: PcbDiagnosticFocusModel.#rounded(point.y)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Rounds a numeric value for stable comparisons.
|
|
417
|
+
* @param {number} value Numeric value.
|
|
418
|
+
* @returns {number}
|
|
419
|
+
*/
|
|
420
|
+
static #rounded(value) {
|
|
421
|
+
return Number(Number(value).toFixed(6))
|
|
422
|
+
}
|
|
423
|
+
}
|