circuit-json-to-tscircuit 0.0.57 → 0.0.59

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 (26) hide show
  1. package/dist/{chunk-U24G34W6.js → chunk-QMIS5SJZ.js} +388 -55
  2. package/dist/cli/main.js +2 -2
  3. package/dist/lib/index.js +1 -1
  4. package/lib/flatten-brep-ring.ts +46 -0
  5. package/lib/generate-copper-pours-tsx.ts +62 -0
  6. package/lib/generate-footprint-tsx/convert-silkscreen-graphics.ts +18 -0
  7. package/lib/generate-footprint-tsx/create-silkscreen-graphic-image.ts +91 -0
  8. package/lib/generate-footprint-tsx/get-brep-rings-bounds.ts +77 -0
  9. package/lib/generate-footprint-tsx.tsx +2 -0
  10. package/lib/get-board-using-template.ts +8 -1
  11. package/lib/get-brep-copper-pour-outline.ts +120 -0
  12. package/lib/get-bulge-arc.ts +41 -0
  13. package/lib/get-copper-pour-outline.ts +25 -0
  14. package/package.json +1 -1
  15. package/tests/test25-support-copper-pours.test.tsx +105 -0
  16. package/tests/test26-support-silkscreen-graphics.test.tsx +91 -0
  17. package/tests/test27-support-netless-copper-pours.test.tsx +80 -0
  18. package/tests/test28-support-curved-copper-pours.test.tsx +77 -0
  19. package/tests/ti-evms/__snapshots__/ti-drv8307evm-roundtrip-pcb-comparison.snap.svg +1 -1
  20. package/tests/ti-evms/__snapshots__/ti-lm251772evm-pd-roundtrip-pcb-comparison.snap.svg +1 -1
  21. package/tests/ti-evms/__snapshots__/ti-lm5155evm-fly-roundtrip-pcb-comparison.snap.svg +1 -1
  22. package/tests/ti-evms/__snapshots__/ti-lmg342x-bb-evm-roundtrip-pcb-comparison.snap.svg +1 -1
  23. package/tests/ti-evms/ti-drv8307evm-roundtrip.test.tsx +73 -0
  24. package/tests/ti-evms/ti-lm251772evm-pd-roundtrip.test.tsx +102 -0
  25. package/tests/ti-evms/ti-lm5155evm-fly-roundtrip.test.tsx +197 -0
  26. package/tests/ti-evms/ti-lmg342x-bb-evm-roundtrip.test.tsx +139 -0
@@ -1,16 +1,224 @@
1
1
  // lib/get-board-using-template.ts
2
2
  import { mmStr as mmStr8 } from "@tscircuit/mm";
3
- import { su as su14 } from "@tscircuit/soup-util";
3
+ import { su as su16 } from "@tscircuit/soup-util";
4
4
 
5
- // lib/generate-footprint-tsx/convert-copper-text.ts
5
+ // lib/generate-copper-pours-tsx.ts
6
6
  import { su } from "@tscircuit/soup-util";
7
7
 
8
+ // lib/get-bulge-arc.ts
9
+ var getBulgeArc = ({
10
+ end,
11
+ start
12
+ }) => {
13
+ const bulge = start.bulge ?? 0;
14
+ const deltaX = end.x - start.x;
15
+ const deltaY = end.y - start.y;
16
+ const chordLength = Math.hypot(deltaX, deltaY);
17
+ if (Math.abs(bulge) < 1e-12 || chordLength < 1e-12) return null;
18
+ const centerOffset = chordLength * (1 - bulge * bulge) / (4 * bulge);
19
+ const midpoint = {
20
+ x: (start.x + end.x) / 2,
21
+ y: (start.y + end.y) / 2
22
+ };
23
+ return {
24
+ center: {
25
+ x: midpoint.x - deltaY / chordLength * centerOffset,
26
+ y: midpoint.y + deltaX / chordLength * centerOffset
27
+ },
28
+ radius: Math.abs(chordLength * (1 + bulge * bulge) / (4 * bulge)),
29
+ sweepAngle: 4 * Math.atan(bulge)
30
+ };
31
+ };
32
+
33
+ // lib/flatten-brep-ring.ts
34
+ var MAX_ANGLE_STEP = Math.PI / 18;
35
+ var flattenBrepRing = (vertices) => {
36
+ if (vertices.length < 2) return vertices.map(toPoint);
37
+ const points = [toPoint(vertices[0])];
38
+ for (let index = 0; index < vertices.length; index++) {
39
+ const start = vertices[index];
40
+ const end = vertices[(index + 1) % vertices.length];
41
+ const isClosingSegment = index === vertices.length - 1;
42
+ const arc = getBulgeArc({ end, start });
43
+ if (!arc) {
44
+ if (!isClosingSegment) points.push(toPoint(end));
45
+ continue;
46
+ }
47
+ const startAngle = Math.atan2(
48
+ start.y - arc.center.y,
49
+ start.x - arc.center.x
50
+ );
51
+ const stepCount = Math.max(
52
+ 2,
53
+ Math.ceil(Math.abs(arc.sweepAngle) / MAX_ANGLE_STEP)
54
+ );
55
+ for (let step = 1; step <= stepCount; step++) {
56
+ if (isClosingSegment && step === stepCount) continue;
57
+ if (step === stepCount) {
58
+ points.push(toPoint(end));
59
+ continue;
60
+ }
61
+ const angle = startAngle + arc.sweepAngle * step / stepCount;
62
+ points.push({
63
+ x: arc.center.x + arc.radius * Math.cos(angle),
64
+ y: arc.center.y + arc.radius * Math.sin(angle)
65
+ });
66
+ }
67
+ }
68
+ return points;
69
+ };
70
+ var toPoint = ({ x, y }) => ({ x, y });
71
+
72
+ // lib/get-brep-copper-pour-outline.ts
73
+ var getBrepCopperPourOutline = (brepShape) => {
74
+ let outline = withWinding(
75
+ flattenBrepRing(brepShape.outer_ring.vertices),
76
+ "ccw"
77
+ );
78
+ const holes = (brepShape.inner_rings ?? []).map((ring) => withWinding(flattenBrepRing(ring.vertices), "cw")).filter((ring) => ring.length >= 3).sort((first, second) => getLeftmostX(first) - getLeftmostX(second));
79
+ for (const hole of holes) {
80
+ const bridgedOutline = bridgeHoleIntoOutline({ hole, outline });
81
+ if (!bridgedOutline) return void 0;
82
+ outline = bridgedOutline;
83
+ }
84
+ return outline;
85
+ };
86
+ var bridgeHoleIntoOutline = ({
87
+ hole,
88
+ outline
89
+ }) => {
90
+ const holeStartIndex = getLeftmostPointIndex(hole);
91
+ const holeStart = hole[holeStartIndex];
92
+ const bridge = findLeftwardBridge({ holeStart, outline });
93
+ if (!bridge) return void 0;
94
+ const outlineWithBridge = [...outline];
95
+ let bridgeIndex = bridge.edgeStartIndex;
96
+ const edgeEndIndex = (bridge.edgeStartIndex + 1) % outline.length;
97
+ if (pointsEqual(bridge.point, outline[edgeEndIndex])) {
98
+ bridgeIndex = edgeEndIndex;
99
+ } else if (!pointsEqual(bridge.point, outline[bridge.edgeStartIndex])) {
100
+ bridgeIndex = bridge.edgeStartIndex + 1;
101
+ outlineWithBridge.splice(bridgeIndex, 0, bridge.point);
102
+ }
103
+ const orderedHole = [
104
+ ...hole.slice(holeStartIndex),
105
+ ...hole.slice(0, holeStartIndex)
106
+ ];
107
+ return [
108
+ ...outlineWithBridge.slice(0, bridgeIndex + 1),
109
+ ...orderedHole,
110
+ holeStart,
111
+ bridge.point,
112
+ ...outlineWithBridge.slice(bridgeIndex + 1)
113
+ ];
114
+ };
115
+ var findLeftwardBridge = ({
116
+ holeStart,
117
+ outline
118
+ }) => {
119
+ let closestBridge;
120
+ for (let index = 0; index < outline.length; index++) {
121
+ const start = outline[index];
122
+ const end = outline[(index + 1) % outline.length];
123
+ if (start.y > holeStart.y === end.y > holeStart.y) continue;
124
+ const x = start.x + (holeStart.y - start.y) * (end.x - start.x) / (end.y - start.y);
125
+ if (x > holeStart.x) continue;
126
+ if (closestBridge && x <= closestBridge.point.x) continue;
127
+ closestBridge = {
128
+ edgeStartIndex: index,
129
+ point: { x, y: holeStart.y }
130
+ };
131
+ }
132
+ return closestBridge;
133
+ };
134
+ var withWinding = (ring, winding) => {
135
+ const isCounterclockwise = getSignedArea(ring) > 0;
136
+ const shouldReverse = winding === "ccw" && !isCounterclockwise || winding === "cw" && isCounterclockwise;
137
+ return shouldReverse ? [...ring].reverse() : [...ring];
138
+ };
139
+ var getSignedArea = (ring) => ring.reduce((area, point, index) => {
140
+ const nextPoint = ring[(index + 1) % ring.length];
141
+ return area + point.x * nextPoint.y - nextPoint.x * point.y;
142
+ }, 0) / 2;
143
+ var getLeftmostPointIndex = (ring) => ring.reduce(
144
+ (leftmostIndex, point, index) => point.x < ring[leftmostIndex].x ? index : leftmostIndex,
145
+ 0
146
+ );
147
+ var getLeftmostX = (ring) => ring[getLeftmostPointIndex(ring)].x;
148
+ var pointsEqual = (first, second) => first.x === second.x && first.y === second.y;
149
+
150
+ // lib/get-copper-pour-outline.ts
151
+ function getCopperPourOutline(pour) {
152
+ if (pour.shape === "polygon") return pour.points;
153
+ if (pour.shape === "brep") {
154
+ return getBrepCopperPourOutline(pour.brep_shape);
155
+ }
156
+ const halfWidth = pour.width / 2;
157
+ const halfHeight = pour.height / 2;
158
+ const ccwRotationRadians = (pour.rotation ?? 0) * Math.PI / 180;
159
+ const cosine = Math.cos(ccwRotationRadians);
160
+ const sine = Math.sin(ccwRotationRadians);
161
+ return [
162
+ { x: -halfWidth, y: -halfHeight },
163
+ { x: halfWidth, y: -halfHeight },
164
+ { x: halfWidth, y: halfHeight },
165
+ { x: -halfWidth, y: halfHeight }
166
+ ].map(({ x, y }) => ({
167
+ x: pour.center.x + x * cosine - y * sine,
168
+ y: pour.center.y + x * sine + y * cosine
169
+ }));
170
+ }
171
+
172
+ // lib/generate-copper-pours-tsx.ts
173
+ function generateCopperPoursTsx(circuitJson) {
174
+ const sourceNetNameById = new Map(
175
+ su(circuitJson).source_net.list().map((sourceNet) => [sourceNet.source_net_id, sourceNet.name])
176
+ );
177
+ const reservedNetNames = new Set(sourceNetNameById.values());
178
+ const usedNetNames = /* @__PURE__ */ new Set();
179
+ const copperPours = su(circuitJson).pcb_copper_pour.list().flatMap((pour) => {
180
+ const resolvedSourceNetName = pour.source_net_id ? sourceNetNameById.get(pour.source_net_id) : void 0;
181
+ const sourceNetName = resolvedSourceNetName ?? getUnassignedCopperPourNetName({
182
+ pcbCopperPourId: pour.pcb_copper_pour_id,
183
+ reservedNetNames
184
+ });
185
+ const outline = getCopperPourOutline(pour);
186
+ if (!outline || outline.length < 3) return [];
187
+ usedNetNames.add(sourceNetName);
188
+ const netSelector = `net[name=${JSON.stringify(sourceNetName)}]`;
189
+ return [
190
+ `<copperpour layer={${JSON.stringify(pour.layer)}} connectsTo={${JSON.stringify(netSelector)}} outline={${JSON.stringify(outline)}} padMargin={0} traceMargin={0} clearance={0} boardEdgeMargin={0} cutoutMargin={0} useThermalReliefs={false} coveredWithSolderMask={${pour.covered_with_solder_mask ?? true}} />`
191
+ ];
192
+ });
193
+ const nets = [...usedNetNames].map(
194
+ (sourceNetName) => `<net name={${JSON.stringify(sourceNetName)}} />`
195
+ );
196
+ return [...nets, ...copperPours];
197
+ }
198
+ var getUnassignedCopperPourNetName = ({
199
+ pcbCopperPourId,
200
+ reservedNetNames
201
+ }) => {
202
+ const baseName = `__circuit_json_unassigned_${pcbCopperPourId}`;
203
+ let name = baseName;
204
+ let suffix = 2;
205
+ while (reservedNetNames.has(name)) {
206
+ name = `${baseName}_${suffix}`;
207
+ suffix += 1;
208
+ }
209
+ reservedNetNames.add(name);
210
+ return name;
211
+ };
212
+
213
+ // lib/generate-footprint-tsx/convert-copper-text.ts
214
+ import { su as su2 } from "@tscircuit/soup-util";
215
+
8
216
  // lib/generate-footprint-tsx/footprint-tsx-attribute-formatters/escape-jsx-text.ts
9
217
  var escapeJsxText = (value) => String(value ?? "").replace(/"/g, '\\"');
10
218
 
11
219
  // lib/generate-footprint-tsx/convert-copper-text.ts
12
220
  var convertCopperText = (circuitJson) => {
13
- const copperTexts = su(circuitJson).pcb_copper_text.list();
221
+ const copperTexts = su2(circuitJson).pcb_copper_text.list();
14
222
  const elementStrings = [];
15
223
  for (const copperText of copperTexts) {
16
224
  const anchorPosition = copperText.anchor_position ?? { x: 0, y: 0 };
@@ -44,7 +252,7 @@ var convertCopperText = (circuitJson) => {
44
252
  };
45
253
 
46
254
  // lib/generate-footprint-tsx/convert-courtyard.ts
47
- import { su as su2 } from "@tscircuit/soup-util";
255
+ import { su as su3 } from "@tscircuit/soup-util";
48
256
 
49
257
  // lib/generate-footprint-tsx/footprint-tsx-attribute-formatters/format-pcb-rotation-attr.ts
50
258
  var formatPcbRotationAttr = (rotation, attrName = "pcbRotation") => {
@@ -55,9 +263,9 @@ var formatPcbRotationAttr = (rotation, attrName = "pcbRotation") => {
55
263
 
56
264
  // lib/generate-footprint-tsx/convert-courtyard.ts
57
265
  var convertCourtyard = (circuitJson) => {
58
- const courtyardOutlines = su2(circuitJson).pcb_courtyard_outline.list();
59
- const courtyardRects = su2(circuitJson).pcb_courtyard_rect.list();
60
- const courtyardCircles = su2(circuitJson).pcb_courtyard_circle.list();
266
+ const courtyardOutlines = su3(circuitJson).pcb_courtyard_outline.list();
267
+ const courtyardRects = su3(circuitJson).pcb_courtyard_rect.list();
268
+ const courtyardCircles = su3(circuitJson).pcb_courtyard_circle.list();
61
269
  const elementStrings = [];
62
270
  for (const courtyardOutline of courtyardOutlines) {
63
271
  const attrs = [
@@ -98,7 +306,7 @@ var convertCourtyard = (circuitJson) => {
98
306
  };
99
307
 
100
308
  // lib/generate-footprint-tsx/convert-cutouts.ts
101
- import { su as su3 } from "@tscircuit/soup-util";
309
+ import { su as su4 } from "@tscircuit/soup-util";
102
310
 
103
311
  // lib/generate-footprint-tsx/footprint-tsx-attribute-formatters/format-mm.ts
104
312
  import { mmStr } from "@tscircuit/mm";
@@ -106,7 +314,7 @@ var formatMm = (value) => mmStr(value ?? 0);
106
314
 
107
315
  // lib/generate-footprint-tsx/convert-cutouts.ts
108
316
  var convertCutouts = (circuitJson) => {
109
- const pcbCutouts = su3(circuitJson).pcb_cutout.list();
317
+ const pcbCutouts = su4(circuitJson).pcb_cutout.list();
110
318
  const elementStrings = [];
111
319
  for (const cutout of pcbCutouts) {
112
320
  if (cutout.shape === "rect") {
@@ -130,12 +338,12 @@ var convertCutouts = (circuitJson) => {
130
338
  };
131
339
 
132
340
  // lib/generate-footprint-tsx/convert-fabrication.ts
133
- import { su as su4 } from "@tscircuit/soup-util";
341
+ import { su as su5 } from "@tscircuit/soup-util";
134
342
  var convertFabrication = (circuitJson) => {
135
- const fabricationNotePaths = su4(circuitJson).pcb_fabrication_note_path.list();
136
- const fabricationNoteTexts = su4(circuitJson).pcb_fabrication_note_text.list();
137
- const fabricationNoteRects = su4(circuitJson).pcb_fabrication_note_rect.list();
138
- const fabricationNoteDimensions = su4(circuitJson).pcb_fabrication_note_dimension.list();
343
+ const fabricationNotePaths = su5(circuitJson).pcb_fabrication_note_path.list();
344
+ const fabricationNoteTexts = su5(circuitJson).pcb_fabrication_note_text.list();
345
+ const fabricationNoteRects = su5(circuitJson).pcb_fabrication_note_rect.list();
346
+ const fabricationNoteDimensions = su5(circuitJson).pcb_fabrication_note_dimension.list();
139
347
  const elementStrings = [];
140
348
  for (const fabPath of fabricationNotePaths) {
141
349
  const attrs = [`route={${JSON.stringify(fabPath.route)}}`];
@@ -207,10 +415,10 @@ var convertFabrication = (circuitJson) => {
207
415
  }
208
416
  for (const fabDimension of fabricationNoteDimensions) {
209
417
  const fromPoint = fabDimension.from ?? { x: 0, y: 0 };
210
- const toPoint = fabDimension.to ?? { x: 0, y: 0 };
418
+ const toPoint2 = fabDimension.to ?? { x: 0, y: 0 };
211
419
  const attrs = [
212
420
  `from={{ x: ${fromPoint.x}, y: ${fromPoint.y} }}`,
213
- `to={{ x: ${toPoint.x}, y: ${toPoint.y} }}`
421
+ `to={{ x: ${toPoint2.x}, y: ${toPoint2.y} }}`
214
422
  ];
215
423
  if (fabDimension.text !== void 0) {
216
424
  attrs.push(`text="${escapeJsxText(fabDimension.text)}"`);
@@ -241,10 +449,10 @@ var convertFabrication = (circuitJson) => {
241
449
  };
242
450
 
243
451
  // lib/generate-footprint-tsx/convert-holes.ts
244
- import { su as su5 } from "@tscircuit/soup-util";
452
+ import { su as su6 } from "@tscircuit/soup-util";
245
453
  import { mmStr as mmStr2 } from "@tscircuit/mm";
246
454
  var convertHoles = (circuitJson) => {
247
- const holes = su5(circuitJson).pcb_hole.list();
455
+ const holes = su6(circuitJson).pcb_hole.list();
248
456
  const elementStrings = [];
249
457
  for (const hole of holes) {
250
458
  if (hole.hole_shape === "circle") {
@@ -269,9 +477,9 @@ var convertHoles = (circuitJson) => {
269
477
  };
270
478
 
271
479
  // lib/generate-footprint-tsx/convert-keepouts.ts
272
- import { su as su6 } from "@tscircuit/soup-util";
480
+ import { su as su7 } from "@tscircuit/soup-util";
273
481
  var convertKeepouts = (circuitJson) => {
274
- const pcbKeepouts = su6(circuitJson).pcb_keepout.list();
482
+ const pcbKeepouts = su7(circuitJson).pcb_keepout.list();
275
483
  const elementStrings = [];
276
484
  for (const keepout of pcbKeepouts) {
277
485
  if (keepout.shape === "rect") {
@@ -290,13 +498,13 @@ var convertKeepouts = (circuitJson) => {
290
498
  };
291
499
 
292
500
  // lib/generate-footprint-tsx/convert-notes.ts
293
- import { su as su7 } from "@tscircuit/soup-util";
501
+ import { su as su8 } from "@tscircuit/soup-util";
294
502
  var convertNotes = (circuitJson) => {
295
- const noteTexts = su7(circuitJson).pcb_note_text.list();
296
- const noteRects = su7(circuitJson).pcb_note_rect.list();
297
- const notePaths = su7(circuitJson).pcb_note_path.list();
298
- const noteLines = su7(circuitJson).pcb_note_line.list();
299
- const noteDimensions = su7(circuitJson).pcb_note_dimension.list();
503
+ const noteTexts = su8(circuitJson).pcb_note_text.list();
504
+ const noteRects = su8(circuitJson).pcb_note_rect.list();
505
+ const notePaths = su8(circuitJson).pcb_note_path.list();
506
+ const noteLines = su8(circuitJson).pcb_note_line.list();
507
+ const noteDimensions = su8(circuitJson).pcb_note_dimension.list();
300
508
  const elementStrings = [];
301
509
  for (const noteText of noteTexts) {
302
510
  const anchorPosition = noteText.anchor_position ?? { x: 0, y: 0 };
@@ -358,10 +566,10 @@ var convertNotes = (circuitJson) => {
358
566
  }
359
567
  for (const noteDimension of noteDimensions) {
360
568
  const fromPoint = noteDimension.from ?? { x: 0, y: 0 };
361
- const toPoint = noteDimension.to ?? { x: 0, y: 0 };
569
+ const toPoint2 = noteDimension.to ?? { x: 0, y: 0 };
362
570
  const attrs = [
363
571
  `from={{ x: ${fromPoint.x}, y: ${fromPoint.y} }}`,
364
- `to={{ x: ${toPoint.x}, y: ${toPoint.y} }}`,
572
+ `to={{ x: ${toPoint2.x}, y: ${toPoint2.y} }}`,
365
573
  `font="${noteDimension.font ?? "tscircuit2024"}"`,
366
574
  `fontSize={${noteDimension.font_size ?? 0}}`
367
575
  ];
@@ -386,11 +594,11 @@ var convertNotes = (circuitJson) => {
386
594
  };
387
595
 
388
596
  // lib/generate-footprint-tsx/convert-pcb-traces.ts
389
- import { su as su8 } from "@tscircuit/soup-util";
390
- var convertPcbTraces = (circuitJson) => su8(circuitJson).pcb_trace.list().map((trace) => `<pcbtrace route={${JSON.stringify(trace.route)}} />`);
597
+ import { su as su9 } from "@tscircuit/soup-util";
598
+ var convertPcbTraces = (circuitJson) => su9(circuitJson).pcb_trace.list().map((trace) => `<pcbtrace route={${JSON.stringify(trace.route)}} />`);
391
599
 
392
600
  // lib/generate-footprint-tsx/convert-plated-holes.ts
393
- import { su as su9 } from "@tscircuit/soup-util";
601
+ import { su as su10 } from "@tscircuit/soup-util";
394
602
  import { mmStr as mmStr4 } from "@tscircuit/mm";
395
603
 
396
604
  // lib/generate-footprint-tsx/footprint-tsx-attribute-formatters/format-optional-mm-attr.ts
@@ -411,7 +619,7 @@ var formatSolderMaskAttrs = (platedHole) => {
411
619
 
412
620
  // lib/generate-footprint-tsx/convert-plated-holes.ts
413
621
  var convertPlatedHoles = (circuitJson) => {
414
- const platedHoles = su9(circuitJson).pcb_plated_hole.list();
622
+ const platedHoles = su10(circuitJson).pcb_plated_hole.list();
415
623
  const elementStrings = [];
416
624
  for (const platedHole of platedHoles) {
417
625
  if (platedHole.shape === "oval" || platedHole.shape === "pill") {
@@ -443,12 +651,12 @@ var convertPlatedHoles = (circuitJson) => {
443
651
  };
444
652
 
445
653
  // lib/generate-footprint-tsx/convert-silkscreen.ts
446
- import { su as su10 } from "@tscircuit/soup-util";
654
+ import { su as su11 } from "@tscircuit/soup-util";
447
655
  var convertSilkscreen = (circuitJson) => {
448
- const silkscreenLines = su10(circuitJson).pcb_silkscreen_line.list();
449
- const silkscreenPaths = su10(circuitJson).pcb_silkscreen_path.list();
450
- const silkscreenRects = su10(circuitJson).pcb_silkscreen_rect.list();
451
- const silkscreenCircles = su10(circuitJson).pcb_silkscreen_circle.list();
656
+ const silkscreenLines = su11(circuitJson).pcb_silkscreen_line.list();
657
+ const silkscreenPaths = su11(circuitJson).pcb_silkscreen_path.list();
658
+ const silkscreenRects = su11(circuitJson).pcb_silkscreen_rect.list();
659
+ const silkscreenCircles = su11(circuitJson).pcb_silkscreen_circle.list();
452
660
  const elementStrings = [];
453
661
  for (const silkscreenPath of silkscreenPaths) {
454
662
  const attrs = [`route={${JSON.stringify(silkscreenPath.route)}}`];
@@ -518,11 +726,131 @@ var convertSilkscreen = (circuitJson) => {
518
726
  return elementStrings;
519
727
  };
520
728
 
729
+ // lib/generate-footprint-tsx/convert-silkscreen-graphics.ts
730
+ import { su as su12 } from "@tscircuit/soup-util";
731
+
732
+ // lib/generate-footprint-tsx/get-brep-rings-bounds.ts
733
+ var getBrepRingsBounds = (rings) => {
734
+ const boundaryPoints = rings.flatMap(getRingBoundaryPoints);
735
+ if (boundaryPoints.length === 0) return null;
736
+ const xs = boundaryPoints.map((point) => point.x);
737
+ const ys = boundaryPoints.map((point) => point.y);
738
+ return {
739
+ maxX: Math.max(...xs),
740
+ maxY: Math.max(...ys),
741
+ minX: Math.min(...xs),
742
+ minY: Math.min(...ys)
743
+ };
744
+ };
745
+ var getRingBoundaryPoints = (ring) => {
746
+ const boundaryPoints = [...ring.vertices];
747
+ for (let index = 0; index < ring.vertices.length; index++) {
748
+ const start = ring.vertices[index];
749
+ const end = ring.vertices[(index + 1) % ring.vertices.length];
750
+ const arc = getBulgeArc({ end, start });
751
+ if (!arc) continue;
752
+ const startAngle = Math.atan2(
753
+ start.y - arc.center.y,
754
+ start.x - arc.center.x
755
+ );
756
+ for (const angle of [0, Math.PI / 2, Math.PI, 3 * Math.PI / 2]) {
757
+ if (!isAngleOnArc({ angle, startAngle, sweepAngle: arc.sweepAngle })) {
758
+ continue;
759
+ }
760
+ boundaryPoints.push({
761
+ x: arc.center.x + arc.radius * Math.cos(angle),
762
+ y: arc.center.y + arc.radius * Math.sin(angle)
763
+ });
764
+ }
765
+ }
766
+ return boundaryPoints;
767
+ };
768
+ var isAngleOnArc = ({
769
+ angle,
770
+ startAngle,
771
+ sweepAngle
772
+ }) => {
773
+ const angularDistance = sweepAngle >= 0 ? normalizeAngle(angle - startAngle) : normalizeAngle(startAngle - angle);
774
+ return angularDistance <= Math.abs(sweepAngle) + 1e-12;
775
+ };
776
+ var normalizeAngle = (angle) => {
777
+ const fullTurn = 2 * Math.PI;
778
+ return (angle % fullTurn + fullTurn) % fullTurn;
779
+ };
780
+
781
+ // lib/generate-footprint-tsx/create-silkscreen-graphic-image.ts
782
+ var createSilkscreenGraphicImage = (graphic) => {
783
+ const rings = [
784
+ graphic.brep_shape.outer_ring,
785
+ ...(graphic.brep_shape.inner_rings ?? []).filter(isRenderableRing)
786
+ ];
787
+ if (!isRenderableRing(graphic.brep_shape.outer_ring)) return null;
788
+ const bounds = getBrepRingsBounds(rings);
789
+ if (!bounds) return null;
790
+ const { maxX, maxY, minX, minY } = bounds;
791
+ const width = maxX - minX;
792
+ const height = maxY - minY;
793
+ if (width <= 0 || height <= 0) return null;
794
+ const pathData = rings.map((ring) => createRingPath({ maxY, minX, ring })).join(" ");
795
+ const svg = [
796
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">`,
797
+ `<path d="${pathData}" fill="black" fill-rule="evenodd"/>`,
798
+ "</svg>"
799
+ ].join("");
800
+ return {
801
+ center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
802
+ dataUrl: `data:image/svg+xml;base64,${btoa(svg)}`,
803
+ height,
804
+ width
805
+ };
806
+ };
807
+ var createRingPath = ({
808
+ maxY,
809
+ minX,
810
+ ring
811
+ }) => {
812
+ const firstVertex = ring.vertices[0];
813
+ const commands = [
814
+ `M${formatCoordinate(firstVertex.x - minX)} ${formatCoordinate(maxY - firstVertex.y)}`
815
+ ];
816
+ for (let index = 0; index < ring.vertices.length; index++) {
817
+ const start = ring.vertices[index];
818
+ const end = ring.vertices[(index + 1) % ring.vertices.length];
819
+ const arc = getBulgeArc({ end, start });
820
+ const x = formatCoordinate(end.x - minX);
821
+ const y = formatCoordinate(maxY - end.y);
822
+ if (arc) {
823
+ const radius = formatCoordinate(arc.radius);
824
+ const largeArcFlag = Math.abs(start.bulge ?? 0) > 1 ? 1 : 0;
825
+ const sweepFlag = (start.bulge ?? 0) < 0 ? 1 : 0;
826
+ commands.push(
827
+ `A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x} ${y}`
828
+ );
829
+ } else if (index < ring.vertices.length - 1) {
830
+ commands.push(`L${x} ${y}`);
831
+ }
832
+ }
833
+ return commands.concat("Z").join(" ");
834
+ };
835
+ var isRenderableRing = (ring) => ring.vertices.length >= 3 || ring.vertices.length >= 2 && ring.vertices.some((vertex) => Math.abs(vertex.bulge ?? 0) > 1e-12);
836
+ var formatCoordinate = (value) => Number(value.toFixed(6)).toString();
837
+
838
+ // lib/generate-footprint-tsx/convert-silkscreen-graphics.ts
839
+ var convertSilkscreenGraphics = (circuitJson) => {
840
+ return su12(circuitJson).pcb_silkscreen_graphic.list().flatMap((graphic) => {
841
+ const image = createSilkscreenGraphicImage(graphic);
842
+ if (!image) return [];
843
+ return [
844
+ `<silkscreengraphic imageUrl=${JSON.stringify(image.dataUrl)} pcbX={${image.center.x}} pcbY={${image.center.y}} width={${image.width}} height={${image.height}} layer=${JSON.stringify(graphic.layer)} />`
845
+ ];
846
+ });
847
+ };
848
+
521
849
  // lib/generate-footprint-tsx/convert-silkscreen-text.ts
522
- import { su as su11 } from "@tscircuit/soup-util";
850
+ import { su as su13 } from "@tscircuit/soup-util";
523
851
  import { mmStr as mmStr5 } from "@tscircuit/mm";
524
852
  var convertSilkscreenText = (circuitJson) => {
525
- const silkscreenTexts = su11(circuitJson).pcb_silkscreen_text.list();
853
+ const silkscreenTexts = su13(circuitJson).pcb_silkscreen_text.list();
526
854
  const elementStrings = [];
527
855
  for (const silkscreenText of silkscreenTexts) {
528
856
  const pcbX = silkscreenText.anchor_position?.x ?? 0;
@@ -537,10 +865,10 @@ var convertSilkscreenText = (circuitJson) => {
537
865
  };
538
866
 
539
867
  // lib/generate-footprint-tsx/convert-smt-pads.ts
540
- import { su as su12 } from "@tscircuit/soup-util";
868
+ import { su as su14 } from "@tscircuit/soup-util";
541
869
  import { mmStr as mmStr6 } from "@tscircuit/mm";
542
870
  var convertSmtPads = (circuitJson) => {
543
- const smtPads = su12(circuitJson).pcb_smtpad.list();
871
+ const smtPads = su14(circuitJson).pcb_smtpad.list();
544
872
  const elementStrings = [];
545
873
  for (const smtPad of smtPads) {
546
874
  const commonAttrs = [];
@@ -589,9 +917,9 @@ var convertSmtPads = (circuitJson) => {
589
917
 
590
918
  // lib/generate-footprint-tsx/convert-vias.ts
591
919
  import { mmStr as mmStr7 } from "@tscircuit/mm";
592
- import { su as su13 } from "@tscircuit/soup-util";
920
+ import { su as su15 } from "@tscircuit/soup-util";
593
921
  var convertVias = (circuitJson) => {
594
- return su13(circuitJson).pcb_via.list().map((via) => {
922
+ return su15(circuitJson).pcb_via.list().map((via) => {
595
923
  const fromLayer = via.from_layer ?? via.layers[0];
596
924
  const toLayer = via.to_layer ?? via.layers[via.layers.length - 1];
597
925
  if (!fromLayer || !toLayer) {
@@ -615,6 +943,7 @@ var generateFootprintTsx = (circuitJson) => {
615
943
  convertSmtPads,
616
944
  convertPcbTraces,
617
945
  convertSilkscreen,
946
+ convertSilkscreenGraphics,
618
947
  convertFabrication,
619
948
  convertSilkscreenText,
620
949
  convertCopperText,
@@ -639,7 +968,7 @@ var getBoardUsingTemplate = ({
639
968
  circuitJson,
640
969
  componentName
641
970
  }) => {
642
- const pcbBoard = su14(circuitJson).pcb_board.list()[0];
971
+ const pcbBoard = su16(circuitJson).pcb_board.list()[0];
643
972
  const boardProps = [];
644
973
  if (pcbBoard) {
645
974
  if (pcbBoard.width !== void 0) {
@@ -679,7 +1008,11 @@ var getBoardUsingTemplate = ({
679
1008
  }
680
1009
  const boardPropsStr = boardProps.join(" ");
681
1010
  const footprintTsx = generateFootprintTsx(circuitJson);
682
- const children = footprintTsx ? `<chip footprint={${footprintTsx}} />` : "";
1011
+ const copperPoursTsx = generateCopperPoursTsx(circuitJson);
1012
+ const children = [
1013
+ footprintTsx ? `<chip footprint={${footprintTsx}} />` : "",
1014
+ ...copperPoursTsx
1015
+ ].filter(Boolean).join("\n");
683
1016
  return `
684
1017
  ${componentName ? `export const ${componentName} =` : "export default"} () => (
685
1018
  <board${boardPropsStr ? ` ${boardPropsStr}` : ""}>
@@ -691,18 +1024,18 @@ ${componentName ? `export default ${componentName}` : ""}
691
1024
  };
692
1025
 
693
1026
  // lib/generate-symbol-tsx.ts
694
- import { su as su15 } from "@tscircuit/soup-util";
1027
+ import { su as su17 } from "@tscircuit/soup-util";
695
1028
  var escapeJsxText2 = (text) => text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
696
1029
  var generateSymbolTsx = (circuitJson) => {
697
- const schematicArcs = su15(circuitJson).schematic_arc.list();
698
- const schematicLines = su15(circuitJson).schematic_line.list();
699
- const schematicPaths = su15(circuitJson).schematic_path.list();
700
- const schematicTexts = su15(circuitJson).schematic_text.list();
701
- const schematicCircles = su15(circuitJson).schematic_circle.list();
702
- const schematicBoxes = su15(circuitJson).schematic_box.list();
703
- const schematicRects = su15(circuitJson).schematic_rect.list();
704
- const schematicTables = su15(circuitJson).schematic_table.list();
705
- const schematicTableCells = su15(circuitJson).schematic_table_cell.list();
1030
+ const schematicArcs = su17(circuitJson).schematic_arc.list();
1031
+ const schematicLines = su17(circuitJson).schematic_line.list();
1032
+ const schematicPaths = su17(circuitJson).schematic_path.list();
1033
+ const schematicTexts = su17(circuitJson).schematic_text.list();
1034
+ const schematicCircles = su17(circuitJson).schematic_circle.list();
1035
+ const schematicBoxes = su17(circuitJson).schematic_box.list();
1036
+ const schematicRects = su17(circuitJson).schematic_rect.list();
1037
+ const schematicTables = su17(circuitJson).schematic_table.list();
1038
+ const schematicTableCells = su17(circuitJson).schematic_table_cell.list();
706
1039
  const elementStrings = [];
707
1040
  for (const arc of schematicArcs) {
708
1041
  const center = arc.center ?? { x: 0, y: 0 };
package/dist/cli/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  convertCircuitJsonToTscircuit
4
- } from "../chunk-U24G34W6.js";
4
+ } from "../chunk-QMIS5SJZ.js";
5
5
 
6
6
  // cli/main.ts
7
7
  import { Command } from "commander";
@@ -16,7 +16,7 @@ var package_default = {
16
16
  "./cli": "./dist/cli/main.js"
17
17
  },
18
18
  type: "module",
19
- version: "0.0.56",
19
+ version: "0.0.58",
20
20
  repository: {
21
21
  type: "git",
22
22
  url: "https://github.com/tscircuit/circuit-json-to-tscircuit.git"
package/dist/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  convertCircuitJsonToTscircuit
3
- } from "../chunk-U24G34W6.js";
3
+ } from "../chunk-QMIS5SJZ.js";
4
4
  export {
5
5
  convertCircuitJsonToTscircuit
6
6
  };
@@ -0,0 +1,46 @@
1
+ import type { Point } from "circuit-json"
2
+ import { getBulgeArc, type BulgePoint } from "./get-bulge-arc"
3
+
4
+ const MAX_ANGLE_STEP = Math.PI / 18
5
+
6
+ export const flattenBrepRing = (vertices: BulgePoint[]): Point[] => {
7
+ if (vertices.length < 2) return vertices.map(toPoint)
8
+
9
+ const points: Point[] = [toPoint(vertices[0]!)]
10
+ for (let index = 0; index < vertices.length; index++) {
11
+ const start = vertices[index]!
12
+ const end = vertices[(index + 1) % vertices.length]!
13
+ const isClosingSegment = index === vertices.length - 1
14
+ const arc = getBulgeArc({ end, start })
15
+
16
+ if (!arc) {
17
+ if (!isClosingSegment) points.push(toPoint(end))
18
+ continue
19
+ }
20
+
21
+ const startAngle = Math.atan2(
22
+ start.y - arc.center.y,
23
+ start.x - arc.center.x,
24
+ )
25
+ const stepCount = Math.max(
26
+ 2,
27
+ Math.ceil(Math.abs(arc.sweepAngle) / MAX_ANGLE_STEP),
28
+ )
29
+ for (let step = 1; step <= stepCount; step++) {
30
+ if (isClosingSegment && step === stepCount) continue
31
+ if (step === stepCount) {
32
+ points.push(toPoint(end))
33
+ continue
34
+ }
35
+ const angle = startAngle + (arc.sweepAngle * step) / stepCount
36
+ points.push({
37
+ x: arc.center.x + arc.radius * Math.cos(angle),
38
+ y: arc.center.y + arc.radius * Math.sin(angle),
39
+ })
40
+ }
41
+ }
42
+
43
+ return points
44
+ }
45
+
46
+ const toPoint = ({ x, y }: BulgePoint): Point => ({ x, y })