circuit-json-to-tscircuit 0.0.41 → 0.0.43

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.
@@ -529,6 +529,7 @@ var convertSmtPads = (circuitJson) => {
529
529
  const elementStrings = [];
530
530
  for (const smtPad of smtPads) {
531
531
  const commonAttrs = [];
532
+ const solderMaskAttrs = formatSolderMaskAttrs(smtPad);
532
533
  if (smtPad.port_hints !== void 0) {
533
534
  commonAttrs.push(`portHints={${JSON.stringify(smtPad.port_hints)}}`);
534
535
  }
@@ -541,85 +542,30 @@ var convertSmtPads = (circuitJson) => {
541
542
  if (smtPad.layer !== void 0) {
542
543
  commonAttrs.push(`layer="${smtPad.layer}"`);
543
544
  }
544
- if (smtPad.is_covered_with_solder_mask !== void 0) {
545
- commonAttrs.push(
546
- `coveredWithSolderMask={${smtPad.is_covered_with_solder_mask}}`
547
- );
548
- }
549
- if (smtPad.soldermask_margin !== void 0) {
550
- commonAttrs.push(`solderMaskMargin="${mmStr6(smtPad.soldermask_margin)}"`);
551
- }
552
545
  if (smtPad.shape === "circle") {
553
546
  elementStrings.push(
554
- `<smtpad ${commonAttrs.join(" ")} radius="${mmStr6(smtPad.radius ?? 0)}" shape="circle" />`
547
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} radius="${mmStr6(smtPad.radius ?? 0)}" shape="circle" />`
555
548
  );
556
549
  } else if (smtPad.shape === "rect") {
557
- if (smtPad.rect_border_radius !== void 0) {
558
- commonAttrs.push(
559
- `rectBorderRadius="${mmStr6(smtPad.rect_border_radius)}"`
560
- );
561
- }
562
- if (smtPad.corner_radius !== void 0) {
563
- commonAttrs.push(`cornerRadius="${mmStr6(smtPad.corner_radius)}"`);
564
- }
565
- if (smtPad.soldermask_margin_left !== void 0) {
566
- commonAttrs.push(
567
- `solderMaskMarginLeft="${mmStr6(smtPad.soldermask_margin_left)}"`
568
- );
569
- }
570
- if (smtPad.soldermask_margin_top !== void 0) {
571
- commonAttrs.push(
572
- `solderMaskMarginTop="${mmStr6(smtPad.soldermask_margin_top)}"`
573
- );
574
- }
575
- if (smtPad.soldermask_margin_right !== void 0) {
576
- commonAttrs.push(
577
- `solderMaskMarginRight="${mmStr6(smtPad.soldermask_margin_right)}"`
578
- );
579
- }
580
- if (smtPad.soldermask_margin_bottom !== void 0) {
581
- commonAttrs.push(
582
- `solderMaskMarginBottom="${mmStr6(smtPad.soldermask_margin_bottom)}"`
583
- );
584
- }
585
550
  elementStrings.push(
586
- `<smtpad ${commonAttrs.join(" ")} width="${mmStr6(smtPad.width ?? 0)}" height="${mmStr6(smtPad.height ?? 0)}" shape="rect" />`
551
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs}${formatOptionalMmAttr("rectBorderRadius", smtPad.rect_border_radius)}${formatOptionalMmAttr("cornerRadius", smtPad.corner_radius)}${formatOptionalMmAttr("solderMaskMarginLeft", smtPad.soldermask_margin_left)}${formatOptionalMmAttr("solderMaskMarginTop", smtPad.soldermask_margin_top)}${formatOptionalMmAttr("solderMaskMarginRight", smtPad.soldermask_margin_right)}${formatOptionalMmAttr("solderMaskMarginBottom", smtPad.soldermask_margin_bottom)} width="${mmStr6(smtPad.width ?? 0)}" height="${mmStr6(smtPad.height ?? 0)}" shape="rect" />`
587
552
  );
588
553
  } else if (smtPad.shape === "pill") {
589
554
  elementStrings.push(
590
- `<smtpad ${commonAttrs.join(" ")} width="${mmStr6(smtPad.width ?? 0)}" height="${mmStr6(smtPad.height ?? 0)}" radius="${mmStr6(smtPad.radius ?? 0)}" shape="pill" />`
555
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} width="${mmStr6(smtPad.width ?? 0)}" height="${mmStr6(smtPad.height ?? 0)}" radius="${mmStr6(smtPad.radius ?? 0)}" shape="pill" />`
556
+ );
557
+ } else if (smtPad.shape === "rotated_pill") {
558
+ elementStrings.push(
559
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} width="${mmStr6(smtPad.width ?? 0)}" height="${mmStr6(smtPad.height ?? 0)}" radius="${mmStr6(smtPad.radius ?? 0)}"${formatPcbRotationAttr(smtPad.ccw_rotation)} shape="pill" />`
591
560
  );
592
561
  } else if (smtPad.shape === "polygon") {
593
562
  elementStrings.push(
594
- `<smtpad ${commonAttrs.join(" ")} shape="polygon" points={${JSON.stringify(smtPad.points)}} />`
563
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} shape="polygon" points={${JSON.stringify(smtPad.points)}} />`
595
564
  );
596
565
  } else if (smtPad.shape === "rotated_rect") {
597
566
  const cornerRadius = smtPad.corner_radius ?? smtPad.rect_border_radius ?? void 0;
598
- if (cornerRadius !== void 0) {
599
- commonAttrs.push(`cornerRadius="${mmStr6(cornerRadius)}"`);
600
- }
601
- if (smtPad.soldermask_margin_left !== void 0) {
602
- commonAttrs.push(
603
- `solderMaskMarginLeft="${mmStr6(smtPad.soldermask_margin_left)}"`
604
- );
605
- }
606
- if (smtPad.soldermask_margin_top !== void 0) {
607
- commonAttrs.push(
608
- `solderMaskMarginTop="${mmStr6(smtPad.soldermask_margin_top)}"`
609
- );
610
- }
611
- if (smtPad.soldermask_margin_right !== void 0) {
612
- commonAttrs.push(
613
- `solderMaskMarginRight="${mmStr6(smtPad.soldermask_margin_right)}"`
614
- );
615
- }
616
- if (smtPad.soldermask_margin_bottom !== void 0) {
617
- commonAttrs.push(
618
- `solderMaskMarginBottom="${mmStr6(smtPad.soldermask_margin_bottom)}"`
619
- );
620
- }
621
567
  elementStrings.push(
622
- `<smtpad ${commonAttrs.join(" ")} width="${mmStr6(smtPad.width ?? 0)}" height="${mmStr6(smtPad.height ?? 0)}"${formatPcbRotationAttr(smtPad.ccw_rotation)} shape="rotated_rect" />`
568
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs}${formatOptionalMmAttr("cornerRadius", cornerRadius)}${formatOptionalMmAttr("solderMaskMarginLeft", smtPad.soldermask_margin_left)}${formatOptionalMmAttr("solderMaskMarginTop", smtPad.soldermask_margin_top)}${formatOptionalMmAttr("solderMaskMarginRight", smtPad.soldermask_margin_right)}${formatOptionalMmAttr("solderMaskMarginBottom", smtPad.soldermask_margin_bottom)} width="${mmStr6(smtPad.width ?? 0)}" height="${mmStr6(smtPad.height ?? 0)}"${formatPcbRotationAttr(smtPad.ccw_rotation)} shape="rotated_rect" />`
623
569
  );
624
570
  }
625
571
  }
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-BA4FQZIF.js";
4
+ } from "../chunk-UXTI3RT6.js";
5
5
 
6
6
  // cli/main.ts
7
7
  import { Command } from "commander";
@@ -16,9 +16,11 @@ var package_default = {
16
16
  "./cli": "./dist/cli/main.js"
17
17
  },
18
18
  type: "module",
19
- version: "0.0.40",
19
+ version: "0.0.42",
20
20
  scripts: {
21
21
  build: "tsup-node lib/index.ts cli/main.ts --format esm --dts",
22
+ "build:site": "bun build ./site/index.html --outdir site-build",
23
+ "dev:site": "bun --hot ./site/index.html",
22
24
  test: "bun test",
23
25
  format: "biome format --write .",
24
26
  "format:check": "biome format ."
@@ -34,6 +36,8 @@ var package_default = {
34
36
  "circuit-json": "^0.0.433",
35
37
  "circuit-to-svg": "^0.0.353",
36
38
  commander: "^13.1.0",
39
+ prettier: "^3.9.5",
40
+ shiki: "^4.3.1",
37
41
  tscircuit: "^0.0.1873",
38
42
  tsup: "^8.3.5"
39
43
  },
package/dist/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  convertCircuitJsonToTscircuit
3
- } from "../chunk-BA4FQZIF.js";
3
+ } from "../chunk-UXTI3RT6.js";
4
4
  export {
5
5
  convertCircuitJsonToTscircuit
6
6
  };
@@ -1,7 +1,9 @@
1
1
  import { su } from "@tscircuit/soup-util"
2
2
  import { mmStr } from "@tscircuit/mm"
3
3
  import type { FootprintElementConverter } from "./converter-types"
4
+ import { formatOptionalMmAttr } from "./footprint-tsx-attribute-formatters/format-optional-mm-attr"
4
5
  import { formatPcbRotationAttr } from "./footprint-tsx-attribute-formatters/format-pcb-rotation-attr"
6
+ import { formatSolderMaskAttrs } from "./footprint-tsx-attribute-formatters/format-solder-mask-attrs"
5
7
 
6
8
  export const convertSmtPads: FootprintElementConverter = (circuitJson) => {
7
9
  const smtPads = su(circuitJson).pcb_smtpad.list()
@@ -9,6 +11,7 @@ export const convertSmtPads: FootprintElementConverter = (circuitJson) => {
9
11
 
10
12
  for (const smtPad of smtPads) {
11
13
  const commonAttrs: string[] = []
14
+ const solderMaskAttrs = formatSolderMaskAttrs(smtPad)
12
15
 
13
16
  if (smtPad.port_hints !== undefined) {
14
17
  commonAttrs.push(`portHints={${JSON.stringify(smtPad.port_hints)}}`)
@@ -22,88 +25,33 @@ export const convertSmtPads: FootprintElementConverter = (circuitJson) => {
22
25
  if (smtPad.layer !== undefined) {
23
26
  commonAttrs.push(`layer="${smtPad.layer}"`)
24
27
  }
25
- if (smtPad.is_covered_with_solder_mask !== undefined) {
26
- commonAttrs.push(
27
- `coveredWithSolderMask={${smtPad.is_covered_with_solder_mask}}`,
28
- )
29
- }
30
- if (smtPad.soldermask_margin !== undefined) {
31
- commonAttrs.push(`solderMaskMargin="${mmStr(smtPad.soldermask_margin)}"`)
32
- }
33
28
 
34
29
  if (smtPad.shape === "circle") {
35
30
  elementStrings.push(
36
- `<smtpad ${commonAttrs.join(" ")} radius="${mmStr(smtPad.radius ?? 0)}" shape="circle" />`,
31
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} radius="${mmStr(smtPad.radius ?? 0)}" shape="circle" />`,
37
32
  )
38
33
  } else if (smtPad.shape === "rect") {
39
- if (smtPad.rect_border_radius !== undefined) {
40
- commonAttrs.push(
41
- `rectBorderRadius="${mmStr(smtPad.rect_border_radius)}"`,
42
- )
43
- }
44
- if (smtPad.corner_radius !== undefined) {
45
- commonAttrs.push(`cornerRadius="${mmStr(smtPad.corner_radius)}"`)
46
- }
47
- if (smtPad.soldermask_margin_left !== undefined) {
48
- commonAttrs.push(
49
- `solderMaskMarginLeft="${mmStr(smtPad.soldermask_margin_left)}"`,
50
- )
51
- }
52
- if (smtPad.soldermask_margin_top !== undefined) {
53
- commonAttrs.push(
54
- `solderMaskMarginTop="${mmStr(smtPad.soldermask_margin_top)}"`,
55
- )
56
- }
57
- if (smtPad.soldermask_margin_right !== undefined) {
58
- commonAttrs.push(
59
- `solderMaskMarginRight="${mmStr(smtPad.soldermask_margin_right)}"`,
60
- )
61
- }
62
- if (smtPad.soldermask_margin_bottom !== undefined) {
63
- commonAttrs.push(
64
- `solderMaskMarginBottom="${mmStr(smtPad.soldermask_margin_bottom)}"`,
65
- )
66
- }
67
34
  elementStrings.push(
68
- `<smtpad ${commonAttrs.join(" ")} width="${mmStr(smtPad.width ?? 0)}" height="${mmStr(smtPad.height ?? 0)}" shape="rect" />`,
35
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs}${formatOptionalMmAttr("rectBorderRadius", smtPad.rect_border_radius)}${formatOptionalMmAttr("cornerRadius", smtPad.corner_radius)}${formatOptionalMmAttr("solderMaskMarginLeft", smtPad.soldermask_margin_left)}${formatOptionalMmAttr("solderMaskMarginTop", smtPad.soldermask_margin_top)}${formatOptionalMmAttr("solderMaskMarginRight", smtPad.soldermask_margin_right)}${formatOptionalMmAttr("solderMaskMarginBottom", smtPad.soldermask_margin_bottom)} width="${mmStr(smtPad.width ?? 0)}" height="${mmStr(smtPad.height ?? 0)}" shape="rect" />`,
69
36
  )
70
37
  } else if (smtPad.shape === "pill") {
71
38
  elementStrings.push(
72
- `<smtpad ${commonAttrs.join(" ")} width="${mmStr(smtPad.width ?? 0)}" height="${mmStr(smtPad.height ?? 0)}" radius="${mmStr(smtPad.radius ?? 0)}" shape="pill" />`,
39
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} width="${mmStr(smtPad.width ?? 0)}" height="${mmStr(smtPad.height ?? 0)}" radius="${mmStr(smtPad.radius ?? 0)}" shape="pill" />`,
40
+ )
41
+ } else if (smtPad.shape === "rotated_pill") {
42
+ elementStrings.push(
43
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} width="${mmStr(smtPad.width ?? 0)}" height="${mmStr(smtPad.height ?? 0)}" radius="${mmStr(smtPad.radius ?? 0)}"${formatPcbRotationAttr(smtPad.ccw_rotation)} shape="pill" />`,
73
44
  )
74
45
  } else if (smtPad.shape === "polygon") {
75
46
  elementStrings.push(
76
- `<smtpad ${commonAttrs.join(" ")} shape="polygon" points={${JSON.stringify(smtPad.points)}} />`,
47
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} shape="polygon" points={${JSON.stringify(smtPad.points)}} />`,
77
48
  )
78
49
  } else if (smtPad.shape === "rotated_rect") {
79
50
  const cornerRadius =
80
51
  smtPad.corner_radius ?? smtPad.rect_border_radius ?? undefined
81
52
 
82
- if (cornerRadius !== undefined) {
83
- commonAttrs.push(`cornerRadius="${mmStr(cornerRadius)}"`)
84
- }
85
- if (smtPad.soldermask_margin_left !== undefined) {
86
- commonAttrs.push(
87
- `solderMaskMarginLeft="${mmStr(smtPad.soldermask_margin_left)}"`,
88
- )
89
- }
90
- if (smtPad.soldermask_margin_top !== undefined) {
91
- commonAttrs.push(
92
- `solderMaskMarginTop="${mmStr(smtPad.soldermask_margin_top)}"`,
93
- )
94
- }
95
- if (smtPad.soldermask_margin_right !== undefined) {
96
- commonAttrs.push(
97
- `solderMaskMarginRight="${mmStr(smtPad.soldermask_margin_right)}"`,
98
- )
99
- }
100
- if (smtPad.soldermask_margin_bottom !== undefined) {
101
- commonAttrs.push(
102
- `solderMaskMarginBottom="${mmStr(smtPad.soldermask_margin_bottom)}"`,
103
- )
104
- }
105
53
  elementStrings.push(
106
- `<smtpad ${commonAttrs.join(" ")} width="${mmStr(smtPad.width ?? 0)}" height="${mmStr(smtPad.height ?? 0)}"${formatPcbRotationAttr(smtPad.ccw_rotation)} shape="rotated_rect" />`,
54
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs}${formatOptionalMmAttr("cornerRadius", cornerRadius)}${formatOptionalMmAttr("solderMaskMarginLeft", smtPad.soldermask_margin_left)}${formatOptionalMmAttr("solderMaskMarginTop", smtPad.soldermask_margin_top)}${formatOptionalMmAttr("solderMaskMarginRight", smtPad.soldermask_margin_right)}${formatOptionalMmAttr("solderMaskMarginBottom", smtPad.soldermask_margin_bottom)} width="${mmStr(smtPad.width ?? 0)}" height="${mmStr(smtPad.height ?? 0)}"${formatPcbRotationAttr(smtPad.ccw_rotation)} shape="rotated_rect" />`,
107
55
  )
108
56
  }
109
57
  }
package/package.json CHANGED
@@ -6,9 +6,11 @@
6
6
  "./cli": "./dist/cli/main.js"
7
7
  },
8
8
  "type": "module",
9
- "version": "0.0.41",
9
+ "version": "0.0.43",
10
10
  "scripts": {
11
11
  "build": "tsup-node lib/index.ts cli/main.ts --format esm --dts",
12
+ "build:site": "bun build ./site/index.html --outdir site-build",
13
+ "dev:site": "bun --hot ./site/index.html",
12
14
  "test": "bun test",
13
15
  "format": "biome format --write .",
14
16
  "format:check": "biome format ."
@@ -24,6 +26,8 @@
24
26
  "circuit-json": "^0.0.433",
25
27
  "circuit-to-svg": "^0.0.353",
26
28
  "commander": "^13.1.0",
29
+ "prettier": "^3.9.5",
30
+ "shiki": "^4.3.1",
27
31
  "tscircuit": "^0.0.1873",
28
32
  "tsup": "^8.3.5"
29
33
  },
@@ -0,0 +1,609 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Circuit JSON → tscircuit</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <style>
14
+ :root {
15
+ --bg: #f5f7fa;
16
+ --surface: #ffffff;
17
+ --surface-alt: #f1f5f9;
18
+ --border: #e2e8f0;
19
+ --text: #0f172a;
20
+ --text-muted: #64748b;
21
+ --accent: #2563eb;
22
+ --accent-strong: #1d4ed8;
23
+ --accent-contrast: #ffffff;
24
+ --success: #059669;
25
+ --success-bg: #ecfdf5;
26
+ --success-border: #a7f3d0;
27
+ --error: #dc2626;
28
+ --error-bg: #fef2f2;
29
+ --error-border: #fecaca;
30
+ --processing: #d97706;
31
+ --processing-bg: #fffbeb;
32
+ --processing-border: #fde68a;
33
+ --code-bg: #ffffff;
34
+ --code-header: #f6f8fa;
35
+ --code-text: #24292f;
36
+ --code-text-muted: #6e7781;
37
+ --focus-ring: #2563eb;
38
+ }
39
+
40
+ * {
41
+ margin: 0;
42
+ padding: 0;
43
+ box-sizing: border-box;
44
+ }
45
+
46
+ html,
47
+ body {
48
+ height: 100%;
49
+ }
50
+
51
+ body {
52
+ font-family:
53
+ ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
54
+ Roboto, sans-serif;
55
+ color: var(--text);
56
+ background: var(--bg);
57
+ display: flex;
58
+ flex-direction: column;
59
+ overflow: hidden;
60
+ }
61
+
62
+ code {
63
+ font-family: "Fira Code", ui-monospace, monospace;
64
+ }
65
+
66
+ /* Top bar */
67
+ .topbar {
68
+ flex: none;
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 20px;
72
+ padding: 0 20px;
73
+ height: 56px;
74
+ border-bottom: 1px solid var(--border);
75
+ background: var(--surface);
76
+ }
77
+
78
+ .brand {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 10px;
82
+ flex: none;
83
+ text-decoration: none;
84
+ }
85
+
86
+ .brand .badge {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ background: var(--accent);
90
+ color: #ffffff;
91
+ font-size: 13px;
92
+ font-weight: 600;
93
+ letter-spacing: -0.01em;
94
+ padding: 4px 9px;
95
+ border-radius: 0.4rem;
96
+ }
97
+
98
+ .brand .path {
99
+ font-family: "Fira Code", ui-monospace, monospace;
100
+ font-size: 12px;
101
+ color: var(--text-muted);
102
+ }
103
+
104
+ .topbar-spacer {
105
+ flex: 1;
106
+ }
107
+
108
+ .status {
109
+ display: none;
110
+ align-items: center;
111
+ gap: 7px;
112
+ padding: 5px 12px;
113
+ border-radius: 999px;
114
+ border: 1px solid transparent;
115
+ font-size: 12.5px;
116
+ flex: none;
117
+ white-space: nowrap;
118
+ }
119
+
120
+ .status.visible {
121
+ display: flex;
122
+ }
123
+
124
+ .status .dot {
125
+ width: 6px;
126
+ height: 6px;
127
+ border-radius: 50%;
128
+ flex-shrink: 0;
129
+ }
130
+
131
+ .status.success {
132
+ background: var(--success-bg);
133
+ color: var(--success);
134
+ border-color: var(--success-border);
135
+ }
136
+ .status.success .dot {
137
+ background: var(--success);
138
+ }
139
+
140
+ .status.error {
141
+ background: var(--error-bg);
142
+ color: var(--error);
143
+ border-color: var(--error-border);
144
+ }
145
+ .status.error .dot {
146
+ background: var(--error);
147
+ }
148
+
149
+ .status.processing {
150
+ background: var(--processing-bg);
151
+ color: var(--processing);
152
+ border-color: var(--processing-border);
153
+ }
154
+ .status.processing .dot {
155
+ background: var(--processing);
156
+ animation: pulse 1.1s ease-in-out infinite;
157
+ }
158
+
159
+ @keyframes pulse {
160
+ 0%,
161
+ 100% {
162
+ opacity: 1;
163
+ }
164
+ 50% {
165
+ opacity: 0.35;
166
+ }
167
+ }
168
+
169
+ button {
170
+ font-family: inherit;
171
+ border: 1px solid transparent;
172
+ border-radius: 0.4rem;
173
+ font-weight: 600;
174
+ cursor: pointer;
175
+ transition:
176
+ transform 0.1s ease,
177
+ background-color 0.15s ease,
178
+ border-color 0.15s ease;
179
+ }
180
+
181
+ button:focus-visible {
182
+ outline: 2px solid var(--focus-ring);
183
+ outline-offset: 2px;
184
+ }
185
+
186
+ .btn-convert {
187
+ flex: none;
188
+ padding: 8px 16px;
189
+ font-size: 13px;
190
+ background: var(--accent);
191
+ color: var(--accent-contrast);
192
+ }
193
+
194
+ .btn-convert:hover:not(:disabled) {
195
+ background: var(--accent-strong);
196
+ }
197
+
198
+ .btn-convert:active:not(:disabled) {
199
+ transform: translateY(1px);
200
+ }
201
+
202
+ .btn-convert:disabled {
203
+ opacity: 0.45;
204
+ cursor: not-allowed;
205
+ }
206
+
207
+ .btn-clear {
208
+ flex: none;
209
+ padding: 8px 14px;
210
+ font-size: 13px;
211
+ background: transparent;
212
+ color: var(--text-muted);
213
+ border-color: var(--border);
214
+ }
215
+
216
+ .btn-clear:hover {
217
+ color: var(--text);
218
+ border-color: var(--text-muted);
219
+ }
220
+
221
+ /* Split layout */
222
+ .split {
223
+ flex: 1;
224
+ display: grid;
225
+ grid-template-columns: 380px 1px minmax(0, 1fr);
226
+ min-height: 0;
227
+ }
228
+
229
+ @media (max-width: 860px) {
230
+ .split {
231
+ grid-template-columns: minmax(0, 1fr);
232
+ grid-auto-rows: min-content minmax(320px, 1fr);
233
+ overflow-y: auto;
234
+ }
235
+ .split-divider {
236
+ display: none;
237
+ }
238
+ }
239
+
240
+ .split-divider {
241
+ background: var(--border);
242
+ }
243
+
244
+ .pane-input {
245
+ display: flex;
246
+ flex-direction: column;
247
+ gap: 16px;
248
+ padding: 20px;
249
+ overflow-y: auto;
250
+ min-height: 0;
251
+ }
252
+
253
+ .pane-output {
254
+ display: flex;
255
+ flex-direction: column;
256
+ min-height: 0;
257
+ background: var(--code-bg);
258
+ }
259
+
260
+ .upload-area {
261
+ flex: none;
262
+ border: 1.5px dashed var(--border);
263
+ border-radius: 0.5rem;
264
+ padding: 22px 16px;
265
+ text-align: center;
266
+ cursor: pointer;
267
+ transition:
268
+ border-color 0.15s ease,
269
+ background-color 0.15s ease;
270
+ background: var(--surface-alt);
271
+ }
272
+
273
+ .upload-area:hover,
274
+ .upload-area:focus-visible {
275
+ border-color: var(--accent);
276
+ background: color-mix(in srgb, var(--accent) 6%, var(--surface-alt));
277
+ }
278
+
279
+ .upload-area.dragover {
280
+ border-color: var(--accent);
281
+ border-style: solid;
282
+ background: color-mix(in srgb, var(--accent) 10%, var(--surface-alt));
283
+ }
284
+
285
+ .upload-area:focus-visible {
286
+ outline: 2px solid var(--focus-ring);
287
+ outline-offset: 2px;
288
+ }
289
+
290
+ .upload-row {
291
+ display: flex;
292
+ align-items: center;
293
+ justify-content: center;
294
+ gap: 10px;
295
+ }
296
+
297
+ .upload-glyph {
298
+ width: 18px;
299
+ height: 18px;
300
+ color: var(--accent);
301
+ flex: none;
302
+ }
303
+
304
+ .upload-text {
305
+ font-size: 13.5px;
306
+ font-weight: 500;
307
+ }
308
+
309
+ .upload-hint {
310
+ color: var(--text-muted);
311
+ font-size: 12px;
312
+ margin-top: 4px;
313
+ font-family: "Fira Code", ui-monospace, monospace;
314
+ }
315
+
316
+ input[type="file"] {
317
+ display: none;
318
+ }
319
+
320
+ .divider {
321
+ flex: none;
322
+ display: flex;
323
+ align-items: center;
324
+ gap: 10px;
325
+ color: var(--text-muted);
326
+ font-size: 11px;
327
+ font-weight: 500;
328
+ letter-spacing: 0.04em;
329
+ text-transform: uppercase;
330
+ }
331
+
332
+ .divider::before,
333
+ .divider::after {
334
+ content: "";
335
+ flex: 1;
336
+ height: 1px;
337
+ background: var(--border);
338
+ }
339
+
340
+ .paste-area {
341
+ flex: 1;
342
+ display: flex;
343
+ flex-direction: column;
344
+ min-height: 100px;
345
+ }
346
+
347
+ .paste-area textarea {
348
+ flex: 1;
349
+ width: 100%;
350
+ resize: none;
351
+ padding: 10px 12px;
352
+ border: 1px solid var(--border);
353
+ border-radius: 0.5rem;
354
+ font-size: 12.5px;
355
+ font-family: "Fira Code", ui-monospace, monospace;
356
+ background: var(--surface-alt);
357
+ color: var(--text);
358
+ line-height: 1.5;
359
+ }
360
+
361
+ .paste-area textarea:focus {
362
+ outline: none;
363
+ border-color: var(--accent);
364
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
365
+ }
366
+
367
+ .paste-area textarea::placeholder {
368
+ color: var(--text-muted);
369
+ }
370
+
371
+ .file-info {
372
+ flex: none;
373
+ display: none;
374
+ align-items: center;
375
+ justify-content: space-between;
376
+ gap: 12px;
377
+ padding: 9px 12px;
378
+ background: var(--surface-alt);
379
+ border: 1px solid var(--border);
380
+ border-radius: 0.5rem;
381
+ font-family: "Fira Code", ui-monospace, monospace;
382
+ font-size: 12.5px;
383
+ }
384
+
385
+ .file-info.visible {
386
+ display: flex;
387
+ }
388
+
389
+ .file-name {
390
+ color: var(--text);
391
+ font-weight: 500;
392
+ overflow: hidden;
393
+ text-overflow: ellipsis;
394
+ white-space: nowrap;
395
+ }
396
+
397
+ .file-size {
398
+ color: var(--text-muted);
399
+ flex-shrink: 0;
400
+ }
401
+
402
+ /* Output pane */
403
+ .output-header {
404
+ flex: none;
405
+ display: flex;
406
+ align-items: center;
407
+ justify-content: space-between;
408
+ padding: 0 16px;
409
+ height: 44px;
410
+ background: var(--code-header);
411
+ border-bottom: 1px solid var(--border);
412
+ font-family: "Fira Code", ui-monospace, monospace;
413
+ font-size: 12px;
414
+ color: var(--code-text-muted);
415
+ gap: 10px;
416
+ }
417
+
418
+ .output-header .output-filename {
419
+ flex: 1;
420
+ overflow: hidden;
421
+ text-overflow: ellipsis;
422
+ white-space: nowrap;
423
+ }
424
+
425
+ .output-header .header-actions {
426
+ display: flex;
427
+ gap: 8px;
428
+ flex: none;
429
+ }
430
+
431
+ .btn-copy,
432
+ .btn-download {
433
+ flex: none;
434
+ background: transparent;
435
+ border: 1px solid color-mix(in srgb, var(--code-text-muted) 45%, transparent);
436
+ color: var(--code-text-muted);
437
+ font-family: "Fira Code", ui-monospace, monospace;
438
+ font-size: 11.5px;
439
+ font-weight: 500;
440
+ padding: 4px 10px;
441
+ border-radius: 0.35rem;
442
+ }
443
+
444
+ .btn-copy:hover,
445
+ .btn-download:hover {
446
+ color: var(--code-text);
447
+ border-color: var(--code-text-muted);
448
+ }
449
+
450
+ .btn-download:disabled,
451
+ .btn-copy:disabled {
452
+ opacity: 0.4;
453
+ cursor: not-allowed;
454
+ }
455
+
456
+ .output-body {
457
+ position: relative;
458
+ flex: 1;
459
+ min-height: 0;
460
+ }
461
+
462
+ .shiki-container {
463
+ height: 100%;
464
+ overflow: auto;
465
+ }
466
+
467
+ .shiki-container .shiki {
468
+ margin: 0;
469
+ padding: 14px 0;
470
+ font-family: "Fira Code", ui-monospace, monospace;
471
+ font-size: 12.5px;
472
+ line-height: 1.5;
473
+ background-color: var(--code-bg) !important;
474
+ }
475
+
476
+ .shiki-container .shiki code {
477
+ display: block;
478
+ counter-reset: line;
479
+ }
480
+
481
+ .shiki-container .shiki .line {
482
+ display: inline-block;
483
+ min-width: 100%;
484
+ padding: 0 16px;
485
+ }
486
+
487
+ .shiki-container .shiki .line::before {
488
+ counter-increment: line;
489
+ content: counter(line);
490
+ display: inline-block;
491
+ width: 1.8rem;
492
+ margin-right: 14px;
493
+ text-align: right;
494
+ color: var(--text-muted);
495
+ opacity: 0.5;
496
+ user-select: none;
497
+ }
498
+
499
+ .output-empty {
500
+ position: absolute;
501
+ inset: 0;
502
+ display: flex;
503
+ flex-direction: column;
504
+ align-items: center;
505
+ justify-content: center;
506
+ gap: 10px;
507
+ color: var(--code-text-muted);
508
+ text-align: center;
509
+ padding: 24px;
510
+ }
511
+
512
+ .output-empty svg {
513
+ width: 26px;
514
+ height: 26px;
515
+ opacity: 0.5;
516
+ }
517
+
518
+ .output-empty p {
519
+ font-size: 13px;
520
+ max-width: 32ch;
521
+ line-height: 1.5;
522
+ }
523
+
524
+ .output-body.has-code .output-empty {
525
+ display: none;
526
+ }
527
+
528
+ @media (prefers-reduced-motion: reduce) {
529
+ * {
530
+ animation-duration: 0.001ms !important;
531
+ transition-duration: 0.001ms !important;
532
+ }
533
+ }
534
+ </style>
535
+ </head>
536
+ <body>
537
+ <div class="topbar">
538
+ <a class="brand" href="https://tscircuit.com" target="_blank" rel="noopener">
539
+ <span class="badge">tscircuit</span>
540
+ <span class="path">circuit-json &rarr; tsx</span>
541
+ </a>
542
+
543
+ <div class="topbar-spacer"></div>
544
+
545
+ <div class="status" id="status" role="status"><span class="dot"></span><span id="statusText"></span></div>
546
+
547
+ <button class="btn-convert" id="convertBtn" disabled>Convert</button>
548
+ <button class="btn-clear" id="clearBtn">Clear</button>
549
+ </div>
550
+
551
+ <div class="split">
552
+ <div class="pane-input">
553
+ <div class="upload-area" id="uploadArea" tabindex="0" role="button" aria-label="Upload circuit JSON file">
554
+ <div class="upload-row">
555
+ <svg class="upload-glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
556
+ <path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" />
557
+ <path d="M7 9l5-5 5 5" />
558
+ <path d="M12 4v12" />
559
+ </svg>
560
+ <span class="upload-text">Drop circuit.json or click to browse</span>
561
+ </div>
562
+ <div class="upload-hint">.json</div>
563
+ </div>
564
+
565
+ <input type="file" id="fileInput" accept=".json,application/json" />
566
+
567
+ <div class="file-info" id="fileInfo">
568
+ <span class="file-name" id="fileName"></span>
569
+ <span class="file-size" id="fileSize"></span>
570
+ </div>
571
+
572
+ <div class="divider">or paste JSON</div>
573
+
574
+ <div class="paste-area">
575
+ <textarea
576
+ id="pasteArea"
577
+ placeholder='[{"type": "pcb_board", ...}, ...]'
578
+ spellcheck="false"
579
+ ></textarea>
580
+ </div>
581
+ </div>
582
+
583
+ <div class="split-divider"></div>
584
+
585
+ <div class="pane-output">
586
+ <div class="output-header">
587
+ <span class="output-filename" id="outputFileName">output.tsx</span>
588
+ <div class="header-actions">
589
+ <button class="btn-copy" id="copyBtn" type="button" disabled>Copy</button>
590
+ <button class="btn-download" id="downloadBtn" type="button" disabled>Download</button>
591
+ </div>
592
+ </div>
593
+ <div class="output-body" id="outputBody">
594
+ <div class="shiki-container" id="outputCode"></div>
595
+ <div class="output-empty" id="outputEmpty">
596
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
597
+ <path d="M9 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4" />
598
+ <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
599
+ <path d="M9 9l3 3-3 3" />
600
+ </svg>
601
+ <p>Your generated TSX will appear here after you convert a circuit.</p>
602
+ </div>
603
+ </div>
604
+ </div>
605
+ </div>
606
+
607
+ <script type="module" src="main.tsx"></script>
608
+ </body>
609
+ </html>
package/site/main.tsx ADDED
@@ -0,0 +1,268 @@
1
+ import type { CircuitJson } from "circuit-json"
2
+ import parserEstree from "prettier/plugins/estree"
3
+ import parserTs from "prettier/plugins/typescript"
4
+ import * as prettier from "prettier/standalone"
5
+ import { codeToHtml } from "shiki/bundle/web"
6
+ import { convertCircuitJsonToTscircuit } from "../lib"
7
+
8
+ async function formatTsx(code: string): Promise<string> {
9
+ try {
10
+ return await prettier.format(code, {
11
+ parser: "typescript",
12
+ plugins: [parserTs, parserEstree],
13
+ semi: false,
14
+ singleQuote: false,
15
+ })
16
+ } catch (error) {
17
+ console.warn("Failed to format generated code:", error)
18
+ return code
19
+ }
20
+ }
21
+
22
+ // Get DOM elements
23
+ const uploadArea = document.getElementById("uploadArea")!
24
+ const fileInput = document.getElementById("fileInput")! as HTMLInputElement
25
+ const fileInfo = document.getElementById("fileInfo")!
26
+ const fileName = document.getElementById("fileName")!
27
+ const fileSize = document.getElementById("fileSize")!
28
+ const convertBtn = document.getElementById("convertBtn")! as HTMLButtonElement
29
+ const clearBtn = document.getElementById("clearBtn")!
30
+ const status = document.getElementById("status")!
31
+ const statusText = document.getElementById("statusText")!
32
+ const outputBody = document.getElementById("outputBody")!
33
+ const outputFileName = document.getElementById("outputFileName")!
34
+ const outputCode = document.getElementById("outputCode")!
35
+ const copyBtn = document.getElementById("copyBtn")! as HTMLButtonElement
36
+ const downloadBtn = document.getElementById("downloadBtn")! as HTMLButtonElement
37
+ const pasteArea = document.getElementById("pasteArea")! as HTMLTextAreaElement
38
+
39
+ let currentFile: File | null = null
40
+ let circuitJson: CircuitJson | null = null
41
+ let lastOutput: { fileName: string; code: string } | null = null
42
+
43
+ // Handle click / keyboard activation on upload area
44
+ uploadArea.addEventListener("click", () => {
45
+ fileInput.click()
46
+ })
47
+
48
+ uploadArea.addEventListener("keydown", (e) => {
49
+ if (e.key === "Enter" || e.key === " ") {
50
+ e.preventDefault()
51
+ fileInput.click()
52
+ }
53
+ })
54
+
55
+ // Handle file selection
56
+ fileInput.addEventListener("change", (e) => {
57
+ const target = e.target as HTMLInputElement
58
+ if (target.files && target.files.length > 0 && target.files[0]) {
59
+ handleFile(target.files[0])
60
+ }
61
+ })
62
+
63
+ // Handle drag over
64
+ uploadArea.addEventListener("dragover", (e) => {
65
+ e.preventDefault()
66
+ uploadArea.classList.add("dragover")
67
+ })
68
+
69
+ // Handle drag leave
70
+ uploadArea.addEventListener("dragleave", () => {
71
+ uploadArea.classList.remove("dragover")
72
+ })
73
+
74
+ // Handle drop
75
+ uploadArea.addEventListener("drop", (e) => {
76
+ e.preventDefault()
77
+ uploadArea.classList.remove("dragover")
78
+
79
+ if (
80
+ e.dataTransfer &&
81
+ e.dataTransfer.files.length > 0 &&
82
+ e.dataTransfer.files[0]
83
+ ) {
84
+ handleFile(e.dataTransfer.files[0])
85
+ }
86
+ })
87
+
88
+ function baseNameFromFile(file: File) {
89
+ return file.name.replace(/\.json$/i, "")
90
+ }
91
+
92
+ // Handle file
93
+ async function handleFile(file: File) {
94
+ currentFile = file
95
+
96
+ // Show file info
97
+ fileName.textContent = file.name
98
+ fileSize.textContent = `${(file.size / 1024).toFixed(2)} KB`
99
+ fileInfo.classList.add("visible")
100
+
101
+ clearOutput()
102
+
103
+ // Read and parse the file
104
+ try {
105
+ showStatus("Reading file...", "processing")
106
+ const text = await file.text()
107
+ circuitJson = JSON.parse(text)
108
+
109
+ // Enable convert button
110
+ convertBtn.disabled = false
111
+ showStatus("File loaded. Ready to convert.", "success")
112
+ } catch (error) {
113
+ showStatus(
114
+ `Error reading file: ${error instanceof Error ? error.message : String(error)}`,
115
+ "error",
116
+ )
117
+ convertBtn.disabled = true
118
+ circuitJson = null
119
+ }
120
+ }
121
+
122
+ // Handle pasted JSON: parse automatically once the user pastes or leaves the field
123
+ function loadPastedJson() {
124
+ const text = pasteArea.value.trim()
125
+
126
+ currentFile = null
127
+ fileInput.value = ""
128
+ fileInfo.classList.remove("visible")
129
+ clearOutput()
130
+
131
+ if (!text) {
132
+ convertBtn.disabled = true
133
+ circuitJson = null
134
+ hideStatus()
135
+ return
136
+ }
137
+
138
+ try {
139
+ circuitJson = JSON.parse(text)
140
+ convertBtn.disabled = false
141
+ showStatus("Pasted JSON loaded. Ready to convert.", "success")
142
+ } catch (error) {
143
+ showStatus(
144
+ `Error parsing pasted JSON: ${error instanceof Error ? error.message : String(error)}`,
145
+ "error",
146
+ )
147
+ convertBtn.disabled = true
148
+ circuitJson = null
149
+ }
150
+ }
151
+
152
+ pasteArea.addEventListener("paste", () => {
153
+ // Wait for the pasted text to land in the textarea before reading it
154
+ setTimeout(loadPastedJson, 0)
155
+ })
156
+
157
+ pasteArea.addEventListener("blur", loadPastedJson)
158
+
159
+ // Convert
160
+ convertBtn.addEventListener("click", async () => {
161
+ if (!circuitJson) return
162
+
163
+ try {
164
+ showStatus("Converting to tscircuit...", "processing")
165
+ convertBtn.disabled = true
166
+
167
+ // Allow UI to update
168
+ await new Promise((resolve) => setTimeout(resolve, 100))
169
+
170
+ const baseName = currentFile ? baseNameFromFile(currentFile) : "Circuit"
171
+ const outFileName = `${baseName}.tsx`
172
+
173
+ const tsxContent = convertCircuitJsonToTscircuit(circuitJson, {
174
+ componentName: "Circuit",
175
+ })
176
+ const formatted = await formatTsx(tsxContent)
177
+
178
+ await setOutput(outFileName, formatted)
179
+
180
+ showStatus("Conversion complete.", "success")
181
+ convertBtn.disabled = false
182
+ } catch (error) {
183
+ showStatus(
184
+ `Error during conversion: ${error instanceof Error ? error.message : String(error)}`,
185
+ "error",
186
+ )
187
+ convertBtn.disabled = false
188
+ console.error(error)
189
+ }
190
+ })
191
+
192
+ // Copy output to clipboard
193
+ copyBtn.addEventListener("click", async () => {
194
+ if (!lastOutput) return
195
+ try {
196
+ await navigator.clipboard.writeText(lastOutput.code)
197
+ const original = copyBtn.textContent
198
+ copyBtn.textContent = "Copied"
199
+ setTimeout(() => {
200
+ copyBtn.textContent = original
201
+ }, 1500)
202
+ } catch (error) {
203
+ console.error("Failed to copy:", error)
204
+ }
205
+ })
206
+
207
+ // Download output as a .tsx file
208
+ downloadBtn.addEventListener("click", () => {
209
+ if (!lastOutput) return
210
+ downloadFile(lastOutput.fileName, lastOutput.code)
211
+ })
212
+
213
+ // Clear button
214
+ clearBtn.addEventListener("click", () => {
215
+ currentFile = null
216
+ circuitJson = null
217
+ fileInput.value = ""
218
+ pasteArea.value = ""
219
+ fileInfo.classList.remove("visible")
220
+ clearOutput()
221
+ convertBtn.disabled = true
222
+ hideStatus()
223
+ })
224
+
225
+ async function setOutput(fileNameStr: string, code: string) {
226
+ lastOutput = { fileName: fileNameStr, code }
227
+ outputFileName.textContent = fileNameStr
228
+ outputCode.innerHTML = await codeToHtml(code, {
229
+ lang: "tsx",
230
+ theme: "github-light",
231
+ })
232
+ outputBody.classList.add("has-code")
233
+ copyBtn.disabled = false
234
+ downloadBtn.disabled = false
235
+ }
236
+
237
+ function clearOutput() {
238
+ lastOutput = null
239
+ outputFileName.textContent = "output.tsx"
240
+ outputCode.innerHTML = ""
241
+ outputBody.classList.remove("has-code")
242
+ copyBtn.disabled = true
243
+ downloadBtn.disabled = true
244
+ }
245
+
246
+ // Helper function to download a file
247
+ function downloadFile(filename: string, content: string) {
248
+ const blob = new Blob([content], { type: "text/plain" })
249
+ const url = URL.createObjectURL(blob)
250
+ const a = document.createElement("a")
251
+ a.href = url
252
+ a.download = filename
253
+ document.body.appendChild(a)
254
+ a.click()
255
+ document.body.removeChild(a)
256
+ URL.revokeObjectURL(url)
257
+ }
258
+
259
+ // Helper function to show status
260
+ function showStatus(message: string, type: "success" | "error" | "processing") {
261
+ statusText.textContent = message
262
+ status.className = `status visible ${type}`
263
+ }
264
+
265
+ // Helper function to hide status
266
+ function hideStatus() {
267
+ status.classList.remove("visible")
268
+ }
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="800" height="600" data-software-used-string="@tscircuit/core@0.0.1090"><style></style><rect class="boundary" x="0" y="0" fill="#000" width="800" height="600" data-type="pcb_background" data-pcb-layer="global"/><rect class="pcb-boundary" fill="none" stroke="#fff" stroke-width="0.3" x="127.27272727272725" y="27.272727272727252" width="545.4545454545455" height="545.4545454545455" data-type="pcb_boundary" data-pcb-layer="global"/><path class="pcb-board" d="M 127.27272727272725 572.7272727272727 L 672.7272727272727 572.7272727272727 L 672.7272727272727 27.272727272727252 L 127.27272727272725 27.272727272727252 Z" fill="none" stroke="rgba(255, 255, 255, 0.5)" stroke-width="2.7272727272727275" data-type="pcb_board" data-pcb-layer="board"/><rect class="pcb-pad" fill="rgb(77, 127, 196)" x="-27.272727272727273" y="-8.181818181818182" width="54.54545454545455" height="16.363636363636363" transform="translate(372.72727272727275 354.54545454545456) rotate(-45)" data-type="pcb_smtpad" data-pcb-layer="bottom" rx="4.090909090909091" ry="4.090909090909091"/><rect class="pcb-pad" fill="rgb(77, 127, 196)" x="443.6363636363636" y="328.6363636363636" width="49.09090909090909" height="24.545454545454547" data-type="pcb_smtpad" data-pcb-layer="bottom" rx="5.454545454545455" ry="5.454545454545455"/><rect class="pcb-pad" fill="rgb(77, 127, 196)" x="-27.272727272727273" y="-8.181818181818182" width="54.54545454545455" height="16.363636363636363" transform="translate(372.72727272727275 354.54545454545456) rotate(-45)" data-type="pcb_smtpad" data-pcb-layer="bottom" rx="4.090909090909091" ry="4.090909090909091"/><rect class="pcb-pad" fill="rgb(77, 127, 196)" x="443.6363636363636" y="328.6363636363636" width="49.09090909090909" height="24.545454545454547" data-type="pcb_smtpad" data-pcb-layer="bottom" rx="5.454545454545455" ry="5.454545454545455"/><rect class="pcb-pad" fill="rgb(200, 52, 52)" x="406.8181818181818" y="234.54545454545453" width="40.90909090909091" height="21.81818181818182" rx="10.90909090909091" ry="10.90909090909091" data-type="pcb_smtpad" data-pcb-layer="top"/><polygon class="pcb-pad" fill="rgb(200, 52, 52)" points="400,300 427.27272727272725,300 413.6363636363636,272.72727272727275" data-type="pcb_smtpad" data-pcb-layer="top"/><rect class="pcb-pad" fill="rgb(200, 52, 52)" x="406.8181818181818" y="234.54545454545453" width="40.90909090909091" height="21.81818181818182" rx="10.90909090909091" ry="10.90909090909091" data-type="pcb_smtpad" data-pcb-layer="top"/><polygon class="pcb-pad" fill="rgb(200, 52, 52)" points="400,300 427.27272727272725,300 413.6363636363636,272.72727272727275" data-type="pcb_smtpad" data-pcb-layer="top"/></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="600" data-software-used-string="@tscircuit/core@0.0.1322"><style></style><rect class="boundary" x="0" y="0" fill="#000" width="800" height="600" data-type="pcb_background" data-pcb-layer="global"/><rect class="pcb-boundary" fill="none" stroke="#fff" stroke-width="0.3" x="127.27272727272725" y="27.272727272727252" width="545.4545454545455" height="545.4545454545455" data-type="pcb_boundary" data-pcb-layer="global"/><path class="pcb-board" d="M 127.27272727272725 572.7272727272727 L 672.7272727272727 572.7272727272727 L 672.7272727272727 27.272727272727252 L 127.27272727272725 27.272727272727252 Z" fill="none" stroke="rgba(255, 255, 255, 0.5)" stroke-width="2.7272727272727275" data-type="pcb_board" data-pcb-layer="board"/><rect class="pcb-pad" fill="rgb(77, 127, 196)" x="-27.272727272727273" y="-8.181818181818182" width="54.54545454545455" height="16.363636363636363" transform="translate(372.72727272727275 354.54545454545456) rotate(-45)" data-type="pcb_smtpad" data-pcb-layer="bottom" rx="4.090909090909091" ry="4.090909090909091"/><rect class="pcb-pad" fill="rgb(77, 127, 196)" x="443.6363636363636" y="328.6363636363636" width="49.09090909090909" height="24.545454545454547" data-type="pcb_smtpad" data-pcb-layer="bottom" rx="5.454545454545455" ry="5.454545454545455"/><rect class="pcb-pad" fill="rgb(77, 127, 196)" x="-27.272727272727273" y="-8.181818181818182" width="54.54545454545455" height="16.363636363636363" transform="translate(372.72727272727275 354.54545454545456) rotate(-45)" data-type="pcb_smtpad" data-pcb-layer="bottom" rx="4.090909090909091" ry="4.090909090909091"/><rect class="pcb-pad" fill="rgb(77, 127, 196)" x="443.6363636363636" y="328.6363636363636" width="49.09090909090909" height="24.545454545454547" data-type="pcb_smtpad" data-pcb-layer="bottom" rx="5.454545454545455" ry="5.454545454545455"/><rect class="pcb-pad" fill="rgb(200, 52, 52)" x="406.8181818181818" y="234.54545454545453" width="40.90909090909091" height="21.81818181818182" rx="10.90909090909091" ry="10.90909090909091" data-type="pcb_smtpad" data-pcb-layer="top"/><polygon class="pcb-pad" fill="rgb(200, 52, 52)" points="400,300 427.27272727272725,300 413.6363636363636,272.72727272727275" data-type="pcb_smtpad" data-pcb-layer="top"/><rect class="pcb-pad" fill="rgb(200, 52, 52)" x="-30.000000000000004" y="-12.272727272727273" width="60.00000000000001" height="24.545454545454547" rx="12.272727272727273" ry="12.272727272727273" data-type="pcb_smtpad" data-pcb-layer="top" transform="translate(334.54545454545456 256.3636363636364) rotate(-30)"/><rect class="pcb-pad" fill="rgb(200, 52, 52)" x="406.8181818181818" y="234.54545454545453" width="40.90909090909091" height="21.81818181818182" rx="10.90909090909091" ry="10.90909090909091" data-type="pcb_smtpad" data-pcb-layer="top"/><polygon class="pcb-pad" fill="rgb(200, 52, 52)" points="400,300 427.27272727272725,300 413.6363636363636,272.72727272727275" data-type="pcb_smtpad" data-pcb-layer="top"/><rect class="pcb-pad" fill="rgb(200, 52, 52)" x="-30.000000000000004" y="-12.272727272727273" width="60.00000000000001" height="24.545454545454547" rx="12.272727272727273" ry="12.272727272727273" data-type="pcb_smtpad" data-pcb-layer="top" transform="translate(334.54545454545456 256.3636363636364) rotate(-30)"/></svg>
@@ -18,6 +18,7 @@ test("test16 support pcb smtpad shapes", async () => {
18
18
  <smtpad portHints={["2"]} layer="top" coveredWithSolderMask={true} solderMaskMargin="0.06mm" shape="polygon" points={[{"x":0,"y":0},{"x":1,"y":0},{"x":0.5,"y":1}]} />
19
19
  <smtpad portHints={["3"]} pcbX="-1mm" pcbY="-2mm" layer="bottom" coveredWithSolderMask={true} solderMaskMargin="0.04mm" cornerRadius="0.15mm" solderMaskMarginLeft="0.01mm" solderMaskMarginTop="0.02mm" solderMaskMarginRight="0.03mm" solderMaskMarginBottom="0.05mm" width="2mm" height="0.6mm" pcbRotation="45deg" shape="rotated_rect" />
20
20
  <smtpad portHints={["4"]} pcbX="2.5mm" pcbY="-1.5mm" layer="bottom" coveredWithSolderMask={true} solderMaskMargin="0.05mm" rectBorderRadius="0.25mm" cornerRadius="0.2mm" solderMaskMarginLeft="0.11mm" solderMaskMarginTop="0.12mm" solderMaskMarginRight="0.13mm" solderMaskMarginBottom="0.14mm" width="1.8mm" height="0.9mm" shape="rect" />
21
+ <smtpad portHints={["5"]} pcbX="-2.4mm" pcbY="1.6mm" layer="top" coveredWithSolderMask={true} solderMaskMargin="0.07mm" width="2.2mm" height="0.9mm" radius="0.45mm" pcbRotation="30deg" shape="pill" />
21
22
  </footprint>}
22
23
  {...props}
23
24
  />
@@ -25,6 +26,7 @@ test("test16 support pcb smtpad shapes", async () => {
25
26
  `)
26
27
  expect(tscircuit).toContain(`pcbRotation="45deg"`)
27
28
  expect(tscircuit).toContain(`shape="rotated_rect"`)
29
+ expect(tscircuit).toContain(`pcbRotation="30deg"`)
28
30
 
29
31
  // The generator emits pcbRotation for consistency, but the current smtpad
30
32
  // runtime still expects ccwRotation for rotated_rect pads.
@@ -47,7 +49,7 @@ circuit.add(
47
49
  (elm): elm is PcbSmtPad => elm.type === "pcb_smtpad",
48
50
  )
49
51
 
50
- expect(renderedSmtPads).toHaveLength(8)
52
+ expect(renderedSmtPads).toHaveLength(10)
51
53
  expect(renderedSmtPads).toEqual(
52
54
  expect.arrayContaining([
53
55
  expect.objectContaining({
@@ -66,6 +68,17 @@ circuit.add(
66
68
  is_covered_with_solder_mask: true,
67
69
  soldermask_margin: 0.05,
68
70
  }),
71
+ expect.objectContaining({
72
+ port_hints: ["5"],
73
+ layer: "top",
74
+ shape: "rotated_pill",
75
+ ccw_rotation: 30,
76
+ radius: 0.45,
77
+ width: 2.2,
78
+ height: 0.9,
79
+ is_covered_with_solder_mask: true,
80
+ soldermask_margin: 0.07,
81
+ }),
69
82
  ]),
70
83
  )
71
84
 
@@ -167,4 +180,20 @@ const circuitJson: any = [
167
180
  soldermask_margin_bottom: 0.14,
168
181
  port_hints: ["4"],
169
182
  },
183
+ {
184
+ type: "pcb_smtpad",
185
+ pcb_smtpad_id: "rotated_pill_pad",
186
+ pcb_component_id: "pcb_generic_component_0",
187
+ shape: "rotated_pill",
188
+ x: -2.4,
189
+ y: 1.6,
190
+ width: 2.2,
191
+ height: 0.9,
192
+ radius: 0.45,
193
+ ccw_rotation: 30,
194
+ layer: "top",
195
+ is_covered_with_solder_mask: true,
196
+ soldermask_margin: 0.07,
197
+ port_hints: ["5"],
198
+ },
170
199
  ]