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.
Files changed (54) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +21 -2
  3. package/docs/api.md +50 -4
  4. package/docs/model-format.md +21 -3
  5. package/package.json +3 -2
  6. package/spec/library-scope.md +4 -1
  7. package/src/core/CircuitJsonBomBuilder.mjs +143 -0
  8. package/src/core/CircuitJsonDocument.mjs +46 -13
  9. package/src/core/CircuitJsonElementValidator.mjs +990 -0
  10. package/src/core/CircuitJsonIndexer.mjs +773 -4
  11. package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
  12. package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
  13. package/src/core/CircuitJsonParser.mjs +22 -6
  14. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
  15. package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
  16. package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
  17. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
  18. package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
  19. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
  20. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
  21. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
  22. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
  23. package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
  24. package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
  25. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
  26. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
  27. package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
  28. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
  29. package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
  30. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
  31. package/src/core/CircuitJsonUnits.mjs +133 -8
  32. package/src/core/PcbBoundsSelectionModel.mjs +250 -0
  33. package/src/core/PcbCandidateSelectionModel.mjs +77 -0
  34. package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
  35. package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
  36. package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
  37. package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
  38. package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
  39. package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
  40. package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
  41. package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
  42. package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
  43. package/src/core/spice/SpiceSimulationService.mjs +92 -0
  44. package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
  45. package/src/index.mjs +8 -0
  46. package/src/renderers.mjs +29 -0
  47. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
  48. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
  49. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
  50. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
  51. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
  52. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
  53. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
  54. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
@@ -0,0 +1,898 @@
1
+ import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
2
+ import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
3
+
4
+ /**
5
+ * Builds manufacturing-oriented metadata from element arrays.
6
+ */
7
+ export class CircuitJsonManufacturingBuilder {
8
+ /**
9
+ * Builds pick-and-place rows and routing exchange text.
10
+ * @param {object[]} circuitJson Parsed element array.
11
+ * @param {{ elementsByType?: Map<string, object[]>, sourceComponentById?: Map<string, object> }} [index] Optional index.
12
+ * @returns {{ pickAndPlaceRows: object[], routingDsn: string, routingGuides: object[], fabricationNotes: object[] }}
13
+ */
14
+ static build(circuitJson, index = CircuitJsonIndexer.index(circuitJson)) {
15
+ return {
16
+ pickAndPlaceRows:
17
+ CircuitJsonManufacturingBuilder.#pickAndPlaceRows(index),
18
+ routingDsn: CircuitJsonManufacturingBuilder.#routingDsn(index),
19
+ routingGuides:
20
+ CircuitJsonManufacturingBuilder.#routingGuides(index),
21
+ fabricationNotes:
22
+ CircuitJsonManufacturingBuilder.#fabricationNotes(index)
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Builds assembly placement rows.
28
+ * @param {{ elementsByType?: Map<string, object[]>, sourceComponentById?: Map<string, object> }} index Element index.
29
+ * @returns {object[]}
30
+ */
31
+ static #pickAndPlaceRows(index) {
32
+ return CircuitJsonManufacturingBuilder.#all(index, 'pcb_component').map(
33
+ (component) =>
34
+ CircuitJsonManufacturingBuilder.#pickAndPlaceRow(
35
+ component,
36
+ index.sourceComponentById || new Map()
37
+ )
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Builds one placement row.
43
+ * @param {object} component PCB component element.
44
+ * @param {Map<string, object>} sourceComponentById Source lookup.
45
+ * @returns {object}
46
+ */
47
+ static #pickAndPlaceRow(component, sourceComponentById) {
48
+ const sourceId = String(component.source_component_id || '').trim()
49
+ const source = sourceComponentById.get(sourceId) || {}
50
+ const center = CircuitJsonUnits.optionalPoint(
51
+ component.center || component
52
+ ) || {
53
+ x: 0,
54
+ y: 0
55
+ }
56
+ const layer = CircuitJsonManufacturingBuilder.#layer(component.layer)
57
+
58
+ return {
59
+ designator: CircuitJsonManufacturingBuilder.#designator(
60
+ component,
61
+ source
62
+ ),
63
+ componentId: String(component.pcb_component_id || ''),
64
+ sourceComponentId: sourceId,
65
+ x: CircuitJsonManufacturingBuilder.#round(center.x),
66
+ y: CircuitJsonManufacturingBuilder.#round(center.y),
67
+ rotation: CircuitJsonUnits.angle(
68
+ component.rotation ?? component.ccw_rotation,
69
+ 0
70
+ ),
71
+ layer,
72
+ side: CircuitJsonManufacturingBuilder.#side(layer),
73
+ value: CircuitJsonManufacturingBuilder.#value(source),
74
+ package: String(
75
+ source.ftype || source.package || source.footprint || ''
76
+ ),
77
+ manufacturerPartNumber: String(
78
+ source.manufacturer_part_number ||
79
+ source.manufacturerPartNumber ||
80
+ ''
81
+ )
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Builds a compact routing exchange text payload.
87
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
88
+ * @returns {string}
89
+ */
90
+ static #routingDsn(index) {
91
+ const lines = ['(pcb assembly)', ' (unit mm)']
92
+ lines.push(...CircuitJsonManufacturingBuilder.#boardLines(index))
93
+ lines.push(...CircuitJsonManufacturingBuilder.#placementLines(index))
94
+ lines.push(...CircuitJsonManufacturingBuilder.#networkLines(index))
95
+ lines.push(')')
96
+ return lines.join('\n')
97
+ }
98
+
99
+ /**
100
+ * Builds board structure lines.
101
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
102
+ * @returns {string[]}
103
+ */
104
+ static #boardLines(index) {
105
+ const board = CircuitJsonManufacturingBuilder.#all(
106
+ index,
107
+ 'pcb_board'
108
+ )[0]
109
+ const center = CircuitJsonUnits.optionalPoint(
110
+ board?.center || board
111
+ ) || {
112
+ x: 0,
113
+ y: 0
114
+ }
115
+ const width = CircuitJsonUnits.length(board?.width, 0)
116
+ const height = CircuitJsonUnits.length(board?.height, 0)
117
+ const minX = CircuitJsonManufacturingBuilder.#round(
118
+ center.x - width / 2
119
+ )
120
+ const minY = CircuitJsonManufacturingBuilder.#round(
121
+ center.y - height / 2
122
+ )
123
+ const maxX = CircuitJsonManufacturingBuilder.#round(
124
+ center.x + width / 2
125
+ )
126
+ const maxY = CircuitJsonManufacturingBuilder.#round(
127
+ center.y + height / 2
128
+ )
129
+
130
+ return [
131
+ ' (structure',
132
+ ' (boundary (rect ' + [minX, minY, maxX, maxY].join(' ') + '))',
133
+ ...CircuitJsonManufacturingBuilder.#layers(board).map(
134
+ (layer) => ' (layer ' + layer + ' signal)'
135
+ ),
136
+ ' )'
137
+ ]
138
+ }
139
+
140
+ /**
141
+ * Builds component placement lines.
142
+ * @param {{ elementsByType?: Map<string, object[]>, sourceComponentById?: Map<string, object> }} index Element index.
143
+ * @returns {string[]}
144
+ */
145
+ static #placementLines(index) {
146
+ return [
147
+ ' (placement',
148
+ ...CircuitJsonManufacturingBuilder.#pickAndPlaceRows(index).map(
149
+ (row) =>
150
+ ' (component ' +
151
+ CircuitJsonManufacturingBuilder.#token(row.designator) +
152
+ ' (place ' +
153
+ [
154
+ row.x,
155
+ row.y,
156
+ row.side || row.layer || 'top',
157
+ row.rotation
158
+ ].join(' ') +
159
+ '))'
160
+ ),
161
+ ' )'
162
+ ]
163
+ }
164
+
165
+ /**
166
+ * Builds network lines for nets, pads, vias, and wires.
167
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
168
+ * @returns {string[]}
169
+ */
170
+ static #networkLines(index) {
171
+ const sourceNetNames =
172
+ CircuitJsonManufacturingBuilder.#sourceNetNames(index)
173
+ return [
174
+ ' (network',
175
+ ...CircuitJsonManufacturingBuilder.#netNames(index).flatMap(
176
+ (netName) => [
177
+ ' (net ' +
178
+ CircuitJsonManufacturingBuilder.#token(netName),
179
+ ...CircuitJsonManufacturingBuilder.#pinLines(
180
+ index,
181
+ netName,
182
+ sourceNetNames
183
+ ),
184
+ ...CircuitJsonManufacturingBuilder.#drillLines(
185
+ index,
186
+ netName,
187
+ sourceNetNames
188
+ ),
189
+ ...CircuitJsonManufacturingBuilder.#wireLines(
190
+ index,
191
+ netName,
192
+ sourceNetNames
193
+ ),
194
+ ' )'
195
+ ]
196
+ ),
197
+ ' )'
198
+ ]
199
+ }
200
+
201
+ /**
202
+ * Builds pad pin lines for one net.
203
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
204
+ * @param {string} netName Net name.
205
+ * @param {Map<string, string>} sourceNetNames Source net lookup.
206
+ * @returns {string[]}
207
+ */
208
+ static #pinLines(index, netName, sourceNetNames) {
209
+ return CircuitJsonManufacturingBuilder.#all(index, 'pcb_smtpad')
210
+ .filter(
211
+ (pad) =>
212
+ CircuitJsonManufacturingBuilder.#netName(
213
+ pad,
214
+ sourceNetNames
215
+ ) === netName
216
+ )
217
+ .map((pad) => {
218
+ const point = CircuitJsonUnits.optionalPoint(
219
+ pad.center || pad
220
+ ) || {
221
+ x: 0,
222
+ y: 0
223
+ }
224
+ return (
225
+ ' (pin ' +
226
+ CircuitJsonManufacturingBuilder.#token(
227
+ pad.pcb_smtpad_id || ''
228
+ ) +
229
+ ' ' +
230
+ [
231
+ CircuitJsonManufacturingBuilder.#round(point.x),
232
+ CircuitJsonManufacturingBuilder.#round(point.y)
233
+ ].join(' ') +
234
+ ')'
235
+ )
236
+ })
237
+ }
238
+
239
+ /**
240
+ * Builds drill feature lines for one net.
241
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
242
+ * @param {string} netName Net name.
243
+ * @param {Map<string, string>} sourceNetNames Source net lookup.
244
+ * @returns {string[]}
245
+ */
246
+ static #drillLines(index, netName, sourceNetNames) {
247
+ return [
248
+ ...CircuitJsonManufacturingBuilder.#all(index, 'pcb_via')
249
+ .filter(
250
+ (via) =>
251
+ CircuitJsonManufacturingBuilder.#netName(
252
+ via,
253
+ sourceNetNames
254
+ ) === netName
255
+ )
256
+ .map((via) =>
257
+ CircuitJsonManufacturingBuilder.#drillLine(via, 'via')
258
+ ),
259
+ ...CircuitJsonManufacturingBuilder.#all(index, 'pcb_plated_hole')
260
+ .filter(
261
+ (hole) =>
262
+ CircuitJsonManufacturingBuilder.#netName(
263
+ hole,
264
+ sourceNetNames
265
+ ) === netName
266
+ )
267
+ .map((hole) =>
268
+ CircuitJsonManufacturingBuilder.#drillLine(
269
+ hole,
270
+ 'plated_hole'
271
+ )
272
+ )
273
+ ].filter(Boolean)
274
+ }
275
+
276
+ /**
277
+ * Builds one drill feature line.
278
+ * @param {object} element Drill-bearing element.
279
+ * @param {'via' | 'plated_hole'} kind Drill line kind.
280
+ * @returns {string}
281
+ */
282
+ static #drillLine(element, kind) {
283
+ const point = CircuitJsonUnits.optionalPoint(element.center || element)
284
+ const layers = CircuitJsonManufacturingBuilder.#elementLayers(element)
285
+ return (
286
+ ' (' +
287
+ kind +
288
+ ' ' +
289
+ [
290
+ CircuitJsonManufacturingBuilder.#token(
291
+ element.pcb_via_id ||
292
+ element.pcb_plated_hole_id ||
293
+ element.pcb_hole_id ||
294
+ ''
295
+ ),
296
+ CircuitJsonManufacturingBuilder.#round(point?.x),
297
+ CircuitJsonManufacturingBuilder.#round(point?.y),
298
+ CircuitJsonManufacturingBuilder.#round(
299
+ CircuitJsonManufacturingBuilder.#holeDiameter(element)
300
+ ),
301
+ CircuitJsonManufacturingBuilder.#round(
302
+ CircuitJsonManufacturingBuilder.#outerDiameter(element)
303
+ ),
304
+ ...layers
305
+ ].join(' ') +
306
+ ')'
307
+ )
308
+ }
309
+
310
+ /**
311
+ * Builds routed wire lines for one net.
312
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
313
+ * @param {string} netName Net name.
314
+ * @param {Map<string, string>} sourceNetNames Source net lookup.
315
+ * @returns {string[]}
316
+ */
317
+ static #wireLines(index, netName, sourceNetNames) {
318
+ return CircuitJsonManufacturingBuilder.#all(index, 'pcb_trace')
319
+ .filter(
320
+ (trace) =>
321
+ CircuitJsonManufacturingBuilder.#netName(
322
+ trace,
323
+ sourceNetNames
324
+ ) === netName
325
+ )
326
+ .flatMap((trace) =>
327
+ CircuitJsonManufacturingBuilder.#traceWireLines(trace)
328
+ )
329
+ }
330
+
331
+ /**
332
+ * Builds routed wire lines for one trace.
333
+ * @param {object} trace Trace element.
334
+ * @returns {string[]}
335
+ */
336
+ static #traceWireLines(trace) {
337
+ const route = Array.isArray(trace.route) ? trace.route : []
338
+ const lines = []
339
+ let previous = null
340
+ for (const entry of route) {
341
+ const current = CircuitJsonUnits.optionalPoint(
342
+ entry.center || entry
343
+ )
344
+ if (!current) continue
345
+ if (previous) {
346
+ const layer = CircuitJsonManufacturingBuilder.#layer(
347
+ entry.layer || previous.layer || trace.layer
348
+ )
349
+ const width = CircuitJsonUnits.length(
350
+ entry.width ?? previous.width ?? trace.width,
351
+ 0
352
+ )
353
+ lines.push(
354
+ ' (wire ' +
355
+ [
356
+ layer || 'top',
357
+ CircuitJsonManufacturingBuilder.#round(previous.x),
358
+ CircuitJsonManufacturingBuilder.#round(previous.y),
359
+ CircuitJsonManufacturingBuilder.#round(current.x),
360
+ CircuitJsonManufacturingBuilder.#round(current.y),
361
+ CircuitJsonManufacturingBuilder.#round(width)
362
+ ].join(' ') +
363
+ ')'
364
+ )
365
+ }
366
+ previous = { ...entry, x: current.x, y: current.y }
367
+ }
368
+ return lines
369
+ }
370
+
371
+ /**
372
+ * Builds sorted net names.
373
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
374
+ * @returns {string[]}
375
+ */
376
+ static #netNames(index) {
377
+ const names = new Set()
378
+ const sourceNetNames =
379
+ CircuitJsonManufacturingBuilder.#sourceNetNames(index)
380
+ for (const name of sourceNetNames.values()) {
381
+ if (name) names.add(name)
382
+ }
383
+ for (const type of [
384
+ 'pcb_smtpad',
385
+ 'pcb_trace',
386
+ 'pcb_via',
387
+ 'pcb_plated_hole',
388
+ 'pcb_trace_hint',
389
+ 'pcb_breakout_point'
390
+ ]) {
391
+ for (const element of CircuitJsonManufacturingBuilder.#all(
392
+ index,
393
+ type
394
+ )) {
395
+ const name = CircuitJsonManufacturingBuilder.#netName(
396
+ element,
397
+ sourceNetNames
398
+ )
399
+ if (name) names.add(name)
400
+ }
401
+ }
402
+ return [...names].sort((left, right) => left.localeCompare(right))
403
+ }
404
+
405
+ /**
406
+ * Builds route guide metadata from hints and breakout points.
407
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
408
+ * @returns {object[]}
409
+ */
410
+ static #routingGuides(index) {
411
+ const sourceNetNames =
412
+ CircuitJsonManufacturingBuilder.#sourceNetNames(index)
413
+ return [
414
+ ...CircuitJsonManufacturingBuilder.#all(index, 'pcb_trace_hint')
415
+ .map((hint) =>
416
+ CircuitJsonManufacturingBuilder.#traceHintGuide(
417
+ hint,
418
+ sourceNetNames
419
+ )
420
+ )
421
+ .filter(Boolean),
422
+ ...CircuitJsonManufacturingBuilder.#all(index, 'pcb_breakout_point')
423
+ .map((point) =>
424
+ CircuitJsonManufacturingBuilder.#breakoutGuide(
425
+ point,
426
+ sourceNetNames
427
+ )
428
+ )
429
+ .filter(Boolean)
430
+ ]
431
+ }
432
+
433
+ /**
434
+ * Builds one trace hint guide.
435
+ * @param {object} hint Trace hint element.
436
+ * @param {Map<string, string>} sourceNetNames Source net lookup.
437
+ * @returns {object | null}
438
+ */
439
+ static #traceHintGuide(hint, sourceNetNames) {
440
+ const route = (Array.isArray(hint.route) ? hint.route : [])
441
+ .map((point) =>
442
+ CircuitJsonManufacturingBuilder.#routeGuidePoint(point)
443
+ )
444
+ .filter(Boolean)
445
+ if (!route.length) return null
446
+ return {
447
+ type: 'trace_hint',
448
+ id: String(hint.pcb_trace_hint_id || ''),
449
+ pcbComponentId: String(hint.pcb_component_id || ''),
450
+ pcbPortId: String(hint.pcb_port_id || ''),
451
+ sourceNetId: String(hint.source_net_id || ''),
452
+ netName: CircuitJsonManufacturingBuilder.#netName(
453
+ hint,
454
+ sourceNetNames
455
+ ),
456
+ subcircuitId: String(hint.subcircuit_id || ''),
457
+ route
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Builds one breakout guide.
463
+ * @param {object} breakout Breakout point element.
464
+ * @param {Map<string, string>} sourceNetNames Source net lookup.
465
+ * @returns {object | null}
466
+ */
467
+ static #breakoutGuide(breakout, sourceNetNames) {
468
+ const point = CircuitJsonUnits.optionalPoint(
469
+ breakout.center || breakout
470
+ )
471
+ if (!point) return null
472
+ return {
473
+ type: 'breakout_point',
474
+ id: String(breakout.pcb_breakout_point_id || ''),
475
+ pcbGroupId: String(breakout.pcb_group_id || ''),
476
+ sourceTraceId: String(breakout.source_trace_id || ''),
477
+ sourcePortId: String(breakout.source_port_id || ''),
478
+ sourceNetId: String(breakout.source_net_id || ''),
479
+ netName: CircuitJsonManufacturingBuilder.#netName(
480
+ breakout,
481
+ sourceNetNames
482
+ ),
483
+ subcircuitId: String(breakout.subcircuit_id || ''),
484
+ point: {
485
+ x: CircuitJsonManufacturingBuilder.#round(point.x),
486
+ y: CircuitJsonManufacturingBuilder.#round(point.y)
487
+ }
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Builds a normalized route guide point.
493
+ * @param {object} value Point row.
494
+ * @returns {{ x: number, y: number, layer: string } | null}
495
+ */
496
+ static #routeGuidePoint(value) {
497
+ const point = CircuitJsonUnits.optionalPoint(value?.center || value)
498
+ if (!point) return null
499
+ return {
500
+ x: CircuitJsonManufacturingBuilder.#round(point.x),
501
+ y: CircuitJsonManufacturingBuilder.#round(point.y),
502
+ layer: CircuitJsonManufacturingBuilder.#layer(value?.layer)
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Builds fabrication note metadata.
508
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
509
+ * @returns {object[]}
510
+ */
511
+ static #fabricationNotes(index) {
512
+ return [
513
+ ...CircuitJsonManufacturingBuilder.#all(
514
+ index,
515
+ 'pcb_fabrication_note_text'
516
+ ).map((note) =>
517
+ CircuitJsonManufacturingBuilder.#fabricationTextNote(note)
518
+ ),
519
+ ...CircuitJsonManufacturingBuilder.#all(
520
+ index,
521
+ 'pcb_fabrication_note_path'
522
+ ).map((note) =>
523
+ CircuitJsonManufacturingBuilder.#fabricationPathNote(note)
524
+ ),
525
+ ...CircuitJsonManufacturingBuilder.#all(
526
+ index,
527
+ 'pcb_fabrication_note_rect'
528
+ ).map((note) =>
529
+ CircuitJsonManufacturingBuilder.#fabricationRectNote(note)
530
+ ),
531
+ ...CircuitJsonManufacturingBuilder.#all(
532
+ index,
533
+ 'pcb_fabrication_note_dimension'
534
+ ).map((note) =>
535
+ CircuitJsonManufacturingBuilder.#fabricationDimensionNote(note)
536
+ )
537
+ ].filter(Boolean)
538
+ }
539
+
540
+ /**
541
+ * Builds one fabrication text note row.
542
+ * @param {object} note Fabrication text element.
543
+ * @returns {object | null}
544
+ */
545
+ static #fabricationTextNote(note) {
546
+ const anchor = CircuitJsonUnits.optionalPoint(
547
+ note.anchor_position || note.center || note
548
+ ) || { x: 0, y: 0 }
549
+ return {
550
+ ...CircuitJsonManufacturingBuilder.#fabricationBaseNote(
551
+ note,
552
+ 'text'
553
+ ),
554
+ text: String(note.text || ''),
555
+ anchor: {
556
+ x: CircuitJsonManufacturingBuilder.#round(anchor.x),
557
+ y: CircuitJsonManufacturingBuilder.#round(anchor.y)
558
+ },
559
+ rotation: CircuitJsonUnits.angle(note.ccw_rotation, 0),
560
+ fontSize: CircuitJsonUnits.length(note.font_size, 1),
561
+ color: String(note.color || '')
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Builds one fabrication path note row.
567
+ * @param {object} note Fabrication path element.
568
+ * @returns {object | null}
569
+ */
570
+ static #fabricationPathNote(note) {
571
+ const route = (Array.isArray(note.route) ? note.route : [])
572
+ .map((point) => CircuitJsonUnits.optionalPoint(point))
573
+ .filter(Boolean)
574
+ .map((point) => ({
575
+ x: CircuitJsonManufacturingBuilder.#round(point.x),
576
+ y: CircuitJsonManufacturingBuilder.#round(point.y)
577
+ }))
578
+ if (!route.length) return null
579
+ return {
580
+ ...CircuitJsonManufacturingBuilder.#fabricationBaseNote(
581
+ note,
582
+ 'path'
583
+ ),
584
+ route,
585
+ strokeWidth: CircuitJsonUnits.length(note.stroke_width, 0),
586
+ color: String(note.color || '')
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Builds one fabrication rectangle note row.
592
+ * @param {object} note Fabrication rectangle element.
593
+ * @returns {object | null}
594
+ */
595
+ static #fabricationRectNote(note) {
596
+ const center = CircuitJsonUnits.optionalPoint(note.center || note)
597
+ if (!center) return null
598
+ return {
599
+ ...CircuitJsonManufacturingBuilder.#fabricationBaseNote(
600
+ note,
601
+ 'rect'
602
+ ),
603
+ center: {
604
+ x: CircuitJsonManufacturingBuilder.#round(center.x),
605
+ y: CircuitJsonManufacturingBuilder.#round(center.y)
606
+ },
607
+ width: CircuitJsonUnits.length(note.width, 0),
608
+ height: CircuitJsonUnits.length(note.height, 0),
609
+ strokeWidth: CircuitJsonUnits.length(note.stroke_width, 0.1),
610
+ cornerRadius: CircuitJsonUnits.length(note.corner_radius, 0),
611
+ isFilled: note.is_filled === true,
612
+ hasStroke: note.has_stroke !== false,
613
+ isStrokeDashed: note.is_stroke_dashed === true,
614
+ color: String(note.color || '')
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Builds one fabrication dimension note row.
620
+ * @param {object} note Fabrication dimension element.
621
+ * @returns {object | null}
622
+ */
623
+ static #fabricationDimensionNote(note) {
624
+ const from = CircuitJsonUnits.optionalPoint(note.from)
625
+ const to = CircuitJsonUnits.optionalPoint(note.to)
626
+ if (!from || !to) return null
627
+ return {
628
+ ...CircuitJsonManufacturingBuilder.#fabricationBaseNote(
629
+ note,
630
+ 'dimension'
631
+ ),
632
+ from: {
633
+ x: CircuitJsonManufacturingBuilder.#round(from.x),
634
+ y: CircuitJsonManufacturingBuilder.#round(from.y)
635
+ },
636
+ to: {
637
+ x: CircuitJsonManufacturingBuilder.#round(to.x),
638
+ y: CircuitJsonManufacturingBuilder.#round(to.y)
639
+ },
640
+ text: String(note.text || ''),
641
+ offset: CircuitJsonUnits.length(note.offset, 0),
642
+ offsetDistance: CircuitJsonUnits.length(note.offset_distance, 0),
643
+ offsetDirection: CircuitJsonManufacturingBuilder.#offsetDirection(
644
+ note.offset_direction
645
+ ),
646
+ rotation: CircuitJsonUnits.angle(
647
+ note.text_ccw_rotation ?? note.ccw_rotation,
648
+ 0
649
+ ),
650
+ fontSize: CircuitJsonUnits.length(note.font_size, 1),
651
+ arrowSize: CircuitJsonUnits.length(note.arrow_size, 1),
652
+ color: String(note.color || '')
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Builds common fabrication note metadata.
658
+ * @param {object} note Fabrication note element.
659
+ * @param {string} type Note type.
660
+ * @returns {object}
661
+ */
662
+ static #fabricationBaseNote(note, type) {
663
+ return {
664
+ type,
665
+ elementType: String(note.type || ''),
666
+ id: CircuitJsonIndexer.getElementId(note),
667
+ pcbComponentId: String(note.pcb_component_id || ''),
668
+ pcbGroupId: String(note.pcb_group_id || ''),
669
+ subcircuitId: String(note.subcircuit_id || ''),
670
+ layer: CircuitJsonManufacturingBuilder.#layer(note.layer)
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Normalizes an offset direction row.
676
+ * @param {object | null | undefined} direction Direction row.
677
+ * @returns {{ x: number, y: number } | null}
678
+ */
679
+ static #offsetDirection(direction) {
680
+ const x = Number(direction?.x)
681
+ const y = Number(direction?.y)
682
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null
683
+ return {
684
+ x: CircuitJsonManufacturingBuilder.#round(x),
685
+ y: CircuitJsonManufacturingBuilder.#round(y)
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Builds source net id to display name lookup.
691
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
692
+ * @returns {Map<string, string>}
693
+ */
694
+ static #sourceNetNames(index) {
695
+ return new Map(
696
+ CircuitJsonManufacturingBuilder.#all(index, 'source_net').map(
697
+ (net) => {
698
+ const id = String(net.source_net_id || '').trim()
699
+ const name = String(net.name || id).trim()
700
+ return [id, name]
701
+ }
702
+ )
703
+ )
704
+ }
705
+
706
+ /**
707
+ * Resolves layer names from a drill-bearing element.
708
+ * @param {object} element Element row.
709
+ * @returns {string[]}
710
+ */
711
+ static #elementLayers(element) {
712
+ const values = Array.isArray(element.layers)
713
+ ? element.layers
714
+ : [element.from_layer, element.to_layer, element.layer]
715
+ const layers = values
716
+ .map((value) => CircuitJsonManufacturingBuilder.#layer(value))
717
+ .filter(Boolean)
718
+ return CircuitJsonManufacturingBuilder.#uniqueStrings(
719
+ layers.length ? layers : ['top', 'bottom']
720
+ )
721
+ }
722
+
723
+ /**
724
+ * Resolves drill hole diameter.
725
+ * @param {object} element Element row.
726
+ * @returns {number}
727
+ */
728
+ static #holeDiameter(element) {
729
+ return CircuitJsonManufacturingBuilder.#maxLength([
730
+ element.hole_diameter,
731
+ element.hole_width,
732
+ element.hole_height
733
+ ])
734
+ }
735
+
736
+ /**
737
+ * Resolves drill outer diameter.
738
+ * @param {object} element Element row.
739
+ * @returns {number}
740
+ */
741
+ static #outerDiameter(element) {
742
+ return CircuitJsonManufacturingBuilder.#maxLength([
743
+ element.outer_diameter,
744
+ element.outer_width,
745
+ element.outer_height,
746
+ element.rect_pad_width,
747
+ element.rect_pad_height
748
+ ])
749
+ }
750
+
751
+ /**
752
+ * Returns the largest valid length in a list.
753
+ * @param {unknown[]} values Candidate values.
754
+ * @returns {number}
755
+ */
756
+ static #maxLength(values) {
757
+ const lengths = values
758
+ .map((value) => CircuitJsonUnits.optionalLength(value))
759
+ .filter((value) => value !== null)
760
+ return lengths.length ? Math.max(...lengths) : 0
761
+ }
762
+
763
+ /**
764
+ * Returns unique strings in input order.
765
+ * @param {string[]} values Candidate strings.
766
+ * @returns {string[]}
767
+ */
768
+ static #uniqueStrings(values) {
769
+ return [
770
+ ...new Set(values.map((value) => String(value || '').trim()))
771
+ ].filter(Boolean)
772
+ }
773
+
774
+ /**
775
+ * Resolves layer names from board metadata.
776
+ * @param {object | undefined} board Board element.
777
+ * @returns {string[]}
778
+ */
779
+ static #layers(board) {
780
+ const count = Math.max(1, Math.round(Number(board?.num_layers || 2)))
781
+ if (count === 1) return ['top']
782
+ return [
783
+ 'top',
784
+ ...Array.from(
785
+ { length: Math.max(count - 2, 0) },
786
+ (_entry, index) => 'inner' + (index + 1)
787
+ ),
788
+ 'bottom'
789
+ ]
790
+ }
791
+
792
+ /**
793
+ * Resolves rows by type.
794
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
795
+ * @param {string} type Element type.
796
+ * @returns {object[]}
797
+ */
798
+ static #all(index, type) {
799
+ return index.elementsByType?.get(type) || []
800
+ }
801
+
802
+ /**
803
+ * Resolves a component designator.
804
+ * @param {object} component PCB component.
805
+ * @param {object} source Source component.
806
+ * @returns {string}
807
+ */
808
+ static #designator(component, source) {
809
+ return String(
810
+ source.name ||
811
+ source.reference ||
812
+ source.designator ||
813
+ component.name ||
814
+ component.pcb_component_id ||
815
+ ''
816
+ ).trim()
817
+ }
818
+
819
+ /**
820
+ * Resolves a source value field.
821
+ * @param {object} source Source component.
822
+ * @returns {string}
823
+ */
824
+ static #value(source) {
825
+ return String(
826
+ source.value ||
827
+ source.resistance ||
828
+ source.capacitance ||
829
+ source.inductance ||
830
+ ''
831
+ )
832
+ }
833
+
834
+ /**
835
+ * Resolves a normalized net name.
836
+ * @param {object} element Element row.
837
+ * @param {Map<string, string>} [sourceNetNames] Source net lookup.
838
+ * @returns {string}
839
+ */
840
+ static #netName(element, sourceNetNames = new Map()) {
841
+ const sourceNetId = String(element?.source_net_id || '').trim()
842
+ return String(
843
+ element?.netName ??
844
+ element?.net ??
845
+ element?.net_name ??
846
+ element?.source_net_name ??
847
+ sourceNetNames.get(sourceNetId) ??
848
+ ''
849
+ ).trim()
850
+ }
851
+
852
+ /**
853
+ * Resolves a layer string.
854
+ * @param {unknown} value Layer candidate.
855
+ * @returns {string}
856
+ */
857
+ static #layer(value) {
858
+ const raw =
859
+ typeof value === 'object' && value !== null ? value.name : value
860
+ const text = String(raw ?? '').trim()
861
+ const lowered = text.toLowerCase()
862
+ if (['front', 'f.cu', '1'].includes(lowered)) return 'top'
863
+ if (['back', 'b.cu', '32'].includes(lowered)) return 'bottom'
864
+ return text || 'top'
865
+ }
866
+
867
+ /**
868
+ * Resolves an assembly side.
869
+ * @param {string} layer Layer name.
870
+ * @returns {'top' | 'bottom' | ''}
871
+ */
872
+ static #side(layer) {
873
+ const text = String(layer || '').toLowerCase()
874
+ if (/\b(bottom|back)\b|\bb[._-]/u.test(text)) return 'bottom'
875
+ if (/\b(top|front)\b|\bf[._-]/u.test(text)) return 'top'
876
+ return ''
877
+ }
878
+
879
+ /**
880
+ * Builds a DSN-safe token.
881
+ * @param {unknown} value Raw value.
882
+ * @returns {string}
883
+ */
884
+ static #token(value) {
885
+ return String(value || 'unnamed').replace(/[^A-Za-z0-9_.:-]+/gu, '_')
886
+ }
887
+
888
+ /**
889
+ * Rounds a numeric value for deterministic output.
890
+ * @param {number} value Number.
891
+ * @returns {number}
892
+ */
893
+ static #round(value) {
894
+ const number = Number(value)
895
+ if (!Number.isFinite(number)) return 0
896
+ return Number(number.toFixed(6))
897
+ }
898
+ }