circuitjson-toolkit 1.0.10 → 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/docs/model-format.md +15 -0
- package/package.json +3 -2
- package/src/core/CircuitJsonBomBuilder.mjs +22 -25
- package/src/core/CircuitJsonElementValidator.mjs +233 -16
- package/src/core/CircuitJsonIndexer.mjs +510 -5
- package/src/core/CircuitJsonManufacturingBuilder.mjs +488 -16
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- 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 +227 -5
- 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/index.mjs +2 -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,992 @@
|
|
|
1
|
+
import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
|
|
2
|
+
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
3
|
+
import { CircuitJsonPcbPrimitiveGeometry } from './CircuitJsonPcbPrimitiveGeometry.mjs'
|
|
4
|
+
import { CircuitJsonPcbPrimitiveFields } from './CircuitJsonPcbPrimitiveFields.mjs'
|
|
5
|
+
import { CircuitJsonPcbTraceLengthModel } from './CircuitJsonPcbTraceLengthModel.mjs'
|
|
6
|
+
import { CircuitJsonPcbDrawingStyle } from './CircuitJsonPcbDrawingStyle.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds derived and documentation PCB primitives from CircuitJSON rows.
|
|
10
|
+
*/
|
|
11
|
+
export class CircuitJsonPcbPrimitiveArtwork {
|
|
12
|
+
/**
|
|
13
|
+
* Builds supplemental primitive rows.
|
|
14
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
15
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
16
|
+
* @returns {object[]}
|
|
17
|
+
*/
|
|
18
|
+
static build(index, componentsByPcbId) {
|
|
19
|
+
return [
|
|
20
|
+
...CircuitJsonPcbPrimitiveArtwork.#solderMaskPrimitives(
|
|
21
|
+
index,
|
|
22
|
+
componentsByPcbId
|
|
23
|
+
),
|
|
24
|
+
...CircuitJsonPcbPrimitiveArtwork.#solderPastePrimitives(
|
|
25
|
+
index,
|
|
26
|
+
componentsByPcbId
|
|
27
|
+
),
|
|
28
|
+
...CircuitJsonPcbPrimitiveArtwork.#notePrimitives(
|
|
29
|
+
index,
|
|
30
|
+
componentsByPcbId
|
|
31
|
+
),
|
|
32
|
+
...CircuitJsonPcbPrimitiveArtwork.#silkscreenShapePrimitives(
|
|
33
|
+
index,
|
|
34
|
+
componentsByPcbId
|
|
35
|
+
),
|
|
36
|
+
...CircuitJsonPcbPrimitiveArtwork.#thermalSpokePrimitives(index),
|
|
37
|
+
...CircuitJsonPcbPrimitiveArtwork.#routeHintPrimitives(index),
|
|
38
|
+
...CircuitJsonPcbPrimitiveArtwork.#breakoutPointPrimitives(index),
|
|
39
|
+
...CircuitJsonPcbPrimitiveArtwork.#panelPrimitives(index)
|
|
40
|
+
].filter(Boolean)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds board detail primitives.
|
|
45
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
46
|
+
* @returns {object[]}
|
|
47
|
+
*/
|
|
48
|
+
static cutoutPrimitives(index) {
|
|
49
|
+
return [
|
|
50
|
+
['pcb_cutout', 'cutout'],
|
|
51
|
+
['pcb_board_cutout', 'cutout'],
|
|
52
|
+
['pcb_keepout', 'keepout'],
|
|
53
|
+
['pcb_courtyard', 'courtyard'],
|
|
54
|
+
['pcb_courtyard_circle', 'courtyard'],
|
|
55
|
+
['pcb_courtyard_line', 'courtyard'],
|
|
56
|
+
['pcb_courtyard_outline', 'courtyard'],
|
|
57
|
+
['pcb_courtyard_path', 'courtyard'],
|
|
58
|
+
['pcb_courtyard_rect', 'courtyard']
|
|
59
|
+
]
|
|
60
|
+
.flatMap(([type, kind]) =>
|
|
61
|
+
CircuitJsonPcbPrimitiveArtwork.#all(index, type).flatMap(
|
|
62
|
+
(element) =>
|
|
63
|
+
CircuitJsonPcbPrimitiveArtwork.#boardDetailPrimitives(
|
|
64
|
+
element,
|
|
65
|
+
kind
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Builds total routed trace length labels.
|
|
74
|
+
* @param {object[]} primitives Primitive rows.
|
|
75
|
+
* @param {{ elementsByType: Map<string, object[]> }} [index] Element index.
|
|
76
|
+
* @returns {object[]}
|
|
77
|
+
*/
|
|
78
|
+
static traceLengths(primitives, index = null) {
|
|
79
|
+
return CircuitJsonPcbTraceLengthModel.build(primitives, index)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Builds generated copper clearance diagnostics.
|
|
84
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
85
|
+
* @param {object[]} primitives Primitive rows.
|
|
86
|
+
* @returns {object[]}
|
|
87
|
+
*/
|
|
88
|
+
static clearanceDiagnostics(index, primitives) {
|
|
89
|
+
const board = CircuitJsonPcbPrimitiveArtwork.#all(index, 'pcb_board')[0]
|
|
90
|
+
const minimum = CircuitJsonUnits.optionalLength(
|
|
91
|
+
board?.min_trace_clearance
|
|
92
|
+
)
|
|
93
|
+
if (minimum === null || minimum <= 0) return []
|
|
94
|
+
|
|
95
|
+
const copper = primitives.filter((primitive) =>
|
|
96
|
+
['pad', 'track', 'via', 'zone'].includes(primitive.kind)
|
|
97
|
+
)
|
|
98
|
+
const diagnostics = []
|
|
99
|
+
for (let leftIndex = 0; leftIndex < copper.length; leftIndex += 1) {
|
|
100
|
+
for (
|
|
101
|
+
let rightIndex = leftIndex + 1;
|
|
102
|
+
rightIndex < copper.length;
|
|
103
|
+
rightIndex += 1
|
|
104
|
+
) {
|
|
105
|
+
const left = copper[leftIndex]
|
|
106
|
+
const right = copper[rightIndex]
|
|
107
|
+
const actual = CircuitJsonPcbPrimitiveArtwork.#clearance(
|
|
108
|
+
left,
|
|
109
|
+
right
|
|
110
|
+
)
|
|
111
|
+
if (actual === null || actual >= minimum) continue
|
|
112
|
+
diagnostics.push(
|
|
113
|
+
CircuitJsonPcbPrimitiveArtwork.#clearanceDiagnostic(
|
|
114
|
+
left,
|
|
115
|
+
right,
|
|
116
|
+
actual,
|
|
117
|
+
minimum,
|
|
118
|
+
diagnostics.length
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return diagnostics
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Builds solder-mask opening primitives from pads with mask metadata.
|
|
129
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
130
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
131
|
+
* @returns {object[]}
|
|
132
|
+
*/
|
|
133
|
+
static #solderMaskPrimitives(index, componentsByPcbId) {
|
|
134
|
+
return CircuitJsonPcbPrimitiveArtwork.#all(index, 'pcb_smtpad')
|
|
135
|
+
.map((element) => {
|
|
136
|
+
const margins =
|
|
137
|
+
CircuitJsonPcbPrimitiveArtwork.#solderMaskMargins(element)
|
|
138
|
+
if (!margins) return null
|
|
139
|
+
return CircuitJsonPcbPrimitiveArtwork.#expandedPadPrimitive(
|
|
140
|
+
element,
|
|
141
|
+
'solder-mask',
|
|
142
|
+
'soldermask',
|
|
143
|
+
margins,
|
|
144
|
+
componentsByPcbId
|
|
145
|
+
)
|
|
146
|
+
})
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Builds explicit solder-paste primitives.
|
|
152
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
153
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
154
|
+
* @returns {object[]}
|
|
155
|
+
*/
|
|
156
|
+
static #solderPastePrimitives(index, componentsByPcbId) {
|
|
157
|
+
return CircuitJsonPcbPrimitiveArtwork.#all(index, 'pcb_solder_paste')
|
|
158
|
+
.map((element) =>
|
|
159
|
+
CircuitJsonPcbPrimitiveArtwork.#shapePrimitive(
|
|
160
|
+
element,
|
|
161
|
+
'solder-paste',
|
|
162
|
+
'paste',
|
|
163
|
+
componentsByPcbId
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Builds note and dimension documentation primitives.
|
|
171
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
172
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
173
|
+
* @returns {object[]}
|
|
174
|
+
*/
|
|
175
|
+
static #notePrimitives(index, componentsByPcbId) {
|
|
176
|
+
return [
|
|
177
|
+
...[
|
|
178
|
+
'pcb_note_text',
|
|
179
|
+
'pcb_note_line',
|
|
180
|
+
'pcb_note_path',
|
|
181
|
+
'pcb_note_rect'
|
|
182
|
+
].flatMap((type) =>
|
|
183
|
+
CircuitJsonPcbPrimitiveArtwork.#all(index, type).flatMap(
|
|
184
|
+
(element) =>
|
|
185
|
+
CircuitJsonPcbPrimitiveArtwork.#documentationPrimitives(
|
|
186
|
+
element,
|
|
187
|
+
'note',
|
|
188
|
+
componentsByPcbId
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
),
|
|
192
|
+
...['pcb_note_dimension', 'pcb_fabrication_note_dimension'].flatMap(
|
|
193
|
+
(type) =>
|
|
194
|
+
CircuitJsonPcbPrimitiveArtwork.#all(index, type).map(
|
|
195
|
+
(element) =>
|
|
196
|
+
CircuitJsonPcbPrimitiveArtwork.#dimensionPrimitive(
|
|
197
|
+
element
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
),
|
|
201
|
+
...CircuitJsonPcbPrimitiveArtwork.#all(
|
|
202
|
+
index,
|
|
203
|
+
'pcb_fabrication_note_rect'
|
|
204
|
+
).map((element) =>
|
|
205
|
+
CircuitJsonPcbPrimitiveArtwork.#shapePrimitive(
|
|
206
|
+
element,
|
|
207
|
+
'fabrication',
|
|
208
|
+
'fabrication',
|
|
209
|
+
componentsByPcbId
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
].filter(Boolean)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Builds silkscreen shape primitives not covered by text/line handling.
|
|
217
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
218
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
219
|
+
* @returns {object[]}
|
|
220
|
+
*/
|
|
221
|
+
static #silkscreenShapePrimitives(index, componentsByPcbId) {
|
|
222
|
+
return [
|
|
223
|
+
'pcb_silkscreen_circle',
|
|
224
|
+
'pcb_silkscreen_oval',
|
|
225
|
+
'pcb_silkscreen_pill',
|
|
226
|
+
'pcb_silkscreen_rect'
|
|
227
|
+
]
|
|
228
|
+
.flatMap((type) => CircuitJsonPcbPrimitiveArtwork.#all(index, type))
|
|
229
|
+
.map((element) =>
|
|
230
|
+
CircuitJsonPcbPrimitiveArtwork.#shapePrimitive(
|
|
231
|
+
element,
|
|
232
|
+
'silkscreen',
|
|
233
|
+
'silkscreen',
|
|
234
|
+
componentsByPcbId
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Builds thermal spoke primitives.
|
|
242
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
243
|
+
* @returns {object[]}
|
|
244
|
+
*/
|
|
245
|
+
static #thermalSpokePrimitives(index) {
|
|
246
|
+
return CircuitJsonPcbPrimitiveArtwork.#all(index, 'pcb_thermal_spoke')
|
|
247
|
+
.map((element) =>
|
|
248
|
+
CircuitJsonPcbPrimitiveArtwork.#linePrimitive(
|
|
249
|
+
element,
|
|
250
|
+
'thermal-spoke'
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
.filter(Boolean)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Builds route hint segment primitives.
|
|
258
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
259
|
+
* @returns {object[]}
|
|
260
|
+
*/
|
|
261
|
+
static #routeHintPrimitives(index) {
|
|
262
|
+
return CircuitJsonPcbPrimitiveArtwork.#all(index, 'pcb_trace_hint')
|
|
263
|
+
.flatMap((element) => {
|
|
264
|
+
const points = (
|
|
265
|
+
Array.isArray(element.route) ? element.route : []
|
|
266
|
+
)
|
|
267
|
+
.map((point) => CircuitJsonPcbPrimitiveFields.point(point))
|
|
268
|
+
.filter(Boolean)
|
|
269
|
+
const rows = []
|
|
270
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
271
|
+
rows.push(
|
|
272
|
+
CircuitJsonPcbPrimitiveArtwork.#linePrimitive(
|
|
273
|
+
{
|
|
274
|
+
...element,
|
|
275
|
+
x1: points[index - 1].x,
|
|
276
|
+
y1: points[index - 1].y,
|
|
277
|
+
x2: points[index].x,
|
|
278
|
+
y2: points[index].y
|
|
279
|
+
},
|
|
280
|
+
'route-hint',
|
|
281
|
+
index - 1
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
return rows
|
|
286
|
+
})
|
|
287
|
+
.filter(Boolean)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Builds breakout point markers.
|
|
292
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
293
|
+
* @returns {object[]}
|
|
294
|
+
*/
|
|
295
|
+
static #breakoutPointPrimitives(index) {
|
|
296
|
+
return CircuitJsonPcbPrimitiveArtwork.#all(index, 'pcb_breakout_point')
|
|
297
|
+
.map((element) =>
|
|
298
|
+
CircuitJsonPcbPrimitiveArtwork.#shapePrimitive(
|
|
299
|
+
{
|
|
300
|
+
...element,
|
|
301
|
+
radius: element.radius ?? 0.16,
|
|
302
|
+
shape: 'circle'
|
|
303
|
+
},
|
|
304
|
+
'breakout-point',
|
|
305
|
+
'breakout',
|
|
306
|
+
new Map()
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
.filter(Boolean)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Builds panel outline primitives.
|
|
314
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
315
|
+
* @returns {object[]}
|
|
316
|
+
*/
|
|
317
|
+
static #panelPrimitives(index) {
|
|
318
|
+
return CircuitJsonPcbPrimitiveArtwork.#all(index, 'pcb_panel')
|
|
319
|
+
.map((element) =>
|
|
320
|
+
CircuitJsonPcbPrimitiveArtwork.#shapePrimitive(
|
|
321
|
+
element,
|
|
322
|
+
'panel',
|
|
323
|
+
'panel',
|
|
324
|
+
new Map()
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
.filter(Boolean)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Builds documentation primitives from common shape or path fields.
|
|
332
|
+
* @param {object} element Element row.
|
|
333
|
+
* @param {string} kind Primitive kind.
|
|
334
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
335
|
+
* @returns {object[]}
|
|
336
|
+
*/
|
|
337
|
+
static #documentationPrimitives(element, kind, componentsByPcbId) {
|
|
338
|
+
if (element.text !== undefined) {
|
|
339
|
+
return [
|
|
340
|
+
CircuitJsonPcbPrimitiveArtwork.#textPrimitive(
|
|
341
|
+
element,
|
|
342
|
+
kind,
|
|
343
|
+
componentsByPcbId
|
|
344
|
+
)
|
|
345
|
+
].filter(Boolean)
|
|
346
|
+
}
|
|
347
|
+
if (CircuitJsonPcbPrimitiveArtwork.#isOpenPath(element)) {
|
|
348
|
+
return CircuitJsonPcbPrimitiveArtwork.#openLinePrimitives(
|
|
349
|
+
element,
|
|
350
|
+
kind
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
return [
|
|
354
|
+
CircuitJsonPcbPrimitiveArtwork.#shapePrimitive(
|
|
355
|
+
element,
|
|
356
|
+
kind,
|
|
357
|
+
kind,
|
|
358
|
+
componentsByPcbId
|
|
359
|
+
)
|
|
360
|
+
].filter(Boolean)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Builds one expanded rectangular pad-derived primitive.
|
|
365
|
+
* @param {object} element Source pad.
|
|
366
|
+
* @param {string} kind Primitive kind.
|
|
367
|
+
* @param {string} layerSuffix Layer suffix.
|
|
368
|
+
* @param {{ left: number, right: number, top: number, bottom: number }} margins Opening margins.
|
|
369
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
370
|
+
* @returns {object | null}
|
|
371
|
+
*/
|
|
372
|
+
static #expandedPadPrimitive(
|
|
373
|
+
element,
|
|
374
|
+
kind,
|
|
375
|
+
layerSuffix,
|
|
376
|
+
margins,
|
|
377
|
+
componentsByPcbId
|
|
378
|
+
) {
|
|
379
|
+
const center = CircuitJsonPcbPrimitiveFields.center(element)
|
|
380
|
+
const width = CircuitJsonUnits.optionalLength(element.width)
|
|
381
|
+
const height = CircuitJsonUnits.optionalLength(element.height)
|
|
382
|
+
if (!center || width === null || height === null) return null
|
|
383
|
+
const nextWidth = Math.max(width + margins.left + margins.right, 0.01)
|
|
384
|
+
const nextHeight = Math.max(height + margins.top + margins.bottom, 0.01)
|
|
385
|
+
const nextCenter = {
|
|
386
|
+
x: center.x + (margins.right - margins.left) / 2,
|
|
387
|
+
y: center.y + (margins.top - margins.bottom) / 2
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return CircuitJsonPcbPrimitiveArtwork.#shapePrimitive(
|
|
391
|
+
{
|
|
392
|
+
...element,
|
|
393
|
+
x: nextCenter.x,
|
|
394
|
+
y: nextCenter.y,
|
|
395
|
+
center: nextCenter,
|
|
396
|
+
width: nextWidth,
|
|
397
|
+
height: nextHeight,
|
|
398
|
+
radius:
|
|
399
|
+
CircuitJsonUnits.optionalLength(element.radius) ??
|
|
400
|
+
Math.min(nextWidth, nextHeight) / 2,
|
|
401
|
+
rotation: CircuitJsonUnits.angle(
|
|
402
|
+
element.ccw_rotation ?? element.rotation,
|
|
403
|
+
0
|
|
404
|
+
),
|
|
405
|
+
derived_id_suffix: kind,
|
|
406
|
+
layer: CircuitJsonPcbPrimitiveArtwork.#surfaceLayer(
|
|
407
|
+
element.layer,
|
|
408
|
+
layerSuffix
|
|
409
|
+
)
|
|
410
|
+
},
|
|
411
|
+
kind,
|
|
412
|
+
layerSuffix,
|
|
413
|
+
componentsByPcbId
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Builds one generic center/size primitive.
|
|
419
|
+
* @param {object} element Element row.
|
|
420
|
+
* @param {string} kind Primitive kind.
|
|
421
|
+
* @param {string} layerSuffix Layer suffix.
|
|
422
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
423
|
+
* @returns {object | null}
|
|
424
|
+
*/
|
|
425
|
+
static #shapePrimitive(element, kind, layerSuffix, componentsByPcbId) {
|
|
426
|
+
const points = CircuitJsonPcbPrimitiveFields.points(element)
|
|
427
|
+
const center =
|
|
428
|
+
CircuitJsonPcbPrimitiveFields.center(element) ||
|
|
429
|
+
CircuitJsonPcbPrimitiveFields.pointsCenter(points)
|
|
430
|
+
if (!center && points.length < 3) return null
|
|
431
|
+
|
|
432
|
+
const radius = CircuitJsonUnits.optionalLength(element.radius)
|
|
433
|
+
const width =
|
|
434
|
+
CircuitJsonUnits.optionalLength(element.width) ??
|
|
435
|
+
(radius === null ? null : radius * 2)
|
|
436
|
+
const height =
|
|
437
|
+
CircuitJsonUnits.optionalLength(element.height) ??
|
|
438
|
+
(radius === null ? null : radius * 2)
|
|
439
|
+
const bounds = points.length
|
|
440
|
+
? CircuitJsonPcbPrimitiveGeometry.pointsBounds(points)
|
|
441
|
+
: CircuitJsonPcbPrimitiveGeometry.centerBounds(
|
|
442
|
+
center,
|
|
443
|
+
width ?? 0.5,
|
|
444
|
+
height ?? 0.5
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
return CircuitJsonPcbPrimitiveArtwork.#primitive({
|
|
448
|
+
id: CircuitJsonPcbPrimitiveFields.derivedId(element),
|
|
449
|
+
kind,
|
|
450
|
+
shape: String(
|
|
451
|
+
element.shape || (points.length ? 'polygon' : 'rect')
|
|
452
|
+
),
|
|
453
|
+
x: center?.x ?? bounds.minX + bounds.width / 2,
|
|
454
|
+
y: center?.y ?? bounds.minY + bounds.height / 2,
|
|
455
|
+
width: width ?? bounds.width,
|
|
456
|
+
height: height ?? bounds.height,
|
|
457
|
+
radius: radius ?? 0,
|
|
458
|
+
rotation: CircuitJsonUnits.angle(
|
|
459
|
+
element.ccw_rotation ?? element.rotation,
|
|
460
|
+
0
|
|
461
|
+
),
|
|
462
|
+
points,
|
|
463
|
+
bounds,
|
|
464
|
+
layer: CircuitJsonPcbPrimitiveArtwork.#detailLayer(
|
|
465
|
+
element,
|
|
466
|
+
layerSuffix
|
|
467
|
+
),
|
|
468
|
+
component: componentsByPcbId.get(
|
|
469
|
+
String(element.pcb_component_id || '').trim()
|
|
470
|
+
),
|
|
471
|
+
netName: CircuitJsonPcbPrimitiveFields.netName(element),
|
|
472
|
+
anchors: points.length
|
|
473
|
+
? points.map((point) => ({ point }))
|
|
474
|
+
: CircuitJsonPcbPrimitiveGeometry.cornerAnchors(bounds),
|
|
475
|
+
...CircuitJsonPcbDrawingStyle.fromElement(element),
|
|
476
|
+
source: element
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Builds one text primitive.
|
|
482
|
+
* @param {object} element Element row.
|
|
483
|
+
* @param {string} kind Primitive kind.
|
|
484
|
+
* @param {Map<string, object>} componentsByPcbId Component lookup.
|
|
485
|
+
* @returns {object | null}
|
|
486
|
+
*/
|
|
487
|
+
static #textPrimitive(element, kind, componentsByPcbId) {
|
|
488
|
+
const center = CircuitJsonPcbPrimitiveFields.center(element)
|
|
489
|
+
if (!center) return null
|
|
490
|
+
const fontSize = CircuitJsonUnits.length(
|
|
491
|
+
element.font_size ?? element.height,
|
|
492
|
+
1
|
|
493
|
+
)
|
|
494
|
+
const bounds = CircuitJsonPcbPrimitiveGeometry.centerBounds(
|
|
495
|
+
center,
|
|
496
|
+
Math.max(
|
|
497
|
+
String(element.text || '').length * fontSize * 0.6,
|
|
498
|
+
fontSize
|
|
499
|
+
),
|
|
500
|
+
fontSize
|
|
501
|
+
)
|
|
502
|
+
return CircuitJsonPcbPrimitiveArtwork.#primitive({
|
|
503
|
+
id: CircuitJsonIndexer.getElementId(element),
|
|
504
|
+
kind,
|
|
505
|
+
text: String(element.text || ''),
|
|
506
|
+
x: center.x,
|
|
507
|
+
y: center.y,
|
|
508
|
+
fontSize,
|
|
509
|
+
bounds,
|
|
510
|
+
layer: CircuitJsonPcbPrimitiveArtwork.#detailLayer(element, kind),
|
|
511
|
+
component: componentsByPcbId.get(
|
|
512
|
+
String(element.pcb_component_id || '').trim()
|
|
513
|
+
),
|
|
514
|
+
anchors: [{ point: center }],
|
|
515
|
+
...CircuitJsonPcbDrawingStyle.fromElement(element),
|
|
516
|
+
source: element
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Builds one line primitive.
|
|
522
|
+
* @param {object} element Element row.
|
|
523
|
+
* @param {string} kind Primitive kind.
|
|
524
|
+
* @param {number} [index] Segment index.
|
|
525
|
+
* @returns {object | null}
|
|
526
|
+
*/
|
|
527
|
+
static #linePrimitive(element, kind, index = 0) {
|
|
528
|
+
const start = CircuitJsonPcbPrimitiveFields.point({
|
|
529
|
+
x: element.x1 ?? element.start?.x,
|
|
530
|
+
y: element.y1 ?? element.start?.y
|
|
531
|
+
})
|
|
532
|
+
const end = CircuitJsonPcbPrimitiveFields.point({
|
|
533
|
+
x: element.x2 ?? element.end?.x,
|
|
534
|
+
y: element.y2 ?? element.end?.y
|
|
535
|
+
})
|
|
536
|
+
if (!start || !end) return null
|
|
537
|
+
const width = CircuitJsonUnits.length(
|
|
538
|
+
element.width ??
|
|
539
|
+
element.stroke_width ??
|
|
540
|
+
element.strokeWidth ??
|
|
541
|
+
element.line_width,
|
|
542
|
+
0.08
|
|
543
|
+
)
|
|
544
|
+
return CircuitJsonPcbPrimitiveArtwork.#primitive({
|
|
545
|
+
id: CircuitJsonIndexer.getElementId(element) + ':' + index,
|
|
546
|
+
kind,
|
|
547
|
+
x1: start.x,
|
|
548
|
+
y1: start.y,
|
|
549
|
+
x2: end.x,
|
|
550
|
+
y2: end.y,
|
|
551
|
+
width,
|
|
552
|
+
bounds: CircuitJsonPcbPrimitiveGeometry.segmentBounds(
|
|
553
|
+
start,
|
|
554
|
+
end,
|
|
555
|
+
width
|
|
556
|
+
),
|
|
557
|
+
layer: CircuitJsonPcbPrimitiveArtwork.#detailLayer(element, kind),
|
|
558
|
+
netName: CircuitJsonPcbPrimitiveFields.netName(element),
|
|
559
|
+
anchors: [{ point: start }, { point: end }],
|
|
560
|
+
...CircuitJsonPcbDrawingStyle.fromElement(element),
|
|
561
|
+
source: element
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Returns true when an element should render as open line segments.
|
|
567
|
+
* @param {object} element Element row.
|
|
568
|
+
* @returns {boolean}
|
|
569
|
+
*/
|
|
570
|
+
static #isOpenPath(element) {
|
|
571
|
+
const type = String(element.type || '')
|
|
572
|
+
const shape = String(element.shape || '').toLowerCase()
|
|
573
|
+
return (
|
|
574
|
+
type.endsWith('_line') ||
|
|
575
|
+
type.endsWith('_path') ||
|
|
576
|
+
shape === 'line' ||
|
|
577
|
+
shape === 'path'
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Builds open line segment primitives from path points.
|
|
583
|
+
* @param {object} element Element row.
|
|
584
|
+
* @param {string} kind Primitive kind.
|
|
585
|
+
* @returns {object[]}
|
|
586
|
+
*/
|
|
587
|
+
static #openLinePrimitives(element, kind) {
|
|
588
|
+
const points = CircuitJsonPcbPrimitiveFields.linePoints(element)
|
|
589
|
+
const rows = []
|
|
590
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
591
|
+
rows.push(
|
|
592
|
+
CircuitJsonPcbPrimitiveArtwork.#linePrimitive(
|
|
593
|
+
{
|
|
594
|
+
...element,
|
|
595
|
+
x1: points[index - 1].x,
|
|
596
|
+
y1: points[index - 1].y,
|
|
597
|
+
x2: points[index].x,
|
|
598
|
+
y2: points[index].y
|
|
599
|
+
},
|
|
600
|
+
kind,
|
|
601
|
+
index - 1
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
}
|
|
605
|
+
return rows.filter(Boolean)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Builds one dimension primitive.
|
|
610
|
+
* @param {object} element Element row.
|
|
611
|
+
* @returns {object | null}
|
|
612
|
+
*/
|
|
613
|
+
static #dimensionPrimitive(element) {
|
|
614
|
+
const line = CircuitJsonPcbPrimitiveArtwork.#linePrimitive(
|
|
615
|
+
element,
|
|
616
|
+
'dimension'
|
|
617
|
+
)
|
|
618
|
+
return line
|
|
619
|
+
? {
|
|
620
|
+
...line,
|
|
621
|
+
text: String(element.text || '')
|
|
622
|
+
}
|
|
623
|
+
: null
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Builds one board detail shape primitive.
|
|
628
|
+
* @param {object} element Element row.
|
|
629
|
+
* @param {string} kind Primitive kind.
|
|
630
|
+
* @returns {object | null}
|
|
631
|
+
*/
|
|
632
|
+
static #boardDetailPrimitive(element, kind) {
|
|
633
|
+
const points = CircuitJsonPcbPrimitiveFields.points(element)
|
|
634
|
+
const center =
|
|
635
|
+
CircuitJsonPcbPrimitiveFields.center(element) ||
|
|
636
|
+
CircuitJsonPcbPrimitiveFields.pointsCenter(points)
|
|
637
|
+
if (!center && points.length < 3) return null
|
|
638
|
+
|
|
639
|
+
const width = CircuitJsonUnits.optionalLength(element.width)
|
|
640
|
+
const height = CircuitJsonUnits.optionalLength(element.height)
|
|
641
|
+
const radius = CircuitJsonUnits.optionalLength(element.radius)
|
|
642
|
+
const bounds = points.length
|
|
643
|
+
? CircuitJsonPcbPrimitiveGeometry.pointsBounds(points)
|
|
644
|
+
: CircuitJsonPcbPrimitiveGeometry.centerBounds(
|
|
645
|
+
center,
|
|
646
|
+
width ?? (radius ?? 0.5) * 2,
|
|
647
|
+
height ?? (radius ?? 0.5) * 2
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
id: CircuitJsonIndexer.getElementId(element),
|
|
652
|
+
kind,
|
|
653
|
+
shape: String(
|
|
654
|
+
element.shape || (points.length ? 'polygon' : 'rect')
|
|
655
|
+
),
|
|
656
|
+
x: center?.x ?? bounds.minX + bounds.width / 2,
|
|
657
|
+
y: center?.y ?? bounds.minY + bounds.height / 2,
|
|
658
|
+
width: width ?? bounds.width,
|
|
659
|
+
height: height ?? bounds.height,
|
|
660
|
+
radius: radius ?? 0,
|
|
661
|
+
points,
|
|
662
|
+
bounds,
|
|
663
|
+
layer: CircuitJsonPcbPrimitiveArtwork.#boardDetailLayer(
|
|
664
|
+
element,
|
|
665
|
+
kind
|
|
666
|
+
),
|
|
667
|
+
side: '',
|
|
668
|
+
componentKey: '',
|
|
669
|
+
componentId: '',
|
|
670
|
+
footprintId: '',
|
|
671
|
+
netName: '',
|
|
672
|
+
anchors: points.length
|
|
673
|
+
? points.map((point) => ({ point }))
|
|
674
|
+
: CircuitJsonPcbPrimitiveGeometry.cornerAnchors(bounds),
|
|
675
|
+
...CircuitJsonPcbDrawingStyle.fromElement(element),
|
|
676
|
+
source: element
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Builds board detail primitives for shape or open path rows.
|
|
682
|
+
* @param {object} element Element row.
|
|
683
|
+
* @param {string} kind Primitive kind.
|
|
684
|
+
* @returns {object[]}
|
|
685
|
+
*/
|
|
686
|
+
static #boardDetailPrimitives(element, kind) {
|
|
687
|
+
if (CircuitJsonPcbPrimitiveArtwork.#isBoardDetailPath(element)) {
|
|
688
|
+
return CircuitJsonPcbPrimitiveArtwork.#boardDetailLinePrimitives(
|
|
689
|
+
element,
|
|
690
|
+
kind
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return [
|
|
695
|
+
CircuitJsonPcbPrimitiveArtwork.#boardDetailPrimitive(element, kind)
|
|
696
|
+
].filter(Boolean)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Returns true when a board detail row should render as open segments.
|
|
701
|
+
* @param {object} element Element row.
|
|
702
|
+
* @returns {boolean}
|
|
703
|
+
*/
|
|
704
|
+
static #isBoardDetailPath(element) {
|
|
705
|
+
const type = String(element.type || '')
|
|
706
|
+
const shape = String(element.shape || '').toLowerCase()
|
|
707
|
+
return (
|
|
708
|
+
type.endsWith('_line') ||
|
|
709
|
+
type.endsWith('_path') ||
|
|
710
|
+
shape === 'line' ||
|
|
711
|
+
shape === 'path'
|
|
712
|
+
)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Builds open board detail segment primitives.
|
|
717
|
+
* @param {object} element Element row.
|
|
718
|
+
* @param {string} kind Primitive kind.
|
|
719
|
+
* @returns {object[]}
|
|
720
|
+
*/
|
|
721
|
+
static #boardDetailLinePrimitives(element, kind) {
|
|
722
|
+
const points = CircuitJsonPcbPrimitiveFields.linePoints(element)
|
|
723
|
+
const rows = []
|
|
724
|
+
|
|
725
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
726
|
+
rows.push(
|
|
727
|
+
CircuitJsonPcbPrimitiveArtwork.#boardDetailLinePrimitive(
|
|
728
|
+
element,
|
|
729
|
+
kind,
|
|
730
|
+
points[index - 1],
|
|
731
|
+
points[index],
|
|
732
|
+
index - 1
|
|
733
|
+
)
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return rows.filter(Boolean)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Builds one open board detail segment primitive.
|
|
742
|
+
* @param {object} element Element row.
|
|
743
|
+
* @param {string} kind Primitive kind.
|
|
744
|
+
* @param {{ x: number, y: number }} start Segment start.
|
|
745
|
+
* @param {{ x: number, y: number }} end Segment end.
|
|
746
|
+
* @param {number} index Segment index.
|
|
747
|
+
* @returns {object | null}
|
|
748
|
+
*/
|
|
749
|
+
static #boardDetailLinePrimitive(element, kind, start, end, index) {
|
|
750
|
+
const width = CircuitJsonUnits.length(
|
|
751
|
+
element.width ??
|
|
752
|
+
element.stroke_width ??
|
|
753
|
+
element.strokeWidth ??
|
|
754
|
+
element.line_width,
|
|
755
|
+
0.08
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
id: CircuitJsonIndexer.getElementId(element) + ':' + index,
|
|
760
|
+
kind,
|
|
761
|
+
x1: start.x,
|
|
762
|
+
y1: start.y,
|
|
763
|
+
x2: end.x,
|
|
764
|
+
y2: end.y,
|
|
765
|
+
width,
|
|
766
|
+
bounds: CircuitJsonPcbPrimitiveGeometry.segmentBounds(
|
|
767
|
+
start,
|
|
768
|
+
end,
|
|
769
|
+
width
|
|
770
|
+
),
|
|
771
|
+
layer: CircuitJsonPcbPrimitiveArtwork.#boardDetailLayer(
|
|
772
|
+
element,
|
|
773
|
+
kind
|
|
774
|
+
),
|
|
775
|
+
side: '',
|
|
776
|
+
componentKey: '',
|
|
777
|
+
componentId: '',
|
|
778
|
+
footprintId: '',
|
|
779
|
+
netName: '',
|
|
780
|
+
anchors: [{ point: start }, { point: end }],
|
|
781
|
+
...CircuitJsonPcbDrawingStyle.fromElement(element),
|
|
782
|
+
source: element
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Adds common primitive metadata.
|
|
788
|
+
* @param {object} primitive Primitive row.
|
|
789
|
+
* @returns {object}
|
|
790
|
+
*/
|
|
791
|
+
static #primitive(primitive) {
|
|
792
|
+
const component = primitive.component || {}
|
|
793
|
+
const layer = CircuitJsonPcbPrimitiveFields.layer(primitive.layer)
|
|
794
|
+
return {
|
|
795
|
+
...primitive,
|
|
796
|
+
layer,
|
|
797
|
+
side: primitive.side ?? CircuitJsonPcbPrimitiveFields.side(layer),
|
|
798
|
+
componentKey: String(component.componentKey || ''),
|
|
799
|
+
componentId: String(component.pcbComponentId || ''),
|
|
800
|
+
footprintId: component.componentKey
|
|
801
|
+
? 'footprint:' + component.componentKey + ':' + primitive.kind
|
|
802
|
+
: '',
|
|
803
|
+
netName: String(primitive.netName || '').trim()
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Resolves indexed element rows.
|
|
809
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
810
|
+
* @param {string} type Element type.
|
|
811
|
+
* @returns {object[]}
|
|
812
|
+
*/
|
|
813
|
+
static #all(index, type) {
|
|
814
|
+
return index.elementsByType.get(type) || []
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Resolves solder-mask margins for one pad.
|
|
819
|
+
* @param {object} element Pad row.
|
|
820
|
+
* @returns {{ left: number, right: number, top: number, bottom: number } | null}
|
|
821
|
+
*/
|
|
822
|
+
static #solderMaskMargins(element) {
|
|
823
|
+
const left = CircuitJsonUnits.optionalLength(
|
|
824
|
+
element.soldermask_margin_left ?? element.solderMaskMarginLeft
|
|
825
|
+
)
|
|
826
|
+
const right = CircuitJsonUnits.optionalLength(
|
|
827
|
+
element.soldermask_margin_right ?? element.solderMaskMarginRight
|
|
828
|
+
)
|
|
829
|
+
const top = CircuitJsonUnits.optionalLength(
|
|
830
|
+
element.soldermask_margin_top ?? element.solderMaskMarginTop
|
|
831
|
+
)
|
|
832
|
+
const bottom = CircuitJsonUnits.optionalLength(
|
|
833
|
+
element.soldermask_margin_bottom ?? element.solderMaskMarginBottom
|
|
834
|
+
)
|
|
835
|
+
if ([left, right, top, bottom].some((value) => value !== null)) {
|
|
836
|
+
return {
|
|
837
|
+
left: left ?? 0,
|
|
838
|
+
right: right ?? 0,
|
|
839
|
+
top: top ?? 0,
|
|
840
|
+
bottom: bottom ?? 0
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const expansion = CircuitJsonPcbPrimitiveFields.optionalLength([
|
|
845
|
+
element.solderMaskExpansion,
|
|
846
|
+
element.solder_mask_expansion,
|
|
847
|
+
element.solderMaskMargin,
|
|
848
|
+
element.solder_mask_margin
|
|
849
|
+
])
|
|
850
|
+
return expansion === null
|
|
851
|
+
? null
|
|
852
|
+
: {
|
|
853
|
+
left: expansion,
|
|
854
|
+
right: expansion,
|
|
855
|
+
top: expansion,
|
|
856
|
+
bottom: expansion
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Resolves a drawing layer for a primitive.
|
|
862
|
+
* @param {object} element Element row.
|
|
863
|
+
* @param {string} suffix Layer suffix.
|
|
864
|
+
* @returns {string}
|
|
865
|
+
*/
|
|
866
|
+
static #detailLayer(element, suffix) {
|
|
867
|
+
if (['paste', 'soldermask'].includes(suffix)) {
|
|
868
|
+
return CircuitJsonPcbPrimitiveArtwork.#surfaceLayer(
|
|
869
|
+
element.layer,
|
|
870
|
+
suffix
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
if (suffix === 'panel') return 'panel'
|
|
874
|
+
if (suffix === 'breakout') return 'breakout_points'
|
|
875
|
+
return CircuitJsonPcbPrimitiveFields.layer(
|
|
876
|
+
element.layer || 'top_fabrication'
|
|
877
|
+
)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Builds a top/bottom-specific fabrication layer key.
|
|
882
|
+
* @param {unknown} layer Layer candidate.
|
|
883
|
+
* @param {string} suffix Layer suffix.
|
|
884
|
+
* @returns {string}
|
|
885
|
+
*/
|
|
886
|
+
static #surfaceLayer(layer, suffix) {
|
|
887
|
+
return (
|
|
888
|
+
(CircuitJsonPcbPrimitiveFields.side(
|
|
889
|
+
CircuitJsonPcbPrimitiveFields.layer(layer)
|
|
890
|
+
) || 'top') +
|
|
891
|
+
'_' +
|
|
892
|
+
suffix
|
|
893
|
+
)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Resolves a board detail layer key.
|
|
898
|
+
* @param {object} element Element row.
|
|
899
|
+
* @param {string} kind Primitive kind.
|
|
900
|
+
* @returns {string}
|
|
901
|
+
*/
|
|
902
|
+
static #boardDetailLayer(element, kind) {
|
|
903
|
+
if (kind === 'cutout') return 'cutouts'
|
|
904
|
+
if (kind === 'keepout') return 'keepouts'
|
|
905
|
+
if (kind === 'courtyard') {
|
|
906
|
+
return CircuitJsonPcbPrimitiveFields.layer(
|
|
907
|
+
element.layer || 'top_courtyard'
|
|
908
|
+
)
|
|
909
|
+
}
|
|
910
|
+
return CircuitJsonPcbPrimitiveFields.layer(element.layer || 'board')
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Computes bounds-based clearance between two copper primitives.
|
|
915
|
+
* @param {object} left Left primitive.
|
|
916
|
+
* @param {object} right Right primitive.
|
|
917
|
+
* @returns {number | null}
|
|
918
|
+
*/
|
|
919
|
+
static #clearance(left, right) {
|
|
920
|
+
if (!left.bounds || !right.bounds) return null
|
|
921
|
+
if (!left.netName || !right.netName || left.netName === right.netName) {
|
|
922
|
+
return null
|
|
923
|
+
}
|
|
924
|
+
if (left.layer && right.layer && left.layer !== right.layer) return null
|
|
925
|
+
|
|
926
|
+
const dx = Math.max(
|
|
927
|
+
right.bounds.minX - left.bounds.maxX,
|
|
928
|
+
left.bounds.minX - right.bounds.maxX,
|
|
929
|
+
0
|
|
930
|
+
)
|
|
931
|
+
const dy = Math.max(
|
|
932
|
+
right.bounds.minY - left.bounds.maxY,
|
|
933
|
+
left.bounds.minY - right.bounds.maxY,
|
|
934
|
+
0
|
|
935
|
+
)
|
|
936
|
+
return Math.hypot(dx, dy)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Builds one generated copper clearance diagnostic.
|
|
941
|
+
* @param {object} left Left primitive.
|
|
942
|
+
* @param {object} right Right primitive.
|
|
943
|
+
* @param {number} actual Actual clearance.
|
|
944
|
+
* @param {number} minimum Minimum clearance.
|
|
945
|
+
* @param {number} index Diagnostic index.
|
|
946
|
+
* @returns {object}
|
|
947
|
+
*/
|
|
948
|
+
static #clearanceDiagnostic(left, right, actual, minimum, index) {
|
|
949
|
+
return {
|
|
950
|
+
id: 'clearance:' + index,
|
|
951
|
+
kind: 'error',
|
|
952
|
+
severity: 'error',
|
|
953
|
+
category: 'clearance',
|
|
954
|
+
code: 'pcb_copper_clearance',
|
|
955
|
+
message: 'Copper clearance is below the configured rule.',
|
|
956
|
+
point: {
|
|
957
|
+
x: CircuitJsonPcbPrimitiveArtwork.#rounded(
|
|
958
|
+
(left.bounds.minX +
|
|
959
|
+
left.bounds.maxX +
|
|
960
|
+
right.bounds.minX +
|
|
961
|
+
right.bounds.maxX) /
|
|
962
|
+
4
|
|
963
|
+
),
|
|
964
|
+
y: CircuitJsonPcbPrimitiveArtwork.#rounded(
|
|
965
|
+
(left.bounds.minY +
|
|
966
|
+
left.bounds.maxY +
|
|
967
|
+
right.bounds.minY +
|
|
968
|
+
right.bounds.maxY) /
|
|
969
|
+
4
|
|
970
|
+
)
|
|
971
|
+
},
|
|
972
|
+
componentKey: String(left.componentKey || right.componentKey || ''),
|
|
973
|
+
netName:
|
|
974
|
+
String(left.netName || '') +
|
|
975
|
+
' / ' +
|
|
976
|
+
String(right.netName || ''),
|
|
977
|
+
clearance: {
|
|
978
|
+
minimum: CircuitJsonPcbPrimitiveArtwork.#rounded(minimum),
|
|
979
|
+
actual: CircuitJsonPcbPrimitiveArtwork.#rounded(actual)
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Rounds a numeric value for deterministic model rows.
|
|
986
|
+
* @param {number} value Numeric value.
|
|
987
|
+
* @returns {number}
|
|
988
|
+
*/
|
|
989
|
+
static #rounded(value) {
|
|
990
|
+
return Number(Number(value).toFixed(6))
|
|
991
|
+
}
|
|
992
|
+
}
|