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.
Files changed (40) hide show
  1. package/AGENTS.md +5 -3
  2. package/docs/model-format.md +15 -0
  3. package/package.json +3 -2
  4. package/src/core/CircuitJsonBomBuilder.mjs +22 -25
  5. package/src/core/CircuitJsonElementValidator.mjs +233 -16
  6. package/src/core/CircuitJsonIndexer.mjs +510 -5
  7. package/src/core/CircuitJsonManufacturingBuilder.mjs +488 -16
  8. package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
  9. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
  10. package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
  11. package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
  12. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
  13. package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
  14. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
  15. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
  16. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
  17. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
  18. package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
  19. package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
  20. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
  21. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
  22. package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
  23. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
  24. package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
  25. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +227 -5
  26. package/src/core/PcbBoundsSelectionModel.mjs +250 -0
  27. package/src/core/PcbCandidateSelectionModel.mjs +77 -0
  28. package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
  29. package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
  30. package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
  31. package/src/index.mjs +2 -0
  32. package/src/renderers.mjs +29 -0
  33. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
  34. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
  35. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
  36. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
  37. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
  38. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
  39. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
  40. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
@@ -9,13 +9,17 @@ export class CircuitJsonManufacturingBuilder {
9
9
  * Builds pick-and-place rows and routing exchange text.
10
10
  * @param {object[]} circuitJson Parsed element array.
11
11
  * @param {{ elementsByType?: Map<string, object[]>, sourceComponentById?: Map<string, object> }} [index] Optional index.
12
- * @returns {{ pickAndPlaceRows: object[], routingDsn: string }}
12
+ * @returns {{ pickAndPlaceRows: object[], routingDsn: string, routingGuides: object[], fabricationNotes: object[] }}
13
13
  */
14
14
  static build(circuitJson, index = CircuitJsonIndexer.index(circuitJson)) {
15
15
  return {
16
16
  pickAndPlaceRows:
17
17
  CircuitJsonManufacturingBuilder.#pickAndPlaceRows(index),
18
- routingDsn: CircuitJsonManufacturingBuilder.#routingDsn(index)
18
+ routingDsn: CircuitJsonManufacturingBuilder.#routingDsn(index),
19
+ routingGuides:
20
+ CircuitJsonManufacturingBuilder.#routingGuides(index),
21
+ fabricationNotes:
22
+ CircuitJsonManufacturingBuilder.#fabricationNotes(index)
19
23
  }
20
24
  }
21
25
 
@@ -164,6 +168,8 @@ export class CircuitJsonManufacturingBuilder {
164
168
  * @returns {string[]}
165
169
  */
166
170
  static #networkLines(index) {
171
+ const sourceNetNames =
172
+ CircuitJsonManufacturingBuilder.#sourceNetNames(index)
167
173
  return [
168
174
  ' (network',
169
175
  ...CircuitJsonManufacturingBuilder.#netNames(index).flatMap(
@@ -172,11 +178,18 @@ export class CircuitJsonManufacturingBuilder {
172
178
  CircuitJsonManufacturingBuilder.#token(netName),
173
179
  ...CircuitJsonManufacturingBuilder.#pinLines(
174
180
  index,
175
- netName
181
+ netName,
182
+ sourceNetNames
183
+ ),
184
+ ...CircuitJsonManufacturingBuilder.#drillLines(
185
+ index,
186
+ netName,
187
+ sourceNetNames
176
188
  ),
177
189
  ...CircuitJsonManufacturingBuilder.#wireLines(
178
190
  index,
179
- netName
191
+ netName,
192
+ sourceNetNames
180
193
  ),
181
194
  ' )'
182
195
  ]
@@ -189,13 +202,17 @@ export class CircuitJsonManufacturingBuilder {
189
202
  * Builds pad pin lines for one net.
190
203
  * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
191
204
  * @param {string} netName Net name.
205
+ * @param {Map<string, string>} sourceNetNames Source net lookup.
192
206
  * @returns {string[]}
193
207
  */
194
- static #pinLines(index, netName) {
208
+ static #pinLines(index, netName, sourceNetNames) {
195
209
  return CircuitJsonManufacturingBuilder.#all(index, 'pcb_smtpad')
196
210
  .filter(
197
211
  (pad) =>
198
- CircuitJsonManufacturingBuilder.#netName(pad) === netName
212
+ CircuitJsonManufacturingBuilder.#netName(
213
+ pad,
214
+ sourceNetNames
215
+ ) === netName
199
216
  )
200
217
  .map((pad) => {
201
218
  const point = CircuitJsonUnits.optionalPoint(
@@ -219,17 +236,92 @@ export class CircuitJsonManufacturingBuilder {
219
236
  })
220
237
  }
221
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
+
222
310
  /**
223
311
  * Builds routed wire lines for one net.
224
312
  * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
225
313
  * @param {string} netName Net name.
314
+ * @param {Map<string, string>} sourceNetNames Source net lookup.
226
315
  * @returns {string[]}
227
316
  */
228
- static #wireLines(index, netName) {
317
+ static #wireLines(index, netName, sourceNetNames) {
229
318
  return CircuitJsonManufacturingBuilder.#all(index, 'pcb_trace')
230
319
  .filter(
231
320
  (trace) =>
232
- CircuitJsonManufacturingBuilder.#netName(trace) === netName
321
+ CircuitJsonManufacturingBuilder.#netName(
322
+ trace,
323
+ sourceNetNames
324
+ ) === netName
233
325
  )
234
326
  .flatMap((trace) =>
235
327
  CircuitJsonManufacturingBuilder.#traceWireLines(trace)
@@ -283,25 +375,402 @@ export class CircuitJsonManufacturingBuilder {
283
375
  */
284
376
  static #netNames(index) {
285
377
  const names = new Set()
286
- for (const net of CircuitJsonManufacturingBuilder.#all(
287
- index,
288
- 'source_net'
289
- )) {
290
- const name = String(net.name || net.source_net_id || '').trim()
378
+ const sourceNetNames =
379
+ CircuitJsonManufacturingBuilder.#sourceNetNames(index)
380
+ for (const name of sourceNetNames.values()) {
291
381
  if (name) names.add(name)
292
382
  }
293
- for (const type of ['pcb_smtpad', 'pcb_trace', 'pcb_via']) {
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
+ ]) {
294
391
  for (const element of CircuitJsonManufacturingBuilder.#all(
295
392
  index,
296
393
  type
297
394
  )) {
298
- const name = CircuitJsonManufacturingBuilder.#netName(element)
395
+ const name = CircuitJsonManufacturingBuilder.#netName(
396
+ element,
397
+ sourceNetNames
398
+ )
299
399
  if (name) names.add(name)
300
400
  }
301
401
  }
302
402
  return [...names].sort((left, right) => left.localeCompare(right))
303
403
  }
304
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
+
305
774
  /**
306
775
  * Resolves layer names from board metadata.
307
776
  * @param {object | undefined} board Board element.
@@ -365,14 +834,17 @@ export class CircuitJsonManufacturingBuilder {
365
834
  /**
366
835
  * Resolves a normalized net name.
367
836
  * @param {object} element Element row.
837
+ * @param {Map<string, string>} [sourceNetNames] Source net lookup.
368
838
  * @returns {string}
369
839
  */
370
- static #netName(element) {
840
+ static #netName(element, sourceNetNames = new Map()) {
841
+ const sourceNetId = String(element?.source_net_id || '').trim()
371
842
  return String(
372
843
  element?.netName ??
373
844
  element?.net ??
374
845
  element?.net_name ??
375
846
  element?.source_net_name ??
847
+ sourceNetNames.get(sourceNetId) ??
376
848
  ''
377
849
  ).trim()
378
850
  }