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,895 @@
|
|
|
1
|
+
import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
|
|
2
|
+
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
3
|
+
import { CircuitJsonPcbClearanceDiagnostics } from './CircuitJsonPcbClearanceDiagnostics.mjs'
|
|
4
|
+
|
|
5
|
+
const VIRTUAL_LAYER_ORDER = [
|
|
6
|
+
'top_silkscreen',
|
|
7
|
+
'bottom_silkscreen',
|
|
8
|
+
'top_fabrication',
|
|
9
|
+
'bottom_fabrication',
|
|
10
|
+
'top_courtyard',
|
|
11
|
+
'bottom_courtyard',
|
|
12
|
+
'top_soldermask',
|
|
13
|
+
'bottom_soldermask',
|
|
14
|
+
'top_paste',
|
|
15
|
+
'bottom_paste',
|
|
16
|
+
'keepouts',
|
|
17
|
+
'cutouts',
|
|
18
|
+
'diagnostics',
|
|
19
|
+
'groups',
|
|
20
|
+
'anchor_offsets',
|
|
21
|
+
'trace_lengths',
|
|
22
|
+
'ratsnest'
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds non-geometric inspection overlays for CircuitJSON PCB primitives.
|
|
27
|
+
*/
|
|
28
|
+
export class CircuitJsonPcbPrimitiveOverlays {
|
|
29
|
+
/**
|
|
30
|
+
* Builds virtual layers, diagnostics, and airwires.
|
|
31
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
32
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
33
|
+
* @param {object[]} primitives Primitive rows.
|
|
34
|
+
* @param {object} bounds Board bounds.
|
|
35
|
+
* @param {{ groups?: object[], anchorOffsets?: object[] }} [groupModel] Group overlay rows.
|
|
36
|
+
* @param {object[]} [extraDiagnostics] Precomputed diagnostic rows.
|
|
37
|
+
* @returns {{ virtualLayers: object[], diagnostics: object[], airwires: object[] }}
|
|
38
|
+
*/
|
|
39
|
+
static build(
|
|
40
|
+
index,
|
|
41
|
+
componentsByPcbId,
|
|
42
|
+
primitives,
|
|
43
|
+
bounds,
|
|
44
|
+
groupModel = {},
|
|
45
|
+
extraDiagnostics = []
|
|
46
|
+
) {
|
|
47
|
+
const ports = CircuitJsonPcbPrimitiveOverlays.#ports(index)
|
|
48
|
+
const diagnostics = [
|
|
49
|
+
...CircuitJsonPcbPrimitiveOverlays.#diagnostics(
|
|
50
|
+
index,
|
|
51
|
+
componentsByPcbId,
|
|
52
|
+
bounds,
|
|
53
|
+
primitives
|
|
54
|
+
),
|
|
55
|
+
...CircuitJsonPcbPrimitiveOverlays.#extraDiagnostics(
|
|
56
|
+
extraDiagnostics,
|
|
57
|
+
bounds,
|
|
58
|
+
index,
|
|
59
|
+
primitives
|
|
60
|
+
),
|
|
61
|
+
...CircuitJsonPcbPrimitiveOverlays.#clearanceDiagnostics(
|
|
62
|
+
index,
|
|
63
|
+
primitives
|
|
64
|
+
)
|
|
65
|
+
]
|
|
66
|
+
const airwires = CircuitJsonPcbPrimitiveOverlays.#airwires(ports)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
virtualLayers: CircuitJsonPcbPrimitiveOverlays.#virtualLayers({
|
|
70
|
+
primitives,
|
|
71
|
+
diagnostics,
|
|
72
|
+
airwires,
|
|
73
|
+
groups: groupModel.groups || [],
|
|
74
|
+
anchorOffsets: groupModel.anchorOffsets || []
|
|
75
|
+
}),
|
|
76
|
+
diagnostics,
|
|
77
|
+
airwires
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Builds source-net-aware PCB port rows.
|
|
83
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
84
|
+
* @returns {object[]}
|
|
85
|
+
*/
|
|
86
|
+
static #ports(index) {
|
|
87
|
+
const sourceNetNames =
|
|
88
|
+
CircuitJsonPcbPrimitiveOverlays.#sourceNetNames(index)
|
|
89
|
+
const sourcePortNetNames =
|
|
90
|
+
CircuitJsonPcbPrimitiveOverlays.#sourcePortNetNames(
|
|
91
|
+
index,
|
|
92
|
+
sourceNetNames
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return CircuitJsonPcbPrimitiveOverlays.#all(index, 'pcb_port')
|
|
96
|
+
.map((port) => {
|
|
97
|
+
const point = CircuitJsonPcbPrimitiveOverlays.#center(port)
|
|
98
|
+
if (!point) return null
|
|
99
|
+
const sourcePortId = String(port.source_port_id || '').trim()
|
|
100
|
+
return {
|
|
101
|
+
id: String(port.pcb_port_id || '').trim(),
|
|
102
|
+
netName:
|
|
103
|
+
CircuitJsonPcbPrimitiveOverlays.#netName(port) ||
|
|
104
|
+
sourcePortNetNames.get(sourcePortId) ||
|
|
105
|
+
'',
|
|
106
|
+
point
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Builds source net display names.
|
|
114
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
115
|
+
* @returns {Map<string, string>}
|
|
116
|
+
*/
|
|
117
|
+
static #sourceNetNames(index) {
|
|
118
|
+
return new Map(
|
|
119
|
+
CircuitJsonPcbPrimitiveOverlays.#all(index, 'source_net')
|
|
120
|
+
.map((net) => [
|
|
121
|
+
String(net.source_net_id || '').trim(),
|
|
122
|
+
String(
|
|
123
|
+
net.name || net.net || net.source_net_id || ''
|
|
124
|
+
).trim()
|
|
125
|
+
])
|
|
126
|
+
.filter(([id, name]) => id && name)
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Builds source-port to source-net display-name lookup data.
|
|
132
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
133
|
+
* @param {Map<string, string>} sourceNetNames Source net names.
|
|
134
|
+
* @returns {Map<string, string>}
|
|
135
|
+
*/
|
|
136
|
+
static #sourcePortNetNames(index, sourceNetNames) {
|
|
137
|
+
const names = new Map()
|
|
138
|
+
for (const port of CircuitJsonPcbPrimitiveOverlays.#all(
|
|
139
|
+
index,
|
|
140
|
+
'source_port'
|
|
141
|
+
)) {
|
|
142
|
+
const portId = String(port.source_port_id || '').trim()
|
|
143
|
+
if (!portId) continue
|
|
144
|
+
const netId = CircuitJsonPcbPrimitiveOverlays.#firstString([
|
|
145
|
+
port.source_net_id,
|
|
146
|
+
...(Array.isArray(port.source_net_ids)
|
|
147
|
+
? port.source_net_ids
|
|
148
|
+
: []),
|
|
149
|
+
...(Array.isArray(port.connected_source_net_ids)
|
|
150
|
+
? port.connected_source_net_ids
|
|
151
|
+
: [])
|
|
152
|
+
])
|
|
153
|
+
const name =
|
|
154
|
+
sourceNetNames.get(netId) ||
|
|
155
|
+
CircuitJsonPcbPrimitiveOverlays.#netName(port)
|
|
156
|
+
if (name) names.set(portId, name)
|
|
157
|
+
}
|
|
158
|
+
return names
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Builds diagnostic marker rows.
|
|
163
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
164
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
165
|
+
* @param {object} bounds Board bounds.
|
|
166
|
+
* @param {object[]} primitives Primitive rows.
|
|
167
|
+
* @returns {object[]}
|
|
168
|
+
*/
|
|
169
|
+
static #diagnostics(index, componentsByPcbId, bounds, primitives) {
|
|
170
|
+
return CircuitJsonPcbPrimitiveOverlays.#elements(index)
|
|
171
|
+
.filter((element) => {
|
|
172
|
+
const type = String(element?.type || '').toLowerCase()
|
|
173
|
+
return type.includes('error') || type.includes('warning')
|
|
174
|
+
})
|
|
175
|
+
.map((element, rowIndex) =>
|
|
176
|
+
CircuitJsonPcbPrimitiveOverlays.#diagnostic(
|
|
177
|
+
element,
|
|
178
|
+
index,
|
|
179
|
+
rowIndex,
|
|
180
|
+
componentsByPcbId,
|
|
181
|
+
bounds,
|
|
182
|
+
primitives
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Builds one diagnostic marker.
|
|
189
|
+
* @param {object} element Diagnostic-like element.
|
|
190
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
191
|
+
* @param {number} rowIndex Diagnostic row index.
|
|
192
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
193
|
+
* @param {object} bounds Board bounds.
|
|
194
|
+
* @param {object[]} primitives Primitive rows.
|
|
195
|
+
* @returns {object}
|
|
196
|
+
*/
|
|
197
|
+
static #diagnostic(
|
|
198
|
+
element,
|
|
199
|
+
index,
|
|
200
|
+
rowIndex,
|
|
201
|
+
componentsByPcbId,
|
|
202
|
+
bounds,
|
|
203
|
+
primitives
|
|
204
|
+
) {
|
|
205
|
+
const component =
|
|
206
|
+
componentsByPcbId.get(
|
|
207
|
+
String(element.pcb_component_id || '').trim()
|
|
208
|
+
) ||
|
|
209
|
+
CircuitJsonPcbPrimitiveOverlays.#sourceComponent(
|
|
210
|
+
element,
|
|
211
|
+
componentsByPcbId
|
|
212
|
+
)
|
|
213
|
+
const relatedPrimitives =
|
|
214
|
+
CircuitJsonPcbPrimitiveOverlays.#relatedPrimitives(
|
|
215
|
+
element,
|
|
216
|
+
index,
|
|
217
|
+
primitives
|
|
218
|
+
)
|
|
219
|
+
const relatedBounds = CircuitJsonPcbPrimitiveOverlays.#mergeBounds(
|
|
220
|
+
relatedPrimitives.map((primitive) => primitive.bounds)
|
|
221
|
+
)
|
|
222
|
+
const point = CircuitJsonPcbPrimitiveOverlays.#center(element) ||
|
|
223
|
+
(relatedBounds
|
|
224
|
+
? CircuitJsonPcbPrimitiveOverlays.#boundsCenter(relatedBounds)
|
|
225
|
+
: null) ||
|
|
226
|
+
(component ? { x: component.x, y: component.y } : null) || {
|
|
227
|
+
x: (bounds.minX + bounds.maxX) / 2,
|
|
228
|
+
y: (bounds.minY + bounds.maxY) / 2
|
|
229
|
+
}
|
|
230
|
+
const severity = String(element.severity || '').toLowerCase()
|
|
231
|
+
const type = String(element.type || '').toLowerCase()
|
|
232
|
+
const isWarning = severity === 'warning' || type.includes('warning')
|
|
233
|
+
|
|
234
|
+
const code = String(
|
|
235
|
+
element.error_type ||
|
|
236
|
+
element.warning_type ||
|
|
237
|
+
element.code ||
|
|
238
|
+
element.type ||
|
|
239
|
+
''
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const diagnostic = {
|
|
243
|
+
id:
|
|
244
|
+
CircuitJsonIndexer.getElementId(element) ||
|
|
245
|
+
'diagnostic:' + rowIndex,
|
|
246
|
+
kind: isWarning ? 'warning' : 'error',
|
|
247
|
+
severity: isWarning ? 'warning' : 'error',
|
|
248
|
+
category: CircuitJsonPcbPrimitiveOverlays.#diagnosticCategory(code),
|
|
249
|
+
code,
|
|
250
|
+
message: String(element.message || 'PCB diagnostic.'),
|
|
251
|
+
point,
|
|
252
|
+
componentKey: String(
|
|
253
|
+
component?.componentKey ||
|
|
254
|
+
relatedPrimitives.find(
|
|
255
|
+
(primitive) => primitive.componentKey
|
|
256
|
+
)?.componentKey ||
|
|
257
|
+
''
|
|
258
|
+
),
|
|
259
|
+
netName:
|
|
260
|
+
CircuitJsonPcbPrimitiveOverlays.#netName(element) ||
|
|
261
|
+
relatedPrimitives.find((primitive) => primitive.netName)
|
|
262
|
+
?.netName ||
|
|
263
|
+
''
|
|
264
|
+
}
|
|
265
|
+
if (relatedPrimitives.length) {
|
|
266
|
+
diagnostic.bounds = relatedBounds
|
|
267
|
+
diagnostic.relatedPrimitiveIds = relatedPrimitives
|
|
268
|
+
.map((primitive) => primitive.id)
|
|
269
|
+
.filter(Boolean)
|
|
270
|
+
}
|
|
271
|
+
return diagnostic
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Applies defaults to precomputed diagnostic rows.
|
|
276
|
+
* @param {object[]} diagnostics Diagnostic rows.
|
|
277
|
+
* @param {object} bounds Board bounds.
|
|
278
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
279
|
+
* @param {object[]} primitives Primitive rows.
|
|
280
|
+
* @returns {object[]}
|
|
281
|
+
*/
|
|
282
|
+
static #extraDiagnostics(diagnostics, bounds, index, primitives) {
|
|
283
|
+
return (diagnostics || []).map((diagnostic, rowIndex) => {
|
|
284
|
+
const relatedPrimitives =
|
|
285
|
+
CircuitJsonPcbPrimitiveOverlays.#relatedPrimitivesForDiagnostic(
|
|
286
|
+
diagnostic,
|
|
287
|
+
index,
|
|
288
|
+
primitives
|
|
289
|
+
)
|
|
290
|
+
const relatedBounds =
|
|
291
|
+
diagnostic.bounds ||
|
|
292
|
+
CircuitJsonPcbPrimitiveOverlays.#mergeBounds(
|
|
293
|
+
relatedPrimitives.map((primitive) => primitive.bounds)
|
|
294
|
+
)
|
|
295
|
+
return {
|
|
296
|
+
id: String(
|
|
297
|
+
diagnostic.id ||
|
|
298
|
+
diagnostic.elementId ||
|
|
299
|
+
'diagnostic:extra:' + rowIndex
|
|
300
|
+
),
|
|
301
|
+
kind: String(diagnostic.kind || 'warning'),
|
|
302
|
+
severity: String(diagnostic.severity || 'warning'),
|
|
303
|
+
category: String(diagnostic.category || 'general'),
|
|
304
|
+
code: String(
|
|
305
|
+
diagnostic.code ||
|
|
306
|
+
diagnostic.type ||
|
|
307
|
+
diagnostic.warningType ||
|
|
308
|
+
diagnostic.errorType ||
|
|
309
|
+
'pcb_diagnostic'
|
|
310
|
+
),
|
|
311
|
+
message: String(diagnostic.message || 'PCB diagnostic.'),
|
|
312
|
+
point:
|
|
313
|
+
diagnostic.point ||
|
|
314
|
+
(relatedBounds
|
|
315
|
+
? CircuitJsonPcbPrimitiveOverlays.#boundsCenter(
|
|
316
|
+
relatedBounds
|
|
317
|
+
)
|
|
318
|
+
: CircuitJsonPcbPrimitiveOverlays.#boundsCenter(
|
|
319
|
+
bounds
|
|
320
|
+
)),
|
|
321
|
+
bounds: relatedBounds || null,
|
|
322
|
+
relatedPrimitiveIds:
|
|
323
|
+
CircuitJsonPcbPrimitiveOverlays.#relatedPrimitiveIds(
|
|
324
|
+
diagnostic,
|
|
325
|
+
relatedPrimitives
|
|
326
|
+
),
|
|
327
|
+
componentKey: String(diagnostic.componentKey || ''),
|
|
328
|
+
netName: String(diagnostic.netName || ''),
|
|
329
|
+
...CircuitJsonPcbPrimitiveOverlays.#diagnosticRelationFields(
|
|
330
|
+
diagnostic
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Finds primitives related to a normalized diagnostic row.
|
|
338
|
+
* @param {object} diagnostic Diagnostic row.
|
|
339
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
340
|
+
* @param {object[]} primitives Primitive rows.
|
|
341
|
+
* @returns {object[]}
|
|
342
|
+
*/
|
|
343
|
+
static #relatedPrimitivesForDiagnostic(diagnostic, index, primitives) {
|
|
344
|
+
const explicitIds = CircuitJsonPcbPrimitiveOverlays.#idValues([
|
|
345
|
+
diagnostic?.relatedPrimitiveIds,
|
|
346
|
+
diagnostic?.related_primitive_ids
|
|
347
|
+
])
|
|
348
|
+
const explicit = primitives.filter((primitive) =>
|
|
349
|
+
explicitIds.includes(String(primitive.id || '').trim())
|
|
350
|
+
)
|
|
351
|
+
const direct = CircuitJsonPcbPrimitiveOverlays.#matchingPrimitives(
|
|
352
|
+
CircuitJsonPcbPrimitiveOverlays.#relatedPrimitiveIdPairs({
|
|
353
|
+
pcb_trace_id:
|
|
354
|
+
diagnostic?.pcbTraceId || diagnostic?.pcb_trace_id,
|
|
355
|
+
pcb_trace_ids:
|
|
356
|
+
diagnostic?.pcbTraceIds || diagnostic?.pcb_trace_ids,
|
|
357
|
+
pcb_smtpad_id:
|
|
358
|
+
diagnostic?.pcbSmtpadId || diagnostic?.pcb_smtpad_id,
|
|
359
|
+
pcb_smtpad_ids:
|
|
360
|
+
diagnostic?.pcbSmtpadIds || diagnostic?.pcb_smtpad_ids,
|
|
361
|
+
pcb_via_id: diagnostic?.pcbViaId || diagnostic?.pcb_via_id,
|
|
362
|
+
pcb_via_ids: diagnostic?.pcbViaIds || diagnostic?.pcb_via_ids,
|
|
363
|
+
pcb_plated_hole_id:
|
|
364
|
+
diagnostic?.pcbPlatedHoleId ||
|
|
365
|
+
diagnostic?.pcb_plated_hole_id,
|
|
366
|
+
pcb_plated_hole_ids:
|
|
367
|
+
diagnostic?.pcbPlatedHoleIds ||
|
|
368
|
+
diagnostic?.pcb_plated_hole_ids,
|
|
369
|
+
pcb_hole_id: diagnostic?.pcbHoleId || diagnostic?.pcb_hole_id,
|
|
370
|
+
pcb_hole_ids:
|
|
371
|
+
diagnostic?.pcbHoleIds || diagnostic?.pcb_hole_ids,
|
|
372
|
+
pcb_port_id: diagnostic?.pcbPortId || diagnostic?.pcb_port_id,
|
|
373
|
+
pcb_port_ids: diagnostic?.pcbPortIds || diagnostic?.pcb_port_ids
|
|
374
|
+
}),
|
|
375
|
+
primitives
|
|
376
|
+
)
|
|
377
|
+
const sourceTrace =
|
|
378
|
+
CircuitJsonPcbPrimitiveOverlays.#sourceTracePrimitives(
|
|
379
|
+
diagnostic,
|
|
380
|
+
primitives
|
|
381
|
+
)
|
|
382
|
+
const sourcePortIds =
|
|
383
|
+
CircuitJsonPcbPrimitiveOverlays.#pcbPortIdsForSourcePort(
|
|
384
|
+
index,
|
|
385
|
+
diagnostic?.sourcePortId || diagnostic?.source_port_id
|
|
386
|
+
)
|
|
387
|
+
const sourcePort = CircuitJsonPcbPrimitiveOverlays.#matchingPrimitives(
|
|
388
|
+
sourcePortIds.map((id) => ['pcb_port_id', id]),
|
|
389
|
+
primitives
|
|
390
|
+
)
|
|
391
|
+
return CircuitJsonPcbPrimitiveOverlays.#uniquePrimitives([
|
|
392
|
+
...explicit,
|
|
393
|
+
...direct,
|
|
394
|
+
...sourceTrace,
|
|
395
|
+
...sourcePort
|
|
396
|
+
])
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Finds primitives that came from one source trace id.
|
|
401
|
+
* @param {object} diagnostic Diagnostic row.
|
|
402
|
+
* @param {object[]} primitives Primitive rows.
|
|
403
|
+
* @returns {object[]}
|
|
404
|
+
*/
|
|
405
|
+
static #sourceTracePrimitives(diagnostic, primitives) {
|
|
406
|
+
const sourceTraceIds = CircuitJsonPcbPrimitiveOverlays.#idValues([
|
|
407
|
+
diagnostic?.sourceTraceId,
|
|
408
|
+
diagnostic?.source_trace_id,
|
|
409
|
+
diagnostic?.sourceTraceIds,
|
|
410
|
+
diagnostic?.source_trace_ids
|
|
411
|
+
])
|
|
412
|
+
if (!sourceTraceIds.length) return []
|
|
413
|
+
return primitives.filter((primitive) =>
|
|
414
|
+
sourceTraceIds.includes(
|
|
415
|
+
String(
|
|
416
|
+
primitive.sourceTraceId ||
|
|
417
|
+
primitive.source?.source_trace_id ||
|
|
418
|
+
''
|
|
419
|
+
).trim()
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Builds related primitive ids for a diagnostic.
|
|
426
|
+
* @param {object} diagnostic Diagnostic row.
|
|
427
|
+
* @param {object[]} relatedPrimitives Matched primitives.
|
|
428
|
+
* @returns {string[]}
|
|
429
|
+
*/
|
|
430
|
+
static #relatedPrimitiveIds(diagnostic, relatedPrimitives) {
|
|
431
|
+
return CircuitJsonPcbPrimitiveOverlays.#idValues([
|
|
432
|
+
diagnostic?.relatedPrimitiveIds,
|
|
433
|
+
diagnostic?.related_primitive_ids,
|
|
434
|
+
relatedPrimitives.map((primitive) => primitive.id)
|
|
435
|
+
])
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Keeps the first instance of each primitive id.
|
|
440
|
+
* @param {object[]} primitives Primitive rows.
|
|
441
|
+
* @returns {object[]}
|
|
442
|
+
*/
|
|
443
|
+
static #uniquePrimitives(primitives) {
|
|
444
|
+
const seen = new Set()
|
|
445
|
+
return primitives.filter((primitive) => {
|
|
446
|
+
const id = String(primitive.id || '').trim()
|
|
447
|
+
if (!id || seen.has(id)) return false
|
|
448
|
+
seen.add(id)
|
|
449
|
+
return true
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Extracts normalized relation fields from a diagnostic row.
|
|
455
|
+
* @param {object} diagnostic Diagnostic row.
|
|
456
|
+
* @returns {object}
|
|
457
|
+
*/
|
|
458
|
+
static #diagnosticRelationFields(diagnostic) {
|
|
459
|
+
return Object.fromEntries(
|
|
460
|
+
[
|
|
461
|
+
['sourceTraceId', diagnostic?.sourceTraceId],
|
|
462
|
+
['sourcePortId', diagnostic?.sourcePortId],
|
|
463
|
+
['sourceNetId', diagnostic?.sourceNetId],
|
|
464
|
+
['pcbTraceId', diagnostic?.pcbTraceId],
|
|
465
|
+
['pcbPortId', diagnostic?.pcbPortId],
|
|
466
|
+
['pcbSmtpadId', diagnostic?.pcbSmtpadId],
|
|
467
|
+
['pcbViaId', diagnostic?.pcbViaId],
|
|
468
|
+
['pcbPlatedHoleId', diagnostic?.pcbPlatedHoleId],
|
|
469
|
+
['pcbHoleId', diagnostic?.pcbHoleId]
|
|
470
|
+
]
|
|
471
|
+
.map(([key, value]) => [key, String(value || '').trim()])
|
|
472
|
+
.filter(([_key, value]) => value)
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Builds generic copper clearance diagnostics when board rules are present.
|
|
478
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
479
|
+
* @param {object[]} primitives Primitive rows.
|
|
480
|
+
* @returns {object[]}
|
|
481
|
+
*/
|
|
482
|
+
static #clearanceDiagnostics(index, primitives) {
|
|
483
|
+
return CircuitJsonPcbClearanceDiagnostics.build(index, primitives)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Builds simple source connectivity airwires.
|
|
488
|
+
* @param {object[]} ports PCB port rows.
|
|
489
|
+
* @returns {object[]}
|
|
490
|
+
*/
|
|
491
|
+
static #airwires(ports) {
|
|
492
|
+
const byNet = new Map()
|
|
493
|
+
for (const port of ports) {
|
|
494
|
+
const netName = String(port.netName || '').trim()
|
|
495
|
+
if (!netName) continue
|
|
496
|
+
if (!byNet.has(netName)) byNet.set(netName, [])
|
|
497
|
+
byNet.get(netName).push(port)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const lines = []
|
|
501
|
+
for (const [netName, netPorts] of byNet) {
|
|
502
|
+
const sorted = [...netPorts].sort((left, right) =>
|
|
503
|
+
String(left.id).localeCompare(String(right.id))
|
|
504
|
+
)
|
|
505
|
+
for (let index = 1; index < sorted.length; index += 1) {
|
|
506
|
+
lines.push({
|
|
507
|
+
id: 'airwire:' + netName + ':' + (index - 1),
|
|
508
|
+
netName,
|
|
509
|
+
start: { ...sorted[0].point },
|
|
510
|
+
end: { ...sorted[index].point }
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return lines
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Builds virtual layer rows for detail and overlay primitives.
|
|
519
|
+
* @param {{ primitives: object[], diagnostics: object[], airwires: object[], groups?: object[], anchorOffsets?: object[] }} model Model fragments.
|
|
520
|
+
* @returns {object[]}
|
|
521
|
+
*/
|
|
522
|
+
static #virtualLayers(model) {
|
|
523
|
+
const keys = new Set()
|
|
524
|
+
for (const primitive of model.primitives) {
|
|
525
|
+
if (
|
|
526
|
+
['silkscreen', 'silkscreen_text', 'silkscreen_line'].includes(
|
|
527
|
+
primitive.kind
|
|
528
|
+
)
|
|
529
|
+
) {
|
|
530
|
+
keys.add(primitive.layer)
|
|
531
|
+
}
|
|
532
|
+
if (primitive.kind === 'fabrication') keys.add(primitive.layer)
|
|
533
|
+
if (primitive.kind === 'courtyard') keys.add(primitive.layer)
|
|
534
|
+
if (primitive.kind === 'solder-mask') keys.add(primitive.layer)
|
|
535
|
+
if (primitive.kind === 'solder-paste') keys.add(primitive.layer)
|
|
536
|
+
if (primitive.kind === 'keepout') keys.add('keepouts')
|
|
537
|
+
if (primitive.kind === 'cutout') keys.add('cutouts')
|
|
538
|
+
if (primitive.kind === 'track') keys.add('trace_lengths')
|
|
539
|
+
if (String(primitive.netName || '').trim()) keys.add('ratsnest')
|
|
540
|
+
}
|
|
541
|
+
if (model.diagnostics.length) keys.add('diagnostics')
|
|
542
|
+
if (model.groups?.length) keys.add('groups')
|
|
543
|
+
if (model.anchorOffsets?.length) keys.add('anchor_offsets')
|
|
544
|
+
if (model.airwires.length) keys.add('ratsnest')
|
|
545
|
+
|
|
546
|
+
return VIRTUAL_LAYER_ORDER.filter((key) => keys.has(key)).map(
|
|
547
|
+
(key) => ({
|
|
548
|
+
key,
|
|
549
|
+
id: key,
|
|
550
|
+
layer: key,
|
|
551
|
+
name: CircuitJsonPcbPrimitiveOverlays.#displayLayerName(key),
|
|
552
|
+
side: CircuitJsonPcbPrimitiveOverlays.#side(key),
|
|
553
|
+
type: 'drawing',
|
|
554
|
+
sourceFormat: 'circuitjson'
|
|
555
|
+
})
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Returns indexed element rows by type.
|
|
561
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
562
|
+
* @param {string} type Element type.
|
|
563
|
+
* @returns {object[]}
|
|
564
|
+
*/
|
|
565
|
+
static #all(index, type) {
|
|
566
|
+
return index.elementsByType.get(type) || []
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Returns all indexed element rows.
|
|
571
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
572
|
+
* @returns {object[]}
|
|
573
|
+
*/
|
|
574
|
+
static #elements(index) {
|
|
575
|
+
return Array.from(index.elementsByType.values()).flat()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Finds primitives referenced by a diagnostic element.
|
|
580
|
+
* @param {object} element Diagnostic element.
|
|
581
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
582
|
+
* @param {object[]} primitives Primitive rows.
|
|
583
|
+
* @returns {object[]}
|
|
584
|
+
*/
|
|
585
|
+
static #relatedPrimitives(element, index, primitives) {
|
|
586
|
+
const directIds =
|
|
587
|
+
CircuitJsonPcbPrimitiveOverlays.#relatedPrimitiveIdPairs(element)
|
|
588
|
+
const direct = CircuitJsonPcbPrimitiveOverlays.#matchingPrimitives(
|
|
589
|
+
directIds,
|
|
590
|
+
primitives
|
|
591
|
+
)
|
|
592
|
+
if (direct.length) return direct
|
|
593
|
+
|
|
594
|
+
const portIds =
|
|
595
|
+
CircuitJsonPcbPrimitiveOverlays.#pcbPortIdsForSourcePort(
|
|
596
|
+
index,
|
|
597
|
+
element?.source_port_id
|
|
598
|
+
)
|
|
599
|
+
const sourcePort = CircuitJsonPcbPrimitiveOverlays.#matchingPrimitives(
|
|
600
|
+
portIds.map((id) => ['pcb_port_id', id]),
|
|
601
|
+
primitives
|
|
602
|
+
)
|
|
603
|
+
if (sourcePort.length) return sourcePort
|
|
604
|
+
|
|
605
|
+
const sourceComponentId = String(
|
|
606
|
+
element?.source_component_id || ''
|
|
607
|
+
).trim()
|
|
608
|
+
if (sourceComponentId) {
|
|
609
|
+
const sourceComponent = primitives.filter(
|
|
610
|
+
(primitive) =>
|
|
611
|
+
String(primitive.sourceComponentId || '').trim() ===
|
|
612
|
+
sourceComponentId &&
|
|
613
|
+
CircuitJsonPcbPrimitiveOverlays.#isDiagnosticPrimitive(
|
|
614
|
+
primitive
|
|
615
|
+
)
|
|
616
|
+
)
|
|
617
|
+
if (sourceComponent.length) return sourceComponent
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const componentIds = CircuitJsonPcbPrimitiveOverlays.#idValues([
|
|
621
|
+
element?.pcb_component_id,
|
|
622
|
+
element?.pcb_component_ids
|
|
623
|
+
])
|
|
624
|
+
if (componentIds.length) {
|
|
625
|
+
return primitives.filter(
|
|
626
|
+
(primitive) =>
|
|
627
|
+
componentIds.includes(
|
|
628
|
+
String(primitive.componentId || '').trim()
|
|
629
|
+
) &&
|
|
630
|
+
CircuitJsonPcbPrimitiveOverlays.#isDiagnosticPrimitive(
|
|
631
|
+
primitive
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return []
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Builds source field/value pairs for directly related primitives.
|
|
641
|
+
* @param {object} element Diagnostic row.
|
|
642
|
+
* @returns {Array<[string, string]>}
|
|
643
|
+
*/
|
|
644
|
+
static #relatedPrimitiveIdPairs(element) {
|
|
645
|
+
return [
|
|
646
|
+
['pcb_trace_id', [element?.pcb_trace_id, element?.pcb_trace_ids]],
|
|
647
|
+
[
|
|
648
|
+
'pcb_smtpad_id',
|
|
649
|
+
[element?.pcb_smtpad_id, element?.pcb_smtpad_ids]
|
|
650
|
+
],
|
|
651
|
+
['pcb_via_id', [element?.pcb_via_id, element?.pcb_via_ids]],
|
|
652
|
+
[
|
|
653
|
+
'pcb_plated_hole_id',
|
|
654
|
+
[element?.pcb_plated_hole_id, element?.pcb_plated_hole_ids]
|
|
655
|
+
],
|
|
656
|
+
['pcb_hole_id', [element?.pcb_hole_id, element?.pcb_hole_ids]],
|
|
657
|
+
['pcb_port_id', [element?.pcb_port_id, element?.pcb_port_ids]]
|
|
658
|
+
].flatMap(([field, values]) =>
|
|
659
|
+
CircuitJsonPcbPrimitiveOverlays.#idValues(values).map((value) => [
|
|
660
|
+
field,
|
|
661
|
+
value
|
|
662
|
+
])
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Normalizes scalar or array ID values.
|
|
668
|
+
* @param {unknown[]} values Candidate values.
|
|
669
|
+
* @returns {string[]}
|
|
670
|
+
*/
|
|
671
|
+
static #idValues(values) {
|
|
672
|
+
return [
|
|
673
|
+
...new Set(
|
|
674
|
+
values
|
|
675
|
+
.flatMap((value) =>
|
|
676
|
+
Array.isArray(value) ? value : [value]
|
|
677
|
+
)
|
|
678
|
+
.map((value) => String(value || '').trim())
|
|
679
|
+
.filter(Boolean)
|
|
680
|
+
)
|
|
681
|
+
]
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Returns true when a component-owned primitive should shape diagnostics.
|
|
686
|
+
* @param {object} primitive Primitive row.
|
|
687
|
+
* @returns {boolean}
|
|
688
|
+
*/
|
|
689
|
+
static #isDiagnosticPrimitive(primitive) {
|
|
690
|
+
return [
|
|
691
|
+
'pad',
|
|
692
|
+
'track',
|
|
693
|
+
'via',
|
|
694
|
+
'zone',
|
|
695
|
+
'solder-mask',
|
|
696
|
+
'solder-paste'
|
|
697
|
+
].includes(primitive.kind)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Finds primitives matching source id fields.
|
|
702
|
+
* @param {Array<[string, unknown]>} ids Field/value pairs.
|
|
703
|
+
* @param {object[]} primitives Primitive rows.
|
|
704
|
+
* @returns {object[]}
|
|
705
|
+
*/
|
|
706
|
+
static #matchingPrimitives(ids, primitives) {
|
|
707
|
+
if (!ids.length) return []
|
|
708
|
+
return primitives.filter((primitive) =>
|
|
709
|
+
ids.some(
|
|
710
|
+
([field, value]) =>
|
|
711
|
+
String(primitive.source?.[field] || '').trim() ===
|
|
712
|
+
String(value || '').trim()
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Resolves PCB port ids linked to one source port.
|
|
719
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
720
|
+
* @param {unknown} sourcePortId Source port id.
|
|
721
|
+
* @returns {string[]}
|
|
722
|
+
*/
|
|
723
|
+
static #pcbPortIdsForSourcePort(index, sourcePortId) {
|
|
724
|
+
const id = String(sourcePortId || '').trim()
|
|
725
|
+
if (!id) return []
|
|
726
|
+
return CircuitJsonPcbPrimitiveOverlays.#all(index, 'pcb_port')
|
|
727
|
+
.filter((port) => String(port.source_port_id || '').trim() === id)
|
|
728
|
+
.map((port) => String(port.pcb_port_id || '').trim())
|
|
729
|
+
.filter(Boolean)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Resolves a PCB component by source component id.
|
|
734
|
+
* @param {object} element Diagnostic element.
|
|
735
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
736
|
+
* @returns {object | undefined}
|
|
737
|
+
*/
|
|
738
|
+
static #sourceComponent(element, componentsByPcbId) {
|
|
739
|
+
const sourceId = String(element?.source_component_id || '').trim()
|
|
740
|
+
if (!sourceId) return undefined
|
|
741
|
+
return [...componentsByPcbId.values()].find(
|
|
742
|
+
(component) =>
|
|
743
|
+
String(component.sourceComponentId || '').trim() === sourceId
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Resolves the center point of bounds.
|
|
749
|
+
* @param {object} bounds Bounds record.
|
|
750
|
+
* @returns {{ x: number, y: number }}
|
|
751
|
+
*/
|
|
752
|
+
static #boundsCenter(bounds) {
|
|
753
|
+
return {
|
|
754
|
+
x: bounds.minX + bounds.width / 2,
|
|
755
|
+
y: bounds.minY + bounds.height / 2
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Merges bounds rows.
|
|
761
|
+
* @param {object[]} rows Bounds rows.
|
|
762
|
+
* @returns {object | null}
|
|
763
|
+
*/
|
|
764
|
+
static #mergeBounds(rows) {
|
|
765
|
+
const validRows = rows.filter(Boolean)
|
|
766
|
+
if (!validRows.length) return null
|
|
767
|
+
const minX = Math.min(...validRows.map((bounds) => bounds.minX))
|
|
768
|
+
const minY = Math.min(...validRows.map((bounds) => bounds.minY))
|
|
769
|
+
const maxX = Math.max(...validRows.map((bounds) => bounds.maxX))
|
|
770
|
+
const maxY = Math.max(...validRows.map((bounds) => bounds.maxY))
|
|
771
|
+
return {
|
|
772
|
+
minX: CircuitJsonPcbPrimitiveOverlays.#round(minX),
|
|
773
|
+
minY: CircuitJsonPcbPrimitiveOverlays.#round(minY),
|
|
774
|
+
maxX: CircuitJsonPcbPrimitiveOverlays.#round(maxX),
|
|
775
|
+
maxY: CircuitJsonPcbPrimitiveOverlays.#round(maxY),
|
|
776
|
+
width: CircuitJsonPcbPrimitiveOverlays.#round(maxX - minX),
|
|
777
|
+
height: CircuitJsonPcbPrimitiveOverlays.#round(maxY - minY)
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Rounds one computed geometry value.
|
|
783
|
+
* @param {number} value Numeric value.
|
|
784
|
+
* @returns {number}
|
|
785
|
+
*/
|
|
786
|
+
static #round(value) {
|
|
787
|
+
return Number(Number(value).toFixed(6))
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Resolves a center point.
|
|
792
|
+
* @param {object} element Element row.
|
|
793
|
+
* @returns {{ x: number, y: number } | null}
|
|
794
|
+
*/
|
|
795
|
+
static #center(element) {
|
|
796
|
+
return CircuitJsonUnits.optionalPoint(element?.center || element)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Resolves a net name from common fields.
|
|
801
|
+
* @param {object} element Element row.
|
|
802
|
+
* @returns {string}
|
|
803
|
+
*/
|
|
804
|
+
static #netName(element) {
|
|
805
|
+
return String(
|
|
806
|
+
element?.netName ??
|
|
807
|
+
element?.net ??
|
|
808
|
+
element?.net_name ??
|
|
809
|
+
element?.source_net_name ??
|
|
810
|
+
''
|
|
811
|
+
).trim()
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Resolves a broad diagnostic category from a diagnostic code.
|
|
816
|
+
* @param {string} code Diagnostic code.
|
|
817
|
+
* @returns {string}
|
|
818
|
+
*/
|
|
819
|
+
static #diagnosticCategory(code) {
|
|
820
|
+
const text = String(code || '').toLowerCase()
|
|
821
|
+
if (text.includes('clearance')) return 'clearance'
|
|
822
|
+
if (text.includes('autorouting') || text.includes('trace_error')) {
|
|
823
|
+
return 'routing'
|
|
824
|
+
}
|
|
825
|
+
if (text.includes('placement') || text.includes('outside_board')) {
|
|
826
|
+
return 'placement'
|
|
827
|
+
}
|
|
828
|
+
if (
|
|
829
|
+
text.includes('trace_missing') ||
|
|
830
|
+
text.includes('missing_trace') ||
|
|
831
|
+
text.includes('not_connected') ||
|
|
832
|
+
text.includes('pin_missing_trace') ||
|
|
833
|
+
text.includes('pin_must_be_connected')
|
|
834
|
+
) {
|
|
835
|
+
return 'connectivity'
|
|
836
|
+
}
|
|
837
|
+
if (text.includes('layout')) return 'layout'
|
|
838
|
+
if (text.includes('simulation')) return 'simulation'
|
|
839
|
+
if (text.includes('footprint')) return 'footprint'
|
|
840
|
+
if (
|
|
841
|
+
text.includes('pin_defined') ||
|
|
842
|
+
text.includes('pins_underspecified') ||
|
|
843
|
+
text.includes('ground_pin') ||
|
|
844
|
+
text.includes('power_pin')
|
|
845
|
+
) {
|
|
846
|
+
return 'pin-definition'
|
|
847
|
+
}
|
|
848
|
+
if (
|
|
849
|
+
text.includes('manufacturer_part') ||
|
|
850
|
+
text.includes('missing_property') ||
|
|
851
|
+
text.includes('property_ignored')
|
|
852
|
+
) {
|
|
853
|
+
return 'metadata'
|
|
854
|
+
}
|
|
855
|
+
if (text.includes('manual_edit_conflict')) return 'edit-conflict'
|
|
856
|
+
if (text.includes('configuration')) return 'configuration'
|
|
857
|
+
return 'general'
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Resolves the first non-empty string.
|
|
862
|
+
* @param {unknown[]} values Candidate values.
|
|
863
|
+
* @returns {string}
|
|
864
|
+
*/
|
|
865
|
+
static #firstString(values) {
|
|
866
|
+
for (const value of values) {
|
|
867
|
+
const text = String(value ?? '').trim()
|
|
868
|
+
if (text) return text
|
|
869
|
+
}
|
|
870
|
+
return ''
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Resolves a side from a layer key.
|
|
875
|
+
* @param {string} layer Layer key.
|
|
876
|
+
* @returns {'top' | 'bottom' | ''}
|
|
877
|
+
*/
|
|
878
|
+
static #side(layer) {
|
|
879
|
+
const text = String(layer || '').toLowerCase()
|
|
880
|
+
if (/\b(bottom|back)\b|\bb[._-]/u.test(text)) return 'bottom'
|
|
881
|
+
if (/\b(top|front)\b|\bf[._-]/u.test(text)) return 'top'
|
|
882
|
+
return ''
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Formats a virtual layer name.
|
|
887
|
+
* @param {string} key Layer key.
|
|
888
|
+
* @returns {string}
|
|
889
|
+
*/
|
|
890
|
+
static #displayLayerName(key) {
|
|
891
|
+
return String(key)
|
|
892
|
+
.replaceAll('_', ' ')
|
|
893
|
+
.replace(/\b\w/gu, (match) => match.toUpperCase())
|
|
894
|
+
}
|
|
895
|
+
}
|