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,872 @@
1
+ import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
2
+ import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
3
+ import { CircuitJsonPcbPrimitiveGeometry } from './CircuitJsonPcbPrimitiveGeometry.mjs'
4
+ import { CircuitJsonPcbPrimitiveOverlays } from './CircuitJsonPcbPrimitiveOverlays.mjs'
5
+ import { CircuitJsonPcbPrimitiveArtwork } from './CircuitJsonPcbPrimitiveArtwork.mjs'
6
+ import { CircuitJsonPcbPrimitiveGroups } from './CircuitJsonPcbPrimitiveGroups.mjs'
7
+ import { CircuitJsonPcbZonePrimitiveBuilder } from './CircuitJsonPcbZonePrimitiveBuilder.mjs'
8
+ import { CircuitJsonPcbHolePrimitiveModel } from './CircuitJsonPcbHolePrimitiveModel.mjs'
9
+ import { CircuitJsonPcbPadPrimitiveModel } from './CircuitJsonPcbPadPrimitiveModel.mjs'
10
+ import { CircuitJsonPcbPrimitiveFields } from './CircuitJsonPcbPrimitiveFields.mjs'
11
+ import { CircuitJsonPcbPrimitiveIndex } from './CircuitJsonPcbPrimitiveIndex.mjs'
12
+ import { CircuitJsonPcbNetMetadata } from './CircuitJsonPcbNetMetadata.mjs'
13
+ import { CircuitJsonPcbDrawingStyle } from './CircuitJsonPcbDrawingStyle.mjs'
14
+
15
+ /**
16
+ * Builds renderer-neutral PCB primitives from standards-native element arrays.
17
+ */
18
+ export class CircuitJsonPcbPrimitiveBuilder {
19
+ /**
20
+ * Builds a normalized PCB primitive model.
21
+ * @param {object | object[]} documentModel Parsed document model.
22
+ * @returns {{ bounds: object, layers: object[], virtualLayers: object[], components: object[], nets: object[], primitives: object[], anchors: object[], diagnostics: object[], airwires: object[], traceLengths: object[], groups: object[], anchorOffsets: object[] }}
23
+ */
24
+ static build(documentModel) {
25
+ const elements = CircuitJsonPcbPrimitiveBuilder.elements(documentModel)
26
+ const index = CircuitJsonPcbPrimitiveIndex.build(elements)
27
+ const boards = CircuitJsonPcbPrimitiveBuilder.#all(index, 'pcb_board')
28
+ const board = boards[0] || null
29
+ const components =
30
+ CircuitJsonPcbPrimitiveBuilder.#componentLookups(index)
31
+ const areaModel = CircuitJsonPcbZonePrimitiveBuilder.build(
32
+ index,
33
+ components.byPcbId
34
+ )
35
+ const primitiveRows = [
36
+ ...CircuitJsonPcbPrimitiveBuilder.#boardPrimitives(boards),
37
+ ...CircuitJsonPcbPrimitiveBuilder.#padPrimitives(
38
+ index,
39
+ components.byPcbId
40
+ ),
41
+ ...CircuitJsonPcbPrimitiveBuilder.#tracePrimitives(
42
+ index,
43
+ components.byPcbId
44
+ ),
45
+ ...CircuitJsonPcbPrimitiveBuilder.#viaPrimitives(
46
+ index,
47
+ components.byPcbId
48
+ ),
49
+ ...areaModel.primitives,
50
+ ...CircuitJsonPcbPrimitiveBuilder.#silkscreenPrimitives(
51
+ index,
52
+ components.byPcbId
53
+ ),
54
+ ...CircuitJsonPcbPrimitiveArtwork.cutoutPrimitives(index),
55
+ ...CircuitJsonPcbPrimitiveArtwork.build(index, components.byPcbId)
56
+ ].filter(Boolean)
57
+ const groupModel = CircuitJsonPcbPrimitiveGroups.build(
58
+ index,
59
+ primitiveRows,
60
+ components.rows
61
+ )
62
+ const primitives = CircuitJsonPcbNetMetadata.decoratePrimitives(
63
+ groupModel.primitives,
64
+ index
65
+ )
66
+ const bounds =
67
+ CircuitJsonPcbPrimitiveFields.mergedBoardBounds(boards) ||
68
+ CircuitJsonPcbPrimitiveGeometry.mergedPrimitiveBounds(primitives) ||
69
+ CircuitJsonPcbPrimitiveGeometry.bounds(0, 0, 1, 1)
70
+ const overlays = CircuitJsonPcbPrimitiveOverlays.build(
71
+ index,
72
+ components.byPcbId,
73
+ primitives,
74
+ bounds,
75
+ groupModel,
76
+ [
77
+ ...CircuitJsonPcbPrimitiveBuilder.#generatedDiagnostics(index),
78
+ ...areaModel.diagnostics
79
+ ]
80
+ )
81
+
82
+ return {
83
+ bounds,
84
+ layers: CircuitJsonPcbPrimitiveFields.layers(board),
85
+ virtualLayers: overlays.virtualLayers,
86
+ components: components.rows,
87
+ nets: CircuitJsonPcbNetMetadata.nets(primitives, index),
88
+ primitives,
89
+ anchors: primitives.flatMap((primitive) =>
90
+ primitive.anchors.map((anchor) => ({ ...anchor, primitive }))
91
+ ),
92
+ diagnostics: overlays.diagnostics,
93
+ airwires: overlays.airwires,
94
+ traceLengths: CircuitJsonPcbPrimitiveArtwork.traceLengths(
95
+ primitives,
96
+ index
97
+ ),
98
+ groups: groupModel.groups,
99
+ anchorOffsets: groupModel.anchorOffsets
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Returns element rows from an array or wrapper object.
105
+ * @param {object | object[]} documentModel Parsed document model.
106
+ * @returns {object[]}
107
+ */
108
+ static elements(documentModel) {
109
+ if (Array.isArray(documentModel)) return documentModel
110
+ if (Array.isArray(documentModel?.elements))
111
+ return documentModel.elements
112
+ if (Array.isArray(documentModel?.circuitJson))
113
+ return documentModel.circuitJson
114
+ return []
115
+ }
116
+
117
+ /**
118
+ * Returns indexed element rows by type.
119
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
120
+ * @param {string} type Element type.
121
+ * @returns {object[]}
122
+ */
123
+ static #all(index, type) {
124
+ return index.elementsByType.get(type) || []
125
+ }
126
+
127
+ /**
128
+ * Returns generated diagnostics from the shared element index.
129
+ * @param {{ diagnostics?: object[] }} index Element index.
130
+ * @returns {object[]}
131
+ */
132
+ static #generatedDiagnostics(index) {
133
+ return (
134
+ Array.isArray(index?.diagnostics) ? index.diagnostics : []
135
+ ).filter((diagnostic) => diagnostic?.isGenerated === true)
136
+ }
137
+
138
+ /**
139
+ * Builds component rows and PCB component lookup maps.
140
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
141
+ * @returns {{ rows: object[], byPcbId: Map<string, object> }}
142
+ */
143
+ static #componentLookups(index) {
144
+ const sourceRows = CircuitJsonPcbPrimitiveBuilder.#all(
145
+ index,
146
+ 'source_component'
147
+ )
148
+ const sourceNames = new Map(
149
+ sourceRows.map((element) => [
150
+ String(element.source_component_id || '').trim(),
151
+ String(
152
+ element.name ||
153
+ element.reference ||
154
+ element.designator ||
155
+ element.source_component_id ||
156
+ ''
157
+ ).trim()
158
+ ])
159
+ )
160
+ const sourceMetadata = new Map(
161
+ sourceRows.map((element) => [
162
+ String(element.source_component_id || '').trim(),
163
+ {
164
+ groupId: String(element.source_group_id || '').trim(),
165
+ subcircuitIds: CircuitJsonPcbPrimitiveFields.uniqueStrings([
166
+ element.subcircuit_id,
167
+ element.subcircuitId
168
+ ])
169
+ }
170
+ ])
171
+ )
172
+ const rows = []
173
+ const byPcbId = new Map()
174
+
175
+ for (const element of CircuitJsonPcbPrimitiveBuilder.#all(
176
+ index,
177
+ 'pcb_component'
178
+ )) {
179
+ const sourceId = String(element.source_component_id || '').trim()
180
+ const pcbId = String(element.pcb_component_id || '').trim()
181
+ const center = CircuitJsonPcbPrimitiveFields.center(element) || {
182
+ x: 0,
183
+ y: 0
184
+ }
185
+ const componentKey = String(
186
+ sourceNames.get(sourceId) ||
187
+ element.name ||
188
+ element.reference ||
189
+ element.designator ||
190
+ pcbId ||
191
+ 'Component ' + (rows.length + 1)
192
+ ).trim()
193
+ const source = sourceMetadata.get(sourceId) || {}
194
+ const component = {
195
+ ...element,
196
+ componentKey,
197
+ designator: componentKey,
198
+ key: componentKey,
199
+ pcbComponentId: pcbId,
200
+ sourceComponentId: sourceId,
201
+ sourceGroupId: String(
202
+ element.source_group_id || source.groupId || ''
203
+ ).trim(),
204
+ x: center.x,
205
+ y: center.y,
206
+ layer: CircuitJsonPcbPrimitiveFields.layer(element.layer),
207
+ rotation: CircuitJsonUnits.angle(element.rotation, 0),
208
+ groupIds: CircuitJsonPcbPrimitiveFields.uniqueStrings([
209
+ element.pcb_group_id,
210
+ element.positioned_relative_to_pcb_group_id,
211
+ element.source_group_id,
212
+ source.groupId
213
+ ]),
214
+ subcircuitIds: CircuitJsonPcbPrimitiveFields.uniqueStrings([
215
+ element.subcircuit_id,
216
+ element.subcircuitId,
217
+ ...(source.subcircuitIds || [])
218
+ ])
219
+ }
220
+ rows.push(component)
221
+ if (pcbId) byPcbId.set(pcbId, component)
222
+ }
223
+
224
+ return { rows, byPcbId }
225
+ }
226
+
227
+ /**
228
+ * Builds board primitives.
229
+ * @param {object[]} boards Board elements.
230
+ * @returns {object[]}
231
+ */
232
+ static #boardPrimitives(boards) {
233
+ return boards
234
+ .map((board) =>
235
+ CircuitJsonPcbPrimitiveBuilder.#boardPrimitive(board)
236
+ )
237
+ .filter(Boolean)
238
+ }
239
+
240
+ /**
241
+ * Builds a board primitive.
242
+ * @param {object | null} board Board element.
243
+ * @returns {object | null}
244
+ */
245
+ static #boardPrimitive(board) {
246
+ const bounds = CircuitJsonPcbPrimitiveFields.boardBounds(board)
247
+ if (!bounds) return null
248
+
249
+ return {
250
+ id: String(board?.pcb_board_id || 'board'),
251
+ kind: 'board',
252
+ layer: 'board',
253
+ side: '',
254
+ points: CircuitJsonPcbPrimitiveFields.points(board),
255
+ bounds,
256
+ anchors: CircuitJsonPcbPrimitiveGeometry.cornerAnchors(bounds),
257
+ source: board || {}
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Builds SMT pad primitives.
263
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
264
+ * @param {Map<string, object>} componentsByPcbId Component lookup.
265
+ * @returns {object[]}
266
+ */
267
+ static #padPrimitives(index, componentsByPcbId) {
268
+ return CircuitJsonPcbPrimitiveBuilder.#all(index, 'pcb_smtpad')
269
+ .map((element) =>
270
+ CircuitJsonPcbPrimitiveBuilder.#padPrimitive(
271
+ element,
272
+ componentsByPcbId
273
+ )
274
+ )
275
+ .filter(Boolean)
276
+ }
277
+
278
+ /**
279
+ * Builds one SMT pad primitive.
280
+ * @param {object} element Pad element.
281
+ * @param {Map<string, object>} componentsByPcbId Component lookup.
282
+ * @returns {object | null}
283
+ */
284
+ static #padPrimitive(element, componentsByPcbId) {
285
+ const shape = CircuitJsonPcbPadPrimitiveModel.shape(element)
286
+ const points =
287
+ shape === 'polygon'
288
+ ? CircuitJsonPcbPrimitiveFields.points(element)
289
+ : []
290
+ const center =
291
+ CircuitJsonPcbPrimitiveFields.center(element) ||
292
+ CircuitJsonPcbPrimitiveFields.pointsCenter(points)
293
+ if (!center) return null
294
+
295
+ const explicitRadius =
296
+ CircuitJsonPcbPadPrimitiveModel.explicitRadius(element)
297
+ const diameter =
298
+ CircuitJsonUnits.optionalLength(element.diameter) ??
299
+ (explicitRadius === null ? null : explicitRadius * 2)
300
+ const width = CircuitJsonUnits.optionalLength(element.width) ?? diameter
301
+ const height =
302
+ CircuitJsonUnits.optionalLength(element.height) ?? diameter ?? width
303
+ if (!points.length && (width === null || height === null)) return null
304
+ const radius = CircuitJsonPcbPadPrimitiveModel.radius(
305
+ element,
306
+ shape,
307
+ width ?? 0,
308
+ height ?? 0
309
+ )
310
+
311
+ const component = componentsByPcbId.get(
312
+ String(element.pcb_component_id || '').trim()
313
+ )
314
+ const layer = CircuitJsonPcbPrimitiveFields.layer(element.layer)
315
+ const bounds = points.length
316
+ ? CircuitJsonPcbPrimitiveGeometry.pointsBounds(points)
317
+ : CircuitJsonPcbPrimitiveGeometry.centerBounds(
318
+ center,
319
+ width,
320
+ height
321
+ )
322
+
323
+ return CircuitJsonPcbPrimitiveBuilder.#primitive({
324
+ id: String(element.pcb_smtpad_id || ''),
325
+ kind: 'pad',
326
+ shape,
327
+ x: center.x,
328
+ y: center.y,
329
+ width: width ?? bounds.width,
330
+ height: height ?? bounds.height,
331
+ radius,
332
+ rotation: CircuitJsonUnits.angle(
333
+ element.ccw_rotation ?? element.rotation,
334
+ 0
335
+ ),
336
+ points,
337
+ bounds,
338
+ layer,
339
+ component,
340
+ netName: CircuitJsonPcbPrimitiveFields.netName(element, null),
341
+ anchors: [
342
+ { point: center },
343
+ ...(points.length
344
+ ? points.map((point) => ({ point }))
345
+ : CircuitJsonPcbPrimitiveGeometry.cornerAnchors(bounds))
346
+ ],
347
+ source: element
348
+ })
349
+ }
350
+
351
+ /**
352
+ * Builds trace segment and route-via primitives.
353
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
354
+ * @param {Map<string, object>} componentsByPcbId Component lookup.
355
+ * @returns {object[]}
356
+ */
357
+ static #tracePrimitives(index, componentsByPcbId) {
358
+ return CircuitJsonPcbPrimitiveBuilder.#all(index, 'pcb_trace').flatMap(
359
+ (trace) =>
360
+ CircuitJsonPcbPrimitiveBuilder.#tracePrimitiveRows(
361
+ trace,
362
+ componentsByPcbId
363
+ )
364
+ )
365
+ }
366
+
367
+ /**
368
+ * Builds rows for one routed trace.
369
+ * @param {object} trace Trace element.
370
+ * @param {Map<string, object>} componentsByPcbId Component lookup.
371
+ * @returns {object[]}
372
+ */
373
+ static #tracePrimitiveRows(trace, componentsByPcbId) {
374
+ const route = Array.isArray(trace.route) ? trace.route : []
375
+ const component = componentsByPcbId.get(
376
+ String(trace.pcb_component_id || '').trim()
377
+ )
378
+ const rows = []
379
+ let previous = null
380
+
381
+ for (const entry of route) {
382
+ if (entry?.route_type === 'through_pad') {
383
+ rows.push(
384
+ CircuitJsonPcbPrimitiveBuilder.#throughPadSegment(
385
+ trace,
386
+ entry,
387
+ component,
388
+ rows.length
389
+ )
390
+ )
391
+ previous = null
392
+ continue
393
+ }
394
+
395
+ const current = CircuitJsonPcbPrimitiveFields.center(entry)
396
+ if (!current) continue
397
+ if (entry?.route_type === 'via') {
398
+ rows.push(
399
+ CircuitJsonPcbPrimitiveBuilder.#routeViaPrimitive(
400
+ trace,
401
+ entry,
402
+ current,
403
+ rows.length
404
+ )
405
+ )
406
+ }
407
+ if (
408
+ previous &&
409
+ CircuitJsonPcbPrimitiveBuilder.#sameSegmentLayer(
410
+ previous,
411
+ entry
412
+ )
413
+ ) {
414
+ rows.push(
415
+ CircuitJsonPcbPrimitiveBuilder.#segmentPrimitive(
416
+ trace,
417
+ previous,
418
+ current,
419
+ entry,
420
+ component,
421
+ rows.length
422
+ )
423
+ )
424
+ }
425
+ previous = { ...entry, x: current.x, y: current.y }
426
+ }
427
+
428
+ return rows.filter(Boolean)
429
+ }
430
+
431
+ /**
432
+ * Returns true when two route points can form a visible planar segment.
433
+ * @param {object} previous Previous route entry.
434
+ * @param {object} current Current route entry.
435
+ * @returns {boolean}
436
+ */
437
+ static #sameSegmentLayer(previous, current) {
438
+ if (previous?.route_type === 'via' || current?.route_type === 'via') {
439
+ return true
440
+ }
441
+
442
+ const previousLayer = CircuitJsonPcbPrimitiveFields.layer(
443
+ previous?.layer
444
+ )
445
+ const currentLayer = CircuitJsonPcbPrimitiveFields.layer(current?.layer)
446
+ return !previousLayer || !currentLayer || previousLayer === currentLayer
447
+ }
448
+
449
+ /**
450
+ * Builds one routed wire segment.
451
+ * @param {object} trace Trace element.
452
+ * @param {object} previous Previous route point.
453
+ * @param {{ x: number, y: number }} current Current point.
454
+ * @param {object} entry Current route entry.
455
+ * @param {object | undefined} component Component row.
456
+ * @param {number} index Segment index.
457
+ * @returns {object}
458
+ */
459
+ static #segmentPrimitive(
460
+ trace,
461
+ previous,
462
+ current,
463
+ entry,
464
+ component,
465
+ index
466
+ ) {
467
+ const width = CircuitJsonUnits.length(
468
+ entry.width ?? previous.width ?? trace.width,
469
+ 0.15
470
+ )
471
+ const layer = CircuitJsonPcbPrimitiveFields.layer(
472
+ entry.layer || previous.layer || trace.layer
473
+ )
474
+
475
+ return CircuitJsonPcbPrimitiveBuilder.#primitive({
476
+ id: String(trace.pcb_trace_id || '') + ':segment:' + index,
477
+ kind: 'track',
478
+ x1: previous.x,
479
+ y1: previous.y,
480
+ x2: current.x,
481
+ y2: current.y,
482
+ width,
483
+ bounds: CircuitJsonPcbPrimitiveGeometry.segmentBounds(
484
+ previous,
485
+ current,
486
+ width
487
+ ),
488
+ layer,
489
+ component,
490
+ netName: CircuitJsonPcbPrimitiveFields.netName(trace, null),
491
+ anchors: [{ point: previous }, { point: current }],
492
+ source: trace,
493
+ sourceTraceId: String(trace.source_trace_id || '')
494
+ })
495
+ }
496
+
497
+ /**
498
+ * Builds one through-pad trace segment.
499
+ * @param {object} trace Trace element.
500
+ * @param {object} entry Route entry.
501
+ * @param {object | undefined} component Component row.
502
+ * @param {number} index Segment index.
503
+ * @returns {object | null}
504
+ */
505
+ static #throughPadSegment(trace, entry, component, index) {
506
+ const start = CircuitJsonPcbPrimitiveFields.point(entry.start)
507
+ const end = CircuitJsonPcbPrimitiveFields.point(entry.end)
508
+ if (!start || !end) return null
509
+ return CircuitJsonPcbPrimitiveBuilder.#segmentPrimitive(
510
+ trace,
511
+ start,
512
+ end,
513
+ {
514
+ width: entry.width,
515
+ layer: entry.start_layer || entry.end_layer
516
+ },
517
+ component,
518
+ index
519
+ )
520
+ }
521
+
522
+ /**
523
+ * Builds one route via primitive.
524
+ * @param {object} trace Trace element.
525
+ * @param {object} entry Route via entry.
526
+ * @param {{ x: number, y: number }} center Via center.
527
+ * @param {number} index Via index.
528
+ * @returns {object}
529
+ */
530
+ static #routeViaPrimitive(trace, entry, center, index) {
531
+ const geometry = CircuitJsonPcbHolePrimitiveModel.build(entry, center)
532
+ return {
533
+ ...CircuitJsonPcbPrimitiveBuilder.#viaRow(
534
+ {
535
+ ...entry,
536
+ pcb_via_id:
537
+ String(trace.pcb_trace_id || '') + ':via:' + index
538
+ },
539
+ center,
540
+ geometry,
541
+ CircuitJsonPcbPrimitiveFields.netName(trace, null)
542
+ ),
543
+ source: trace,
544
+ sourceRoute: entry
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Builds standalone via, plated-hole, and hole primitives.
550
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
551
+ * @param {Map<string, object>} componentsByPcbId Component lookup.
552
+ * @returns {object[]}
553
+ */
554
+ static #viaPrimitives(index, componentsByPcbId) {
555
+ return ['pcb_via', 'pcb_plated_hole', 'pcb_hole']
556
+ .flatMap((type) => CircuitJsonPcbPrimitiveBuilder.#all(index, type))
557
+ .map((element) => {
558
+ const center = CircuitJsonPcbPrimitiveFields.center(element)
559
+ if (!center) return null
560
+ const geometry = CircuitJsonPcbHolePrimitiveModel.build(
561
+ element,
562
+ center
563
+ )
564
+ const component = componentsByPcbId.get(
565
+ String(element.pcb_component_id || '').trim()
566
+ )
567
+ return CircuitJsonPcbPrimitiveBuilder.#primitive({
568
+ ...CircuitJsonPcbPrimitiveBuilder.#viaRow(
569
+ element,
570
+ center,
571
+ geometry,
572
+ CircuitJsonPcbPrimitiveFields.netName(element, null)
573
+ ),
574
+ component
575
+ })
576
+ })
577
+ .filter(Boolean)
578
+ }
579
+
580
+ /**
581
+ * Builds one via-like primitive row.
582
+ * @param {object} element Via-like element.
583
+ * @param {{ x: number, y: number }} center Center point.
584
+ * @param {object} geometry Normalized drilled geometry.
585
+ * @param {string} netName Net name.
586
+ * @returns {object}
587
+ */
588
+ static #viaRow(element, center, geometry, netName) {
589
+ const layer = CircuitJsonPcbPrimitiveFields.layer(
590
+ element.layer || element.from_layer || element.to_layer
591
+ )
592
+ return {
593
+ id: String(
594
+ element.pcb_via_id ||
595
+ element.pcb_plated_hole_id ||
596
+ element.pcb_hole_id ||
597
+ ''
598
+ ),
599
+ kind: 'via',
600
+ x: center.x,
601
+ y: center.y,
602
+ ...geometry,
603
+ layer,
604
+ side: '',
605
+ netName,
606
+ anchors: [{ point: center }],
607
+ source: element
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Builds silkscreen and text primitives.
613
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
614
+ * @param {Map<string, object>} componentsByPcbId Component lookup.
615
+ * @returns {object[]}
616
+ */
617
+ static #silkscreenPrimitives(index, componentsByPcbId) {
618
+ return [
619
+ ...CircuitJsonPcbPrimitiveBuilder.#textPrimitives(
620
+ index,
621
+ componentsByPcbId
622
+ ),
623
+ ...CircuitJsonPcbPrimitiveBuilder.#linePrimitives(
624
+ index,
625
+ componentsByPcbId
626
+ )
627
+ ]
628
+ }
629
+
630
+ /**
631
+ * Builds text primitives.
632
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
633
+ * @param {Map<string, object>} componentsByPcbId Component lookup.
634
+ * @returns {object[]}
635
+ */
636
+ static #textPrimitives(index, componentsByPcbId) {
637
+ return [
638
+ 'pcb_silkscreen_text',
639
+ 'pcb_text',
640
+ 'pcb_copper_text',
641
+ 'pcb_fabrication_note_text'
642
+ ]
643
+ .flatMap((type) => CircuitJsonPcbPrimitiveBuilder.#all(index, type))
644
+ .flatMap((element) => {
645
+ const center =
646
+ CircuitJsonPcbPrimitiveFields.center(element) ||
647
+ CircuitJsonPcbPrimitiveFields.point(element.anchor_position)
648
+ if (!center) return []
649
+ const size = CircuitJsonUnits.length(
650
+ element.font_size ?? element.fontSize ?? element.height,
651
+ 1
652
+ )
653
+ const component = componentsByPcbId.get(
654
+ String(element.pcb_component_id || '').trim()
655
+ )
656
+ const bounds = CircuitJsonPcbPrimitiveGeometry.centerBounds(
657
+ center,
658
+ Math.max(
659
+ String(element.text || '').length * size * 0.6,
660
+ size
661
+ ),
662
+ size
663
+ )
664
+ const primitive = CircuitJsonPcbPrimitiveBuilder.#primitive({
665
+ id: CircuitJsonIndexer.getElementId(element),
666
+ kind: CircuitJsonPcbPrimitiveBuilder.#textKind(element),
667
+ text: String(element.text || ''),
668
+ x: center.x,
669
+ y: center.y,
670
+ fontSize: size,
671
+ anchorAlignment: String(
672
+ element.anchor_alignment ||
673
+ element.anchorAlignment ||
674
+ ''
675
+ ).trim(),
676
+ isKnockout:
677
+ element.is_knockout === true ||
678
+ element.isKnockout === true,
679
+ rotation: CircuitJsonUnits.angle(element.ccw_rotation, 0),
680
+ bounds,
681
+ layer: CircuitJsonPcbPrimitiveFields.layer(element.layer),
682
+ component,
683
+ netName: CircuitJsonPcbPrimitiveFields.netName(
684
+ element,
685
+ null
686
+ ),
687
+ anchors: [{ point: center }],
688
+ ...CircuitJsonPcbDrawingStyle.fromElement(element),
689
+ source: element
690
+ })
691
+ if (element.type !== 'pcb_silkscreen_text') return [primitive]
692
+
693
+ return [
694
+ primitive,
695
+ {
696
+ ...primitive,
697
+ id: primitive.id + ':silkscreen',
698
+ kind: 'silkscreen',
699
+ footprintId: primitive.componentKey
700
+ ? 'footprint:' +
701
+ primitive.componentKey +
702
+ ':silkscreen'
703
+ : ''
704
+ }
705
+ ]
706
+ })
707
+ .filter(Boolean)
708
+ }
709
+
710
+ /**
711
+ * Builds line primitives.
712
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
713
+ * @param {Map<string, object>} componentsByPcbId Component lookup.
714
+ * @returns {object[]}
715
+ */
716
+ static #linePrimitives(index, componentsByPcbId) {
717
+ return [
718
+ 'pcb_silkscreen_line',
719
+ 'pcb_silkscreen_path',
720
+ 'pcb_fabrication_note_line',
721
+ 'pcb_fabrication_note_path'
722
+ ]
723
+ .flatMap((type) => CircuitJsonPcbPrimitiveBuilder.#all(index, type))
724
+ .flatMap((element) =>
725
+ CircuitJsonPcbPrimitiveBuilder.#linePrimitiveRows(
726
+ element,
727
+ componentsByPcbId
728
+ )
729
+ )
730
+ .filter(Boolean)
731
+ }
732
+
733
+ /**
734
+ * Builds renderable segment rows for one documentation path or line.
735
+ * @param {object} element Path or line element.
736
+ * @param {Map<string, object>} componentsByPcbId Component lookup.
737
+ * @returns {object[]}
738
+ */
739
+ static #linePrimitiveRows(element, componentsByPcbId) {
740
+ const points = CircuitJsonPcbPrimitiveFields.linePoints(element)
741
+ const rows = []
742
+
743
+ for (let index = 1; index < points.length; index += 1) {
744
+ rows.push(
745
+ CircuitJsonPcbPrimitiveBuilder.#linePrimitive(
746
+ element,
747
+ points[index - 1],
748
+ points[index],
749
+ index - 1,
750
+ componentsByPcbId
751
+ )
752
+ )
753
+ }
754
+
755
+ return rows.filter(Boolean)
756
+ }
757
+
758
+ /**
759
+ * Builds one silkscreen or fabrication segment primitive.
760
+ * @param {object} element Source element.
761
+ * @param {{ x: number, y: number }} start Segment start.
762
+ * @param {{ x: number, y: number }} end Segment end.
763
+ * @param {number} index Segment index.
764
+ * @param {Map<string, object>} componentsByPcbId Component lookup.
765
+ * @returns {object | null}
766
+ */
767
+ static #linePrimitive(element, start, end, index, componentsByPcbId) {
768
+ const width = CircuitJsonUnits.length(
769
+ element.width ??
770
+ element.stroke_width ??
771
+ element.strokeWidth ??
772
+ element.line_width,
773
+ 0.12
774
+ )
775
+ const component = componentsByPcbId.get(
776
+ String(element.pcb_component_id || '').trim()
777
+ )
778
+ const baseId = CircuitJsonIndexer.getElementId(element)
779
+
780
+ return CircuitJsonPcbPrimitiveBuilder.#primitive({
781
+ id: baseId + ':' + index,
782
+ kind: CircuitJsonPcbPrimitiveBuilder.#lineKind(element),
783
+ x1: start.x,
784
+ y1: start.y,
785
+ x2: end.x,
786
+ y2: end.y,
787
+ width,
788
+ bounds: CircuitJsonPcbPrimitiveGeometry.segmentBounds(
789
+ start,
790
+ end,
791
+ width
792
+ ),
793
+ layer: CircuitJsonPcbPrimitiveBuilder.#lineLayer(element),
794
+ component,
795
+ anchors: [{ point: start }, { point: end }],
796
+ ...CircuitJsonPcbDrawingStyle.fromElement(element),
797
+ source: element
798
+ })
799
+ }
800
+
801
+ /**
802
+ * Resolves the primitive kind for a documentation line.
803
+ * @param {object} element Source element.
804
+ * @returns {'fabrication' | 'silkscreen'}
805
+ */
806
+ static #lineKind(element) {
807
+ return String(element.type || '').includes('fabrication')
808
+ ? 'fabrication'
809
+ : 'silkscreen'
810
+ }
811
+
812
+ /**
813
+ * Resolves the display layer for a documentation line.
814
+ * @param {object} element Source element.
815
+ * @returns {string}
816
+ */
817
+ static #lineLayer(element) {
818
+ if (element.layer) {
819
+ return CircuitJsonPcbPrimitiveFields.layer(element.layer)
820
+ }
821
+ return CircuitJsonPcbPrimitiveBuilder.#lineKind(element) ===
822
+ 'fabrication'
823
+ ? 'top_fabrication'
824
+ : 'top_silkscreen'
825
+ }
826
+
827
+ /**
828
+ * Resolves a normalized text primitive kind.
829
+ * @param {object} element Text element.
830
+ * @returns {string}
831
+ */
832
+ static #textKind(element) {
833
+ if (element.type === 'pcb_copper_text') return 'copper-text'
834
+ if (element.type === 'pcb_fabrication_note_text') return 'fabrication'
835
+ return 'silkscreen_text'
836
+ }
837
+
838
+ /**
839
+ * Adds common primitive metadata.
840
+ * @param {object} primitive Primitive data.
841
+ * @returns {object}
842
+ */
843
+ static #primitive(primitive) {
844
+ const component = primitive.component || {}
845
+ const layer = CircuitJsonPcbPrimitiveFields.layer(primitive.layer)
846
+ const source = primitive.source || {}
847
+ return {
848
+ ...primitive,
849
+ layer,
850
+ side: primitive.side ?? CircuitJsonPcbPrimitiveFields.side(layer),
851
+ componentKey: String(component.componentKey || ''),
852
+ componentId: String(component.pcbComponentId || ''),
853
+ footprintId: component.componentKey
854
+ ? 'footprint:' + component.componentKey + ':' + primitive.kind
855
+ : '',
856
+ netName: String(primitive.netName || '').trim(),
857
+ groupIds: CircuitJsonPcbPrimitiveFields.uniqueStrings([
858
+ source.pcb_group_id,
859
+ source.source_group_id,
860
+ component.pcb_group_id,
861
+ component.positioned_relative_to_pcb_group_id,
862
+ component.sourceGroupId
863
+ ]),
864
+ subcircuitIds: CircuitJsonPcbPrimitiveFields.uniqueStrings([
865
+ source.subcircuit_id,
866
+ source.subcircuitId,
867
+ ...(component.subcircuitIds || [])
868
+ ]),
869
+ sourceComponentId: String(component.sourceComponentId || '')
870
+ }
871
+ }
872
+ }