From 27ca45f5d2b7f208c0f3bef1a2afd8fed24e8ae3 Mon Sep 17 00:00:00 2001 From: gaofeng fan Date: Sun, 22 Feb 2026 14:30:02 -0800 Subject: [PATCH 001/105] Add GitHub Pages deployment workflow and set base path --- .github/workflows/deploy-pages.yml | 41 ++++++++++++++++++++++++++++++ vite.config.mts | 1 + 2 files changed, 42 insertions(+) create mode 100644 .github/workflows/deploy-pages.yml diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 0000000..7a76144 --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,41 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - run: npm run build + + - uses: actions/configure-pages@v4 + + - uses: actions/upload-pages-artifact@v3 + with: + path: dist + + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/vite.config.mts b/vite.config.mts index 314a01c..e685f03 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -4,6 +4,7 @@ import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ + base: "/EEcircuit/", resolve: { preserveSymlinks: true, }, From 9dc994aa38d695fef673c65666f0c9dce5d0647b Mon Sep 17 00:00:00 2001 From: gaofeng fan Date: Sun, 22 Feb 2026 14:32:16 -0800 Subject: [PATCH 002/105] Switch Pages deploy to gh-pages branch strategy --- .github/workflows/deploy-pages.yml | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 7a76144..898d98b 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -5,20 +5,11 @@ on: branches: [main] permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: true + contents: write jobs: build-and-deploy: runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} steps: - uses: actions/checkout@v4 @@ -31,11 +22,8 @@ jobs: - run: npm run build - - uses: actions/configure-pages@v4 - - - uses: actions/upload-pages-artifact@v3 + - name: Deploy to gh-pages branch + uses: peaceiris/actions-gh-pages@v3 with: - path: dist - - - id: deployment - uses: actions/deploy-pages@v4 + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist From 0af4a26336e97dfe75ecc78d88d76f4a8b473d77 Mon Sep 17 00:00:00 2001 From: gaofeng fan Date: Sun, 22 Feb 2026 15:11:57 -0800 Subject: [PATCH 003/105] Fix base path for enterprise GitHub Pages deployment Co-Authored-By: Claude Opus 4.6 --- deploy.py | 105 ++++++++++++++++++++++++++++++++++++++++++++++++ vite.config.mts | 2 +- 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100755 deploy.py diff --git a/deploy.py b/deploy.py new file mode 100755 index 0000000..2bda1e2 --- /dev/null +++ b/deploy.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Build and deploy EEcircuit to GitHub Pages on enterprise GitHub.""" + +import subprocess +import sys +import shutil +import tempfile +from pathlib import Path + +REPO_DIR = Path(__file__).parent.resolve() +REMOTE = "enterprise" +BRANCH = "gh-pages" + + +def run(cmd, **kwargs): + print(f" → {cmd}") + result = subprocess.run(cmd, shell=True, cwd=REPO_DIR, capture_output=True, text=True, **kwargs) + if result.returncode != 0: + print(f" ✗ {result.stderr.strip()}") + sys.exit(1) + return result.stdout.strip() + + +def main(): + # Ensure we're on main + current = run("git branch --show-current") + if current != "main": + print(f"ERROR: Expected to be on 'main', but on '{current}'") + sys.exit(1) + + # Check for uncommitted changes (excluding package-lock.json) + status = run("git status --porcelain") + dirty = [line for line in status.splitlines() if "package-lock.json" not in line] + if dirty: + print("ERROR: You have uncommitted changes. Commit or stash them first.") + for line in dirty: + print(f" {line}") + sys.exit(1) + + print("[1/5] Installing dependencies...") + run("npm ci") + + print("[2/5] Building project...") + run("npm run build") + + # Copy dist to temp location + dist_dir = REPO_DIR / "dist" + if not dist_dir.exists(): + print("ERROR: dist/ not found after build") + sys.exit(1) + + with tempfile.TemporaryDirectory() as tmpdir: + tmp_dist = Path(tmpdir) / "dist" + shutil.copytree(dist_dir, tmp_dist) + + print("[3/5] Switching to gh-pages branch...") + run("git stash --include-untracked") + # Delete local gh-pages if it exists, then create fresh orphan + subprocess.run("git branch -D gh-pages 2>/dev/null", shell=True, cwd=REPO_DIR) + run("git switch --orphan gh-pages") + # Remove all tracked and untracked files + subprocess.run("git rm -rf --cached . > /dev/null 2>&1", shell=True, cwd=REPO_DIR) + subprocess.run("git clean -fd > /dev/null 2>&1", shell=True, cwd=REPO_DIR) + # Remove any leftover directories + for item in REPO_DIR.iterdir(): + if item.name == ".git": + continue + if item.is_dir(): + shutil.rmtree(item) + else: + item.unlink() + + print("[4/5] Copying build output...") + for item in tmp_dist.iterdir(): + dest = REPO_DIR / item.name + if item.is_dir(): + shutil.copytree(item, dest) + else: + shutil.copy2(item, dest) + + run("git add .") + run('git commit -m "Deploy site"') + + print(f"[5/5] Pushing to {REMOTE}/{BRANCH}...") + run(f"git push --force {REMOTE} {BRANCH}") + + # Switch back to main + print("Restoring main branch...") + # Remove deployed files before switching + for item in REPO_DIR.iterdir(): + if item.name == ".git": + continue + if item.is_dir(): + shutil.rmtree(item) + else: + item.unlink() + run("git switch main") + run("git stash pop") + + print("\nDone! Site deployed to gh-pages branch.") + print("URL: https://pages.github.pie.apple.com/gaofeng-fan/EEcircuit/") + + +if __name__ == "__main__": + main() diff --git a/vite.config.mts b/vite.config.mts index e685f03..e4fcb16 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -4,7 +4,7 @@ import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ - base: "/EEcircuit/", + base: "/gaofeng-fan/EEcircuit/", resolve: { preserveSymlinks: true, }, From 035927872a217269639ae6f51d5bc790a46a8408 Mon Sep 17 00:00:00 2001 From: gaofeng fan Date: Sun, 22 Feb 2026 15:15:06 -0800 Subject: [PATCH 004/105] Fix deploy script to use npm install instead of npm ci Co-Authored-By: Claude Opus 4.6 --- deploy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy.py b/deploy.py index 2bda1e2..eb3a987 100755 --- a/deploy.py +++ b/deploy.py @@ -38,7 +38,7 @@ def main(): sys.exit(1) print("[1/5] Installing dependencies...") - run("npm ci") + run("npm install") print("[2/5] Building project...") run("npm run build") From 8aeff165a1d52a29172d79c0b823e56d9f80577b Mon Sep 17 00:00:00 2001 From: gaofeng fan Date: Sun, 22 Feb 2026 15:16:01 -0800 Subject: [PATCH 005/105] Fix deploy script: use --legacy-peer-deps for npm compatibility Co-Authored-By: Claude Opus 4.6 --- deploy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy.py b/deploy.py index eb3a987..55035f3 100755 --- a/deploy.py +++ b/deploy.py @@ -38,7 +38,7 @@ def main(): sys.exit(1) print("[1/5] Installing dependencies...") - run("npm install") + run("npm install --legacy-peer-deps") print("[2/5] Building project...") run("npm run build") From 4112ce21135ed07063b304a50aea937a3c9589dd Mon Sep 17 00:00:00 2001 From: gaofeng fan Date: Sun, 22 Feb 2026 16:33:00 -0800 Subject: [PATCH 006/105] Enable axis by default, move x-axis to top, and add simulation error handling - Plot axis is now on by default (isAxis=true) - X-axis moved from bottom to top edge of the plot grid - Wrap simulation run in try/catch and check for empty results to prevent UI crash on invalid netlist syntax Co-Authored-By: Claude Opus 4.6 --- src/EEcircuit.tsx | 29 +++++++++++++++++++++--- src/plotArray.tsx | 58 +++++++++++++++++++++++------------------------ 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/EEcircuit.tsx b/src/EEcircuit.tsx index 844dce6..86ac1f9 100644 --- a/src/EEcircuit.tsx +++ b/src/EEcircuit.tsx @@ -196,9 +196,32 @@ export default function EEcircuit(): JSX.Element { //setParser(getParser(netList)); store.setItem("netList", netList); sim.setNetList(netList); - const resultArray = await sim.runSim(); - setResultArray(resultArray); - setInfo(initialSimInfo + "\n\n" + (await sim.getInfo()) + "\n\n"); + try { + const resultArray = await sim.runSim(); + const errors = await sim.getError(); + if (errors.length > 0) { + errors.forEach((e) => { + toaster.create({ + description: e, + type: "error", + }); + }); + } + if (resultArray.results.length === 0) { + toaster.create({ + description: "Simulation returned no results. Check your netlist syntax.", + type: "error", + }); + } else { + setResultArray(resultArray); + } + setInfo(initialSimInfo + "\n\n" + (await sim.getInfo()) + "\n\n"); + } catch (e) { + toaster.create({ + description: e instanceof Error ? e.message : String(e), + type: "error", + }); + } setIsSimRunning(false); } else { //spawn worker thread diff --git a/src/plotArray.tsx b/src/plotArray.tsx index 688aa37..1690d28 100644 --- a/src/plotArray.tsx +++ b/src/plotArray.tsx @@ -92,7 +92,7 @@ function PlotArray({ sweepSlider: false, }); const [isSweep, SetIsSweep] = useState(false); - const [isAxis, SetIsAxis] = useState(false); + const [isAxis, SetIsAxis] = useState(true); const [sliderValue, SetSliderValue] = useState(0); @@ -635,7 +635,7 @@ function PlotArray({ return ( <> - + Axis @@ -692,7 +692,7 @@ function PlotArray({ )} @@ -700,6 +700,31 @@ function PlotArray({ rowStart={1} colStart={1} bg="bg.subtle" + borderBottom="solid 2px" + borderRight="solid 2px" + /> + + {isAxis ? ( + + ) : ( + <> + )} + + {isAxis ? ( @@ -714,7 +739,7 @@ function PlotArray({ <> )} - + - - - {isAxis ? ( - - ) : ( - <> - )} - ); From 5fe9aae14258dbe828d09c3ad941a9f358abf2cf Mon Sep 17 00:00:00 2001 From: Gaofeng Fan Date: Sun, 22 Feb 2026 16:38:44 -0800 Subject: [PATCH 007/105] Update package-lock.json dependencies Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 423 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 408 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd6ac8a..25b1855 100644 --- a/package-lock.json +++ b/package-lock.json @@ -755,6 +755,74 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", @@ -772,6 +840,346 @@ "node": ">=18" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -10620,21 +11028,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", From 5fab1b15703be53f628571bbdfff5f4a73c24ce6 Mon Sep 17 00:00:00 2001 From: Gaofeng Fan Date: Sun, 22 Feb 2026 22:55:50 -0800 Subject: [PATCH 008/105] Update default Python example with rotation and ngspice --- src/EEcircuit.tsx | 321 +++++++++++++++++++++++++++++++++--- src/editor/editorCustom.tsx | 11 +- src/python/pyodideRunner.ts | 68 ++++++++ src/python/pyodideWorker.ts | 172 +++++++++++++++++++ 4 files changed, 545 insertions(+), 27 deletions(-) create mode 100644 src/python/pyodideRunner.ts create mode 100644 src/python/pyodideWorker.ts diff --git a/src/EEcircuit.tsx b/src/EEcircuit.tsx index 86ac1f9..a2a0e44 100644 --- a/src/EEcircuit.tsx +++ b/src/EEcircuit.tsx @@ -61,13 +61,17 @@ import { useColorMode, useColorModeValue, } from "./components/ui/color-mode.tsx"; +import { PyodideRunner } from "./python/pyodideRunner.ts"; + +type EditorMode = "spice" | "python"; let sim: SimArray; +let pyRunner: PyodideRunner | null = null; const store = globalThis.localStorage; let initialSimInfo = ""; let threadCount = 1; -const circuitDefault = `Basic RLC circuit +const circuitDefault = `Basic RLC circuit .include modelcard.CMOS90 r vdd 2 100.0 @@ -81,9 +85,43 @@ vin 1 0 0 pulse (0 1.8 0 0.1 0.1 15 30) .end`; -export default function EEcircuit(): JSX.Element { - // Create the count state. +const pythonDefault = `from analogpy import ( + Testbench, resistor, capacitor, inductor, + nmos, vsource, vpulse, Transient, + generate_ngspice, +) + +tb = Testbench("rlc_circuit") +tb.include("modelcard.CMOS90") + +vdd_net = tb.net("vdd") +net_rc = tb.net("net_rc") +gate = tb.net("gate") +gnd = tb.gnd() + +# Passive components between vdd and net_rc +tb.add_instance(resistor, "r", p=vdd_net, n=net_rc, r=100.0) +tb.add_instance(inductor, "l", p=vdd_net, n=net_rc, l=1) +tb.add_instance(capacitor, "c", p=vdd_net, n=net_rc, c=0.01, rotation=180) + +# NMOS transistor +tb.add_instance(nmos, "m1", d=net_rc, g=gate, s=gnd, b=gnd, + model="N90", w=100e-6, l=0.09e-6) + +# Power supply +tb.add_instance(vsource, "vdd", p=vdd_net, n=gnd, dc=1.8) + +# Input pulse (using vpulse convenience device) +tb.add_instance(vpulse, "vin", p=gate, n=gnd, + val0=0, val1=1.8, delay=0, + rise=0.1, fall=0.1, width=15, period=30) + +tb.add_analysis(Transient(stop=50, step=0.1)) +print(generate_ngspice(tb)) +`; + +export default function EEcircuit(): JSX.Element { const [isSimLoaded, setIsSimLoaded] = React.useState(false); const [isSimLoading, setIsSimLoading] = React.useState(false); const [isSimRunning, setIsSimRunning] = React.useState(false); @@ -96,11 +134,33 @@ export default function EEcircuit(): JSX.Element { const [progress, setProgress] = React.useState(0); const [threadCountNew, setThreadCountNew] = React.useState(1); + // Python / editor mode state + const [editorMode, setEditorMode] = React.useState( + () => (store.getItem("editorMode") as EditorMode) || "spice" + ); + const [pythonCode, setPythonCode] = React.useState( + () => store.getItem("pythonCode") || pythonDefault + ); + const [generatedNgspice, setGeneratedNgspice] = React.useState(""); + const [generatedSpectre, setGeneratedSpectre] = React.useState(""); + const [schematicSvg, setSchematicSvg] = React.useState(""); + const [schematicZoom, setSchematicZoom] = React.useState(1.0); + const [isPyLoading, setIsPyLoading] = React.useState(false); + const [isFullscreen, setIsFullscreen] = React.useState(false); + const tabsContainerRef = React.useRef(null); + const colorMode = useColorModeValue("light", "dark"); useEffect(() => { - const loadedNetList = store.getItem("netList"); - setNetList(loadedNetList ? loadedNetList : circuitDefault); + const loadedMode = store.getItem("editorMode") as EditorMode | null; + if (loadedMode === "python") { + setEditorMode("python"); + const loadedPy = store.getItem("pythonCode"); + if (loadedPy) setPythonCode(loadedPy); + } else { + const loadedNetList = store.getItem("netList"); + setNetList(loadedNetList ? loadedNetList : circuitDefault); + } const loadedDisplayDataString = store.getItem("displayData"); if (loadedDisplayDataString) { @@ -191,11 +251,77 @@ export default function EEcircuit(): JSX.Element { }, []);*/ const btRun = async () => { + // Python mode: run Python code first to generate netlist + if (editorMode === "python") { + setIsSimRunning(true); + store.setItem("pythonCode", pythonCode); + + // Initialize Pyodide if needed + if (!pyRunner || !pyRunner.isReady) { + setIsPyLoading(true); + toaster.create({ + description: "Loading Python runtime (first time only)...", + type: "info", + }); + pyRunner = new PyodideRunner(); + try { + await pyRunner.init(); + } catch (e) { + toaster.create({ + description: "Failed to load Python runtime: " + (e instanceof Error ? e.message : String(e)), + type: "error", + }); + setIsPyLoading(false); + setIsSimRunning(false); + return; + } + setIsPyLoading(false); + } + + // Run Python code + const pyResult = await pyRunner.runPython(pythonCode); + if (pyResult.error) { + toaster.create({ + description: "Python error: " + pyResult.error, + type: "error", + }); + setIsSimRunning(false); + return; + } + + if (!pyResult.ngspice) { + toaster.create({ + description: "Python code did not produce an ngspice netlist. Make sure to call generate_ngspice() or print() the netlist.", + type: "error", + }); + setIsSimRunning(false); + return; + } + + setGeneratedNgspice(pyResult.ngspice); + setGeneratedSpectre(pyResult.spectre); + const svgData = pyResult.schematicSvg || ""; + console.log("schematicSvg length:", svgData.length, "first 100:", svgData.substring(0, 100)); + setSchematicSvg(svgData); + + // Use the generated netlist for simulation + setNetList(pyResult.ngspice); + setIsSimRunning(false); + + // Now run the simulation with the generated netlist + await runSimulation(pyResult.ngspice); + return; + } + + // SPICE mode: run directly + store.setItem("netList", netList); + await runSimulation(netList); + }; + + const runSimulation = async (netlistToRun: string) => { if (sim && threadCount === threadCountNew) { setIsSimRunning(true); - //setParser(getParser(netList)); - store.setItem("netList", netList); - sim.setNetList(netList); + sim.setNetList(netlistToRun); try { const resultArray = await sim.runSim(); const errors = await sim.getError(); @@ -236,8 +362,7 @@ export default function EEcircuit(): JSX.Element { setIsSimLoaded(true); setIsSimLoading(false); setProgress(0); - //initialSimInfo = await sim.getInfo(); //not yet working??????? - btRun(); + runSimulation(netlistToRun); } }; @@ -282,9 +407,23 @@ export default function EEcircuit(): JSX.Element { const handleEditor = React.useCallback((value: string | undefined) => { if (value) { - setNetList(value); + if (editorMode === "python") { + setPythonCode(value); + } else { + setNetList(value); + } } - }, []); + }, [editorMode]); + + const handleModeSwitch = React.useCallback(() => { + const newMode = editorMode === "spice" ? "python" : "spice"; + setEditorMode(newMode); + store.setItem("editorMode", newMode); + // Clear generated netlists when switching mode + setGeneratedNgspice(""); + setGeneratedSpectre(""); + setSchematicSvg(""); + }, [editorMode]); const handleDeSelectButton = React.useCallback(() => { if (displayData) { @@ -398,30 +537,32 @@ export default function EEcircuit(): JSX.Element {
- }> - - + + }> + + + {displayBreakpoint == "base" ? <> : LineSelectBox()} + + { + {editorMode === "python" && ( + <> + + ngspice + + + Spectre + + + Schematic + + + )} @@ -587,6 +773,89 @@ export default function EEcircuit(): JSX.Element { + + {editorMode === "python" && ( + <> + +