circuitjson-toolkit 1.0.2 → 1.0.10

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.
@@ -0,0 +1,426 @@
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 }}
13
+ */
14
+ static build(circuitJson, index = CircuitJsonIndexer.index(circuitJson)) {
15
+ return {
16
+ pickAndPlaceRows:
17
+ CircuitJsonManufacturingBuilder.#pickAndPlaceRows(index),
18
+ routingDsn: CircuitJsonManufacturingBuilder.#routingDsn(index)
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Builds assembly placement rows.
24
+ * @param {{ elementsByType?: Map<string, object[]>, sourceComponentById?: Map<string, object> }} index Element index.
25
+ * @returns {object[]}
26
+ */
27
+ static #pickAndPlaceRows(index) {
28
+ return CircuitJsonManufacturingBuilder.#all(index, 'pcb_component').map(
29
+ (component) =>
30
+ CircuitJsonManufacturingBuilder.#pickAndPlaceRow(
31
+ component,
32
+ index.sourceComponentById || new Map()
33
+ )
34
+ )
35
+ }
36
+
37
+ /**
38
+ * Builds one placement row.
39
+ * @param {object} component PCB component element.
40
+ * @param {Map<string, object>} sourceComponentById Source lookup.
41
+ * @returns {object}
42
+ */
43
+ static #pickAndPlaceRow(component, sourceComponentById) {
44
+ const sourceId = String(component.source_component_id || '').trim()
45
+ const source = sourceComponentById.get(sourceId) || {}
46
+ const center = CircuitJsonUnits.optionalPoint(
47
+ component.center || component
48
+ ) || {
49
+ x: 0,
50
+ y: 0
51
+ }
52
+ const layer = CircuitJsonManufacturingBuilder.#layer(component.layer)
53
+
54
+ return {
55
+ designator: CircuitJsonManufacturingBuilder.#designator(
56
+ component,
57
+ source
58
+ ),
59
+ componentId: String(component.pcb_component_id || ''),
60
+ sourceComponentId: sourceId,
61
+ x: CircuitJsonManufacturingBuilder.#round(center.x),
62
+ y: CircuitJsonManufacturingBuilder.#round(center.y),
63
+ rotation: CircuitJsonUnits.angle(
64
+ component.rotation ?? component.ccw_rotation,
65
+ 0
66
+ ),
67
+ layer,
68
+ side: CircuitJsonManufacturingBuilder.#side(layer),
69
+ value: CircuitJsonManufacturingBuilder.#value(source),
70
+ package: String(
71
+ source.ftype || source.package || source.footprint || ''
72
+ ),
73
+ manufacturerPartNumber: String(
74
+ source.manufacturer_part_number ||
75
+ source.manufacturerPartNumber ||
76
+ ''
77
+ )
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Builds a compact routing exchange text payload.
83
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
84
+ * @returns {string}
85
+ */
86
+ static #routingDsn(index) {
87
+ const lines = ['(pcb assembly)', ' (unit mm)']
88
+ lines.push(...CircuitJsonManufacturingBuilder.#boardLines(index))
89
+ lines.push(...CircuitJsonManufacturingBuilder.#placementLines(index))
90
+ lines.push(...CircuitJsonManufacturingBuilder.#networkLines(index))
91
+ lines.push(')')
92
+ return lines.join('\n')
93
+ }
94
+
95
+ /**
96
+ * Builds board structure lines.
97
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
98
+ * @returns {string[]}
99
+ */
100
+ static #boardLines(index) {
101
+ const board = CircuitJsonManufacturingBuilder.#all(
102
+ index,
103
+ 'pcb_board'
104
+ )[0]
105
+ const center = CircuitJsonUnits.optionalPoint(
106
+ board?.center || board
107
+ ) || {
108
+ x: 0,
109
+ y: 0
110
+ }
111
+ const width = CircuitJsonUnits.length(board?.width, 0)
112
+ const height = CircuitJsonUnits.length(board?.height, 0)
113
+ const minX = CircuitJsonManufacturingBuilder.#round(
114
+ center.x - width / 2
115
+ )
116
+ const minY = CircuitJsonManufacturingBuilder.#round(
117
+ center.y - height / 2
118
+ )
119
+ const maxX = CircuitJsonManufacturingBuilder.#round(
120
+ center.x + width / 2
121
+ )
122
+ const maxY = CircuitJsonManufacturingBuilder.#round(
123
+ center.y + height / 2
124
+ )
125
+
126
+ return [
127
+ ' (structure',
128
+ ' (boundary (rect ' + [minX, minY, maxX, maxY].join(' ') + '))',
129
+ ...CircuitJsonManufacturingBuilder.#layers(board).map(
130
+ (layer) => ' (layer ' + layer + ' signal)'
131
+ ),
132
+ ' )'
133
+ ]
134
+ }
135
+
136
+ /**
137
+ * Builds component placement lines.
138
+ * @param {{ elementsByType?: Map<string, object[]>, sourceComponentById?: Map<string, object> }} index Element index.
139
+ * @returns {string[]}
140
+ */
141
+ static #placementLines(index) {
142
+ return [
143
+ ' (placement',
144
+ ...CircuitJsonManufacturingBuilder.#pickAndPlaceRows(index).map(
145
+ (row) =>
146
+ ' (component ' +
147
+ CircuitJsonManufacturingBuilder.#token(row.designator) +
148
+ ' (place ' +
149
+ [
150
+ row.x,
151
+ row.y,
152
+ row.side || row.layer || 'top',
153
+ row.rotation
154
+ ].join(' ') +
155
+ '))'
156
+ ),
157
+ ' )'
158
+ ]
159
+ }
160
+
161
+ /**
162
+ * Builds network lines for nets, pads, vias, and wires.
163
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
164
+ * @returns {string[]}
165
+ */
166
+ static #networkLines(index) {
167
+ return [
168
+ ' (network',
169
+ ...CircuitJsonManufacturingBuilder.#netNames(index).flatMap(
170
+ (netName) => [
171
+ ' (net ' +
172
+ CircuitJsonManufacturingBuilder.#token(netName),
173
+ ...CircuitJsonManufacturingBuilder.#pinLines(
174
+ index,
175
+ netName
176
+ ),
177
+ ...CircuitJsonManufacturingBuilder.#wireLines(
178
+ index,
179
+ netName
180
+ ),
181
+ ' )'
182
+ ]
183
+ ),
184
+ ' )'
185
+ ]
186
+ }
187
+
188
+ /**
189
+ * Builds pad pin lines for one net.
190
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
191
+ * @param {string} netName Net name.
192
+ * @returns {string[]}
193
+ */
194
+ static #pinLines(index, netName) {
195
+ return CircuitJsonManufacturingBuilder.#all(index, 'pcb_smtpad')
196
+ .filter(
197
+ (pad) =>
198
+ CircuitJsonManufacturingBuilder.#netName(pad) === netName
199
+ )
200
+ .map((pad) => {
201
+ const point = CircuitJsonUnits.optionalPoint(
202
+ pad.center || pad
203
+ ) || {
204
+ x: 0,
205
+ y: 0
206
+ }
207
+ return (
208
+ ' (pin ' +
209
+ CircuitJsonManufacturingBuilder.#token(
210
+ pad.pcb_smtpad_id || ''
211
+ ) +
212
+ ' ' +
213
+ [
214
+ CircuitJsonManufacturingBuilder.#round(point.x),
215
+ CircuitJsonManufacturingBuilder.#round(point.y)
216
+ ].join(' ') +
217
+ ')'
218
+ )
219
+ })
220
+ }
221
+
222
+ /**
223
+ * Builds routed wire lines for one net.
224
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
225
+ * @param {string} netName Net name.
226
+ * @returns {string[]}
227
+ */
228
+ static #wireLines(index, netName) {
229
+ return CircuitJsonManufacturingBuilder.#all(index, 'pcb_trace')
230
+ .filter(
231
+ (trace) =>
232
+ CircuitJsonManufacturingBuilder.#netName(trace) === netName
233
+ )
234
+ .flatMap((trace) =>
235
+ CircuitJsonManufacturingBuilder.#traceWireLines(trace)
236
+ )
237
+ }
238
+
239
+ /**
240
+ * Builds routed wire lines for one trace.
241
+ * @param {object} trace Trace element.
242
+ * @returns {string[]}
243
+ */
244
+ static #traceWireLines(trace) {
245
+ const route = Array.isArray(trace.route) ? trace.route : []
246
+ const lines = []
247
+ let previous = null
248
+ for (const entry of route) {
249
+ const current = CircuitJsonUnits.optionalPoint(
250
+ entry.center || entry
251
+ )
252
+ if (!current) continue
253
+ if (previous) {
254
+ const layer = CircuitJsonManufacturingBuilder.#layer(
255
+ entry.layer || previous.layer || trace.layer
256
+ )
257
+ const width = CircuitJsonUnits.length(
258
+ entry.width ?? previous.width ?? trace.width,
259
+ 0
260
+ )
261
+ lines.push(
262
+ ' (wire ' +
263
+ [
264
+ layer || 'top',
265
+ CircuitJsonManufacturingBuilder.#round(previous.x),
266
+ CircuitJsonManufacturingBuilder.#round(previous.y),
267
+ CircuitJsonManufacturingBuilder.#round(current.x),
268
+ CircuitJsonManufacturingBuilder.#round(current.y),
269
+ CircuitJsonManufacturingBuilder.#round(width)
270
+ ].join(' ') +
271
+ ')'
272
+ )
273
+ }
274
+ previous = { ...entry, x: current.x, y: current.y }
275
+ }
276
+ return lines
277
+ }
278
+
279
+ /**
280
+ * Builds sorted net names.
281
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
282
+ * @returns {string[]}
283
+ */
284
+ static #netNames(index) {
285
+ 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()
291
+ if (name) names.add(name)
292
+ }
293
+ for (const type of ['pcb_smtpad', 'pcb_trace', 'pcb_via']) {
294
+ for (const element of CircuitJsonManufacturingBuilder.#all(
295
+ index,
296
+ type
297
+ )) {
298
+ const name = CircuitJsonManufacturingBuilder.#netName(element)
299
+ if (name) names.add(name)
300
+ }
301
+ }
302
+ return [...names].sort((left, right) => left.localeCompare(right))
303
+ }
304
+
305
+ /**
306
+ * Resolves layer names from board metadata.
307
+ * @param {object | undefined} board Board element.
308
+ * @returns {string[]}
309
+ */
310
+ static #layers(board) {
311
+ const count = Math.max(1, Math.round(Number(board?.num_layers || 2)))
312
+ if (count === 1) return ['top']
313
+ return [
314
+ 'top',
315
+ ...Array.from(
316
+ { length: Math.max(count - 2, 0) },
317
+ (_entry, index) => 'inner' + (index + 1)
318
+ ),
319
+ 'bottom'
320
+ ]
321
+ }
322
+
323
+ /**
324
+ * Resolves rows by type.
325
+ * @param {{ elementsByType?: Map<string, object[]> }} index Element index.
326
+ * @param {string} type Element type.
327
+ * @returns {object[]}
328
+ */
329
+ static #all(index, type) {
330
+ return index.elementsByType?.get(type) || []
331
+ }
332
+
333
+ /**
334
+ * Resolves a component designator.
335
+ * @param {object} component PCB component.
336
+ * @param {object} source Source component.
337
+ * @returns {string}
338
+ */
339
+ static #designator(component, source) {
340
+ return String(
341
+ source.name ||
342
+ source.reference ||
343
+ source.designator ||
344
+ component.name ||
345
+ component.pcb_component_id ||
346
+ ''
347
+ ).trim()
348
+ }
349
+
350
+ /**
351
+ * Resolves a source value field.
352
+ * @param {object} source Source component.
353
+ * @returns {string}
354
+ */
355
+ static #value(source) {
356
+ return String(
357
+ source.value ||
358
+ source.resistance ||
359
+ source.capacitance ||
360
+ source.inductance ||
361
+ ''
362
+ )
363
+ }
364
+
365
+ /**
366
+ * Resolves a normalized net name.
367
+ * @param {object} element Element row.
368
+ * @returns {string}
369
+ */
370
+ static #netName(element) {
371
+ return String(
372
+ element?.netName ??
373
+ element?.net ??
374
+ element?.net_name ??
375
+ element?.source_net_name ??
376
+ ''
377
+ ).trim()
378
+ }
379
+
380
+ /**
381
+ * Resolves a layer string.
382
+ * @param {unknown} value Layer candidate.
383
+ * @returns {string}
384
+ */
385
+ static #layer(value) {
386
+ const raw =
387
+ typeof value === 'object' && value !== null ? value.name : value
388
+ const text = String(raw ?? '').trim()
389
+ const lowered = text.toLowerCase()
390
+ if (['front', 'f.cu', '1'].includes(lowered)) return 'top'
391
+ if (['back', 'b.cu', '32'].includes(lowered)) return 'bottom'
392
+ return text || 'top'
393
+ }
394
+
395
+ /**
396
+ * Resolves an assembly side.
397
+ * @param {string} layer Layer name.
398
+ * @returns {'top' | 'bottom' | ''}
399
+ */
400
+ static #side(layer) {
401
+ const text = String(layer || '').toLowerCase()
402
+ if (/\b(bottom|back)\b|\bb[._-]/u.test(text)) return 'bottom'
403
+ if (/\b(top|front)\b|\bf[._-]/u.test(text)) return 'top'
404
+ return ''
405
+ }
406
+
407
+ /**
408
+ * Builds a DSN-safe token.
409
+ * @param {unknown} value Raw value.
410
+ * @returns {string}
411
+ */
412
+ static #token(value) {
413
+ return String(value || 'unnamed').replace(/[^A-Za-z0-9_.:-]+/gu, '_')
414
+ }
415
+
416
+ /**
417
+ * Rounds a numeric value for deterministic output.
418
+ * @param {number} value Number.
419
+ * @returns {number}
420
+ */
421
+ static #round(value) {
422
+ const number = Number(value)
423
+ if (!Number.isFinite(number)) return 0
424
+ return Number(number.toFixed(6))
425
+ }
426
+ }
@@ -1,4 +1,8 @@
1
1
  import { CircuitJsonDocument } from './CircuitJsonDocument.mjs'
2
+ import { CircuitJsonBomBuilder } from './CircuitJsonBomBuilder.mjs'
3
+ import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
4
+ import { CircuitJsonManufacturingBuilder } from './CircuitJsonManufacturingBuilder.mjs'
5
+ import { CircuitJsonSupportMatrixBuilder } from './CircuitJsonSupportMatrixBuilder.mjs'
2
6
 
3
7
  /**
4
8
  * Parses standalone CircuitJSON files.
@@ -22,10 +26,15 @@ export class CircuitJsonParser {
22
26
  }
23
27
 
24
28
  CircuitJsonDocument.assertModel(parsed)
29
+ const index = CircuitJsonIndexer.index(parsed)
25
30
  return CircuitJsonDocument.attachMetadata(parsed, {
26
31
  fileName: options.fileName || '',
27
32
  fileType: 'circuitjson',
28
- kind: CircuitJsonParser.#resolveKind(parsed)
33
+ kind: CircuitJsonParser.#resolveKind(index),
34
+ diagnostics: index.diagnostics,
35
+ bom: CircuitJsonBomBuilder.build(parsed),
36
+ supportMatrix: CircuitJsonSupportMatrixBuilder.build(parsed),
37
+ manufacturing: CircuitJsonManufacturingBuilder.build(parsed, index)
29
38
  })
30
39
  }
31
40
 
@@ -44,12 +53,19 @@ export class CircuitJsonParser {
44
53
 
45
54
  /**
46
55
  * Resolves a broad document kind from available elements.
47
- * @param {object[]} model CircuitJSON model.
56
+ * @param {{ elementsByType?: Map<string, object[]> }} index Model index.
48
57
  * @returns {string}
49
58
  */
50
- static #resolveKind(model) {
51
- return model.some((element) => String(element?.type) === 'pcb_board')
52
- ? 'pcb'
53
- : 'circuitjson'
59
+ static #resolveKind(index) {
60
+ if (index.elementsByType?.has('pcb_board')) return 'pcb'
61
+ if (
62
+ [...(index.elementsByType?.keys() || [])].some((type) =>
63
+ String(type).startsWith('schematic_')
64
+ )
65
+ ) {
66
+ return 'schematic'
67
+ }
68
+
69
+ return 'circuitjson'
54
70
  }
55
71
  }