From 36e064bad5d568adcacf44cd4ab5bb5f7d6361ae Mon Sep 17 00:00:00 2001 From: Arnav K Date: Sun, 2 Feb 2025 17:00:14 +0530 Subject: [PATCH] feat: import circuit json (#599) * feat: import circuit json * patch: naming * chore: removed caps as per review * fix: json badge color * chore: format * patch: json crd type badge * feat: Allow them to paste or upload circuit json, not a url * test: broken tests because playwright not working on my device * tests: sad * wtf * add 4 tests * tests: circuit json import modal full tests * chore: deleted debug file * chore: remove unused dep * validCircuitJson -> exampleCircuitJson --- bun.lock | 18 +- package.json | 2 + playwright-tests/circuit-json-import.spec.ts | 133 +++++ playwright-tests/exampleCircuitJson.ts | 498 +++++++++++++++++++ src/components/CircuitJsonImportDialog.tsx | 186 +++++++ src/pages/quickstart.tsx | 24 + 6 files changed, 859 insertions(+), 2 deletions(-) create mode 100644 playwright-tests/circuit-json-import.spec.ts create mode 100644 playwright-tests/exampleCircuitJson.ts create mode 100644 src/components/CircuitJsonImportDialog.tsx diff --git a/bun.lock b/bun.lock index 146bdddb..43316fb1 100644 --- a/bun.lock +++ b/bun.lock @@ -61,6 +61,7 @@ "circuit-json-to-gerber": "^0.0.16", "circuit-json-to-pnp-csv": "^0.0.6", "circuit-json-to-readable-netlist": "^0.0.7", + "circuit-json-to-tscircuit": "^0.0.4", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -115,6 +116,7 @@ "@types/babel__standalone": "^7.1.7", "@types/bun": "^1.1.10", "@types/country-list": "^2.1.4", + "@types/node": "^22.13.0", "@types/prismjs": "^1.26.4", "@types/react": "^18.3.9", "@types/react-dom": "^18.3.0", @@ -805,7 +807,7 @@ "@types/ms": ["@types/ms@0.7.34", "", {}, "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="], - "@types/node": ["@types/node@18.19.54", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw=="], + "@types/node": ["@types/node@22.13.0", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA=="], "@types/node-fetch": ["@types/node-fetch@2.6.11", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g=="], @@ -1041,6 +1043,8 @@ "circuit-json-to-readable-netlist": ["circuit-json-to-readable-netlist@0.0.7", "", { "dependencies": { "@tscircuit/core": "^0.0.286", "@tscircuit/soup-util": "^0.0.41", "circuit-json-to-connectivity-map": "^0.0.17" }, "peerDependencies": { "typescript": "^5.7.2" } }, "sha512-GvlVMzEzLpB9WTsLkN4p5aHITjKhfEOQKFjZaUNQrd3FbyFbUXnx1e8vR1cB2M0fQXwaUQ0cxGTjK3W2AihKng=="], + "circuit-json-to-tscircuit": ["circuit-json-to-tscircuit@0.0.4", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-LpHbOwdPE4+CooWPAPoKXWs4vxTrjJgu/avnxE3AqGwCD9r0ZnT73mEAB9oQi6T1i7T53zdkSR6y+zpsyCSE7g=="], + "circuit-to-svg": ["circuit-to-svg@0.0.101", "", { "dependencies": { "@tscircuit/footprinter": "^0.0.91", "@tscircuit/routing": "^1.3.5", "@tscircuit/soup-util": "^0.0.41", "@types/node": "^22.5.5", "bun-types": "^1.1.40", "svgson": "^5.3.1", "transformation-matrix": "^2.16.1" }, "peerDependencies": { "circuit-json": "*", "schematic-symbols": "*" } }, "sha512-AwTD5Ww5ujzK5pEkrVDFtFx5nfGqVbtbIHgXNEeji5RKfDpb0WzeXtaw75kkHl715JB1WBwUupewKO7mTaI06A=="], "class-variance-authority": ["class-variance-authority@0.7.0", "", { "dependencies": { "clsx": "2.0.0" } }, "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A=="], @@ -2301,7 +2305,7 @@ "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], - "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], @@ -2407,6 +2411,8 @@ "zustand-hoist": ["zustand-hoist@2.0.1", "", { "peerDependencies": { "zustand": ">=4.0.0" } }, "sha512-Lhvv3RlLQx1NSUtuhk8jegXe1Wyav9RAOnLd4CRs1SbB5qcFoarAGQTE43vIxXizrm1UQJl1q5uRbOZuXGXGpQ=="], + "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.54", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw=="], + "@babel/core/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -2817,6 +2823,8 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], @@ -3097,12 +3105,16 @@ "@types/serve-static/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "@types/ws/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@vercel/nft/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "circuit-json-to-readable-netlist/@tscircuit/core/@tscircuit/footprinter": ["@tscircuit/footprinter@0.0.97", "", { "dependencies": { "@tscircuit/mm": "^0.0.8", "zod": "^3.23.8" }, "peerDependencies": { "circuit-json": "*" } }, "sha512-LeqjpXqPwR++kcshlfe0E3IOsv0Y9BVRjIllDaHFA2OM+gJ91z/SS3CdweXB+qtF4t9G+8MLKf4nU5L1HDGjmg=="], "circuit-json-to-readable-netlist/@tscircuit/core/@tscircuit/math-utils": ["@tscircuit/math-utils@0.0.9", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-sPzfXndijet8z29X6f5vnSZddiso2tRg7m6rB+268bVj60mxnxUMD14rKuMlLn6n84fMOpD/X7pRTZUfi6M+Tg=="], @@ -3463,6 +3475,8 @@ "@vercel/nft/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + "circuit-to-svg/bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "glob-promise/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], diff --git a/package.json b/package.json index bd023d4a..d5a0de5d 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "circuit-json-to-bom-csv": "^0.0.6", "circuit-json-to-gerber": "^0.0.16", "circuit-json-to-pnp-csv": "^0.0.6", + "circuit-json-to-tscircuit": "^0.0.4", "circuit-json-to-readable-netlist": "^0.0.7", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -135,6 +136,7 @@ "@types/babel__standalone": "^7.1.7", "@types/bun": "^1.1.10", "@types/country-list": "^2.1.4", + "@types/node": "^22.13.0", "@types/prismjs": "^1.26.4", "@types/react": "^18.3.9", "@types/react-dom": "^18.3.0", diff --git a/playwright-tests/circuit-json-import.spec.ts b/playwright-tests/circuit-json-import.spec.ts new file mode 100644 index 00000000..bf2b15fc --- /dev/null +++ b/playwright-tests/circuit-json-import.spec.ts @@ -0,0 +1,133 @@ +import { test, expect } from "@playwright/test" +import { exampleCircuitJson } from "./exampleCircuitJson" + +async function loginToSite(page) { + const loginButton = page.getByRole("button", { name: "Log in" }) + if (await loginButton.isVisible()) { + await loginButton.click() + await page.waitForLoadState("networkidle") + } +} + +test.beforeEach(async ({ page }) => { + await page.goto("http://127.0.0.1:5177/quickstart") + await page.waitForTimeout(3000) + await loginToSite(page).catch(() => {}) +}) + +test("should open and close the Circuit Json Import Dialog", async ({ + page, +}) => { + const importButton = page.locator('button:has-text("Import Circuit JSON")') + await importButton.click() + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + + const closeButton = dialog.getByRole("button", { name: "Close" }) + await closeButton.click() + + await expect(dialog).not.toBeVisible() +}) + +test("should handle valid Circuit JSON input", async ({ page }) => { + const importButton = page.getByRole("button", { name: "Import Circuit JSON" }) + await importButton.click() + const textarea = page.locator( + 'textarea[placeholder="Paste the Circuit JSON."]', + ) + await textarea.fill(JSON.stringify(exampleCircuitJson)) + + const importDialogButton = page.getByRole("button", { name: "Import" }) + await importDialogButton.click() + + const successToast = page.locator( + 'div.text-sm.font-semibold:has-text("Import Successful")', + ) + await successToast.waitFor({ state: "visible", timeout: 5000 }) + await expect(successToast).toBeVisible() +}) + +test("should handle valid Circuit JSON file upload", async ({ page }) => { + const importButton = page.locator('button:has-text("Import Circuit JSON")') + await importButton.click() + + const fileInput = page.locator('input[type="file"]') + + await fileInput.setInputFiles({ + name: "circuit.json", + mimeType: "application/json", + // @ts-expect-error didnt add node types to tsconfig + buffer: Buffer.from(JSON.stringify(exampleCircuitJson)), + }) + + const importDialogButton = page.getByRole("button", { name: "Import" }) + await importDialogButton.click() + const successToast = page.locator( + 'div.text-sm.font-semibold:has-text("Import Successful")', + ) + await successToast.waitFor({ state: "visible", timeout: 5000 }) + await expect(successToast).toBeVisible() +}) + +test("should handle invalid Circuit JSON input", async ({ page }) => { + const importButton = page.locator('button:has-text("Import Circuit JSON")') + await importButton.click() + + const textarea = page.locator( + 'textarea[placeholder="Paste the Circuit JSON."]', + ) + await textarea.fill("invalid json content") + + const importDialogButton = page.getByRole("button", { name: "Import" }) + await importDialogButton.click() + + const errorToast = page.locator( + 'div.text-sm.font-semibold:has-text("Invalid Input")', + ) + await errorToast.waitFor({ state: "visible", timeout: 5000 }) + await expect(errorToast).toBeVisible() +}) + +test("should handle invalid Circuit JSON file upload", async ({ page }) => { + const importButton = page.locator('button:has-text("Import Circuit JSON")') + await importButton.click() + + const fileInput = page.locator('input[type="file"]') + await fileInput.setInputFiles({ + name: "circuit.json", + mimeType: "application/json", + // @ts-expect-error didnt add node types to tsconfig + buffer: Buffer.from(JSON.stringify({})), + }) + + const importDialogButton = page.getByRole("button", { name: "Import" }) + await importDialogButton.click() + + const errorToast = page.locator( + 'div.text-sm.font-semibold:has-text("Import Failed")', + ) + await errorToast.waitFor({ state: "visible", timeout: 5000 }) + await expect(errorToast).toBeVisible() +}) + +test("should handle non-JSON file upload", async ({ page }) => { + const importButton = page.locator('button:has-text("Import Circuit JSON")') + await importButton.click() + + const fileInput = page.locator('input[type="file"]') + await fileInput.setInputFiles({ + name: "circuit.txt", + mimeType: "application/text", + // @ts-expect-error didnt add node types to tsconfig + buffer: Buffer.from(""), + }) + + const importDialogButton = page.getByRole("button", { name: "Import" }) + await importDialogButton.click() + + const errorToast = page.locator( + 'div.pb-4 > p:has-text("Please select a valid JSON file.")', + ) + await expect(errorToast).toBeVisible() +}) diff --git a/playwright-tests/exampleCircuitJson.ts b/playwright-tests/exampleCircuitJson.ts new file mode 100644 index 00000000..0c1007ac --- /dev/null +++ b/playwright-tests/exampleCircuitJson.ts @@ -0,0 +1,498 @@ +export const exampleCircuitJson = [ + { + type: "source_port", + source_port_id: "source_port_0", + name: "pin1", + pin_number: 1, + port_hints: ["anode", "pos", "left", "pin1", "1"], + source_component_id: "source_component_0", + }, + { + type: "source_port", + source_port_id: "source_port_1", + name: "pin2", + pin_number: 2, + port_hints: ["cathode", "neg", "right", "pin2", "2"], + source_component_id: "source_component_0", + }, + { + type: "source_component", + source_component_id: "source_component_0", + ftype: "simple_resistor", + name: "R1", + supplier_part_numbers: { + jlcpcb: ["C11702", "C25543", "C226166"], + }, + resistance: 1000, + display_resistance: "1kΩ", + }, + { + type: "source_port", + source_port_id: "source_port_2", + name: "pin1", + pin_number: 1, + port_hints: ["anode", "pos", "left", "pin1", "1"], + source_component_id: "source_component_1", + }, + { + type: "source_port", + source_port_id: "source_port_3", + name: "pin2", + pin_number: 2, + port_hints: ["cathode", "neg", "right", "pin2", "2"], + source_component_id: "source_component_1", + }, + { + type: "source_component", + source_component_id: "source_component_1", + ftype: "simple_capacitor", + name: "C1", + supplier_part_numbers: { + jlcpcb: ["C106205", "C1523", "C14442"], + }, + capacitance: 1e-9, + display_capacitance: "1nF", + }, + { + type: "source_group", + source_group_id: "source_group_0", + is_subcircuit: true, + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "source_trace", + source_trace_id: "source_trace_0", + connected_source_port_ids: ["source_port_0", "source_port_2"], + connected_source_net_ids: [], + max_length: null, + display_name: ".R1 > .pin1 to .C1 > .pin1", + subcircuit_connectivity_map_key: "unnamedsubcircuit27_connectivity_net0", + }, + { + type: "schematic_component", + schematic_component_id: "schematic_component_0", + center: { + x: 3, + y: 0, + }, + rotation: 0, + size: { + width: 1.0583332999999997, + height: 0.388910699999999, + }, + source_component_id: "source_component_0", + symbol_name: "boxresistor_right", + symbol_display_value: "1kΩ", + }, + { + type: "schematic_component", + schematic_component_id: "schematic_component_1", + center: { + x: -3, + y: 0, + }, + rotation: 0, + size: { + width: 1.0583333000000001, + height: 0.8400173000000031, + }, + source_component_id: "source_component_1", + symbol_name: "capacitor_right", + symbol_display_value: "1nF", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_0", + schematic_component_id: "schematic_component_0", + center: { + x: 2.4662093, + y: 0.045805199999999324, + }, + source_port_id: "source_port_0", + facing_direction: "left", + distance_from_component_edge: 0.4, + pin_number: 1, + display_pin_label: "left", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_1", + schematic_component_id: "schematic_component_0", + center: { + x: 3.5337907000000004, + y: 0.04525870000000065, + }, + source_port_id: "source_port_1", + facing_direction: "right", + distance_from_component_edge: 0.4, + pin_number: 2, + display_pin_label: "right", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_2", + schematic_component_id: "schematic_component_1", + center: { + x: -3.5512093000000005, + y: 0.016380250000000984, + }, + source_port_id: "source_port_2", + facing_direction: "left", + distance_from_component_edge: 0.4, + pin_number: 1, + display_pin_label: "left", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_3", + schematic_component_id: "schematic_component_1", + center: { + x: -2.4487907, + y: 0.016926950000000218, + }, + source_port_id: "source_port_3", + facing_direction: "right", + distance_from_component_edge: 0.4, + pin_number: 2, + display_pin_label: "right", + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_0", + source_trace_id: "source_trace_0", + edges: [ + { + from: { + route_type: "wire", + x: 2.4662093, + y: 0.045805199999999324, + width: 0.1, + layer: "top", + }, + to: { + route_type: "wire", + x: -2.0487906999999996, + y: 0.045805199999999324, + width: 0.1, + layer: "top", + }, + }, + { + from: { + route_type: "wire", + x: -2.0487906999999996, + y: 0.045805199999999324, + width: 0.1, + layer: "top", + }, + to: { + route_type: "wire", + x: -2.0487906999999996, + y: 0.7174736499999994, + width: 0.1, + layer: "top", + }, + }, + { + from: { + route_type: "wire", + x: -2.0487906999999996, + y: 0.7174736499999994, + width: 0.1, + layer: "top", + }, + to: { + route_type: "wire", + x: -3.5512093000000005, + y: 0.7174736499999994, + width: 0.1, + layer: "top", + }, + }, + { + from: { + route_type: "wire", + x: -3.5512093000000005, + y: 0.7174736499999994, + width: 0.1, + layer: "top", + }, + to: { + route_type: "wire", + x: -3.5512093000000005, + y: 0.016380250000000984, + width: 0.1, + layer: "top", + }, + }, + ], + junctions: [], + }, + { + type: "pcb_component", + pcb_component_id: "pcb_component_0", + center: { + x: 3, + y: 0, + }, + width: 1.5999999999999996, + height: 0.6000000000000001, + layer: "top", + rotation: 0, + source_component_id: "source_component_0", + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "pcb_component", + pcb_component_id: "pcb_component_1", + center: { + x: -3, + y: 0, + }, + width: 1.5999999999999996, + height: 0.6000000000000001, + layer: "top", + rotation: 0, + source_component_id: "source_component_1", + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "pcb_board", + pcb_board_id: "pcb_board_0", + center: { + x: 0, + y: 0, + }, + thickness: 1.4, + num_layers: 4, + width: 10, + height: 10, + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pcb_smtpad_0", + pcb_component_id: "pcb_component_0", + pcb_port_id: "pcb_port_0", + layer: "top", + shape: "rect", + width: 0.6000000000000001, + height: 0.6000000000000001, + port_hints: ["1", "left"], + x: 2.5, + y: 0, + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "pcb_solder_paste", + pcb_solder_paste_id: "pcb_solder_paste_0", + layer: "top", + shape: "rect", + width: 0.42000000000000004, + height: 0.42000000000000004, + x: 2.5, + y: 0, + pcb_component_id: "pcb_component_0", + pcb_smtpad_id: "pcb_smtpad_0", + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pcb_smtpad_1", + pcb_component_id: "pcb_component_0", + pcb_port_id: "pcb_port_1", + layer: "top", + shape: "rect", + width: 0.6000000000000001, + height: 0.6000000000000001, + port_hints: ["2", "right"], + x: 3.5, + y: 0, + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "pcb_solder_paste", + pcb_solder_paste_id: "pcb_solder_paste_1", + layer: "top", + shape: "rect", + width: 0.42000000000000004, + height: 0.42000000000000004, + x: 3.5, + y: 0, + pcb_component_id: "pcb_component_0", + pcb_smtpad_id: "pcb_smtpad_1", + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pcb_smtpad_2", + pcb_component_id: "pcb_component_1", + pcb_port_id: "pcb_port_2", + layer: "top", + shape: "rect", + width: 0.6000000000000001, + height: 0.6000000000000001, + port_hints: ["1", "left"], + x: -3.5, + y: 0, + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "pcb_solder_paste", + pcb_solder_paste_id: "pcb_solder_paste_2", + layer: "top", + shape: "rect", + width: 0.42000000000000004, + height: 0.42000000000000004, + x: -3.5, + y: 0, + pcb_component_id: "pcb_component_1", + pcb_smtpad_id: "pcb_smtpad_2", + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pcb_smtpad_3", + pcb_component_id: "pcb_component_1", + pcb_port_id: "pcb_port_3", + layer: "top", + shape: "rect", + width: 0.6000000000000001, + height: 0.6000000000000001, + port_hints: ["2", "right"], + x: -2.5, + y: 0, + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "pcb_solder_paste", + pcb_solder_paste_id: "pcb_solder_paste_3", + layer: "top", + shape: "rect", + width: 0.42000000000000004, + height: 0.42000000000000004, + x: -2.5, + y: 0, + pcb_component_id: "pcb_component_1", + pcb_smtpad_id: "pcb_smtpad_3", + subcircuit_id: "subcircuit_source_group_0", + }, + { + type: "pcb_port", + pcb_port_id: "pcb_port_0", + pcb_component_id: "pcb_component_0", + layers: ["top"], + subcircuit_id: "subcircuit_source_group_0", + x: 2.5, + y: 0, + source_port_id: "source_port_0", + }, + { + type: "pcb_port", + pcb_port_id: "pcb_port_1", + pcb_component_id: "pcb_component_0", + layers: ["top"], + subcircuit_id: "subcircuit_source_group_0", + x: 3.5, + y: 0, + source_port_id: "source_port_1", + }, + { + type: "pcb_port", + pcb_port_id: "pcb_port_2", + pcb_component_id: "pcb_component_1", + layers: ["top"], + subcircuit_id: "subcircuit_source_group_0", + x: -3.5, + y: 0, + source_port_id: "source_port_2", + }, + { + type: "pcb_port", + pcb_port_id: "pcb_port_3", + pcb_component_id: "pcb_component_1", + layers: ["top"], + subcircuit_id: "subcircuit_source_group_0", + x: -2.5, + y: 0, + source_port_id: "source_port_3", + }, + { + type: "pcb_trace", + pcb_trace_id: "pcb_trace_0", + route: [ + { + route_type: "wire", + x: 2.5, + y: 0, + width: 0.16, + layer: "top", + start_pcb_port_id: "pcb_port_0", + }, + { + route_type: "wire", + x: -1.2000000000000002, + y: 0, + width: 0.16, + layer: "top", + }, + { + route_type: "wire", + x: -1.2000000000000002, + y: 1.3, + width: 0.16, + layer: "top", + }, + { + route_type: "wire", + x: -3.5, + y: 1.3, + width: 0.16, + layer: "top", + }, + { + route_type: "wire", + x: -3.5, + y: 0, + width: 0.16, + layer: "top", + end_pcb_port_id: "pcb_port_2", + }, + ], + source_trace_id: "source_trace_0", + trace_length: 8.6, + }, + { + type: "cad_component", + cad_component_id: "cad_component_0", + position: { + x: 3, + y: 0, + z: 0.7, + }, + rotation: { + x: 0, + y: 0, + z: 0, + }, + pcb_component_id: "pcb_component_0", + source_component_id: "source_component_0", + footprinter_string: "0402", + }, + { + type: "cad_component", + cad_component_id: "cad_component_1", + position: { + x: -3, + y: 0, + z: 0.7, + }, + rotation: { + x: 0, + y: 0, + z: 0, + }, + pcb_component_id: "pcb_component_1", + source_component_id: "source_component_1", + footprinter_string: "0402", + }, +] diff --git a/src/components/CircuitJsonImportDialog.tsx b/src/components/CircuitJsonImportDialog.tsx new file mode 100644 index 00000000..20c67ed4 --- /dev/null +++ b/src/components/CircuitJsonImportDialog.tsx @@ -0,0 +1,186 @@ +import React, { useState } from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { useAxios } from "@/hooks/use-axios" +import { useToast } from "@/hooks/use-toast" +import { useLocation } from "wouter" +import { useGlobalStore } from "@/hooks/use-global-store" +import { convertCircuitJsonToTscircuit } from "circuit-json-to-tscircuit" + +interface CircuitJsonImportDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +const isValidJSON = (code: string) => { + try { + JSON.parse(code) + return true + } catch { + return false + } +} + +export function CircuitJsonImportDialog({ + open, + onOpenChange, +}: CircuitJsonImportDialogProps) { + const [circuitJson, setcircuitJson] = useState("") + const [file, setFile] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const axios = useAxios() + const { toast } = useToast() + const [, navigate] = useLocation() + const isLoggedIn = useGlobalStore((s) => Boolean(s.session)) + const session = useGlobalStore((s) => s.session) + + const handleFileChange = async (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0] + if (selectedFile && selectedFile.type === "application/json") { + setFile(selectedFile) + } else { + setError("Please select a valid JSON file.") + } + } + + const handleImport = async () => { + let importedCircuitJson + + if (file) { + try { + const fileText = await file.text() + importedCircuitJson = JSON.parse(fileText) + } catch (err) { + setError("Error reading JSON file. Please ensure it is valid.") + return + } + } else if (isValidJSON(circuitJson)) { + setIsLoading(true) + setError(null) + importedCircuitJson = JSON.parse(circuitJson) + } else { + toast({ + title: "Invalid Input", + description: "Please provide a valid JSON content or file.", + variant: "destructive", + }) + return + } + let tscircuit + try { + tscircuit = convertCircuitJsonToTscircuit(importedCircuitJson as any, { + componentName: "circuit", + }) + console.info(tscircuit) + } catch { + toast({ + title: "Import Failed", + description: "Invalid Circuit JSON was provided.", + variant: "destructive", + }) + setIsLoading(false) + return + } + + try { + const newSnippetData = { + snippet_type: importedCircuitJson.type ?? "board", + circuit_json: importedCircuitJson, + code: tscircuit, + } + const response = await axios + .post("/snippets/create", newSnippetData) + .catch((e) => e) + const { snippet, message } = response.data + if (message) { + setError(message) + setIsLoading(false) + return + } + toast({ + title: "Import Successful", + description: "Circuit Json has been imported successfully.", + }) + onOpenChange(false) + navigate(`/editor?snippet_id=${snippet.snippet_id}`) + } catch (error) { + console.error("Error importing Circuit Json:", error) + toast({ + title: "Import Failed", + description: "Failed to import the Circuit Json. Please try again.", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } + + return ( + + + + Import Circuit JSON + +
+