├── .github ├── CODEOWNERS └── workflows │ ├── buildcheck.yml │ ├── bunaider-review-response.yml │ ├── bunaider.yml │ ├── npm-semantic-release.yml │ ├── npm-test.yml │ ├── prettier-check.yml │ └── typecheck.yml ├── .gitignore ├── .prettierrc ├── DEVELOPMENT.md ├── README.md ├── ava.config.js ├── biome.json ├── package-lock.json ├── package.json ├── release.config.js ├── src ├── index.ts └── lib │ ├── apply-selector │ ├── convert-abbr-to-ftype.ts │ └── index.ts │ ├── bom-csv │ └── index.ts │ ├── builder │ ├── board-builder.ts │ ├── builder-interface.ts │ ├── component-builder │ │ ├── BugBuilder.ts │ │ ├── CapacitorBuilder.ts │ │ ├── ComponentBuilder.ts │ │ ├── DiodeBuilder.ts │ │ ├── EagleImportBuilder.ts │ │ ├── GroundBuilder.ts │ │ ├── InductorBuilder.ts │ │ ├── LedBuilder.ts │ │ ├── NetAliasBuilder.ts │ │ ├── PowerSourceBuilder.ts │ │ ├── ResistorBuilder.ts │ │ ├── ViaBuilder.ts │ │ ├── index.ts │ │ └── remap-prop.ts │ ├── constrained-layout-builder │ │ ├── constrained-layout-builder.ts │ │ ├── constraint-builder.ts │ │ ├── index.ts │ │ └── spatial-util.ts │ ├── define-new-component │ │ └── index.ts │ ├── drawing-builder │ │ ├── drawing-builder.ts │ │ └── index.ts │ ├── footprint-builder │ │ ├── associate-pcb-ports-with-pads.ts │ │ ├── basic-pcb-trace-builder.ts │ │ ├── fabrication-note-path-builder.ts │ │ ├── fabrication-note-text-builder.ts │ │ ├── footprint-builder.ts │ │ ├── hole-builder.ts │ │ ├── index.ts │ │ ├── match-pcb-port-with-pad.ts │ │ ├── pcb-via-builder.ts │ │ ├── plated-hole-builder.ts │ │ ├── silkscreen-circle-builder.ts │ │ ├── silkscreen-line-builder.ts │ │ ├── silkscreen-path-builder.ts │ │ ├── silkscreen-rect-builder.ts │ │ ├── silkscreen-text-builder.ts │ │ └── smt-pad-builder.ts │ ├── group-builder.ts │ ├── index.ts │ ├── net-builder │ │ └── net-builder.ts │ ├── ports-builder │ │ ├── index.ts │ │ ├── port-builder.ts │ │ └── ports-builder.ts │ ├── project-builder.ts │ ├── schematic-symbol-builder │ │ ├── index.ts │ │ ├── schematic-box-builder.ts │ │ ├── schematic-line-builder.ts │ │ ├── schematic-path-builder.ts │ │ ├── schematic-symbol-builder.ts │ │ └── schematic-text-builder.ts │ ├── simple-data-builder.ts │ ├── trace-builder │ │ ├── build-pcb-trace-elements.ts │ │ ├── build-trace-for-single-port-and-net.ts │ │ ├── convert-to-readable-route-tree.ts │ │ ├── index.ts │ │ ├── match-pcb-ports-with-footprint.ts │ │ ├── pcb-errors.ts │ │ ├── pcb-routing │ │ │ ├── find-possible-trace-layer-combinations.ts │ │ │ ├── get-pcb-obstacles.ts │ │ │ ├── merge-routes.ts │ │ │ ├── pcb-solver-grid.ts │ │ │ ├── solve-for-route.ts │ │ │ ├── solve-for-single-layer-route.ts │ │ │ └── trace-pcb-routing-context.ts │ │ ├── route-solvers │ │ │ ├── port-offset-wrapper.ts │ │ │ ├── rmst-or-route1-solver.ts │ │ │ ├── rmst-solver.ts │ │ │ ├── route1-solver.ts │ │ │ └── straight-route-solver.ts │ │ └── schematic-routing │ │ │ └── get-schematic-obstacles-from-elements.ts │ ├── trace-hint-builder │ │ └── index.ts │ └── transform-elements.ts │ ├── pick-and-place-csv │ └── index.ts │ ├── project │ ├── create-project-from-elements.ts │ ├── defaults.ts │ ├── index.ts │ └── project-class.ts │ ├── types │ ├── build-context.ts │ ├── builders.ts │ ├── core.ts │ ├── index.ts │ ├── layout-debug-object.ts │ ├── manual_layout.ts │ ├── route-solver.ts │ ├── source-component.ts │ └── util.ts │ └── utils │ ├── combined.ts │ ├── convert-si-unit-to-number.ts │ ├── convert-side-to-direction.ts │ ├── convert-to-degrees.ts │ ├── direction-to-vec.ts │ ├── extract-ids.ts │ ├── find-bounds-and-center.ts │ ├── get-layout-debug-object.ts │ ├── get-port-position.ts │ ├── get-zod-schema-defaults.ts │ ├── index.ts │ ├── is-truthy.ts │ ├── maybe-convert-to-point.ts │ ├── pairs.ts │ ├── point-math.ts │ ├── point-operations.ts │ ├── remove-nulls.ts │ ├── string-hash.ts │ └── uniq.ts ├── tests ├── apply-selector │ ├── apply-selector-1.test.ts │ ├── apply-selector-2-via.test.ts │ ├── apply-selector-3-port-hints.test.ts │ └── apply-selector-4-nets.ts ├── basic-interfaces │ ├── footprint-hole.test.ts │ └── snapshots │ │ ├── footprint-hole.test.ts.md │ │ └── footprint-hole.test.ts.snap ├── board-builder │ ├── board-builder.test.ts │ └── center-with-one-prop.test.ts ├── bom-csv │ ├── convert-soup-to-bom-rows-resistor.test.ts │ └── convert-soup-to-bom-rows-variety.test.ts ├── bug-builder │ ├── bug-footprint-with-translation.test.tsx │ ├── bug-resistor-connection.test.tsx │ ├── bug-with-pin-spacing.test.tsx │ ├── custom-port-arrangement.test.tsx │ ├── duplicate-port-hints.test.tsx │ ├── one-sided-bug.test.tsx │ └── three-sided-bug.test.tsx ├── component-builder │ ├── cad-model-bottom-layer.test.ts │ └── cad-model.test.ts ├── constrained-layout-builder │ ├── basic-pcb.test.ts │ └── basic-schematic.test.ts ├── fixtures │ ├── get-test-fixture.ts │ ├── pcb-snapshot-output.ts │ ├── schematic-snapshot-output.ts │ └── setup-debug-logging.ts ├── footprints │ ├── basic-pcb-trace.test.tsx │ ├── move-footprint.test.tsx │ └── pcb-component-size.test.tsx ├── group-builder │ └── routing-disabled.test.ts ├── match-pcb-ports-with-footprint.test.ts ├── net-builder │ ├── __snapshots__ │ │ └── net-builder-1-pcb.snapshot.png │ ├── net-builder-1.test.ts │ ├── net-builder-2.test.ts │ ├── net-builder-3.test.ts │ ├── net-builder-4.test.ts │ └── net-builder-5.test.ts ├── pcb-manual-layout │ ├── group-builder-pcb-manual-layout-1.test.ts │ ├── group-builder-pcb-manual-layout-2.test.ts │ └── group-builder-pcb-manual-trace-hint.test.ts ├── primitives │ └── schematic-path.test.ts ├── routing-bugs │ ├── dont-route-through-plateholes.test.ts │ ├── netalias-routing-to-negative.test.ts │ ├── pcb-port-without-xy.test.ts │ └── second-degree-port-aliasing.test.ts ├── routing │ ├── pcb-routing-hints.test.ts │ └── schematic-routing-hints.test.ts ├── schematic-autolayout │ └── automatic-schematic-layout.test.ts ├── smoke-tests │ ├── diode.test.ts │ ├── fabrication.test.ts │ ├── net-alias.test.ts │ ├── resistor.test.ts │ └── silkscreen.test.ts ├── stories │ ├── footprint-with-traces.test.ts │ ├── generic-component-builder.test.ts │ ├── led-circuit.test.ts │ ├── schematic-text.test.ts │ ├── set-footprint-as-builder.test.ts │ └── snapshots │ │ ├── resistor.test.ts.md │ │ ├── resistor.test.ts.snap │ │ ├── sparkfun-footprint.test.ts.md │ │ └── sparkfun-footprint.test.ts.snap ├── trace-builder │ ├── __snapshots__ │ │ ├── multi-layer-route-2-pcb.snapshot.png │ │ └── multi-layer-route-2.snapshot.png │ ├── auto-route-segment-size-benchmark.test.ts │ ├── bad-schematic-route-1.test.ts │ ├── default-trace-builder.test.ts │ ├── find-possible-trace-layer-combinations.test.ts │ ├── intersection-test.test.ts │ ├── multi-layer-route-1.test.ts │ ├── multi-layer-route-2.test.ts │ ├── no-traces-through-pcb-pads.test.ts │ ├── no-traces-through-schematic-pins.test.ts │ ├── route-to-nets.test.ts │ └── simple-trace-test.test.ts ├── tracehint │ ├── trace-hint-1.test.ts │ └── trace-hint-2.test.ts ├── transformations │ └── port-rotation.test.ts ├── utils │ ├── get-explicit-to-normal-pin-mapping.test.ts │ └── log-layout.ts └── via-builder │ └── via-builder.test.ts ├── tsconfig.json ├── tsup.config.ts └── typedoc.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @seveibar @tscircuit/core -------------------------------------------------------------------------------- /.github/workflows/buildcheck.yml: -------------------------------------------------------------------------------- 1 | name: Build Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build-check: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 20 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Run Build Check 28 | run: npm run build 29 | -------------------------------------------------------------------------------- /.github/workflows/bunaider-review-response.yml: -------------------------------------------------------------------------------- 1 | name: Bunaider PR Review Response 2 | on: 3 | pull_request_review: 4 | types: [submitted] 5 | 6 | jobs: 7 | respond-to-review: 8 | if: github.event.review.state == 'changes_requested' && contains(github.event.review.body, 'aider:') 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup Bun 15 | uses: oven-sh/setup-bun@v1 16 | 17 | - name: Install bunaider 18 | run: bun install -g bunaider 19 | 20 | - run: bunaider init 21 | 22 | - name: Run bunaider fix on PR 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }} 25 | ANTHROPIC_API_KEY: ${{ secrets.TSCIRCUIT_BOT_ANTHROPIC_API_KEY }} 26 | AIDER_SONNET: 1 27 | run: bunaider fix ${{ github.event.pull_request.number }} 28 | -------------------------------------------------------------------------------- /.github/workflows/bunaider.yml: -------------------------------------------------------------------------------- 1 | name: Bunaider Auto-Fix 2 | on: 3 | issues: 4 | types: [labeled] 5 | 6 | jobs: 7 | auto-fix: 8 | if: github.event.label.name == 'aider' 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup Bun 15 | uses: oven-sh/setup-bun@v1 16 | 17 | - name: Install bunaider 18 | run: bun install -g bunaider 19 | 20 | - run: bunaider init 21 | 22 | - name: Run bunaider fix 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }} 25 | ANTHROPIC_API_KEY: ${{ secrets.TSCIRCUIT_BOT_ANTHROPIC_API_KEY }} 26 | AIDER_SONNET: 1 27 | run: bunaider fix ${{ github.event.issue.number }} 28 | -------------------------------------------------------------------------------- /.github/workflows/npm-semantic-release.yml: -------------------------------------------------------------------------------- 1 | name: NPM Semantic Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | publish: 8 | if: "!contains(github.event.head_commit.message, 'skip ci')" 9 | name: Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v1 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: "20.x" 18 | cache: "npm" 19 | - name: Install dependencies 20 | run: npm ci 21 | - name: Build 22 | run: npm run build 23 | - name: Release 24 | env: 25 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | run: npx semantic-release 28 | -------------------------------------------------------------------------------- /.github/workflows/npm-test.yml: -------------------------------------------------------------------------------- 1 | name: NPM Test 2 | on: [push] 3 | jobs: 4 | npm_test: 5 | if: "!contains(github.event.head_commit.message, 'skip ci')" 6 | name: Run NPM Test 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v1 11 | - name: Setup Node.js 12 | uses: actions/setup-node@v2 13 | with: 14 | node-version: 18 15 | cache: "npm" 16 | - name: Run NPM Install 17 | run: npm install 18 | - name: Run NPM Test 19 | run: npm run test 20 | -------------------------------------------------------------------------------- /.github/workflows/prettier-check.yml: -------------------------------------------------------------------------------- 1 | name: Prettier Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | prettier-check: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 18 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Run Prettier Check 28 | run: npx prettier --check . 29 | -------------------------------------------------------------------------------- /.github/workflows/typecheck.yml: -------------------------------------------------------------------------------- 1 | name: Type Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | prettier-check: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 20 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Run Type Check 28 | run: npm run typecheck 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vercel 4 | .next 5 | yarn-error.log 6 | .nsm 7 | .vscode 8 | .env* 9 | !.env.example 10 | .DS_Store 11 | .yalc 12 | yalc.lock 13 | docs 14 | *.artifact.* 15 | gerber-output 16 | gerber-output.zip 17 | tmp-debug-logs 18 | .aider* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "semi": false } 2 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development Guide 2 | 3 | `@tscircuit/builder` is a core library where essentially "all the tscircuit modules" are combined 4 | together to produce [tscircuit soup json](https://github.com/tscircuit/soup) as well as convert 5 | to various industry file formats needed for making electronics. 6 | 7 | It is also the oldest repo and pretty messy. We're actively working to refactor the builder and 8 | replace some big parts of it with separate modules. Until that's done, it's just something 9 | we have to deal with! 10 | 11 | The builder is a library with a ton of tests, you can't "run the builder" but it's in the 12 | background building circuits for you when you use `tsci dev`. 13 | 14 | When you're improving/fixing a bug in builder, you'll generally pick a test or create a new 15 | test. Every test looks the same: 16 | 17 | ```ts 18 | import test from "ava" 19 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 20 | 21 | test("something i want to test", async (t) => { 22 | const { logSoup, pb } = await getTestServer(t) 23 | 24 | // Customize this part, use the builder to make some soup! 25 | // The syntax sucks but it's similar to the react code 26 | const soup = await pb 27 | .add("resistor", (rb) => 28 | rb.setProps({ resistance: "10k", footprint: "0402" }) 29 | ) 30 | .build() 31 | 32 | // OPTIONAL: It's usually a good idea to have some kind of regression test, you can use 33 | // "su" from "@tscircuit/soup-util" to quickly check the soup for elements 34 | // t.is(su(soup).source_resistor.list().length, 1) 35 | 36 | // IMPORTANT! This will log the soup so you can _visually view the footprint_ on debug.tscircuit.com 37 | await logSoup(soup) 38 | }) 39 | ``` 40 | 41 | To run the test, you use this command: 42 | 43 | ```bash 44 | npx ava ./path/to/test.test.ts 45 | ``` 46 | 47 | ## Doing an Issue Guide 48 | 49 | 1. Pick a test or create a new test that replicates a bug/tests a new feature 50 | 2. Keep running that `npx ava ./path/to/test.test.ts` command until everything looks right/passes 51 | 3. Create a PR for your change 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @tscircuit/builder 2 | 3 | [tscircuit docs](https://docs.tscircuit.com) · [github](https://github.com/tscircuit/builder) · [tscircuit](https://tscircuit.com) · [discord](https://tscircuit.com/join) · [builder reference docs](https://tscircuit.github.io/builder) 4 | 5 | TSCircuit Builder is a Typescript builder pattern for constructing schematic and PCB layouts. `@tscircuit/builder` is an internal module, [use tscircuit tsx instead](https://github.com/tscircuit/tscircuit). The builder is basically "the DOM for building circuits" 6 | 7 | TSCircuit TSX eventually renders to a builder. The builder will build 8 | TSCircuit Soup, a JSON output. Soup can be rendered to 9 | a webpage as either a schematic or PCB layout. 10 | 11 | > [!NOTE] 12 | > Why not go directly from TSX -> Schematic/PCB? 13 | > 14 | > If you think about how React works, there's a layer between React and 15 | > the rendered HTML image you see on your screen, that layer is the DOM. 16 | > The DOM simplifies the amount of work React has to do. In the same way, 17 | > the Builder simplifies the amount of work that the TSCircuit TSX 18 | > have to do while providing a lot of flexibility for different renderers. It's 19 | > like the DOM for Circuits. 20 | 21 | > [!NOTE] 22 | > 23 | > `@tscircuit/builder` is going to be replaced by `@tscircuit/core` eventually, 24 | > the API and types are a bit wrong because it's an old package. Try to use 25 | > React with tscircuit to avoid building on the builder API (tscircuit React 26 | > has a much more permanent API) 27 | 28 | ## Example 29 | 30 | ```ts 31 | const projectBuilder = await createProjectBuilder() 32 | .add("resistor", (rb) => 33 | rb 34 | .setProps({ 35 | resistance: "10 ohm", 36 | name: "R1", 37 | schX: 2, 38 | schY: 1 39 | }) 40 | ) 41 | 42 | const projectBuilderOutput = await projectBuilder.build() 43 | 44 | 45 | /* 46 | // Soup JSON, very verbose! Looks easy to render though! 47 | [ 48 | { 49 | ftype: 'simple_resistor', 50 | name: 'R1', 51 | resistance: '10 ohm', 52 | source_component_id: 'simple_resistor_0', 53 | type: 'source_component', 54 | }, 55 | { 56 | center: { 57 | x: 2, 58 | y: 1, 59 | }, 60 | rotation: 0, 61 | schematic_component_id: 'schematic_component_simple_resistor_0', 62 | size: { 63 | height: 0.3, 64 | width: 1, 65 | }, 66 | source_component_id: 'simple_resistor_0', 67 | type: 'schematic_component', 68 | }, 69 | ... 70 | ] 71 | ``` 72 | 73 | ## Installation 74 | 75 | ```bash 76 | npm install --save @tscircuit/builder 77 | ``` 78 | -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | files: ["tests/**/*.test.ts"], 3 | extensions: ["ts"], 4 | require: ["esbuild-register", "./tests/fixtures/setup-debug-logging.ts"], 5 | ignoredByWatcher: [".next", ".nsm"], 6 | } 7 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "formatter": { 7 | "enabled": true, 8 | "indentStyle": "space" 9 | }, 10 | "javascript": { 11 | "formatter": { 12 | "jsxQuoteStyle": "double", 13 | "quoteProperties": "asNeeded", 14 | "trailingCommas": "all", 15 | "semicolons": "asNeeded", 16 | "arrowParentheses": "always", 17 | "bracketSpacing": true, 18 | "bracketSameLine": false 19 | } 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true, 25 | "suspicious": { 26 | "noExplicitAny": "off" 27 | }, 28 | "style": { 29 | "noNonNullAssertion": "off", 30 | "useFilenamingConvention": { 31 | "level": "error", 32 | "options": { 33 | "strictCase": true, 34 | "requireAscii": true, 35 | "filenameCases": ["kebab-case", "export"] 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tscircuit/builder", 3 | "version": "1.11.7", 4 | "type": "module", 5 | "types": "./dist/index.d.cts", 6 | "main": "./dist/index.cjs", 7 | "repository": "tscircuit/builder", 8 | "license": "MIT", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "test": "FULL_TEST=1 ava", 14 | "build": "tsup", 15 | "build:docs": "typedoc", 16 | "typecheck": "tsc --noEmit", 17 | "check:circular": "dpdm --skip-dynamic-imports circular --no-warning --no-tree -T --exit-code circular:1 ./src/index.ts", 18 | "yalc": "npm run build && yalc publish --push" 19 | }, 20 | "devDependencies": { 21 | "@biomejs/biome": "^1.7.3", 22 | "@semantic-release/commit-analyzer": "^9.0.2", 23 | "@semantic-release/git": "^10.0.1", 24 | "@semantic-release/npm": "^9.0.1", 25 | "@semantic-release/release-notes-generator": "^10.0.3", 26 | "@tscircuit/footprinter": "^0.0.44", 27 | "@tscircuit/log-soup": "^1.0.2", 28 | "@tscircuit/props": "^0.0.41", 29 | "@tscircuit/soup": "^0.0.57", 30 | "@tscircuit/soup-util": "^0.0.13", 31 | "@types/node": "^22.0.0", 32 | "ava": "^4.3.3", 33 | "circuit-to-png": "^0.0.3", 34 | "dpdm": "^3.13.0", 35 | "esbuild": "^0.15.18", 36 | "esbuild-register": "^3.3.3", 37 | "esbuild-runner": "^2.2.1", 38 | "parsel-js": "^1.0.2", 39 | "prettier": "^2.7.1", 40 | "redaxios": "^0.5.1", 41 | "tsup": "^8.0.2", 42 | "type-fest": "^2.19.0", 43 | "typescript": "^5.4.3", 44 | "zod": "^3.23.8" 45 | }, 46 | "peerDependencies": { 47 | "@tscircuit/footprinter": "*", 48 | "@tscircuit/props": "*", 49 | "@tscircuit/soup": "*", 50 | "@tscircuit/soup-util": "*" 51 | }, 52 | "dependencies": { 53 | "@lume/kiwi": "^0.1.0", 54 | "@tscircuit/layout": "^0.0.25", 55 | "@tscircuit/routing": "1.3.1", 56 | "@tscircuit/schematic-autolayout": "^0.0.5", 57 | "circuit-json-to-gerber": "^0.0.5", 58 | "convert-units": "^2.3.4", 59 | "fast-json-stable-stringify": "^2.1.0", 60 | "format-si-prefix": "^0.3.2", 61 | "papaparse": "^5.4.1", 62 | "rectilinear-router": "^1.0.1", 63 | "svg-path-bounds": "^1.0.2", 64 | "transformation-matrix": "^2.12.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | branches: ["main"], 3 | plugins: [ 4 | [ 5 | "@semantic-release/commit-analyzer", 6 | { 7 | preset: "angular", 8 | releaseRules: [ 9 | { type: "docs", scope: "README", release: "patch" }, 10 | { type: "fix", release: "patch" }, 11 | { type: "chore", release: "patch" }, 12 | { type: "refactor", release: "patch" }, 13 | { type: "style", release: "patch" }, 14 | { type: "test", release: "patch" }, 15 | { type: "feat", release: "minor" }, 16 | { type: "perf", release: "minor" }, 17 | { breaking: true, release: "major" }, 18 | { type: "build", scope: "deps", release: "patch" }, 19 | { message: "*", release: "patch" }, 20 | ], 21 | parserOpts: { 22 | noteKeywords: ["BREAKING CHANGE", "BREAKING CHANGES"], 23 | }, 24 | }, 25 | ], 26 | "@semantic-release/release-notes-generator", 27 | ["@semantic-release/npm", { npmPublish: true }], 28 | [ 29 | "@semantic-release/git", 30 | { 31 | assets: ["package.json"], 32 | message: 33 | "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", 34 | }, 35 | ], 36 | ], 37 | } 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/project" 2 | export * from "./lib/builder" 3 | export * from "./lib/apply-selector" 4 | export * from "./lib/types" 5 | export * from "./lib/utils" 6 | export * from "circuit-json-to-gerber" 7 | export * from "./lib/bom-csv" 8 | export * from "./lib/pick-and-place-csv" 9 | export { buildPcbTraceElements } from "./lib/builder/trace-builder/build-pcb-trace-elements" 10 | -------------------------------------------------------------------------------- /src/lib/apply-selector/convert-abbr-to-ftype.ts: -------------------------------------------------------------------------------- 1 | export const convertAbbrToFType = (abbr: string): string => { 2 | switch (abbr) { 3 | case "port": 4 | return "source_port" 5 | case "net": 6 | return "source_net" 7 | case "power": 8 | return "simple_power_source" 9 | } 10 | return abbr 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/apply-selector/index.ts: -------------------------------------------------------------------------------- 1 | export { applySelector } from "@tscircuit/soup-util" 2 | -------------------------------------------------------------------------------- /src/lib/builder/builder-interface.ts: -------------------------------------------------------------------------------- 1 | export interface BuilderInterface { 2 | builder_type: string 3 | setProps(props: any): this 4 | build(...args: any[]): any 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/builder/component-builder/DiodeBuilder.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { defineNewComponent } from "../define-new-component" 3 | 4 | export const { DiodeBuilderClass, createDiodeBuilder } = defineNewComponent({ 5 | pascal_name: "Diode", 6 | underscore_name: "diode", 7 | source_properties: z.object({ 8 | ftype: z.literal("simple_diode").default("simple_diode"), 9 | }), 10 | schematic_properties: z.object({}), 11 | pcb_properties: z.object({}), 12 | fixed_schematic_size: { width: 1, height: 0.5 }, 13 | configurePorts(builder, ctx) { 14 | builder.ports 15 | .add("port", (pb) => 16 | pb 17 | .setName("left") 18 | .setSchematicPosition({ x: -0.5, y: 0 }) 19 | .setPinNumber(1) 20 | .setSchematicPinNumberVisible(false) 21 | .setPortHints(["anode", "positive"]) 22 | .setSchematicDirection("left") 23 | ) 24 | .add("port", (pb) => 25 | pb 26 | .setName("right") 27 | .setSchematicPosition({ x: 0.5, y: 0 }) 28 | .setPinNumber(2) 29 | .setPortHints(["cathode", "negative"]) 30 | .setSchematicPinNumberVisible(false) 31 | .setSchematicDirection("right") 32 | ) 33 | }, 34 | configureSchematicSymbols(builder, ctx) { 35 | // { stroke: "red", strokeWidth: 2, d: "M 0,0 H 21" }, 36 | // { stroke: "red", strokeWidth: 2, d: "M 49,0 H 59" }, 37 | // { stroke: "red", strokeWidth: 2, d: "M 49,0 L 21 14 V -14 Z" }, 38 | // { stroke: "red", strokeWidth: 2, d: "M 49,-14 V 14" }, 39 | 40 | // scaled to be 1mm wide 41 | 42 | // Horizontal start and end line 43 | // M 0 0 H 0.3443 44 | // M 0.8033 0 H 0.9672 45 | 46 | // triangle 47 | // M 0.8033 0 L 0.3443 0.2295 V -0.2295 Z 48 | 49 | // Vertical line 50 | // M 0.8033 -0.2295 V 0.2295 51 | 52 | const h1 = { x1: 0, y1: 0, x2: 0.3443, y2: 0 } 53 | const h2 = { x1: 0.8033, y1: 0, x2: 1, y2: 0 } 54 | 55 | const t1 = { x1: 0.8033, y1: 0, x2: 0.3443, y2: 0.2295 } 56 | const t2 = { x1: 0.3443, y1: 0.2295, x2: 0.3443, y2: -0.2295 } 57 | const t3 = { x1: 0.3443, y1: -0.2295, x2: 0.8033, y2: 0 } 58 | 59 | const v1 = { x1: 0.8033, y1: -0.2295, x2: 0.8033, y2: 0.2295 } 60 | 61 | const lines = [h1, h2, t1, t2, t3, v1] 62 | 63 | // drawn inside schematic-viewer based on ftype 64 | // for (const line of lines) { 65 | // builder.schematic_symbol.add( 66 | // "schematic_line", 67 | // (sb) => 68 | // sb.setProps({ 69 | // x1: line.x1 - 0.5, 70 | // y1: line.y1, 71 | // x2: line.x2 - 0.5, 72 | // y2: line.y2, 73 | // }) 74 | // // sb.setProps({ 75 | // // x1: `${line.x1}mm`, 76 | // // y1: `${line.y1}mm`, 77 | // // x2: `${line.x2}mm`, 78 | // // y2: `${line.y2}mm`, 79 | // // }) 80 | // ) 81 | // } 82 | 83 | builder.schematic_symbol.add("schematic_text", (stb) => 84 | stb.setProps({ 85 | text: ctx.source_properties.name, 86 | anchor: "center", 87 | position: { 88 | x: 0, 89 | y: "-0.25mm", 90 | }, 91 | }) 92 | ) 93 | }, 94 | }) 95 | 96 | export type DiodeBuilder = ReturnType 97 | 98 | // Added for legacy compat 99 | export type DiodeBuilderCallback = (rb: DiodeBuilder) => unknown 100 | -------------------------------------------------------------------------------- /src/lib/builder/component-builder/EagleImportBuilder.ts: -------------------------------------------------------------------------------- 1 | import type * as Type from "lib/types" 2 | import type { ProjectBuilder } from "../project-builder" 3 | import { 4 | ComponentBuilderClass, 5 | type BaseComponentBuilder, 6 | } from "./ComponentBuilder" 7 | 8 | export type EagleImportBuilderCallback = (rb: EagleImportBuilder) => unknown 9 | export interface EagleImportBuilder 10 | extends BaseComponentBuilder { 11 | importEagle(eagleXML: string): EagleImportBuilder 12 | } 13 | 14 | export class EagleImportBuilderClass 15 | extends ComponentBuilderClass 16 | implements EagleImportBuilder 17 | { 18 | constructor(project_builder: ProjectBuilder) { 19 | super(project_builder) 20 | this.source_properties = { 21 | ...this.source_properties, 22 | ftype: "simple_bug", 23 | } 24 | } 25 | 26 | importEagle(eagleXML: string) { 27 | return this 28 | } 29 | 30 | async build() { 31 | const elements: Type.AnyElement[] = [] 32 | // Eagle components can have multiple schematic elements e.g. bugs 33 | return elements 34 | } 35 | } 36 | 37 | export const createEagleImportBuilder = ( 38 | project_builder: ProjectBuilder 39 | ): EagleImportBuilder => { 40 | return new EagleImportBuilderClass(project_builder) 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/builder/component-builder/LedBuilder.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { defineNewComponent } from "../define-new-component" 3 | 4 | export const { LedBuilderClass, createLedBuilder } = defineNewComponent({ 5 | pascal_name: "Led", 6 | underscore_name: "led", 7 | source_properties: z.object({ 8 | ftype: z.literal("simple_led").default("simple_led"), 9 | }), 10 | schematic_properties: z.object({}), 11 | pcb_properties: z.object({}), 12 | configurePorts(builder, ctx) { 13 | builder.ports 14 | .add("port", (pb) => 15 | pb 16 | .setName("left") 17 | .setSchematicPosition({ x: -0.5, y: 0 }) 18 | .setPinNumber(1) 19 | .setSchematicPinNumberVisible(false) 20 | .setPortHints(["anode", "positive"]) 21 | .setSchematicDirection("left") 22 | ) 23 | .add("port", (pb) => 24 | pb 25 | .setName("right") 26 | .setSchematicPosition({ x: 0.5, y: 0 }) 27 | .setPinNumber(2) 28 | .setPortHints(["cathode", "negative"]) 29 | .setSchematicPinNumberVisible(false) 30 | .setSchematicDirection("right") 31 | ) 32 | }, 33 | configureSchematicSymbols(builder, ctx) { 34 | builder.schematic_symbol.add("schematic_text", (stb) => 35 | stb.setProps({ 36 | text: ctx.source_properties.name, 37 | anchor: "center", 38 | position: { 39 | x: 0, 40 | y: "-0.25mm", 41 | }, 42 | }) 43 | ) 44 | }, 45 | }) 46 | 47 | export type LedBuilder = ReturnType 48 | -------------------------------------------------------------------------------- /src/lib/builder/component-builder/NetAliasBuilder.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { defineNewComponent } from "../define-new-component" 3 | 4 | export const { NetAliasBuilderClass, createNetAliasBuilder } = 5 | defineNewComponent({ 6 | pascal_name: "NetAlias", 7 | underscore_name: "net_alias", 8 | source_properties: z.object({ 9 | net: z.string(), 10 | }), 11 | schematic_properties: z.object({}), 12 | pcb_properties: z.object({}), 13 | configurePorts(builder, ctx) { 14 | builder.ports.add("port", (pb) => 15 | pb.setName(ctx.source_properties.net).setSchematicPosition({ 16 | x: 0, 17 | y: "0.25mm", 18 | }) 19 | ) 20 | }, 21 | configureSchematicSymbols(builder, ctx) { 22 | builder.schematic_symbol 23 | .add("schematic_line", (sb) => 24 | sb.setProps({ 25 | x1: "-0.4mm", 26 | y1: 0, 27 | x2: "0.4mm", 28 | y2: 0, 29 | }) 30 | ) 31 | .add("schematic_line", (sb) => 32 | sb.setProps({ 33 | x1: 0, 34 | y1: 0, 35 | x2: 0, 36 | y2: "0.25mm", 37 | }) 38 | ) 39 | .add("schematic_text", (stb) => 40 | stb.setProps({ 41 | text: ctx.source_properties.net, 42 | anchor: "center", 43 | position: { 44 | x: 0, 45 | y: "-0.175mm", 46 | }, 47 | }) 48 | ) 49 | }, 50 | }) 51 | 52 | export type NetAliasBuilder = ReturnType 53 | -------------------------------------------------------------------------------- /src/lib/builder/component-builder/index.ts: -------------------------------------------------------------------------------- 1 | import type { TraceBuilder } from "../trace-builder" 2 | import type { BugBuilder } from "./BugBuilder" 3 | import type { CapacitorBuilder } from "./CapacitorBuilder" 4 | import type { GenericComponentBuilder } from "./ComponentBuilder" 5 | import type { DiodeBuilder } from "./DiodeBuilder" 6 | import type { GroundBuilder } from "./GroundBuilder" 7 | import type { InductorBuilder } from "./InductorBuilder" 8 | import type { LedBuilder } from "./LedBuilder" 9 | import type { NetAliasBuilder } from "./NetAliasBuilder" 10 | import type { PowerSourceBuilder } from "./PowerSourceBuilder" 11 | import type { ResistorBuilder } from "./ResistorBuilder" 12 | import type { ViaBuilder } from "./ViaBuilder" 13 | 14 | export * from "./BugBuilder" 15 | export * from "./CapacitorBuilder" 16 | export * from "./ComponentBuilder" 17 | export * from "./DiodeBuilder" 18 | export * from "./GroundBuilder" 19 | export * from "./InductorBuilder" 20 | export * from "./LedBuilder" 21 | export * from "./NetAliasBuilder" 22 | export * from "./PowerSourceBuilder" 23 | export * from "./ResistorBuilder" 24 | export * from "./ViaBuilder" 25 | 26 | export type ComponentBuilder = 27 | | GenericComponentBuilder 28 | | ResistorBuilder 29 | | CapacitorBuilder 30 | | InductorBuilder 31 | | BugBuilder 32 | | DiodeBuilder 33 | | PowerSourceBuilder 34 | | GroundBuilder 35 | | TraceBuilder 36 | | NetAliasBuilder 37 | | ViaBuilder 38 | | LedBuilder 39 | -------------------------------------------------------------------------------- /src/lib/builder/component-builder/remap-prop.ts: -------------------------------------------------------------------------------- 1 | import { length } from "@tscircuit/soup" 2 | import { removeNulls } from "lib/utils/remove-nulls" 3 | 4 | export const remapProp = (prop: string, val: any): [string, any] => { 5 | switch (prop) { 6 | case "schPortArrangement": 7 | return [ 8 | "port_arrangement", 9 | removeNulls({ 10 | left_size: val.leftSize, 11 | right_size: val.rightSize, 12 | top_size: val.topSize, 13 | bottom_size: val.bottomSize, 14 | left_side: val.leftSide, 15 | right_side: val.rightSide, 16 | top_side: val.topSide, 17 | bottom_side: val.bottomSide, 18 | }), 19 | ] 20 | case "pcbX": 21 | return ["pcb_x", length.parse(val)] 22 | case "pcbY": 23 | return ["pcb_y", length.parse(val)] 24 | case "pcbRotation": 25 | return ["pcb_rotation", val] 26 | case "pinLabels": 27 | case "schPinLabels": 28 | return ["port_labels", val] 29 | case "pinSpacing": 30 | case "schPinSpacing": 31 | return ["pin_spacing", length.parse(val)] 32 | case "holeDiameter": 33 | return ["hole_diameter", length.parse(val)] 34 | case "outerDiameter": 35 | return ["outer_diameter", length.parse(val)] 36 | case "outerWidth": 37 | return ["outer_width", length.parse(val)] 38 | case "outerHeight": 39 | return ["outer_height", length.parse(val)] 40 | case "holeWidth": 41 | return ["hole_width", length.parse(val)] 42 | case "holeHeight": 43 | return ["hole_height", length.parse(val)] 44 | case "schX": 45 | return ["x", length.parse(val)] 46 | case "schY": 47 | return ["y", length.parse(val)] 48 | default: 49 | return [prop, val] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/builder/constrained-layout-builder/constraint-builder.ts: -------------------------------------------------------------------------------- 1 | import type { BuildContext, Dimension } from "lib/types" 2 | import type { RequireAtLeastOne } from "type-fest" 3 | import type { ProjectBuilder } from "../project-builder" 4 | import { createSimpleDataBuilderClass } from "../simple-data-builder" 5 | 6 | export type ConstraintContextFlag = RequireAtLeastOne<{ 7 | schematic: boolean 8 | pcb: boolean 9 | }> 10 | 11 | export type ConstraintBuilderFields = 12 | | ({ 13 | type: "xdist" 14 | dist: Dimension 15 | left: string 16 | right: string 17 | } & ConstraintContextFlag) 18 | | ({ 19 | type: "ydist" 20 | dist: Dimension 21 | top: string 22 | bottom: string 23 | } & ConstraintContextFlag) 24 | 25 | export interface ConstraintBuilder { 26 | builder_type: "constraint_builder" 27 | props: ConstraintBuilderFields 28 | setProps(props: Partial): ConstraintBuilder 29 | build( 30 | bc: BuildContext 31 | ): Omit & { dist: number } 32 | } 33 | 34 | export const ConstraintBuilderClass = createSimpleDataBuilderClass< 35 | "constraint_builder", 36 | ConstraintBuilderFields, 37 | "dist" 38 | >("constraint_builder", {}, ["dist"]) 39 | 40 | export const createConstraintBuilder = ( 41 | project_builder: ProjectBuilder 42 | ): ConstraintBuilder => { 43 | return new ConstraintBuilderClass(project_builder) as any 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/builder/constrained-layout-builder/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constrained-layout-builder" 2 | export * from "./constraint-builder" 3 | -------------------------------------------------------------------------------- /src/lib/builder/constrained-layout-builder/spatial-util.ts: -------------------------------------------------------------------------------- 1 | import type { AnyElement } from "lib/types" 2 | 3 | export type SpatialElement = { x: number; y: number; w: number; h: number } 4 | 5 | export const getSpatialBoundsFromSpatialElements = ( 6 | elements: SpatialElement[] 7 | ) => { 8 | if (elements.length === 0) return { x: 0, y: 0, w: 0, h: 0 } 9 | let { x: lx, y: ly, w, h } = elements[0] 10 | lx -= w / 2 11 | ly -= h / 2 12 | let hx = lx + w 13 | let hy = ly + h 14 | for (let i = 1; i < elements.length; i++) { 15 | const { x, y, w, h } = elements[i] 16 | lx = Math.min(lx, x - w / 2) 17 | ly = Math.min(ly, y - h / 2) 18 | hx = Math.max(hx, x + w / 2) 19 | hy = Math.max(hy, y + h / 2) 20 | } 21 | return { 22 | x: (lx + hx) / 2, 23 | y: (ly + hy) / 2, 24 | w: hx - lx, // 25 | h: hy - ly, 26 | } 27 | } 28 | 29 | export const toCenteredSpatialObj = (obj: any): SpatialElement => { 30 | let x = obj.x ?? obj.center?.x 31 | let y = obj.y ?? obj.center?.y 32 | let w = obj.w ?? obj.width ?? obj.size?.width ?? obj.outer_diameter ?? 0 33 | let h = obj.h ?? obj.height ?? obj.size?.height ?? obj.outer_diameter ?? 0 34 | const align = obj.align ?? "center" 35 | 36 | if ( 37 | obj.x1 !== undefined && 38 | obj.x2 !== undefined && 39 | obj.y1 !== undefined && 40 | obj.y2 !== undefined 41 | ) { 42 | // It's a line 43 | x = (obj.x1 + obj.x2) / 2 44 | y = (obj.y1 + obj.y2) / 2 45 | w = Math.abs(obj.x1 - obj.x2) 46 | h = Math.abs(obj.y1 - obj.y2) 47 | } 48 | 49 | if (x === undefined || y === undefined) { 50 | throw new Error( 51 | `Cannot convert to spatial obj (no x,y): ${JSON.stringify( 52 | obj, 53 | null, 54 | " " 55 | )}` 56 | ) 57 | } 58 | if (align !== "center") { 59 | throw new Error( 60 | `Cannot convert to spatial obj (align not center not implemented): ${JSON.stringify( 61 | obj, 62 | null, 63 | " " 64 | )}` 65 | ) 66 | } 67 | 68 | return { x, y, w, h } 69 | } 70 | 71 | /** 72 | * Get element size with any children elements. e.g. for a pcb component, 73 | * compute it's size from it's children. 74 | */ 75 | export const getSpatialElementIncludingChildren = ( 76 | elm: AnyElement, 77 | elements: AnyElement[] 78 | ): SpatialElement => { 79 | if (elm.type === "pcb_component") { 80 | const children = getElementChildren(elm, elements).map((e) => 81 | toCenteredSpatialObj(e) 82 | ) 83 | return getSpatialBoundsFromSpatialElements(children) 84 | // component size is computed from children 85 | } else if (elm.type === "schematic_component") { 86 | return toCenteredSpatialObj(elm) 87 | } 88 | throw new Error( 89 | `Get spatial elements including children not implemented for: "${elm.type}"` 90 | ) 91 | } 92 | 93 | export const getElementChildren = ( 94 | matchElm: AnyElement, 95 | elements: AnyElement[] 96 | ) => { 97 | // TODO get deep children 98 | return elements.filter( 99 | (elm) => 100 | elm[`${matchElm.type}_id`] === matchElm[`${matchElm.type}_id`] && 101 | elm !== matchElm 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /src/lib/builder/drawing-builder/drawing-builder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The drawing builder is what is used to draw simple shapes, it's used by 3 | * the schematic-symbol-builder and the pcb-symbol-builder to construct simple 4 | * shapes with boxes, circles, lines and text. 5 | */ 6 | 7 | export const createDrawingBuilder = () => { 8 | throw new Error("createDrawingBuilder not implemented") 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/builder/drawing-builder/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./drawing-builder" 2 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/associate-pcb-ports-with-pads.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PCBPlatedHole, 3 | PCBPort, 4 | PCBSMTPad, 5 | SourcePort, 6 | } from "@tscircuit/soup" 7 | import type * as Type from "lib/types" 8 | import { matchPcbPortWithPad } from "./match-pcb-port-with-pad" 9 | 10 | export const associatePcbPortsWithPads = (elms: Type.AnyElement[]) => { 11 | const ports = elms 12 | .filter((elm): elm is PCBPort => elm.type === "pcb_port") 13 | .map((elm) => ({ 14 | pcb_port: elm, 15 | source_port: elms.find( 16 | (elm2): elm2 is SourcePort => 17 | elm2.type === "source_port" && 18 | elm2.source_port_id === elm.source_port_id 19 | )!, 20 | })) 21 | 22 | const pads = elms.filter( 23 | (elm): elm is PCBPlatedHole | PCBSMTPad => 24 | elm.type === "pcb_plated_hole" || elm.type === "pcb_smtpad" 25 | ) 26 | 27 | for (const { pcb_port, source_port } of ports) { 28 | const matched_pad = matchPcbPortWithPad({ 29 | pcb_port, 30 | source_port, 31 | pads, 32 | }) 33 | 34 | if (matched_pad) { 35 | matched_pad.pcb_port_id = pcb_port.pcb_port_id 36 | pcb_port.x = matched_pad.x 37 | pcb_port.y = matched_pad.y 38 | if ("layers" in matched_pad) { 39 | pcb_port.layers = matched_pad.layers 40 | } else if ("layer" in matched_pad) { 41 | ;(pcb_port as any).layers = [matched_pad.layer] 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/basic-pcb-trace-builder.ts: -------------------------------------------------------------------------------- 1 | import type { PcbTraceProps } from "@tscircuit/props" 2 | import type { AnySoupElement, PCBTrace } from "@tscircuit/soup" 3 | import type { BuildContext } from "lib/types" 4 | import type { BuilderInterface } from "../builder-interface" 5 | 6 | export interface BasicPcbTraceBuilder extends BuilderInterface { 7 | builder_type: "basic_pcb_trace_builder" 8 | setProps(props: PcbTraceProps): this 9 | build(bc: BuildContext): AnySoupElement[] 10 | } 11 | 12 | export class BasicPcbTraceBuilderClass implements BasicPcbTraceBuilder { 13 | builder_type = "basic_pcb_trace_builder" as const 14 | props: Partial 15 | constructor() { 16 | this.props = {} 17 | } 18 | setProps(props: Partial): this { 19 | this.props = { ...this.props, ...props } 20 | return this 21 | } 22 | build(bc) { 23 | const trace: PCBTrace = { 24 | type: "pcb_trace", 25 | pcb_trace_id: bc.getId("pcb_trace"), 26 | route: 27 | this.props.route?.map((rp): PCBTrace["route"][number] => { 28 | if (rp.via && "" in rp) { 29 | return { 30 | x: bc.convert(rp.x), 31 | y: bc.convert(rp.y), 32 | route_type: "via", 33 | to_layer: (rp.to_layer ?? this.props.layer ?? "top") as any, 34 | from_layer: (rp.to_layer ?? this.props.layer ?? "bottom") as any, 35 | // TODO to_layer and from_layer 36 | } 37 | } else { 38 | return { 39 | route_type: "wire", 40 | x: bc.convert(rp.x), 41 | y: bc.convert(rp.y), 42 | layer: (rp.to_layer ?? this.props.layer ?? "top") as any, 43 | width: bc.convert(this.props.thickness ?? 0.1), // TODO use bc.default_trace_width when it's available 44 | } as any 45 | } 46 | }) ?? [], 47 | } 48 | return [trace] 49 | } 50 | } 51 | 52 | export const createBasicPcbTraceBuilder = () => { 53 | return new BasicPcbTraceBuilderClass() 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/fabrication-note-path-builder.ts: -------------------------------------------------------------------------------- 1 | import type { FabricationNotePathProps } from "@tscircuit/props" 2 | import { 3 | pcb_route_hints, 4 | type AnySoupElement, 5 | type PcbFabricationNotePath, 6 | } from "@tscircuit/soup" 7 | import type { BuildContext } from "lib/types" 8 | import type { BuilderInterface } from "../builder-interface" 9 | 10 | export interface FabricationNotePathBuilder extends BuilderInterface { 11 | builder_type: "fabrication_note_path_builder" 12 | setProps(props: FabricationNotePathProps): this 13 | build(bc: BuildContext): AnySoupElement[] 14 | } 15 | 16 | export class FabricationNotePathBuilderClass 17 | implements FabricationNotePathBuilder 18 | { 19 | builder_type = "fabrication_note_path_builder" as const 20 | props: Partial 21 | constructor() { 22 | this.props = {} 23 | } 24 | setProps(props: Partial): this { 25 | this.props = { ...this.props, ...props } 26 | return this 27 | } 28 | build(bc) { 29 | const fabrication_note_path: PcbFabricationNotePath = { 30 | type: "pcb_fabrication_note_path", 31 | layer: "top", 32 | pcb_component_id: bc.pcb_component_id, 33 | fabrication_note_path_id: bc.getId("fabrication_note_path"), 34 | route: pcb_route_hints.parse(this.props.route!), 35 | stroke_width: bc.convert(this.props.strokeWidth) ?? 0.1, 36 | } 37 | return [fabrication_note_path] 38 | } 39 | } 40 | 41 | export const createFabricationNotePathBuilder = 42 | (): FabricationNotePathBuilder => { 43 | return new FabricationNotePathBuilderClass() 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/fabrication-note-text-builder.ts: -------------------------------------------------------------------------------- 1 | import type { FabricationNoteTextProps } from "@tscircuit/props" 2 | import type { AnySoupElement, PcbFabricationNoteText } from "@tscircuit/soup" 3 | import type { BuildContext } from "lib/types" 4 | import type { BuilderInterface } from "../builder-interface" 5 | 6 | export interface FabricationNoteTextBuilder extends BuilderInterface { 7 | builder_type: "fabrication_note_text_builder" 8 | setProps(props: FabricationNoteTextProps): this 9 | build(bc: BuildContext): AnySoupElement[] 10 | } 11 | 12 | export class FabricationNoteTextBuilderClass 13 | implements FabricationNoteTextBuilder 14 | { 15 | builder_type = "fabrication_note_text_builder" as const 16 | props: Partial 17 | constructor() { 18 | this.props = {} 19 | } 20 | setProps(props: Partial): this { 21 | this.props = { ...this.props, ...props } 22 | return this 23 | } 24 | build(bc) { 25 | const fabrication_note_text: PcbFabricationNoteText = { 26 | type: "pcb_fabrication_note_text", 27 | layer: this.props.layer as any, 28 | font: this.props.font ?? "tscircuit2024", 29 | font_size: bc.convert(this.props.fontSize) ?? 0.2, 30 | pcb_component_id: bc.pcb_component_id, 31 | anchor_position: { 32 | x: bc.convert(this.props.pcbX), 33 | y: bc.convert(this.props.pcbY), 34 | }, 35 | anchor_alignment: this.props.anchorAlignment ?? "center", 36 | text: this.props.text!, 37 | } 38 | return [fabrication_note_text] 39 | } 40 | } 41 | 42 | export const createFabricationNoteTextBuilder = 43 | (): FabricationNoteTextBuilder => { 44 | return new FabricationNoteTextBuilderClass() 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/hole-builder.ts: -------------------------------------------------------------------------------- 1 | import type { PCBHole } from "@tscircuit/soup" 2 | import type { ProjectBuilder } from "lib/project" 3 | 4 | export interface HoleBuilder { 5 | builder_type: "hole_builder" 6 | project_builder: ProjectBuilder 7 | setProps(props: Partial): HoleBuilder 8 | build(): Promise 9 | } 10 | 11 | export class HoleBuilderClass implements HoleBuilder { 12 | project_builder: ProjectBuilder 13 | builder_type = "hole_builder" as const 14 | 15 | hole_diameter: number 16 | x: number 17 | y: number 18 | 19 | constructor(project_builder: ProjectBuilder) { 20 | this.project_builder = project_builder 21 | } 22 | 23 | setProps(props: Partial): HoleBuilder { 24 | for (const k in props) { 25 | this[k] = props[k] 26 | } 27 | return this 28 | } 29 | 30 | async build(): Promise { 31 | return [ 32 | { 33 | type: "pcb_hole", 34 | x: this.x, 35 | y: this.y, 36 | hole_diameter: this.hole_diameter, 37 | hole_shape: "round", 38 | }, 39 | ] 40 | } 41 | } 42 | 43 | export const createHoleBuilder = (project_builder: ProjectBuilder) => { 44 | return new HoleBuilderClass(project_builder) 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./plated-hole-builder" 2 | export * from "./hole-builder" 3 | export * from "./smt-pad-builder" 4 | export * from "./footprint-builder" 5 | export * from "./silkscreen-path-builder" 6 | export * from "./silkscreen-text-builder" 7 | export * from "./silkscreen-line-builder" 8 | export * from "./silkscreen-rect-builder" 9 | export * from "./silkscreen-circle-builder" 10 | export * from "./fabrication-note-path-builder" 11 | export * from "./fabrication-note-text-builder" 12 | export * from "./basic-pcb-trace-builder" 13 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/match-pcb-port-with-pad.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PCBPlatedHole, 3 | PCBPort, 4 | PCBSMTPad, 5 | SourcePort, 6 | } from "@tscircuit/soup" 7 | 8 | export const matchPcbPortWithPad = ({ 9 | pcb_port, 10 | source_port, 11 | pads, 12 | }: { 13 | pcb_port: PCBPort 14 | source_port: SourcePort 15 | pads: (PCBPlatedHole | PCBSMTPad)[] 16 | }) => { 17 | for (const pad of pads) { 18 | const port_hints = pad.port_hints ?? [] 19 | if ( 20 | port_hints.includes(source_port.name) || 21 | (source_port.pin_number != null && 22 | port_hints.includes(source_port.pin_number.toString())) 23 | ) { 24 | return pad 25 | } 26 | } 27 | return null 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/pcb-via-builder.ts: -------------------------------------------------------------------------------- 1 | import type { LayerRef, PCBVia, PCBViaInput } from "@tscircuit/soup" 2 | import type { ProjectBuilder } from "lib/project" 3 | import type * as Type from "lib/types" 4 | import type { BuildContext } from "lib/types/build-context" 5 | 6 | export interface PcbViaBuilder { 7 | builder_type: "pcb_via_builder" 8 | project_builder: ProjectBuilder 9 | setProps(props: Omit): PcbViaBuilder 10 | build(bc: BuildContext): Promise 11 | } 12 | 13 | export class PcbViaBuilderClass implements PcbViaBuilder { 14 | project_builder: ProjectBuilder 15 | builder_type = "pcb_via_builder" as const 16 | 17 | outer_diameter: Type.Dimension 18 | hole_diameter: Type.Dimension 19 | pcb_x: Type.Dimension 20 | pcb_y: Type.Dimension 21 | layers?: LayerRef[] 22 | port_hints: string[] 23 | 24 | constructor(project_builder: ProjectBuilder) { 25 | this.project_builder = project_builder 26 | this.port_hints = [] 27 | } 28 | 29 | setProps(props: Partial): PcbViaBuilder { 30 | const remap = { 31 | x: "pcb_x", 32 | y: "pcb_y", 33 | } 34 | for (const k in props) { 35 | this[remap[k] ?? k] = props[k] 36 | } 37 | return this 38 | } 39 | 40 | async build(bc: BuildContext): Promise { 41 | return [ 42 | { 43 | type: "pcb_via", 44 | x: bc.convert(this.pcb_x), 45 | y: bc.convert(this.pcb_y), 46 | hole_diameter: bc.convert(this.hole_diameter), 47 | outer_diameter: bc.convert(this.outer_diameter), 48 | layers: this.layers ?? bc.all_copper_layers, 49 | // legacy compat 50 | from_layer: this.layers?.[0], 51 | to_layer: this.layers?.[1], 52 | }, 53 | ] 54 | } 55 | } 56 | 57 | export const createPcbViaBuilder = ( 58 | project_builder: ProjectBuilder 59 | ): PcbViaBuilder => { 60 | return new PcbViaBuilderClass(project_builder) 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/plated-hole-builder.ts: -------------------------------------------------------------------------------- 1 | import type { PlatedHoleProps } from "@tscircuit/props" 2 | import type { PCBPlatedHole } from "@tscircuit/soup" 3 | import type { ProjectBuilder } from "lib/project" 4 | import type * as Type from "lib/types" 5 | import type { BuildContext } from "lib/types/build-context" 6 | import { remapProp } from "../component-builder/remap-prop" 7 | 8 | export interface PlatedHoleBuilder { 9 | builder_type: "plated_hole_builder" 10 | project_builder: ProjectBuilder 11 | setProps( 12 | props: 13 | | Partial< 14 | Omit< 15 | PCBPlatedHole, 16 | | "x" 17 | | "y" 18 | | "outer_width" 19 | | "outer_height" 20 | | "hole_width" 21 | | "hole_height" 22 | | "shape" 23 | > & { 24 | outer_diameter?: Type.Dimension 25 | outer_width?: Type.Dimension 26 | outer_height?: Type.Dimension 27 | hole_diameter?: Type.Dimension 28 | hole_width?: Type.Dimension 29 | hole_height?: Type.Dimension 30 | x: Type.Dimension 31 | y: Type.Dimension 32 | shape: "circle" | "oval" | "pill" 33 | } 34 | > 35 | | Partial 36 | ): PlatedHoleBuilder 37 | build(bc: BuildContext): Promise 38 | } 39 | 40 | export class PlatedHoleBuilderClass implements PlatedHoleBuilder { 41 | project_builder: ProjectBuilder 42 | builder_type = "plated_hole_builder" as const 43 | 44 | outer_diameter?: Type.Dimension 45 | outer_width?: Type.Dimension 46 | outer_height?: Type.Dimension 47 | hole_diameter?: Type.Dimension 48 | hole_width?: Type.Dimension 49 | hole_height?: Type.Dimension 50 | x: Type.Dimension 51 | y: Type.Dimension 52 | shape: "circle" | "oval" | "pill" 53 | port_hints: string[] 54 | 55 | constructor(project_builder: ProjectBuilder) { 56 | this.project_builder = project_builder 57 | this.port_hints = [] 58 | } 59 | 60 | setProps(props: Partial<{}>): PlatedHoleBuilder { 61 | for (const k in props) { 62 | const [new_key, new_val] = remapProp(k, props[k]) 63 | this[new_key] = new_val 64 | } 65 | return this 66 | } 67 | 68 | async build(bc: BuildContext): Promise { 69 | if (this.shape === "circle" || (!this.shape && this.outer_diameter)) { 70 | return [ 71 | { 72 | type: "pcb_plated_hole", 73 | x: bc.convert(this.x), 74 | y: bc.convert(this.y), 75 | layers: bc.all_copper_layers, 76 | hole_diameter: bc.convert(this.hole_diameter!), 77 | shape: "circle", 78 | outer_diameter: bc.convert(this.outer_diameter!), 79 | port_hints: this.port_hints, 80 | }, 81 | ] 82 | } 83 | return [ 84 | { 85 | type: "pcb_plated_hole", 86 | x: bc.convert(this.x), 87 | y: bc.convert(this.y), 88 | layers: bc.all_copper_layers, 89 | outer_width: bc.convert(this.outer_width!), 90 | outer_height: bc.convert(this.outer_height!), 91 | hole_width: bc.convert(this.hole_width ?? this.hole_diameter!), 92 | hole_height: bc.convert(this.hole_height ?? this.hole_diameter!), 93 | shape: this.shape, 94 | port_hints: this.port_hints, 95 | }, 96 | ] 97 | } 98 | } 99 | 100 | export const createPlatedHoleBuilder = ( 101 | project_builder: ProjectBuilder 102 | ): PlatedHoleBuilder => { 103 | return new PlatedHoleBuilderClass(project_builder) 104 | } 105 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/silkscreen-circle-builder.ts: -------------------------------------------------------------------------------- 1 | import type { SilkscreenCircleProps } from "@tscircuit/props" 2 | import type { AnySoupElement, PcbSilkscreenCircle } from "@tscircuit/soup" 3 | import type { BuildContext } from "lib/types" 4 | import type { BuilderInterface } from "../builder-interface" 5 | 6 | export interface SilkscreenCircleBuilder extends BuilderInterface { 7 | builder_type: "silkscreen_circle_builder" 8 | setProps(props: SilkscreenCircleProps): this 9 | build(bc: BuildContext): AnySoupElement[] 10 | } 11 | 12 | export class SilkscreenCircleBuilderClass implements SilkscreenCircleBuilder { 13 | builder_type = "silkscreen_circle_builder" as const 14 | props: Partial 15 | constructor() { 16 | this.props = {} 17 | } 18 | setProps(props: Partial): this { 19 | this.props = { ...this.props, ...props } 20 | return this 21 | } 22 | build(bc) { 23 | const silkscreen_circle: PcbSilkscreenCircle = { 24 | type: "pcb_silkscreen_circle", 25 | layer: "top", 26 | pcb_component_id: bc.pcb_component_id, 27 | pcb_silkscreen_circle_id: bc.getId("pcb_silkscreen_circle"), 28 | radius: bc.convert(this.props.radius!), 29 | center: { 30 | x: bc.convert(this.props.pcbX), 31 | y: bc.convert(this.props.pcbY), 32 | }, 33 | } 34 | return [silkscreen_circle] 35 | } 36 | } 37 | 38 | export const createSilkscreenCircleBuilder = () => { 39 | return new SilkscreenCircleBuilderClass() 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/silkscreen-line-builder.ts: -------------------------------------------------------------------------------- 1 | import type { SilkscreenLineProps } from "@tscircuit/props" 2 | import type { AnySoupElement, PcbSilkscreenLine } from "@tscircuit/soup" 3 | import type { BuildContext } from "lib/types" 4 | import type { BuilderInterface } from "../builder-interface" 5 | 6 | export interface SilkscreenLineBuilder extends BuilderInterface { 7 | builder_type: "silkscreen_line_builder" 8 | setProps(props: SilkscreenLineProps): this 9 | build(bc: BuildContext): AnySoupElement[] 10 | } 11 | 12 | export class SilkscreenLineBuilderClass implements SilkscreenLineBuilder { 13 | builder_type = "silkscreen_line_builder" as const 14 | props: Partial 15 | constructor() { 16 | this.props = {} 17 | } 18 | setProps(props: Partial): this { 19 | this.props = { ...this.props, ...props } 20 | return this 21 | } 22 | build(bc) { 23 | const silkscreen_line: PcbSilkscreenLine = { 24 | type: "pcb_silkscreen_line", 25 | layer: (this.props.layer as "top" | "bottom") ?? "top", 26 | pcb_component_id: bc.pcb_component_id, 27 | pcb_silkscreen_line_id: bc.getId("pcb_silkscreen_path"), 28 | x1: bc.convert(this.props.x1!), 29 | x2: bc.convert(this.props.x2!), 30 | y1: bc.convert(this.props.y1!), 31 | y2: bc.convert(this.props.y2!), 32 | stroke_width: this.props.strokeWidth 33 | ? bc.convert(this.props.strokeWidth) 34 | : 0.1, 35 | } 36 | return [silkscreen_line] 37 | } 38 | } 39 | 40 | export const createSilkscreenLineBuilder = (): SilkscreenLineBuilder => { 41 | return new SilkscreenLineBuilderClass() 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/silkscreen-path-builder.ts: -------------------------------------------------------------------------------- 1 | import type { SilkscreenPathProps } from "@tscircuit/props" 2 | import { 3 | pcb_route_hints, 4 | type AnySoupElement, 5 | type PcbSilkscreenPath, 6 | } from "@tscircuit/soup" 7 | import type { BuildContext } from "lib/types" 8 | import type { BuilderInterface } from "../builder-interface" 9 | 10 | export interface SilkscreenPathBuilder extends BuilderInterface { 11 | builder_type: "silkscreen_path_builder" 12 | setProps(props: SilkscreenPathProps): this 13 | build(bc: BuildContext): AnySoupElement[] 14 | } 15 | 16 | export class SilkscreenPathBuilderClass implements SilkscreenPathBuilder { 17 | builder_type = "silkscreen_path_builder" as const 18 | props: Partial 19 | constructor() { 20 | this.props = {} 21 | } 22 | setProps(props: Partial): this { 23 | this.props = { ...this.props, ...props } 24 | return this 25 | } 26 | build(bc) { 27 | const silkscreen_path: PcbSilkscreenPath = { 28 | type: "pcb_silkscreen_path", 29 | layer: "top", 30 | pcb_component_id: bc.pcb_component_id, 31 | pcb_silkscreen_path_id: bc.getId("pcb_silkscreen_path"), 32 | route: pcb_route_hints.parse(this.props.route!), 33 | stroke_width: bc.convert(this.props.strokeWidth) ?? 0.1, 34 | } 35 | return [silkscreen_path] 36 | } 37 | } 38 | 39 | export const createSilkscreenPathBuilder = (): SilkscreenPathBuilder => { 40 | return new SilkscreenPathBuilderClass() 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/silkscreen-rect-builder.ts: -------------------------------------------------------------------------------- 1 | import type { SilkscreenRectProps } from "@tscircuit/props" 2 | import type { AnySoupElement, PcbSilkscreenRect } from "@tscircuit/soup" 3 | import type { BuildContext } from "lib/types" 4 | import type { BuilderInterface } from "../builder-interface" 5 | 6 | export interface SilkscreenRectBuilder extends BuilderInterface { 7 | builder_type: "silkscreen_rect_builder" 8 | setProps(props: SilkscreenRectProps): this 9 | build(bc: BuildContext): AnySoupElement[] 10 | } 11 | 12 | export class SilkscreenRectBuilderClass implements SilkscreenRectBuilder { 13 | builder_type = "silkscreen_rect_builder" as const 14 | props: Partial 15 | constructor() { 16 | this.props = {} 17 | } 18 | setProps(props: Partial): this { 19 | this.props = { ...this.props, ...props } 20 | return this 21 | } 22 | build(bc) { 23 | const silkscreen_rect: PcbSilkscreenRect = { 24 | type: "pcb_silkscreen_rect", 25 | pcb_silkscreen_rect_id: bc.getId("pcb_silkscreen_rect"), 26 | center: { 27 | x: bc.convert(this.props.pcbX), 28 | y: bc.convert(this.props.pcbY), 29 | }, 30 | width: bc.convert(this.props.width!), 31 | height: bc.convert(this.props.height!), 32 | layer: "top", 33 | pcb_component_id: bc.pcb_component_id, 34 | } 35 | return [silkscreen_rect] 36 | } 37 | } 38 | 39 | export const createSilkscreenRectBuilder = (): SilkscreenRectBuilder => { 40 | return new SilkscreenRectBuilderClass() 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/builder/footprint-builder/silkscreen-text-builder.ts: -------------------------------------------------------------------------------- 1 | import type { SilkscreenTextProps } from "@tscircuit/props" 2 | import type { AnySoupElement, PcbSilkscreenText } from "@tscircuit/soup" 3 | import type { BuildContext } from "lib/types" 4 | import type { BuilderInterface } from "../builder-interface" 5 | 6 | export interface SilkscreenTextBuilder extends BuilderInterface { 7 | builder_type: "silkscreen_text_builder" 8 | setProps(props: SilkscreenTextProps): this 9 | build(bc: BuildContext): AnySoupElement[] 10 | } 11 | 12 | export class SilkscreenTextBuilderClass implements SilkscreenTextBuilder { 13 | builder_type = "silkscreen_text_builder" as const 14 | props: Partial 15 | constructor() { 16 | this.props = {} 17 | } 18 | setProps(props: Partial): this { 19 | this.props = { ...this.props, ...props } 20 | return this 21 | } 22 | build(bc) { 23 | const silkscreen_text: PcbSilkscreenText = { 24 | type: "pcb_silkscreen_text", 25 | layer: this.props.layer as any, 26 | font: this.props.font ?? "tscircuit2024", 27 | font_size: bc.convert(this.props.fontSize) ?? 0.2, 28 | pcb_component_id: bc.pcb_component_id, 29 | anchor_position: { 30 | x: bc.convert(this.props.pcbX), 31 | y: bc.convert(this.props.pcbY), 32 | }, 33 | anchor_alignment: this.props.anchorAlignment ?? "center", 34 | text: this.props.text!, 35 | } 36 | return [silkscreen_text] 37 | } 38 | } 39 | 40 | export const createSilkscreenTextBuilder = (): SilkscreenTextBuilder => { 41 | return new SilkscreenTextBuilderClass() 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/builder/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./component-builder" 2 | export * from "./group-builder" 3 | export * from "./board-builder" 4 | export * from "./project-builder" 5 | export * from "./trace-builder" 6 | export * from "./trace-hint-builder" 7 | export * from "./ports-builder" 8 | export * from "./footprint-builder" 9 | export * from "./schematic-symbol-builder" 10 | export * from "./constrained-layout-builder" 11 | export * from "./transform-elements" 12 | -------------------------------------------------------------------------------- /src/lib/builder/net-builder/net-builder.ts: -------------------------------------------------------------------------------- 1 | import type { AnySoupElement } from "@tscircuit/soup" 2 | import type { BuildContext } from "lib/types" 3 | import type { BuilderInterface } from "../builder-interface" 4 | import type { ProjectBuilder } from "../project-builder" 5 | 6 | export type Props = { 7 | name: string 8 | is_analog_signal?: boolean 9 | is_digital_signal?: boolean 10 | is_power?: boolean 11 | is_ground?: boolean 12 | trace_width?: number 13 | } 14 | 15 | export interface NetBuilder extends BuilderInterface { 16 | builder_type: "net_builder" 17 | props: Partial 18 | setProps(props: Props): this 19 | build(bc: BuildContext): AnySoupElement[] 20 | } 21 | 22 | export class NetBuilderClass implements NetBuilder { 23 | builder_type = "net_builder" as const 24 | props: Partial 25 | 26 | constructor() { 27 | this.props = {} 28 | } 29 | 30 | setProps(props: Partial) { 31 | this.props = { ...this.props, ...props } 32 | return this 33 | } 34 | 35 | build(bc: BuildContext): AnySoupElement[] { 36 | if (!this.props.name) throw new Error("Net name is required") 37 | 38 | // TODO try to infer if this net is already in current contextual net scope, 39 | // you can pull registered nets from the build context. This means that 40 | // every usage of can be the same "source_net" 41 | 42 | // The schematic_net_label will be created inside the trace builder 43 | 44 | const source_net_id = bc.getId("net") 45 | return [ 46 | { 47 | type: "source_net", 48 | member_source_group_ids: [], 49 | source_net_id, 50 | name: this.props.name!, 51 | is_analog_signal: this.props.is_analog_signal, 52 | is_digital_signal: this.props.is_digital_signal, 53 | is_power: this.props.is_power, 54 | is_ground: this.props.is_ground, 55 | trace_width: this.props.trace_width, 56 | }, 57 | ] 58 | } 59 | } 60 | 61 | export const createNetBuilder = (project_builder: ProjectBuilder) => { 62 | return new NetBuilderClass() 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/builder/ports-builder/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ports-builder" 2 | export * from "./port-builder" 3 | -------------------------------------------------------------------------------- /src/lib/builder/project-builder.ts: -------------------------------------------------------------------------------- 1 | import type { AnySoupElement } from "@tscircuit/soup" 2 | import convertUnits from "convert-units" 3 | import type * as Type from "lib/types" 4 | import { createProjectFromElements } from "../project/create-project-from-elements" 5 | import { createBoardBuilder } from "./board-builder" 6 | import { 7 | createGroupBuilder, 8 | type GroupBuilder, 9 | type GroupBuilderAddables, 10 | type GroupBuilderCallback, 11 | } from "./group-builder" 12 | 13 | export type ProjectBuilder = Omit & { 14 | build_context: Type.BuildContext 15 | getId: (prefix: string) => string 16 | addGroup: (groupBuilderCallback: GroupBuilderCallback) => ProjectBuilder 17 | buildProject: () => Promise 18 | build(): Promise 19 | add( 20 | builder_type: T, 21 | callback: ( 22 | builder: ReturnType< 23 | T extends "board" 24 | ? typeof createBoardBuilder 25 | : T extends keyof GroupBuilderAddables 26 | ? GroupBuilderAddables[T] 27 | : never 28 | > 29 | ) => unknown 30 | ): ProjectBuilder 31 | setProps: (props: any) => ProjectBuilder 32 | createBuildContext: () => Type.BuildContext 33 | } 34 | 35 | export const createProjectBuilder = (): ProjectBuilder => { 36 | const builder: any = createGroupBuilder() 37 | builder.project_builder = builder 38 | const idCount = {} 39 | const resetIdCount = () => Object.keys(idCount).map((k) => (idCount[k] = 0)) 40 | builder.build_group = builder.build 41 | builder.createBuildContext = (): Type.BuildContext => ({ 42 | distance_unit: "mm", 43 | all_copper_layers: ["top", "bottom"], 44 | getId(prefix: string) { 45 | idCount[prefix] = idCount[prefix] || 0 46 | return `${prefix}_${idCount[prefix]++}` 47 | }, 48 | convert(v) { 49 | if (typeof v === "undefined") return undefined 50 | if (typeof v === "number") return v 51 | if (v.x !== undefined && v.y !== undefined) { 52 | return { 53 | x: this.convert(v.x), 54 | y: this.convert(v.y), 55 | } 56 | } 57 | const unit_reversed = v 58 | .split("") 59 | .reverse() 60 | .join("") 61 | .match(/[a-zA-Z]+/)?.[0] 62 | if (!unit_reversed) { 63 | throw new Error(`Could not determine unit: ${v}`) 64 | } 65 | const unit = unit_reversed.split("").reverse().join("") 66 | const value = v.slice(0, -unit.length) 67 | return convertUnits(Number.parseFloat(value)) 68 | .from(unit) 69 | .to(this.distance_unit) 70 | }, 71 | fork(mutation) { 72 | return { ...this, ...mutation, parent: this } 73 | }, 74 | }) 75 | builder.getId = builder.createBuildContext().getId 76 | 77 | const groupBuilderAdd = builder.add.bind(builder) 78 | builder.add = (builder_type, callback) => { 79 | if (builder_type === "board") { 80 | const board_builder = createBoardBuilder(builder) 81 | callback(board_builder) 82 | builder.appendChild(board_builder) 83 | return builder 84 | } 85 | return groupBuilderAdd(builder_type, callback) 86 | } 87 | 88 | builder.build = async () => { 89 | resetIdCount() 90 | const build_context = builder.createBuildContext() 91 | return await builder.build_group(build_context) 92 | } 93 | builder.buildProject = async () => { 94 | const group = await builder.build() 95 | return createProjectFromElements(group) 96 | } 97 | return builder 98 | } 99 | -------------------------------------------------------------------------------- /src/lib/builder/schematic-symbol-builder/index.ts: -------------------------------------------------------------------------------- 1 | import { SchematicBoxBuilder } from "./schematic-box-builder" 2 | import { SchematicLineBuilder } from "./schematic-line-builder" 3 | import { SchematicPathBuilder } from "./schematic-path-builder" 4 | import { SchematicTextBuilder } from "./schematic-text-builder" 5 | 6 | export * from "./schematic-symbol-builder" 7 | export * from "./schematic-box-builder" 8 | export * from "./schematic-line-builder" 9 | export * from "./schematic-text-builder" 10 | export * from "./schematic-path-builder" 11 | 12 | export type SchematicSymbolPrimitiveBuilder = 13 | | SchematicBoxBuilder 14 | | SchematicLineBuilder 15 | | SchematicTextBuilder 16 | | SchematicPathBuilder 17 | -------------------------------------------------------------------------------- /src/lib/builder/schematic-symbol-builder/schematic-box-builder.ts: -------------------------------------------------------------------------------- 1 | import type { Dimension } from "lib/types" 2 | import type { ProjectBuilder } from "../project-builder" 3 | import { createSimpleDataBuilderClass } from "../simple-data-builder" 4 | 5 | export interface SchematicBoxBuilderFields { 6 | type: "schematic_box" 7 | width: Dimension 8 | height: Dimension 9 | align: "center" 10 | x: Dimension 11 | y: Dimension 12 | cx: Dimension 13 | cy: Dimension 14 | center: [Dimension, Dimension] 15 | name: string 16 | drawing_type: "box" 17 | } 18 | 19 | export interface SchematicBoxBuilder { 20 | builder_type: "schematic_box_builder" 21 | props: SchematicBoxBuilderFields 22 | setProps(props: Partial): SchematicBoxBuilder 23 | build(): Omit< 24 | SchematicBoxBuilderFields, 25 | "width" | "height" | "x" | "y" | "cx" | "cy" | "center" 26 | > & { 27 | width: number 28 | height: number 29 | x: number 30 | y: number 31 | cx: number 32 | cy: number 33 | center: [number, number] 34 | } 35 | } 36 | 37 | export const SchematicBoxBuilderClass = createSimpleDataBuilderClass( 38 | "schematic_box_builder", 39 | { 40 | drawing_type: "box", 41 | type: "schematic_box", 42 | } as SchematicBoxBuilder["props"], 43 | ["x", "y", "cx", "cy", "center", "width", "height"] 44 | ) 45 | 46 | export const createSchematicBoxBuilder = ( 47 | project_builder: ProjectBuilder 48 | ): SchematicBoxBuilder => { 49 | return new SchematicBoxBuilderClass(project_builder) as any 50 | } 51 | 52 | // Boxes can be used for both pcbs and schematics, react-fiber should probably 53 | // determine which to use based on context... 54 | export const createBoxBuilder = createSchematicBoxBuilder 55 | -------------------------------------------------------------------------------- /src/lib/builder/schematic-symbol-builder/schematic-line-builder.ts: -------------------------------------------------------------------------------- 1 | import type { BuildContext, Dimension } from "lib/types" 2 | import type { ProjectBuilder } from "../project-builder" 3 | import { createSimpleDataBuilderClass } from "../simple-data-builder" 4 | 5 | export interface SchematicLineBuilderFields { 6 | type: "schematic_line" 7 | x1: Dimension 8 | y1: Dimension 9 | x2: Dimension 10 | y2: Dimension 11 | drawing_type: "line" 12 | } 13 | 14 | export interface SchematicLineBuilder { 15 | builder_type: "schematic_line_builder" 16 | props: SchematicLineBuilderFields 17 | setProps(props: Partial): SchematicLineBuilder 18 | build(bc: BuildContext): Omit< 19 | SchematicLineBuilderFields, 20 | "x1" | "y1" | "x2" | "y2" 21 | > & { 22 | x1: number 23 | y1: number 24 | x2: number 25 | y2: number 26 | } 27 | } 28 | 29 | export const SchematicLineBuilder = createSimpleDataBuilderClass( 30 | "schematic_line_builder", 31 | { 32 | drawing_type: "line", 33 | type: "schematic_line", 34 | } as SchematicLineBuilder["props"], 35 | ["x1", "y1", "x2", "y2"] 36 | ) 37 | 38 | export const createSchematicLineBuilder = ( 39 | project_builder: ProjectBuilder 40 | ): SchematicLineBuilder => { 41 | return new SchematicLineBuilder(project_builder) as any 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/builder/schematic-symbol-builder/schematic-path-builder.ts: -------------------------------------------------------------------------------- 1 | import type * as Soup from "@tscircuit/soup" 2 | import type { Dimension } from "lib/types" 3 | import type { ProjectBuilder } from "../project-builder" 4 | import { createSimpleDataBuilderClass } from "../simple-data-builder" 5 | 6 | export type SchematicPathBuilderFields = Partial< 7 | Omit & { 8 | position: { x: Dimension; y: Dimension } 9 | points: { x: Dimension; y: Dimension }[] 10 | } 11 | > 12 | 13 | export interface SchematicPathBuilder { 14 | builder_type: "schematic_path_builder" 15 | props: SchematicPathBuilderFields 16 | setProps(props: Partial): SchematicPathBuilder 17 | build(): Soup.SchematicPath[] 18 | } 19 | 20 | export const SchematicPathBuilder = createSimpleDataBuilderClass( 21 | "schematic_path_builder", 22 | { 23 | type: "schematic_path", 24 | points: [], 25 | } as SchematicPathBuilder["props"], 26 | ["position"], 27 | (props, bc) => ({ 28 | ...props, 29 | points: props.points.map((point) => ({ 30 | x: bc.convert(point.x), 31 | y: bc.convert(point.y), 32 | })), 33 | }) 34 | ) 35 | 36 | export const createSchematicPathBuilder = ( 37 | project_builder: ProjectBuilder 38 | ): SchematicPathBuilder => { 39 | return new SchematicPathBuilder(project_builder) as any 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/builder/schematic-symbol-builder/schematic-text-builder.ts: -------------------------------------------------------------------------------- 1 | import type { SchematicText } from "@tscircuit/soup" 2 | import type { Dimension } from "lib/types" 3 | import type { ProjectBuilder } from "../project-builder" 4 | import { createSimpleDataBuilderClass } from "../simple-data-builder" 5 | 6 | export type SchematicTextBuilderFields = Partial< 7 | Omit & { 8 | position: { x: Dimension; y: Dimension } 9 | } 10 | > 11 | 12 | export interface SchematicTextBuilder { 13 | builder_type: "schematic_text_builder" 14 | props: SchematicTextBuilderFields 15 | setProps(props: Partial): SchematicTextBuilder 16 | build(): Omit & { 17 | x: Dimension 18 | y: Dimension 19 | } 20 | } 21 | 22 | export const SchematicTextBuilder = createSimpleDataBuilderClass( 23 | "schematic_text_builder", 24 | { 25 | anchor: "center", 26 | type: "schematic_text", 27 | position: { x: 0, y: 0 }, 28 | } as SchematicTextBuilder["props"], 29 | ["position"] 30 | ) 31 | 32 | export const createSchematicTextBuilder = ( 33 | project_builder: ProjectBuilder 34 | ): SchematicTextBuilder => { 35 | return new SchematicTextBuilder(project_builder) as any 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/builder/simple-data-builder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple data builder is a builder that constructs a JSON object 3 | */ 4 | 5 | import type { BuildContext } from "lib/types" 6 | import type { ProjectBuilder } from "./project-builder" 7 | 8 | export interface SimpleDataBuilder< 9 | BuilderType extends string, 10 | Fields extends Object, 11 | UnitFields extends keyof Fields = never 12 | > { 13 | builder_type: BuilderType 14 | project_builder: ProjectBuilder 15 | 16 | props: Fields 17 | setProps(props: Partial): SimpleDataBuilder 18 | 19 | build(bc: BuildContext): Omit & Record 20 | } 21 | 22 | export const createSimpleDataBuilderClass = < 23 | BuilderType extends string, 24 | Fields extends object, 25 | UnitField extends keyof Fields = keyof Fields, 26 | OutputFields extends object = Fields 27 | >( 28 | builder_type: BuilderType, 29 | default_fields: Partial, 30 | unit_fields: UnitField[] = [], 31 | propsPostprocessor?: (props: Fields, bc: BuildContext) => OutputFields 32 | ): { 33 | new (project_builder: ProjectBuilder): SimpleDataBuilder< 34 | BuilderType, 35 | Fields, 36 | UnitField 37 | > 38 | } => { 39 | class SimpleDataBuilderClass 40 | implements SimpleDataBuilder 41 | { 42 | builder_type: BuilderType = builder_type 43 | project_builder: ProjectBuilder 44 | 45 | props: Fields 46 | 47 | constructor(project_builder: ProjectBuilder) { 48 | this.project_builder = project_builder 49 | this.props = default_fields as any 50 | } 51 | 52 | setProps(props: Partial) { 53 | this.props = { ...this.props, ...props } 54 | return this 55 | } 56 | 57 | build(bc: BuildContext) { 58 | const ret_obj: any = { ...this.props } 59 | for (const unit_field of unit_fields) { 60 | ret_obj[unit_field] = bc.convert(ret_obj[unit_field]) 61 | } 62 | if (propsPostprocessor) { 63 | return propsPostprocessor(ret_obj, bc) 64 | } 65 | return ret_obj 66 | } 67 | } 68 | 69 | return SimpleDataBuilderClass as any 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/convert-to-readable-route-tree.ts: -------------------------------------------------------------------------------- 1 | import * as Type from "lib/types" 2 | 3 | export const getParent = ( 4 | child: Type.AnyElement, 5 | allElms: Type.AnyElement[] 6 | ) => { 7 | return allElms.find( 8 | (candParent) => 9 | candParent[`${candParent.type}_id`] === child[`${candParent.type}_id`] && 10 | candParent !== child 11 | ) 12 | } 13 | 14 | export const getChildren = ( 15 | parent: Type.AnyElement, 16 | allElms: Type.AnyElement[] 17 | ) => { 18 | return allElms.filter( 19 | (elm) => 20 | elm[`${parent.type}_id`] === parent[`${parent.type}_id`] && elm !== parent 21 | ) 22 | } 23 | 24 | export const getReadableName = (elm: any) => { 25 | return elm[`${elm.type}_id`] + ` (ftype:${elm.ftype} name:${elm.name})` 26 | } 27 | 28 | export const convertToReadableTreeUsingRoot = ( 29 | rootElm: Type.AnyElement, 30 | allElms: Type.AnyElement[] 31 | ) => { 32 | const children = getChildren(rootElm, allElms) 33 | const tree = {} 34 | for (const child of children) { 35 | tree[getReadableName(child)] = convertToReadableTreeUsingRoot( 36 | child, 37 | allElms 38 | ) 39 | } 40 | return tree 41 | } 42 | 43 | export const convertToReadableTraceTree = (allElms: Type.AnyElement[]): any => { 44 | const componentsWithoutParent: Type.AnyElement[] = [] 45 | for (const elm of allElms) { 46 | if (!getParent(elm, allElms)) { 47 | componentsWithoutParent.push(elm) 48 | } 49 | } 50 | const tree = {} 51 | for (const elm of componentsWithoutParent) { 52 | tree[getReadableName(elm)] = convertToReadableTreeUsingRoot(elm, allElms) 53 | } 54 | return tree 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/pcb-errors.ts: -------------------------------------------------------------------------------- 1 | import { PCBTraceError } from "@tscircuit/soup" 2 | import type { TracePcbRoutingContext } from "./pcb-routing/trace-pcb-routing-context" 3 | 4 | export const createNoCommonLayersError = ( 5 | ctx: TracePcbRoutingContext 6 | ): PCBTraceError => ({ 7 | pcb_error_id: ctx.getId("pcb_error"), 8 | type: "pcb_error", 9 | error_type: "pcb_trace_error", 10 | message: `No common layers could be resolved for terminals`, 11 | pcb_trace_id: ctx.pcb_trace_id, 12 | source_trace_id: ctx.source_trace_id, 13 | pcb_component_ids: [], // TODO 14 | pcb_port_ids: ctx.pcb_terminal_port_ids, 15 | }) 16 | 17 | export const createNoLayersSpecifiedError = ( 18 | ctx: TracePcbRoutingContext 19 | ): PCBTraceError => ({ 20 | pcb_error_id: ctx.getId("pcb_error"), 21 | type: "pcb_error", 22 | error_type: "pcb_trace_error", 23 | message: `No layers specified for terminals`, 24 | pcb_trace_id: ctx.pcb_trace_id, 25 | source_trace_id: ctx.source_trace_id, 26 | pcb_component_ids: [], // TODO 27 | pcb_port_ids: ctx.pcb_terminal_port_ids, 28 | }) 29 | 30 | export const createPcbTraceError = ( 31 | msg: string, 32 | ctx: TracePcbRoutingContext 33 | ): PCBTraceError => ({ 34 | pcb_error_id: "pcb_error", 35 | type: "pcb_error", 36 | error_type: "pcb_trace_error", 37 | message: msg, 38 | pcb_trace_id: ctx.pcb_trace_id, 39 | source_trace_id: ctx.source_trace_id, 40 | pcb_component_ids: [], // TODO 41 | pcb_port_ids: ctx.pcb_terminal_port_ids, 42 | }) 43 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/pcb-routing/find-possible-trace-layer-combinations.ts: -------------------------------------------------------------------------------- 1 | interface Hint { 2 | via?: boolean 3 | optional_via?: boolean 4 | layers?: Array 5 | } 6 | 7 | const LAYER_SELECTION_PREFERENCE = ["top", "bottom", "inner1", "inner2"] 8 | 9 | // ORDERING OF CANDIDATES: Example: 10 | // top -> top -> bottom -> bottom 11 | // bottom -> bottom -> top -> top 12 | // top -> bottom -> bottom -> top 13 | // bottom -> top -> top -> bottom 14 | 15 | interface CandidateTraceLayerCombination { 16 | layer_path: string[] 17 | } 18 | 19 | // EXAMPLE 1: 20 | // INPUT: 21 | // [top,bottom] -> unspecified -> unspecified/via -> [top, bottom] 22 | // OUTPUT: 23 | // top -> top -> bottom -> bottom 24 | // bottom -> bottom -> top -> top 25 | // 26 | // EXAMPLE 2: 27 | // INPUT: 28 | // [top,bottom] -> unspecified -> unspecified/via -> unspecified/via -> [top, bottom] 29 | // OUTPUT: 30 | // top -> top -> bottom -> top -> top 31 | // bottom -> bottom -> top-> bottom -> bottom 32 | // bottom -> bottom -> inner-1 -> inner-1 -> bottom 33 | export const findPossibleTraceLayerCombinations = ( 34 | hints: Hint[], 35 | layer_path: string[] = [] 36 | ): CandidateTraceLayerCombination[] => { 37 | const candidates: CandidateTraceLayerCombination[] = [] 38 | if (layer_path.length === 0) { 39 | const starting_layers = hints[0].layers! 40 | for (const layer of starting_layers) { 41 | candidates.push( 42 | ...findPossibleTraceLayerCombinations(hints.slice(1), [layer]) 43 | ) 44 | } 45 | return candidates 46 | } 47 | 48 | if (hints.length === 0) return [] 49 | const current_hint = hints[0] 50 | const is_possibly_via = current_hint.via || current_hint.optional_via 51 | const last_layer = layer_path[layer_path.length - 1] 52 | 53 | if (hints.length === 1) { 54 | const last_hint = current_hint 55 | if (last_hint.layers && is_possibly_via) { 56 | return last_hint.layers.map((layer) => ({ 57 | layer_path: [...layer_path, layer], 58 | })) 59 | } 60 | if (last_hint.layers?.includes(last_layer)) { 61 | return [{ layer_path: [...layer_path, last_layer] }] 62 | } 63 | return [] 64 | } 65 | 66 | if (!is_possibly_via) { 67 | if (current_hint.layers) { 68 | if (!current_hint.layers.includes(last_layer)) { 69 | return [] 70 | } 71 | } 72 | 73 | return findPossibleTraceLayerCombinations( 74 | hints.slice(1), 75 | layer_path.concat([last_layer]) 76 | ) 77 | } 78 | 79 | const candidate_next_layers = ( 80 | current_hint.optional_via 81 | ? LAYER_SELECTION_PREFERENCE 82 | : LAYER_SELECTION_PREFERENCE.filter((layer) => layer !== last_layer) 83 | ).filter( 84 | (layer) => !current_hint.layers || current_hint.layers?.includes(layer) 85 | ) 86 | 87 | for (const candidate_next_layer of candidate_next_layers) { 88 | candidates.push( 89 | ...findPossibleTraceLayerCombinations( 90 | hints.slice(1), 91 | layer_path.concat(candidate_next_layer) 92 | ) 93 | ) 94 | } 95 | 96 | return candidates 97 | } 98 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/pcb-routing/get-pcb-obstacles.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AnySoupElement, 3 | LayerRef, 4 | PCBPlatedHole, 5 | PCBSMTPad, 6 | } from "@tscircuit/soup" 7 | 8 | export type PcbObstacle = { 9 | center: { x: number; y: number } 10 | width: number 11 | height: number 12 | layers: LayerRef[] 13 | } 14 | 15 | export const getPcbObstacles = (params: { 16 | elements: AnySoupElement[] 17 | pcb_terminal_port_ids: string[] 18 | obstacle_margin: number 19 | }): PcbObstacle[] => { 20 | const { elements, pcb_terminal_port_ids, obstacle_margin } = params 21 | 22 | const obstacles: PcbObstacle[] = [ 23 | ...elements 24 | .filter((elm): elm is PCBSMTPad => elm.type === "pcb_smtpad") 25 | // Exclude the pads that are connected to the trace 26 | .filter((elm) => !pcb_terminal_port_ids.includes(elm.pcb_port_id!)) 27 | .map((pad) => { 28 | if (pad.shape === "rect") { 29 | return { 30 | center: { x: pad.x, y: pad.y }, 31 | width: pad.width + obstacle_margin * 2, 32 | height: pad.height + obstacle_margin * 2, 33 | layers: [pad.layer], 34 | } 35 | } else if (pad.shape === "circle") { 36 | // TODO support better circle obstacles 37 | return { 38 | center: { x: pad.x, y: pad.y }, 39 | width: pad.radius * 2 + obstacle_margin * 2, 40 | height: pad.radius * 2 + obstacle_margin * 2, 41 | layers: [pad.layer], 42 | } 43 | } 44 | throw new Error( 45 | `Invalid pad shape for pcb_smtpad "${(pad as any).shape}"` 46 | ) 47 | }), 48 | ...elements 49 | .filter((elm): elm is PCBPlatedHole => elm.type === "pcb_plated_hole") 50 | // Exclude the holes that are connected to the trace 51 | .filter((elm) => !pcb_terminal_port_ids.includes(elm.pcb_port_id!)) 52 | .map((hole) => { 53 | let width: number = 0 54 | let height: number = 0 55 | 56 | if (hole.shape === "circle") { 57 | width = hole.outer_diameter + obstacle_margin * 2 58 | height = hole.outer_diameter + obstacle_margin * 2 59 | } else if (hole.shape === "oval" || hole.shape === "pill") { 60 | width = hole.outer_width + obstacle_margin * 2 61 | height = hole.outer_height + obstacle_margin * 2 62 | } 63 | 64 | return { 65 | center: { x: hole.x, y: hole.y }, 66 | width, 67 | height, 68 | layers: hole.layers, 69 | } 70 | }), 71 | ] 72 | 73 | return obstacles 74 | } 75 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/pcb-routing/merge-routes.ts: -------------------------------------------------------------------------------- 1 | import type { PCBTrace } from "@tscircuit/soup" 2 | 3 | function pdist(a, b) { 4 | return Math.hypot(a.x - b.x, a.y - b.y) 5 | } 6 | 7 | /** 8 | * Merge multiple routes into a single route. 9 | * 10 | * If the end of the next route is closer to the end of the previous route, 11 | * reverse the next route and append it to the previous route. 12 | */ 13 | export const mergeRoutes = (routes: PCBTrace["route"][]) => { 14 | // routes = routes.filter((route) => route.length > 0) 15 | if (routes.some((r) => r.length === 0)) { 16 | throw new Error("Cannot merge routes with zero length") 17 | } 18 | // for (const route of routes) { 19 | // console.table(route) 20 | // } 21 | const merged: PCBTrace["route"] = [] 22 | // const reverse_log: boolean[] = [] 23 | 24 | // Determine if the first route should be reversed 25 | const first_route_fp = routes[0][0] 26 | const first_route_lp = routes[0][routes[0].length - 1] 27 | 28 | const second_route_fp = routes[1][0] 29 | const second_route_lp = routes[1][routes[1].length - 1] 30 | 31 | const best_reverse_dist = Math.min( 32 | pdist(first_route_fp, second_route_fp), 33 | pdist(first_route_fp, second_route_lp) 34 | ) 35 | 36 | const best_normal_dist = Math.min( 37 | pdist(first_route_lp, second_route_fp), 38 | pdist(first_route_lp, second_route_lp) 39 | ) 40 | 41 | if (best_reverse_dist < best_normal_dist) { 42 | merged.push(...routes[0].reverse()) 43 | // reverse_log.push(true) 44 | } else { 45 | merged.push(...routes[0]) 46 | // reverse_log.push(false) 47 | } 48 | 49 | for (let i = 1; i < routes.length; i++) { 50 | const last_merged_point = merged[merged.length - 1] 51 | const next_route = routes[i] 52 | 53 | const next_first_point = next_route[0] 54 | const next_last_point = next_route[next_route.length - 1] 55 | 56 | const distance_to_first = pdist(last_merged_point, next_first_point) 57 | const distance_to_last = pdist(last_merged_point, next_last_point) 58 | 59 | if (distance_to_first < distance_to_last) { 60 | // reverse_log.push(false) 61 | merged.push(...next_route) 62 | } else { 63 | // reverse_log.push(true) 64 | merged.push(...next_route.reverse()) 65 | } 66 | } 67 | // console.log(reverse_log) 68 | // console.table(merged) 69 | return merged 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/pcb-routing/pcb-solver-grid.ts: -------------------------------------------------------------------------------- 1 | export type PcbSolverGrid = { 2 | marginSegments: number 3 | maxGranularSearchSegments: number 4 | segmentSize: number 5 | } 6 | 7 | export const default_pcb_solver_grid: PcbSolverGrid = { 8 | marginSegments: 20, 9 | maxGranularSearchSegments: 50, 10 | segmentSize: 0.1, // mm 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/pcb-routing/solve-for-single-layer-route.ts: -------------------------------------------------------------------------------- 1 | import { findRoute } from "@tscircuit/routing" 2 | import type { LayerRef, PCBTrace, Point } from "@tscircuit/soup" 3 | import Debug from "debug" 4 | import { default_pcb_solver_grid, type PcbSolverGrid } from "./pcb-solver-grid" 5 | import type { TracePcbRoutingContext } from "./trace-pcb-routing-context" 6 | 7 | const debug = Debug("tscircuit:builder:trace-builder") 8 | 9 | export function solveForSingleLayerRoute( 10 | params: { 11 | terminals: Point[] 12 | layer: LayerRef 13 | pcb_solver_grid?: PcbSolverGrid 14 | }, 15 | ctx: TracePcbRoutingContext 16 | ): PCBTrace["route"] { 17 | const { terminals, layer, pcb_solver_grid = default_pcb_solver_grid } = params 18 | const { thickness_mm, pcb_obstacles } = ctx 19 | try { 20 | debug("sending to @tscircuit/routing findRoute...") 21 | const findRouteArgs = { 22 | grid: pcb_solver_grid, 23 | obstacles: pcb_obstacles.filter((obstacle) => 24 | obstacle.layers.includes(layer) 25 | ), 26 | pointsToConnect: terminals, 27 | } 28 | 29 | if (debug.enabled && globalThis.logTmpFile) { 30 | globalThis.logTmpFile("findRouteArgs", findRouteArgs) 31 | } 32 | const solved_route = findRoute(findRouteArgs) 33 | 34 | debug("route found?", solved_route.pathFound) 35 | 36 | if (solved_route.pathFound) { 37 | const route: PCBTrace["route"] = [] 38 | for (let i = 0; i < solved_route.points.length; i++) { 39 | const point = solved_route.points[i] 40 | route.push({ 41 | route_type: "wire", 42 | layer, 43 | width: thickness_mm, 44 | x: point.x, 45 | y: point.y, 46 | start_pcb_port_id: 47 | i === 0 48 | ? ctx.pcb_terminal_port_ids[0] 49 | : i === solved_route.points.length - 1 50 | ? ctx.pcb_terminal_port_ids[1] 51 | : undefined, 52 | }) 53 | } 54 | return route 55 | } 56 | 57 | ctx.mutable_pcb_errors.push({ 58 | pcb_error_id: ctx.getId("pcb_error"), 59 | type: "pcb_error", 60 | error_type: "pcb_trace_error", 61 | message: `No route found for pcb_trace_id ${ctx.pcb_trace_id}`, 62 | pcb_trace_id: ctx.pcb_trace_id!, 63 | source_trace_id: ctx.source_trace_id!, 64 | pcb_component_ids: [], // TODO 65 | pcb_port_ids: ctx.pcb_terminal_port_ids!, 66 | }) 67 | 68 | return [] 69 | } catch (e) { 70 | ctx.mutable_pcb_errors.push({ 71 | pcb_error_id: ctx.getId("pcb_error"), 72 | type: "pcb_error", 73 | error_type: "pcb_trace_error", 74 | message: `Error while pcb-trace-route solving: ${e.toString()}`, 75 | pcb_trace_id: ctx.pcb_trace_id!, 76 | source_trace_id: ctx.source_trace_id!, 77 | pcb_component_ids: [], // TODO 78 | pcb_port_ids: ctx.pcb_terminal_port_ids!, 79 | }) 80 | return [] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/pcb-routing/trace-pcb-routing-context.ts: -------------------------------------------------------------------------------- 1 | import { AnySoupElement, PCBTraceError } from "@tscircuit/soup" 2 | import { BuildContext } from "lib/types" 3 | import { PcbObstacle } from "./get-pcb-obstacles" 4 | 5 | export type TracePcbRoutingContext = { 6 | mutable_pcb_errors: PCBTraceError[] 7 | source_trace_id: string 8 | pcb_trace_id: string 9 | pcb_terminal_port_ids: string[] 10 | thickness_mm: number 11 | elements: AnySoupElement[] 12 | pcb_obstacles: PcbObstacle[] 13 | getId: BuildContext["getId"] 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/route-solvers/port-offset-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { RouteSolver } from "lib/types" 2 | import { directionToVec } from "lib/utils/direction-to-vec" 3 | 4 | /** 5 | * This route-solver wrapper moves the terminals forward by the direction 6 | * they're facing before route-solving and creates the associated edges. 7 | * 8 | * This ensures that the direction of the line out of each terminal is the 9 | * direction each terminal is facing. 10 | */ 11 | export const portOffsetWrapper = 12 | (routeSolver: RouteSolver): RouteSolver => 13 | async ({ terminals, obstacles }: Parameters[0]) => { 14 | const offsetTerminals = terminals.map((t) => { 15 | if (!t.facing_direction) return t 16 | const dir = directionToVec(t.facing_direction) 17 | return { 18 | ...t, 19 | x: t.x + dir.x * 0.15, // move away by 0.15mm on X axis 20 | y: t.y + dir.y * 0.15, // move away by 0.15mm on Y axis 21 | } 22 | }) 23 | 24 | let edges = await routeSolver({ 25 | terminals: offsetTerminals, 26 | obstacles, 27 | }) 28 | 29 | // Add edges from the original terminal points to the offset terminal points 30 | // TODO maybe add these in the correct order 31 | // TODO maybe use a faster algo, this has N^2 complexity 32 | edges = edges.concat( 33 | terminals.map((t, i) => { 34 | const ot = offsetTerminals[i] 35 | 36 | // Find nearest point in the route to the offset terminal 37 | const nearestPoint = edges 38 | .flatMap((edge) => [edge.from, edge.to]) 39 | .reduce( 40 | (nearest, p) => { 41 | const dist = Math.hypot(p.x - ot.x, p.y - ot.y) 42 | if (dist < nearest.dist) return { dist, point: p } 43 | return nearest 44 | }, 45 | { dist: Infinity, point: { x: 0, y: 0 } } 46 | ) 47 | 48 | return { 49 | from: { x: t.x, y: t.y, ti: i }, 50 | to: nearestPoint.point, 51 | } 52 | }) 53 | ) 54 | 55 | return edges 56 | } 57 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/route-solvers/rmst-or-route1-solver.ts: -------------------------------------------------------------------------------- 1 | import * as Type from "lib/types" 2 | 3 | /** 4 | * Attempt to use RMST, if there's an obstacle collision, switch to the 5 | * route solver. 6 | */ 7 | export const rmstOrRoute1Solver: Type.RouteSolver = async ({ 8 | terminals, 9 | obstacles, 10 | }) => { 11 | throw new Error("rmstOrRoute1Solver not implemented") 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/route-solvers/route1-solver.ts: -------------------------------------------------------------------------------- 1 | import * as Type from "lib/types" 2 | import { 3 | findSchematicRoute, 4 | movePointsOutsideObstacles, 5 | } from "@tscircuit/routing" 6 | import { straightRouteSolver } from "./straight-route-solver" 7 | 8 | /** 9 | * Uses a path-finding algorithm with a simplification step. It's tuned to 10 | * generally work but given version 1 to reflect the tuning goal/method/determinism. 11 | * 12 | * Tuning goals: 13 | * * Path should approximately minimize turns, but it's ok to make an incorrect 14 | * turn as long as no wrong turn ever takes you temporarily further from the 15 | * objective. 16 | */ 17 | export const route1Solver: Type.RouteSolver = async ({ 18 | terminals, 19 | obstacles, 20 | }) => { 21 | const transformedObstacles = obstacles.map((obstacle) => ({ 22 | center: { x: obstacle.cx, y: obstacle.cy }, 23 | width: obstacle.w, 24 | height: obstacle.h, 25 | })) 26 | 27 | const pathFindingParams = { 28 | pointsToConnect: terminals.map((t) => ({ 29 | ...t, 30 | directionBias: t.facing_direction, 31 | })), 32 | obstacles: transformedObstacles, 33 | grid: { 34 | maxGranularSearchSegments: 50, 35 | marginSegments: 10, 36 | segmentSize: 0.1, 37 | }, 38 | } 39 | 40 | const result = findSchematicRoute( 41 | movePointsOutsideObstacles(pathFindingParams) as any 42 | ) 43 | 44 | // TODO log pathFindingParams for submission to 45 | // https://routing.tscircuit.com for debugging 46 | // console.dir(pathFindingParams, { depth: 10 }) 47 | 48 | if (!result.pathFound) { 49 | return straightRouteSolver({ terminals, obstacles }) 50 | } 51 | 52 | // TODO this should be handled in findSchematicRoute, but for now 53 | // find the point/terminal association for each returned point along 54 | // the route 55 | // const terminalIndexToPointIndex: Record = {} 56 | // for (let i = 0; i < terminals.length; i++) { 57 | // const terminal = terminals[i] 58 | // let closestDist = Infinity 59 | // let closestPointIndex = -1 60 | // for (let j = 0; j < result.points.length; j++) { 61 | // const point = result.points[j] 62 | // const dist = Math.hypot(point[0] - terminal[0], point[1] - terminal[1]) 63 | // if (dist < closestDist) { 64 | // closestDist = dist 65 | // closestPointIndex = j 66 | // } 67 | // } 68 | // terminalIndexToPointIndex[i] = closestPointIndex 69 | // } 70 | 71 | // const edges = result.points.map( 72 | // ({ from, to, fromTerminalIndex, toTerminalIndex }) => ({ 73 | // from: { x: from[0], y: from[1], ti: fromTerminalIndex }, 74 | // to: { x: to[0], y: to[1], ti: toTerminalIndex }, 75 | // }) 76 | // ) 77 | 78 | return result.points.slice(0, -1).map((point, i) => ({ 79 | from: point, 80 | to: result.points[i + 1], 81 | })) 82 | } 83 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/route-solvers/straight-route-solver.ts: -------------------------------------------------------------------------------- 1 | import type * as Type from "lib/types" 2 | 3 | /** 4 | * Straight Route Solver 5 | * This basically just connects the terminals in the provided order, simple and 6 | * naive. 7 | */ 8 | export const straightRouteSolver: Type.RouteSolver = async ({ 9 | terminals, 10 | obstacles, 11 | }) => { 12 | const edges: Type.RouteEdge[] = [] 13 | for (let i = 0; i < terminals.length - 1; i++) { 14 | edges.push({ 15 | from: terminals[i], 16 | to: terminals[i + 1], 17 | }) 18 | } 19 | return edges 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/builder/trace-builder/schematic-routing/get-schematic-obstacles-from-elements.ts: -------------------------------------------------------------------------------- 1 | import * as Type from "lib/types" 2 | 3 | type Obstacle2 = { 4 | cx: number 5 | cy: number 6 | w: number 7 | h: number 8 | } 9 | 10 | type Options = { 11 | excluded_schematic_port_ids?: string[] 12 | } 13 | 14 | export const getSchematicObstaclesFromElements = ( 15 | elms: Type.AnyElement[], 16 | opts?: Options 17 | ): Array => { 18 | const obstacles: Obstacle2[] = [] 19 | for (const elm of elms) { 20 | switch (elm.type) { 21 | case "schematic_component": { 22 | obstacles.push({ 23 | cx: elm.center.x, 24 | cy: elm.center.y, 25 | w: elm.size.width, 26 | h: elm.size.height, 27 | }) 28 | continue 29 | } 30 | case "schematic_port": { 31 | if (opts?.excluded_schematic_port_ids?.includes(elm.schematic_port_id)) 32 | continue 33 | obstacles.push({ 34 | cx: elm.center.x, 35 | cy: elm.center.y, 36 | w: 0.4, 37 | h: 0.4, 38 | }) 39 | continue 40 | } 41 | case "schematic_net_label": { 42 | let offsetX = 0, 43 | offsetY = 0 44 | const labelWidth = 0.4 + elm.text.length * 0.1 45 | const labelHeight = 0.25 46 | 47 | // Adjust the obstacle position based on the anchor side 48 | switch (elm.anchor_side) { 49 | case "top": 50 | offsetY = -labelHeight / 2 51 | break 52 | case "bottom": 53 | offsetY = labelHeight / 2 54 | break 55 | } 56 | 57 | obstacles.push({ 58 | cx: elm.center.x + offsetX, 59 | cy: elm.center.y + offsetY, 60 | w: labelWidth, 61 | h: labelHeight, 62 | }) 63 | continue 64 | } 65 | } 66 | } 67 | 68 | return obstacles 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/builder/transform-elements.ts: -------------------------------------------------------------------------------- 1 | export { 2 | transformPCBElement, 3 | transformPCBElements, 4 | transformSchematicElement, 5 | transformSchematicElements, 6 | } from "@tscircuit/soup-util" 7 | -------------------------------------------------------------------------------- /src/lib/pick-and-place-csv/index.ts: -------------------------------------------------------------------------------- 1 | import type { AnySoupElement, LayerRef } from "@tscircuit/soup" 2 | import Papa from "papaparse" 3 | 4 | interface PickAndPlaceRow { 5 | designator: string 6 | mid_x: number 7 | mid_y: number 8 | layer: LayerRef 9 | rotation: number 10 | } 11 | 12 | export const convertSoupToPickAndPlaceRows = ( 13 | soup: AnySoupElement[], 14 | opts: { flip_y_axis?: boolean } = {} 15 | ): PickAndPlaceRow[] => { 16 | opts.flip_y_axis ??= false 17 | 18 | const rows: PickAndPlaceRow[] = [] 19 | for (const element of soup) { 20 | if (element.type === "pcb_component") { 21 | rows.push({ 22 | designator: element.pcb_component_id, 23 | mid_x: element.center.x, 24 | mid_y: element.center.y * (opts.flip_y_axis ? -1 : 1), 25 | layer: element.layer, 26 | rotation: element.rotation, 27 | }) 28 | } 29 | } 30 | return rows 31 | } 32 | 33 | export const convertSoupToPickAndPlaceCsv = (soup: AnySoupElement[]): string => 34 | Papa.unparse( 35 | convertSoupToPickAndPlaceRows(soup).map((row) => ({ 36 | Designator: row.designator, 37 | "Mid X": row.mid_x, 38 | "Mid Y": row.mid_y, 39 | Layer: row.layer, 40 | Rotation: row.rotation, 41 | })) 42 | ) 43 | -------------------------------------------------------------------------------- /src/lib/project/create-project-from-elements.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PCBComponent, 3 | PCBPort, 4 | PCBTrace, 5 | SchematicComponent, 6 | SchematicPort, 7 | SchematicText, 8 | SchematicTrace, 9 | SourcePort, 10 | SourceTrace, 11 | } from "@tscircuit/soup" 12 | import type * as Type from "lib/types/index" 13 | 14 | export const createProjectFromElements = ( 15 | objects: Type.AnyElement[] 16 | ): Type.Project => { 17 | const project: Type.Project = { 18 | type: "project", 19 | // schematic_config: 20 | // (objects.find( 21 | // (o) => o.type === "schematic_config" 22 | // ) as SchematicConfig) || defaultSchematicConfig, 23 | schematic_components: objects.filter( 24 | (o) => o.type === "schematic_component" 25 | ) as SchematicComponent[], 26 | // schematic_groups: objects.filter( 27 | // (o) => o.type === "schematic_group" 28 | // ) as SchematicGroup[], 29 | schematic_traces: objects.filter( 30 | (o) => o.type === "schematic_trace" 31 | ) as SchematicTrace[], 32 | schematic_ports: objects.filter( 33 | (o) => o.type === "schematic_port" 34 | ) as SchematicPort[], 35 | schematic_texts: objects.filter( 36 | (o) => o.type === "schematic_text" 37 | ) as SchematicText[], 38 | // pcb_config: 39 | // (objects.find((o) => o.type === "pcb_config") as PCBConfig) || 40 | // defaultPCBConfig, 41 | // pcb_groups: objects.filter( 42 | // (o) => o.type === "pcb_group" 43 | // ) as PCBGroup[], 44 | pcb_components: objects.filter( 45 | (o) => o.type === "pcb_component" 46 | ) as PCBComponent[], 47 | pcb_traces: objects.filter((o) => o.type === "pcb_trace") as PCBTrace[], 48 | pcb_ports: objects.filter((o) => o.type === "pcb_port") as PCBPort[], 49 | // source_config: 50 | // (objects.find((o) => o.type === "source_config") as SourceConfig) || 51 | // defaultSourceConfig, 52 | source_traces: objects.filter( 53 | (o) => o.type === "source_trace" 54 | ) as SourceTrace[], 55 | // source_groups: objects.filter( 56 | // (o) => o.type === "source_group" 57 | // ) as SourceGroup[], 58 | source_components: objects.filter( 59 | (o) => o.type === "source_component" 60 | ) as Type.SourceComponent[], 61 | source_ports: objects.filter( 62 | (o) => o.type === "source_port" 63 | ) as SourcePort[], 64 | } 65 | return project 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/project/defaults.ts: -------------------------------------------------------------------------------- 1 | import type { PCBConfig, SchematicConfig, SourceConfig } from "lib/types/index" 2 | 3 | export const defaultSchematicConfig: SchematicConfig = { 4 | type: "schematic_config", 5 | } 6 | 7 | export const defaultPCBConfig: PCBConfig = { 8 | type: "pcb_config", 9 | dimension_unit: "mm", 10 | } 11 | 12 | export const defaultSourceConfig: SourceConfig = { 13 | type: "source_config", 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/project/index.ts: -------------------------------------------------------------------------------- 1 | export * from "../builder" 2 | export * from "./create-project-from-elements" 3 | export * from "./project-class" 4 | -------------------------------------------------------------------------------- /src/lib/project/project-class.ts: -------------------------------------------------------------------------------- 1 | import type { SourcePort, SourceTrace } from "@tscircuit/soup" 2 | import type * as Type from "lib/types" 3 | 4 | const elementArrayKeys = [ 5 | "schematic_components", 6 | "schematic_groups", 7 | "schematic_traces", 8 | "schematic_ports", 9 | "pcb_groups", 10 | "pcb_components", 11 | "pcb_labels", 12 | "pcb_traces", 13 | "pcb_ports", 14 | "source_traces", 15 | "source_groups", 16 | "source_components", 17 | "source_ports", 18 | ] 19 | 20 | /** Representation of a project with more utilities */ 21 | export class ProjectClass { 22 | project: Type.Project 23 | constructor(project: Type.Project) { 24 | this.project = project 25 | } 26 | getElements(): Type.AnyElement[] { 27 | return elementArrayKeys.flatMap((k) => this.project[k]) 28 | } 29 | get(id: string): Type.AnyElement | null { 30 | return this.getElements().find((e) => e[`${e.type}_id`] === id) ?? null 31 | } 32 | getRelated( 33 | type: T, 34 | id: string 35 | ): Array> { 36 | const mainElm = this.get(id) 37 | if (!mainElm) return [] 38 | const joiningId = `${mainElm.type}_id` 39 | return this.getElements().filter( 40 | (e) => e.type === type && e[joiningId] === id 41 | ) as any 42 | } 43 | getSourceComponent(id: string): Type.SourceComponent | null { 44 | return ( 45 | this.project.source_components.find( 46 | (c) => c.source_component_id === id 47 | ) ?? null 48 | ) 49 | } 50 | getSourcePort(id: string): SourcePort | null { 51 | return ( 52 | this.project.source_ports.find((c) => c.source_port_id === id) ?? null 53 | ) 54 | } 55 | getSourceTrace(id: string): SourceTrace | null { 56 | return ( 57 | this.project.source_traces.find((c) => c.source_trace_id === id) ?? null 58 | ) 59 | } 60 | } 61 | 62 | export default ProjectClass 63 | -------------------------------------------------------------------------------- /src/lib/types/build-context.ts: -------------------------------------------------------------------------------- 1 | import type { LayerRef } from "@tscircuit/soup" 2 | import type { Dimension, NumberWithAnyUnit } from "./util" 3 | 4 | export type BuildContext = { 5 | distance_unit: "mm" | "in" 6 | convert(v: NumberWithAnyUnit): number 7 | convert(v: number): number 8 | convert(v: string | number): number 9 | convert(v: Dimension): number 10 | convert(v: { x: Dimension; y: Dimension }): { 11 | x: number 12 | y: number 13 | } 14 | convert(v: string): number 15 | 16 | getId: (prefix: string) => string 17 | 18 | schematic_component_id?: string 19 | source_component_id?: string 20 | pcb_component_id?: string 21 | all_copper_layers: LayerRef[] 22 | 23 | board_thickness?: number 24 | 25 | routing_disabled?: boolean 26 | 27 | source_ports_for_nets_in_group?: Record 28 | 29 | parent?: BuildContext 30 | fork: (mutation: Partial) => BuildContext 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/types/builders.ts: -------------------------------------------------------------------------------- 1 | import type * as builder from "lib/builder" 2 | 3 | export type Builder = 4 | | builder.ComponentBuilder 5 | | builder.FootprintBuilder 6 | | builder.GroupBuilder 7 | | builder.PortsBuilder 8 | | builder.PortBuilder 9 | | builder.ProjectBuilder 10 | | builder.SMTPadBuilder 11 | | builder.TraceBuilder 12 | | builder.SchematicSymbolBuilder 13 | | builder.SchematicSymbolPrimitiveBuilder 14 | 15 | export type BuilderType = builder.ComponentBuilder["builder_type"] 16 | -------------------------------------------------------------------------------- /src/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./core" 2 | export * from "./source-component" 3 | export * from "./util" 4 | export * from "./route-solver" 5 | export * from "./builders" 6 | export * from "./build-context" 7 | export * from "./layout-debug-object" 8 | export * from "./manual_layout" 9 | export * as Soup from "@tscircuit/soup" 10 | -------------------------------------------------------------------------------- /src/lib/types/layout-debug-object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a generic sort of layout object, it's representative 3 | * of most classes of AnyElement 4 | * 5 | * @deprecated 6 | */ 7 | export type VagueLayoutObject = ( 8 | | { 9 | x: number 10 | y: number 11 | width: number 12 | height: number 13 | } 14 | | { 15 | center: { x: number; y: number } 16 | size?: { width: number; height: number } 17 | } 18 | | { 19 | position: { x: number; y: number } 20 | anchor: "left" 21 | text: string 22 | } 23 | | { 24 | x: number 25 | y: number 26 | outer_diameter: number 27 | } 28 | | { 29 | x1: number 30 | y1: number 31 | x2: number 32 | y2: number 33 | } 34 | ) & { 35 | type: string 36 | text?: string 37 | name?: string 38 | source?: { text?: string; name?: string } 39 | } 40 | 41 | /** 42 | * A debugging standard object, it represents the general layout 43 | * position of an object 44 | */ 45 | export type LayoutDebugObject = { 46 | x: number 47 | y: number 48 | width: number 49 | height: number 50 | bg_color: string 51 | title: string 52 | content: Object 53 | secondary?: boolean 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/types/manual_layout.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { point } from "@tscircuit/soup" 3 | 4 | export const manual_pcb_position = z.object({ 5 | selector: z.string(), 6 | relative_to: z 7 | .string() 8 | .optional() 9 | .default("group_center") 10 | .describe("Can be a selector or 'group_center'"), 11 | center: point, 12 | }) 13 | 14 | export const manual_layout = z.object({ 15 | pcb_positions: z.array(manual_pcb_position).optional(), 16 | }) 17 | 18 | export type ManualPcbPosition = z.infer 19 | export type ManualPcbPositionInput = z.input 20 | 21 | export type ManualLayout = z.infer 22 | export type ManualLayoutInput = z.input 23 | -------------------------------------------------------------------------------- /src/lib/types/route-solver.ts: -------------------------------------------------------------------------------- 1 | export type RouteEdge = { 2 | from: { x: number; y: number } 3 | to: { x: number; y: number } 4 | } 5 | 6 | export type Terminal = { 7 | x: number 8 | y: number 9 | facing_direction?: "up" | "down" | "left" | "right" 10 | } 11 | 12 | export type RouteSolver = (params: { 13 | terminals: readonly Terminal[] 14 | obstacles: Array<{ cx: number; cy: number; w: number; h: number }> 15 | }) => Promise> 16 | -------------------------------------------------------------------------------- /src/lib/types/source-component.ts: -------------------------------------------------------------------------------- 1 | import type { AnySourceComponent } from "@tscircuit/soup" 2 | 3 | // export interface SourceComponentBase { 4 | // type: "source_component" 5 | // /** The functional type of this component, e.g. resistor, capacitor etc. */ 6 | // ftype?: string 7 | // source_component_id: string 8 | // name: string 9 | // } 10 | 11 | // export interface SimpleResistor extends SourceComponentBase { 12 | // ftype: "simple_resistor" 13 | // // Resistance measured in ohms 14 | // resistance: NumberWithUnit<"ohm"> 15 | // } 16 | 17 | // export interface SimpleCapacitor extends SourceComponentBase { 18 | // ftype: "simple_capacitor" 19 | // // Capacitance measured in farads 20 | // capacitance: NumberWithUnit<"farad"> 21 | // } 22 | 23 | // export interface SimpleInductor extends SourceComponentBase { 24 | // ftype: "simple_inductor" 25 | // // Inductance measured in henries 26 | // inductance: NumberWithUnit<"henry"> 27 | // } 28 | 29 | // export interface SimpleDiode extends SourceComponentBase { 30 | // ftype: "simple_diode" 31 | // } 32 | // export type LightEmittingDiode = SimpleDiode & { 33 | // ftype: "led" 34 | // } 35 | // export interface SimpleBug extends SourceComponentBase { 36 | // ftype: "simple_bug" 37 | // } 38 | // export interface SimplePowerSource extends SourceComponentBase { 39 | // ftype: "simple_power_source" 40 | // voltage: NumberWithUnit<"volt"> 41 | // } 42 | // export interface SimpleGround extends SourceComponentBase { 43 | // ftype: "simple_ground" 44 | // } 45 | // export type AnySourceComponent = 46 | // | SimpleResistor 47 | // | SimpleCapacitor 48 | // | SimpleBug 49 | // | SimpleInductor 50 | // | SimplePowerSource 51 | // | SimpleGround 52 | // | SimpleDiode 53 | // | LightEmittingDiode 54 | 55 | export type SourceComponentFType = AnySourceComponent["ftype"] 56 | export type SourceComponent< 57 | T extends SourceComponentFType = SourceComponentFType 58 | > = Extract 59 | -------------------------------------------------------------------------------- /src/lib/types/util.ts: -------------------------------------------------------------------------------- 1 | import { Simplify } from "type-fest" 2 | 3 | // Currently, removing uncommon SI Prefixes for type simplicity. 4 | export type SIPrefix = 5 | // | "y" 6 | // | "yocto" 7 | // | "z" 8 | // | "zepto" 9 | // | "atto" 10 | // | "a" 11 | | "femto" 12 | | "f" 13 | | "u" 14 | | "micro" 15 | // | "d" 16 | // | "deci" 17 | | "c" 18 | | "centi" 19 | | "m" 20 | | "milli" 21 | | "k" 22 | | "kilo" 23 | | "M" 24 | | "mega" 25 | // | "G" 26 | // | "T" 27 | // | "P" 28 | // | "E" 29 | // | "Z" 30 | // | "Y" 31 | 32 | export type UnitAbbreviations = { 33 | farad: "F" 34 | ohm: "Ω" 35 | henry: "H" 36 | meter: "m" 37 | volt: "V" 38 | inch: "in" 39 | foot: "ft" 40 | } 41 | 42 | export type Unit = keyof UnitAbbreviations 43 | 44 | export type UnitOrAbbreviation = UnitAbbreviations[Unit] | Unit 45 | 46 | export type NumberWithAnyUnit = 47 | | `${number}${UnitOrAbbreviation}` 48 | | `${number} ${UnitOrAbbreviation}` 49 | | `${number}${SIPrefix}${UnitOrAbbreviation}` 50 | | `${number} ${SIPrefix}${UnitOrAbbreviation}` 51 | 52 | export type NumberWithUnit = 53 | | `${number}${T | UnitAbbreviations[T]}` 54 | | `${number} ${T | UnitAbbreviations[T]}` 55 | | `${number}${SIPrefix}${T | UnitAbbreviations[T]}` 56 | | `${number} ${SIPrefix}${T | UnitAbbreviations[T]}` 57 | 58 | export type Dimension = number | NumberWithUnit<"meter" | "inch" | "foot"> 59 | -------------------------------------------------------------------------------- /src/lib/utils/combined.ts: -------------------------------------------------------------------------------- 1 | type Combined = { 2 | [P in string]?: P extends keyof T ? T[P] : any 3 | } 4 | 5 | export const combined = (obj: T): Combined => obj as Combined 6 | -------------------------------------------------------------------------------- /src/lib/utils/convert-si-unit-to-number.ts: -------------------------------------------------------------------------------- 1 | import convertUnits from "convert-units" 2 | 3 | const si_prefix_multiplier = { 4 | tera: 10e12, 5 | T: 10e12, 6 | giga: 10e9, 7 | G: 10e9, 8 | mega: 10e6, 9 | M: 10e6, 10 | kilo: 10e3, 11 | k: 10e3, 12 | deci: 10e-1, 13 | d: 10e-1, 14 | centi: 10e-2, 15 | c: 10e-2, 16 | milli: 10e-3, 17 | m: 10e-3, 18 | micro: 10e-6, 19 | u: 10e-6, 20 | µ: 10e-6, 21 | nano: 10e-9, 22 | n: 10e-9, 23 | pico: 10e-12, 24 | p: 10e-12, 25 | } 26 | const si_prefixes = Object.keys(si_prefix_multiplier) 27 | 28 | const target_conversion = { 29 | mass: "g", 30 | length: "mm", 31 | time: "ms", 32 | volume: "ml", 33 | } 34 | 35 | function getSiPrefixMultiplierFromUnit(v: string): number { 36 | for (const prefix of si_prefixes) { 37 | if (v.startsWith(prefix)) { 38 | return si_prefix_multiplier[prefix] 39 | } 40 | } 41 | return 1 42 | } 43 | 44 | export const parseAndConvertSiUnit = < 45 | T extends 46 | | string 47 | | number 48 | | undefined 49 | | { x: string | number; y: string | number } 50 | >( 51 | v: T 52 | ): { 53 | unit: string | null 54 | value: T extends { x: string | number; y: string | number } 55 | ? null | { x: number; y: number } 56 | : null | number 57 | } => { 58 | if (typeof v === "undefined") return { unit: null, value: null } 59 | if (typeof v === "string" && v.match(/^[\d\.]+$/)) 60 | return { value: parseFloat(v) as any, unit: null } 61 | if (typeof v === "number") return { value: v as any, unit: null } 62 | if (typeof v === "object" && "x" in v && "y" in v) { 63 | return { 64 | unit: parseAndConvertSiUnit(v.x).unit, 65 | value: { 66 | x: parseAndConvertSiUnit(v.x as any).value as number, 67 | y: parseAndConvertSiUnit(v.y as any).value as number, 68 | } as any, 69 | } 70 | } 71 | const unit_reversed = v 72 | .split("") 73 | .reverse() 74 | .join("") 75 | .match(/[a-zA-Z]+/)?.[0] 76 | if (!unit_reversed) { 77 | throw new Error(`Could not determine unit: "${v}"`) 78 | } 79 | const unit = unit_reversed.split("").reverse().join("") 80 | const value = v.slice(0, -unit.length) 81 | const measure = convertUnits().describe(unit)?.measure 82 | if (measure) { 83 | return { 84 | unit, 85 | value: convertUnits(parseFloat(value)) 86 | .from(unit) 87 | .to(target_conversion[measure]), 88 | } 89 | } else { 90 | return { 91 | unit, 92 | value: (getSiPrefixMultiplierFromUnit(unit) * parseFloat(value)) as any, 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/lib/utils/convert-side-to-direction.ts: -------------------------------------------------------------------------------- 1 | export const convertSideToDirection = ( 2 | side: "top" | "bottom" | "left" | "right" 3 | ): "up" | "down" | "left" | "right" => { 4 | if (side === "top") return "up" 5 | if (side === "bottom") return "down" 6 | return side 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/utils/convert-to-degrees.ts: -------------------------------------------------------------------------------- 1 | export const convertToDegrees = (rotation: number | string) => { 2 | if (typeof rotation === "number") { 3 | return rotation 4 | } 5 | return parseFloat(rotation.split("deg")[0]) 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/utils/direction-to-vec.ts: -------------------------------------------------------------------------------- 1 | export { 2 | directionToVec, 3 | oppositeDirection, 4 | rotateClockwise, 5 | rotateCounterClockwise, 6 | rotateDirection, 7 | oppositeSide, 8 | vecToDirection, 9 | } from "@tscircuit/soup-util" 10 | -------------------------------------------------------------------------------- /src/lib/utils/extract-ids.ts: -------------------------------------------------------------------------------- 1 | import * as Type from "lib/types" 2 | 3 | export const extractIds = ( 4 | elm: any 5 | ): Partial> => { 6 | let ids = {} 7 | for (let key in elm) { 8 | if (key.endsWith("_id")) { 9 | ids[key] = elm[key] 10 | } 11 | } 12 | return ids 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/utils/find-bounds-and-center.ts: -------------------------------------------------------------------------------- 1 | import type { AnyElement } from "lib/types/core" 2 | import { getDebugLayoutObject } from "./get-layout-debug-object" 3 | import { isTruthy } from "./is-truthy" 4 | 5 | export const findBoundsAndCenter = ( 6 | elms: AnyElement[] 7 | ): { center: { x: number; y: number }; width: number; height: number } => { 8 | const debugObjects = elms 9 | .concat( 10 | elms 11 | .filter((elm) => elm.type === "pcb_trace") 12 | // add each point on a route to the bounds consideration 13 | .flatMap((elm) => elm.route as any) 14 | ) 15 | .map((elm) => getDebugLayoutObject(elm)) 16 | .filter(isTruthy) 17 | 18 | if (debugObjects.length === 0) 19 | return { center: { x: 0, y: 0 }, width: 0, height: 0 } 20 | 21 | let minX = debugObjects[0].x - debugObjects[0].width / 2 22 | let maxX = debugObjects[0].x + debugObjects[0].width / 2 23 | let minY = debugObjects[0].y - debugObjects[0].height / 2 24 | let maxY = debugObjects[0].y + debugObjects[0].height / 2 25 | 26 | for (const obj of debugObjects.slice(1)) { 27 | minX = Math.min(minX, obj.x - obj.width / 2) 28 | maxX = Math.max(maxX, obj.x + obj.width / 2) 29 | minY = Math.min(minY, obj.y - obj.height / 2) 30 | maxY = Math.max(maxY, obj.y + obj.height / 2) 31 | } 32 | 33 | const width = maxX - minX 34 | const height = maxY - minY 35 | const center = { x: minX + width / 2, y: minY + height / 2 } 36 | 37 | return { center, width, height } 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/utils/get-zod-schema-defaults.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | // https://github.com/colinhacks/zod/discussions/1953 4 | export function getZodSchemaDefaults( 5 | schema: Schema 6 | ) { 7 | return Object.fromEntries( 8 | Object.entries(schema.shape) 9 | .map(([key, value]) => { 10 | if (value instanceof z.ZodDefault) 11 | return [key, value._def.defaultValue()] 12 | return [key, undefined] 13 | }) 14 | .filter(([key, value]) => value !== undefined) 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./direction-to-vec" 2 | export * from "./find-bounds-and-center" 3 | export * from "./get-layout-debug-object" 4 | export * from "./point-math" 5 | export * from "./string-hash" 6 | export * from "./convert-side-to-direction" 7 | export * from "./get-port-position" 8 | export * from "./point-operations" 9 | -------------------------------------------------------------------------------- /src/lib/utils/is-truthy.ts: -------------------------------------------------------------------------------- 1 | export const isTruthy = (value: T): value is NonNullable => Boolean(value) 2 | -------------------------------------------------------------------------------- /src/lib/utils/maybe-convert-to-point.ts: -------------------------------------------------------------------------------- 1 | export const maybeConvertToPoint = (p: unknown): { x: any; y: any } | null => { 2 | if (p === null) { 3 | return null 4 | } 5 | if (Array.isArray(p) && p.length === 2) { 6 | return { x: p[0], y: p[1] } 7 | } 8 | if (typeof p === "object") { 9 | const x = "x" in p ? p.x : undefined 10 | const y = "y" in p ? p.y : undefined 11 | if (x !== undefined || y !== undefined) { 12 | return { x, y } 13 | } 14 | } 15 | return null 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/utils/pairs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return pairs of adjacent elements in an array. 3 | */ 4 | export function pairs(arr: Array): Array<[T, T]> { 5 | const result: Array<[T, T]> = [] 6 | for (let i = 0; i < arr.length - 1; i++) { 7 | result.push([arr[i], arr[i + 1]]) 8 | } 9 | return result 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/utils/point-math.ts: -------------------------------------------------------------------------------- 1 | type Point = { x: number; y: number } 2 | 3 | export function sub(p1: Point, p2: Point) { 4 | return { x: p1.x - p2.x, y: p1.y - p2.y } 5 | } 6 | 7 | export function mult(p1: Point, p2: Point) { 8 | return { x: p1.x * p2.x, y: p1.y * p2.y } 9 | } 10 | 11 | export function mag(p1: Point, p2: Point) { 12 | const dx = p1.x - p2.x 13 | const dy = p1.y - p2.y 14 | return Math.sqrt(dx ** 2 + dy ** 2) 15 | } 16 | 17 | export function componentSum(p1: Point) { 18 | return p1.x + p1.y 19 | } 20 | 21 | export function norm(p1: Point) { 22 | return { 23 | x: Math.sign(p1.x), 24 | y: Math.sign(p1.y), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/utils/point-operations.ts: -------------------------------------------------------------------------------- 1 | type Point = { x: number; y: number } 2 | 3 | export const sumPoints = (points: Array): Point => { 4 | return points.reduce( 5 | (acc, point) => { 6 | acc.x += point.x 7 | acc.y += point.y 8 | return acc 9 | }, 10 | { x: 0, y: 0 } 11 | ) 12 | } 13 | 14 | export const multPoint = (point: Point, factor: number): Point => { 15 | return { 16 | x: point.x * factor, 17 | y: point.y * factor, 18 | } 19 | } 20 | 21 | export const rotatePoint = ({ 22 | point, 23 | center, 24 | rotationDeg, 25 | }: { 26 | point: Point 27 | center: Point 28 | rotationDeg: number 29 | }): Point => { 30 | const rotationRad = (rotationDeg * Math.PI) / 180 31 | const cos = Math.cos(rotationRad) 32 | const sin = Math.sin(rotationRad) 33 | const x = point.x - center.x 34 | const y = point.y - center.y 35 | return { 36 | x: x * cos - y * sin + center.x, 37 | y: x * sin + y * cos + center.y, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/utils/remove-nulls.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash" 2 | 3 | export const removeNulls = ( 4 | obj: T 5 | ): { 6 | [K in keyof T]: NonNullable 7 | } => _.omitBy(obj as any, _.isNil) as any 8 | -------------------------------------------------------------------------------- /src/lib/utils/string-hash.ts: -------------------------------------------------------------------------------- 1 | export function stringHash(str: string) { 2 | let hash = 0 3 | if (str.length == 0) return hash 4 | for (var i = 0; i < str.length; i++) { 5 | var char = str.charCodeAt(i) 6 | hash = (hash << 5) - hash + char 7 | hash = hash & hash // Convert to 32bit integer 8 | } 9 | return Math.abs(hash) 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/utils/uniq.ts: -------------------------------------------------------------------------------- 1 | export function uniq(array: T[]): T[] { 2 | return Array.from(new Set(array)) 3 | } 4 | -------------------------------------------------------------------------------- /tests/apply-selector/apply-selector-1.test.ts: -------------------------------------------------------------------------------- 1 | import * as Type from "lib/types" 2 | import test from "ava" 3 | import { createProjectBuilder, ProjectClass } from "lib/project" 4 | import { applySelector } from "lib/apply-selector" 5 | 6 | test("applySelector use css selector to select circuit elements", async (t) => { 7 | const elements = await createProjectBuilder() 8 | .add("group", (gb) => 9 | gb 10 | .addResistor((cb) => 11 | cb.setSourceProperties({ 12 | resistance: "10 ohm", 13 | name: "R1", 14 | }) 15 | ) 16 | .addCapacitor((cb) => 17 | cb.setSourceProperties({ 18 | name: "C1", 19 | capacitance: "10 uF", 20 | }) 21 | ) 22 | ) 23 | .build() 24 | 25 | const selector = ".R1 > port.right" 26 | 27 | t.deepEqual(applySelector(elements, selector), [ 28 | { 29 | type: "source_port", 30 | name: "right", 31 | pin_number: undefined, 32 | port_hints: undefined, 33 | source_port_id: "source_port_1", 34 | source_component_id: "simple_resistor_0", 35 | }, 36 | ]) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/apply-selector/apply-selector-2-via.test.ts: -------------------------------------------------------------------------------- 1 | import * as Type from "lib/types" 2 | import test from "ava" 3 | import { createProjectBuilder, ProjectClass } from "lib/project" 4 | import { applySelector } from "lib/apply-selector" 5 | 6 | test.skip("applySelector should work to resolve via layer ambiguity", async (t) => { 7 | const elements = await createProjectBuilder() 8 | .add("group", (gb) => 9 | gb 10 | .addResistor((cb) => 11 | cb.setSourceProperties({ 12 | resistance: "10 ohm", 13 | name: "R1", 14 | }) 15 | ) 16 | .add("via", (vb) => 17 | vb.setProps({ 18 | pcb_x: "1mm", 19 | pcb_y: "1mm", 20 | hole_diameter: "0.5mm", 21 | outer_diameter: "1mm", 22 | }) 23 | ) 24 | ) 25 | .build() 26 | 27 | const selector = ".V1" 28 | 29 | // We should select for via.top or whatever using the layer of R1 to determine 30 | // if we're connecting to e.g. top or bottom 31 | 32 | // t.deepEqual(applySelector(elements, selector), [ 33 | // { 34 | // type: "source_port", 35 | // name: "right", 36 | // pin_number: undefined, 37 | // source_port_id: "source_port_1", 38 | // source_component_id: "simple_resistor_0", 39 | // }, 40 | // ]) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/apply-selector/apply-selector-3-port-hints.test.ts: -------------------------------------------------------------------------------- 1 | import * as Type from "lib/types" 2 | import test from "ava" 3 | import { createProjectBuilder, ProjectClass } from "lib/project" 4 | import { applySelector } from "lib/apply-selector" 5 | 6 | test("applySelector 3: port hints", async (t) => { 7 | const elements = await createProjectBuilder() 8 | .add("group", (gb) => 9 | gb 10 | .addResistor((cb) => 11 | cb.setSourceProperties({ 12 | resistance: "10 ohm", 13 | name: "R1", 14 | }) 15 | ) 16 | .add("bug", (bb) => 17 | bb.setProps({ 18 | name: "U1", 19 | port_arrangement: { 20 | left_size: 1, 21 | right_size: 1, 22 | }, 23 | port_labels: { 24 | 1: "A", 25 | 2: "B", 26 | }, 27 | }) 28 | ) 29 | ) 30 | .build() 31 | 32 | t.deepEqual(applySelector(elements, ".U1 > port.A"), [ 33 | { 34 | type: "source_port", 35 | name: "A", 36 | source_port_id: "source_port_2", 37 | source_component_id: "simple_bug_0", 38 | pin_number: 1, 39 | port_hints: ["A", "1"], 40 | }, 41 | ]) 42 | // @ts-ignore 43 | t.is(applySelector(elements, ".U1 > port.1")[0]!.name, "A") 44 | // @ts-ignore 45 | t.is(applySelector(elements, ".U1 > .1")[0]!.name, "A") 46 | // @ts-ignore 47 | t.is(applySelector(elements, ".U1 > .A")[0]!.name, "A") 48 | }) 49 | -------------------------------------------------------------------------------- /tests/apply-selector/apply-selector-4-nets.ts: -------------------------------------------------------------------------------- 1 | import * as Type from "lib/types" 2 | import test from "ava" 3 | import { createProjectBuilder, ProjectClass } from "lib/project" 4 | import { applySelector } from "lib/apply-selector" 5 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 6 | 7 | test("applySelector 4: nets", async (t) => { 8 | const { pb, logSoup } = await getTestFixture(t) 9 | 10 | const soup = await pb 11 | .add("resistor", (rb) => rb.setProps({ resistance: 100, name: "R1" })) 12 | .add("net", (nb) => nb.setProps({ name: "N1" })) 13 | .add("trace", (tb) => tb.setProps({ from: ".R1 > .right", to: "net.N1" })) 14 | .build() 15 | 16 | t.like(applySelector(soup, ".N1")[0], { 17 | type: "source_net", 18 | name: "N1", 19 | }) 20 | t.like(applySelector(soup, "net.N1")[0], { 21 | type: "source_net", 22 | name: "N1", 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /tests/basic-interfaces/footprint-hole.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createProjectBuilder } from "lib/builder/project-builder" 3 | import { logLayout } from "../utils/log-layout" 4 | import { su } from "@tscircuit/soup-util" 5 | 6 | test("footprint hole should be created", async (t) => { 7 | const projectBuilder = await createProjectBuilder().add( 8 | "generic_component", 9 | (gb) => 10 | gb.footprint.add("platedhole", (ph) => 11 | ph.setProps({ 12 | shape: "circle", 13 | hole_diameter: "1mm", 14 | outerDiameter: "1.5mm", 15 | x: 0, 16 | y: 0, 17 | }) 18 | ) 19 | ) 20 | 21 | const soup = await projectBuilder.build() 22 | 23 | const [plated_hole] = su(soup).pcb_plated_hole.list() 24 | if (plated_hole.shape !== "circle") throw new Error("not a circle") 25 | 26 | t.is(plated_hole.hole_diameter, 1) 27 | t.is(plated_hole.outer_diameter, 1.5) 28 | 29 | await logLayout("footprint-hole", soup) 30 | t.snapshot(soup) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/basic-interfaces/snapshots/footprint-hole.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `tests/basic-interfaces/footprint-hole.test.ts` 2 | 3 | The actual snapshot is saved in `footprint-hole.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## footprint hole should be created 8 | 9 | > Snapshot 1 10 | 11 | [ 12 | { 13 | source_component_id: 'generic_0', 14 | supplier_part_numbers: {}, 15 | type: 'source_component', 16 | }, 17 | { 18 | center: { 19 | x: 0, 20 | y: 0, 21 | }, 22 | rotation: 0, 23 | schematic_component_id: 'schematic_generic_component_0', 24 | size: { 25 | height: 0, 26 | width: 0, 27 | }, 28 | source_component_id: 'generic_0', 29 | type: 'schematic_component', 30 | }, 31 | { 32 | center: { 33 | x: 0, 34 | y: 0, 35 | }, 36 | height: 1.5, 37 | layer: 'top', 38 | pcb_component_id: 'pcb_generic_component_0', 39 | rotation: 0, 40 | source_component_id: 'generic_0', 41 | type: 'pcb_component', 42 | width: 1.5, 43 | }, 44 | { 45 | hole_diameter: 1, 46 | layers: [ 47 | 'top', 48 | 'bottom', 49 | ], 50 | outer_diameter: 1.5, 51 | pcb_component_id: 'pcb_generic_component_0', 52 | port_hints: [], 53 | shape: 'circle', 54 | type: 'pcb_plated_hole', 55 | x: 0, 56 | y: 0, 57 | }, 58 | ] 59 | -------------------------------------------------------------------------------- /tests/basic-interfaces/snapshots/footprint-hole.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscircuit/builder/0e9ac742c2c714fdb7daf6b57cca2a49f325b575/tests/basic-interfaces/snapshots/footprint-hole.test.ts.snap -------------------------------------------------------------------------------- /tests/board-builder/board-builder.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { su } from "@tscircuit/soup-util" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("[smoke] board builder", async (t) => { 6 | const { pb, logSoup } = getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("board", (bb) => { 10 | bb.setProps({ width: 100, height: 100, pcbX: 10, pcbY: 5 }) 11 | }) 12 | .build() 13 | 14 | const [pcb_board] = su(soup).pcb_board.list() 15 | t.is(pcb_board.center.x, 10) 16 | t.is(pcb_board.center.y, 5) 17 | await logSoup(soup) 18 | t.pass() 19 | }) 20 | -------------------------------------------------------------------------------- /tests/board-builder/center-with-one-prop.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 3 | 4 | test("Center with one prop", async (t) => { 5 | const { logSoup, pb } = await getTestFixture(t) 6 | const soup = await pb 7 | .add("bug", (bb) => 8 | bb.setProps({ 9 | name: "U1", 10 | port_arrangement: { 11 | left_size: 1, 12 | right_size: 1, 13 | }, 14 | port_labels: { 15 | 1: "A", 16 | 2: "B", 17 | 3: "C", 18 | }, 19 | schPortArrangement: { 20 | leftSide: { 21 | direction: "top-to-bottom", 22 | pins: [3], 23 | }, 24 | bottomSide: { 25 | direction: "left-to-right", 26 | pins: [1], 27 | }, 28 | rightSide: { 29 | direction: "left-to-right", 30 | pins: [2], 31 | }, 32 | }, 33 | schPinLabels: { 34 | 1: "A", 35 | 2: "B", 36 | 3: "C", 37 | }, 38 | cadModel: { 39 | rotationOffset: { x: 0, y: 0, z: 180 }, 40 | objUrl: 41 | "https://modelcdn.tscircuit.com/easyeda_models/download?uuid=d777607a152f4f3aac9bb0d0c14ed6fd&pn=C4355039", 42 | }, 43 | }) 44 | ) 45 | .add("capacitor", (cb) => 46 | cb 47 | .setProps({ 48 | name: "C1", 49 | capacitance: "10 uF", 50 | center: { x: 8 }, // <-- this is the only prop that's different 51 | }) 52 | .setSchematicRotation("90deg") 53 | ) 54 | .add("trace", (tb) => 55 | tb.setProps({ 56 | from: ".U1 > .B", 57 | to: ".C1 > .right", 58 | }) 59 | ) 60 | .add("bug", (bb) => 61 | bb 62 | .setProps({ 63 | name: "U2", 64 | port_arrangement: { 65 | left_size: 1, 66 | right_size: 1, 67 | }, 68 | port_labels: { 69 | 1: "A", 70 | 2: "B", 71 | }, 72 | schPortArrangement: { 73 | rightSide: { 74 | direction: "top-to-bottom", 75 | pins: [2, 1], 76 | }, 77 | }, 78 | cadModel: { 79 | rotationOffset: { x: 0, y: 0, z: 180 }, 80 | objUrl: 81 | "https://modelcdn.tscircuit.com/easyeda_models/download?uuid=d777607a152f4f3aac9bb0d0c14ed6fd&pn=C4355039", 82 | }, 83 | }) 84 | .labelPort(1, "PWRIN") 85 | .labelPort(2, "GND") 86 | .setSchematicCenter(-3, 0) 87 | ) 88 | .add("trace", (tb) => 89 | tb.setProps({ from: ".U2 > .GND", to: ".C1 > .left" }) 90 | ) 91 | .add("trace", (tb) => tb.setProps({ from: ".U2 > .PWRIN", to: ".U1 > .C" })) 92 | .build() 93 | 94 | await logSoup(soup) 95 | t.pass() 96 | }) 97 | -------------------------------------------------------------------------------- /tests/bom-csv/convert-soup-to-bom-rows-resistor.test.ts: -------------------------------------------------------------------------------- 1 | import type { SourceComponentBase } from "@tscircuit/soup" 2 | import test from "ava" 3 | import { convertBomRowsToCsv, convertSoupToBomRows } from "lib/bom-csv" 4 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 5 | 6 | test("convert soup to bom rows (resistor)", async (t) => { 7 | const { pb } = await getTestFixture(t) 8 | 9 | const soup = await pb 10 | .add("resistor", (rb) => 11 | rb.setProps({ 12 | name: "R1", 13 | footprint: "0805", 14 | resistance: "10k", 15 | supplier_part_numbers: { 16 | jlcpcb: "C22775", 17 | }, 18 | }) 19 | ) 20 | .build() 21 | 22 | const source_component = soup.find( 23 | (elm) => elm.type === "source_component" 24 | )! as SourceComponentBase 25 | 26 | t.truthy(source_component.supplier_part_numbers!.jlcpcb) 27 | 28 | // @ts-ignore 29 | const bom_rows = await convertSoupToBomRows({ soup }) 30 | 31 | t.is(bom_rows[0]!.supplier_part_number_columns!["JLCPCB Part#"], "C22775") 32 | 33 | const csv_string = convertBomRowsToCsv(bom_rows) 34 | 35 | t.truthy(csv_string.includes("C22775")) 36 | }) 37 | -------------------------------------------------------------------------------- /tests/bom-csv/convert-soup-to-bom-rows-variety.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { convertBomRowsToCsv, convertSoupToBomRows } from "lib/bom-csv" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("convert soup to bom rows (variety)", async (t) => { 6 | const { pb } = await getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("resistor", (rb) => 10 | rb.setProps({ 11 | name: "R1", 12 | footprint: "0805", 13 | resistance: "10k", 14 | supplier_part_numbers: { 15 | jlcpcb: ["C22775"], 16 | }, 17 | }) 18 | ) 19 | .add("capacitor", (cb) => 20 | cb.setProps({ 21 | name: "C1", 22 | footprint: "0805", 23 | capacitance: "10u", 24 | supplier_part_numbers: { 25 | jlcpcb: ["C22776"], 26 | }, 27 | }) 28 | ) 29 | .add("bug", (bb) => 30 | bb 31 | .setProps({ 32 | name: "B1", 33 | supplier_part_numbers: { 34 | jlcpcb: ["C596355"], 35 | }, 36 | port_arrangement: { 37 | left_size: 3, 38 | right_size: 3, 39 | }, 40 | }) 41 | .labelPort(1, "PWR") 42 | .labelPort(2, "NC") 43 | .labelPort(3, "RG") 44 | .labelPort(4, "D0") 45 | .labelPort(5, "D1") 46 | .labelPort(6, "GND") 47 | ) 48 | .build() 49 | 50 | // @ts-ignore 51 | const bom_rows = await convertSoupToBomRows({ soup }) 52 | const csv_string = convertBomRowsToCsv(bom_rows) 53 | 54 | t.truthy(csv_string.includes("C22775"), "has resistor part number") 55 | t.truthy(csv_string.includes("C22776"), "has capacitor part number") 56 | t.truthy(csv_string.includes("C596355"), "has bug part number") 57 | }) 58 | -------------------------------------------------------------------------------- /tests/bug-builder/bug-footprint-with-translation.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "../fixtures/get-test-fixture" 3 | 4 | test("bug with footprint and cad with translation", async (t) => { 5 | const { logSoup, pb } = await getTestFixture(t) 6 | 7 | const soup = await pb 8 | .add("board", (board) => 9 | board 10 | .setProps({ 11 | center_x: 0, 12 | center_y: 0, 13 | width: 10, 14 | height: 10, 15 | }) 16 | .add("bug", (bug) => 17 | bug.setProps({ 18 | schPinLabels: { 19 | 1: "P1", 20 | 2: "P2", 21 | 3: "P3", 22 | 4: "P4", 23 | 5: "P5", 24 | 6: "P6", 25 | 7: "P7", 26 | 8: "P8", 27 | }, 28 | schPortArrangement: { 29 | leftSize: 4, 30 | rightSize: 4, 31 | }, 32 | pcbRotation: "90deg", 33 | cadModel: { 34 | objUrl: 35 | "https://modelcdn.tscircuit.com/easyeda_models/download?pn=C128415&uuid=dc694c23844346e9981bdbac7bb76421", 36 | }, 37 | footprint: "soic8_w7.2mm", 38 | pcbX: 3, 39 | pcbY: 3, 40 | }) 41 | ) 42 | ) 43 | .build() 44 | 45 | await logSoup(soup) 46 | t.pass() 47 | }) 48 | -------------------------------------------------------------------------------- /tests/bug-builder/bug-resistor-connection.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createProjectBuilder } from "../../src" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("bug that has a footprint and connects to a resistor", async (t) => { 6 | const result = await createProjectBuilder() 7 | .add("bug", (bb) => 8 | bb 9 | .setSourceProperties({ name: "B1" }) 10 | .setSchematicProperties({ 11 | port_arrangement: { 12 | left_size: 3, 13 | right_size: 3, 14 | }, 15 | }) 16 | .setFootprint("sot236") 17 | .labelPort(1, "D0") 18 | .labelPort(2, "D1") 19 | .setFootprintRotation("90deg") 20 | .setSchematicCenter(0, 0) 21 | ) 22 | .add("resistor", (bb) => 23 | bb 24 | .setSourceProperties({ name: "R1" }) 25 | .setFootprint("0805") 26 | .setFootprintCenter(-4, 0) 27 | .setSchematicCenter(-2, -0.5) 28 | ) 29 | .add("trace", (tb) => tb.addConnections([".B1 > .D0", ".R1 > .right"])) 30 | .build() 31 | 32 | const d0_port = result.find((elm) => { 33 | if (elm.type !== "pcb_port") return false 34 | if (!elm.source_port_id) return false 35 | const elm_source = result.find( 36 | (e2) => 37 | e2.source_port_id === elm.source_port_id && e2.type === "source_port" 38 | ) 39 | if (elm_source && elm_source.name === "D0") return true 40 | return false 41 | }) 42 | 43 | t.truthy(d0_port.x) 44 | t.truthy(d0_port.y) 45 | 46 | await logLayout(`bug connected to resistor`, result) 47 | t.pass() 48 | }) 49 | -------------------------------------------------------------------------------- /tests/bug-builder/bug-with-pin-spacing.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "../fixtures/get-test-fixture" 3 | 4 | test("bug with pin spacing", async (t) => { 5 | const { logSoup, pb } = await getTestFixture(t) 6 | 7 | const soup = await pb 8 | .add("bug", (bb) => 9 | bb.setProps({ 10 | name: "B1", 11 | schPortArrangement: { 12 | leftSize: 0, 13 | rightSize: 4, 14 | }, 15 | pinLabels: { 16 | 1: "D0", 17 | 2: "D1", 18 | }, 19 | pinSpacing: 1, 20 | center: { x: 0, y: 0 }, 21 | }) 22 | ) 23 | .build() 24 | 25 | await logSoup(soup) 26 | t.pass() 27 | }) 28 | -------------------------------------------------------------------------------- /tests/bug-builder/custom-port-arrangement.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createProjectBuilder } from "../../src" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("custom-port-arrangement bug", async (t) => { 6 | const result = await createProjectBuilder() 7 | .add("bug", (bb) => 8 | bb 9 | .setSourceProperties({ name: "B1" }) 10 | .setSchematicProperties({ 11 | port_arrangement: { 12 | left_side: { 13 | pins: [1, 8], 14 | }, 15 | bottom_side: { 16 | pins: [3], 17 | }, 18 | right_side: { 19 | pin_definition_direction: "top-to-bottom", 20 | pins: [2, 4], 21 | }, 22 | }, 23 | }) 24 | .labelPort(3, "IN") 25 | .labelPort(2, "OUT") 26 | .setSchematicCenter(8, 3) 27 | ) 28 | .build() 29 | 30 | await logLayout(`custom-port-arrangement bug`, result) 31 | t.pass() 32 | }) 33 | -------------------------------------------------------------------------------- /tests/bug-builder/duplicate-port-hints.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "../fixtures/get-test-fixture" 3 | import { su } from "@tscircuit/soup-util" 4 | 5 | test("replicate duplicate port hints issue", async (t) => { 6 | const { logSoup, pb } = await getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("bug", (cb) => { 10 | cb.setProps({ footprint: "soic8" }) 11 | }) 12 | .build() 13 | 14 | const sourcePorts = su(soup).source_port.list() 15 | 16 | sourcePorts.forEach((port) => { 17 | if (port.port_hints) { 18 | const portHintsSet = new Set(port.port_hints) 19 | 20 | t.is( 21 | port.port_hints.length, 22 | portHintsSet.size, 23 | `Duplicate hints found in port ${port.name}` 24 | ) 25 | } else { 26 | t.fail(`port_hints is undefined for port ${port.name}`) 27 | } 28 | }) 29 | 30 | await logSoup(soup) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/bug-builder/one-sided-bug.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createProjectBuilder } from "../../src" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("one-sided bug", async (t) => { 6 | const result = await createProjectBuilder() 7 | .add("bug", (bb) => 8 | bb 9 | .setSourceProperties({ name: "B1" }) 10 | .setSchematicProperties({ 11 | port_arrangement: { 12 | left_size: 0, 13 | right_size: 4, 14 | }, 15 | }) 16 | .labelPort(1, "D0") 17 | .labelPort(2, "D1") 18 | .setSchematicCenter(8, 3) 19 | ) 20 | .build() 21 | 22 | await logLayout(`one-sided bug`, result) 23 | t.pass() 24 | }) 25 | -------------------------------------------------------------------------------- /tests/bug-builder/three-sided-bug.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createProjectBuilder } from "../../src" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("three-sided bug", async (t) => { 6 | const result = await createProjectBuilder() 7 | .add("bug", (bb) => 8 | bb 9 | .setSourceProperties({ name: "B1" }) 10 | .setSchematicProperties({ 11 | port_arrangement: { 12 | left_size: 3, 13 | right_size: 3, 14 | top_size: 0, 15 | bottom_size: 5, 16 | }, 17 | }) 18 | .labelPort(1, "D0") 19 | .labelPort(2, "D1") 20 | .setSchematicCenter(8, 3) 21 | ) 22 | .build() 23 | 24 | await logLayout(`three-sided bug`, result) 25 | t.pass() 26 | }) 27 | -------------------------------------------------------------------------------- /tests/component-builder/cad-model-bottom-layer.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 3 | import { su } from "@tscircuit/soup-util" 4 | 5 | test("add cad_component on bottom layer", async (t) => { 6 | const { pb, logSoup } = await getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("board", (bb) => 10 | bb 11 | .setProps({ 12 | width: 18, 13 | height: 26, 14 | }) 15 | .add("bug", (bb) => 16 | bb.setProps({ 17 | name: "J1", 18 | footprint: "pinrow5", 19 | layer: "bottom", 20 | cadModel: { 21 | objUrl: 22 | "https://modelcdn.tscircuit.com/easyeda_models/download?uuid=6331f645d89e4b919bdba0cb4f3544ce&pn=C124379", 23 | }, 24 | }) 25 | ) 26 | ) 27 | .build() 28 | 29 | const cadComponent = su(soup).cad_component.list()[0] 30 | t.is(cadComponent.layer, "bottom") 31 | t.deepEqual(cadComponent.rotation, { x: 0, y: 180, z: 0 }) 32 | 33 | await logSoup(soup) 34 | }) 35 | -------------------------------------------------------------------------------- /tests/component-builder/cad-model.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 3 | import { su } from "@tscircuit/soup-util" 4 | 5 | test("add cad_component when cadModel specified 2", async (t) => { 6 | const { pb, logSoup } = await getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("board", (bb) => 10 | bb 11 | .setProps({ 12 | width: 10, 13 | height: 10, 14 | }) 15 | .add("resistor", (rb) => 16 | rb 17 | .setProps({ 18 | resistance: 100, 19 | name: "R2", 20 | rotation: "90deg", 21 | footprint: "0805", 22 | cadModel: { 23 | rotationOffset: "90deg", 24 | objUrl: 25 | "https://modelcdn.tscircuit.com/easyeda_models/download?uuid=c7acac53bcbc44d68fbab8f60a747688&pn=C17414", 26 | }, 27 | }) 28 | .setSchematicCenter(0, 2) 29 | ) 30 | .add("resistor", (rb) => 31 | rb.setProps({ 32 | resistance: 1_000, 33 | name: "R1", 34 | pcb_x: 2, 35 | pcb_y: 2, 36 | footprint: "0805", 37 | pcbRotation: "90deg", 38 | cadModel: { 39 | objUrl: 40 | "https://modelcdn.tscircuit.com/easyeda_models/download?uuid=c7acac53bcbc44d68fbab8f60a747688&pn=C17414", 41 | }, 42 | }) 43 | ) 44 | .add("trace", (tb) => 45 | tb.setProps({ from: ".R1 > .left", to: ".R2 > .right" }) 46 | ) 47 | ) 48 | .build() 49 | 50 | t.is(su(soup).cad_component.list().length, 2) 51 | t.is(su(soup).pcb_board.list().length, 1) 52 | 53 | await logSoup(soup) 54 | t.pass() 55 | }) 56 | -------------------------------------------------------------------------------- /tests/constrained-layout-builder/basic-schematic.test.ts: -------------------------------------------------------------------------------- 1 | import type { SchematicComponent } from "@tscircuit/soup" 2 | import test from "ava" 3 | import { createConstrainedLayoutBuilder, createProjectBuilder } from "../../src" 4 | import { logLayout } from "../utils/log-layout" 5 | 6 | test("basic schematic constraint builder test", async (t) => { 7 | const pb = createProjectBuilder() 8 | const cb = createConstrainedLayoutBuilder(pb) 9 | .add("resistor", (rb) => 10 | rb.setSourceProperties({ 11 | resistance: "10 ohm", 12 | name: "R1", 13 | }) 14 | ) 15 | .add("capacitor", (cb) => 16 | cb.setSourceProperties({ 17 | name: "C1", 18 | capacitance: "10 uF", 19 | }) 20 | ) 21 | .addConstraint({ 22 | type: "xdist", 23 | schematic: true, 24 | dist: 2, 25 | left: ".R1", 26 | right: ".C1", 27 | }) 28 | 29 | const elements = await cb.build(pb.createBuildContext()) 30 | await logLayout("basic-schematic", elements) 31 | const [e1, e2] = elements.filter( 32 | (e) => e.type === "schematic_component" 33 | ) as SchematicComponent[] 34 | t.is(e1.center.x + e1.size.width / 2 + 2, e2.center.x - e2.size.width / 2) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/fixtures/get-test-fixture.ts: -------------------------------------------------------------------------------- 1 | import type { AnySoupElement } from "@tscircuit/soup" 2 | import type { ExecutionContext } from "ava" 3 | import test from "ava" 4 | import { createProjectBuilder } from "../../src" 5 | import { logLayout } from "../utils/log-layout" 6 | import { writeSchematicSnapshotPng } from "./schematic-snapshot-output" 7 | import { writePcbSnapshotPng } from "./pcb-snapshot-output" 8 | 9 | /** 10 | * Consolidates common test functions and components for simpler test definition 11 | */ 12 | export const getTestFixture = (t: ExecutionContext) => { 13 | return { 14 | logSoup: (soup: AnySoupElement[]) => logLayout(t.title, soup), 15 | pb: createProjectBuilder(), 16 | writeSchematicSnapshotPng: (soup: AnySoupElement[]) => 17 | writeSchematicSnapshotPng(t.title, soup, test.meta.file), 18 | writePcbSnapshotPng: (soup: AnySoupElement[]) => 19 | writePcbSnapshotPng(t.title, soup, test.meta.file), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/fixtures/pcb-snapshot-output.ts: -------------------------------------------------------------------------------- 1 | import type { AnySoupElement } from "@tscircuit/soup" 2 | import { circuitToPng } from "circuit-to-png" 3 | import { mkdir, writeFile } from "node:fs/promises" 4 | import path from "node:path" 5 | import { fileURLToPath } from "node:url" 6 | 7 | export const writePcbSnapshotPng = async ( 8 | fileName: string, 9 | circuit: AnySoupElement[], 10 | dirName: string 11 | ) => { 12 | const pngBuffer = circuitToPng(circuit, "pcb") 13 | const fileNameWithoutSpaces = fileName.replaceAll(" ", "-") 14 | 15 | const filePath = fileURLToPath(dirName) 16 | const directoryPath = path.dirname(filePath) 17 | const snapshotDir = path.join(directoryPath, "__snapshots__") 18 | 19 | try { 20 | await mkdir(snapshotDir, { recursive: true }) 21 | } catch (err) { 22 | if (err.code !== "EEXIST") { 23 | throw err 24 | } 25 | } 26 | 27 | const snapshotPath = path.join( 28 | snapshotDir, 29 | `${fileNameWithoutSpaces}-pcb.snapshot.png` 30 | ) 31 | await writeFile(snapshotPath, pngBuffer) 32 | } 33 | -------------------------------------------------------------------------------- /tests/fixtures/schematic-snapshot-output.ts: -------------------------------------------------------------------------------- 1 | import type { AnySoupElement } from "@tscircuit/soup" 2 | import { circuitToPng } from "circuit-to-png" 3 | import { mkdir, writeFile } from "node:fs/promises" 4 | import path from "node:path" 5 | import { fileURLToPath } from "node:url" 6 | 7 | export const writeSchematicSnapshotPng = async ( 8 | fileName: string, 9 | circuit: AnySoupElement[], 10 | dirName: string 11 | ) => { 12 | const pngBuffer = circuitToPng(circuit, "schematic") 13 | const fileNameWithoutSpaces = fileName.replaceAll(" ", "-") 14 | 15 | const filePath = fileURLToPath(dirName) 16 | const directoryPath = path.dirname(filePath) 17 | const snapshotDir = path.join(directoryPath, "__snapshots__") 18 | 19 | try { 20 | await mkdir(snapshotDir, { recursive: true }) 21 | } catch (err) { 22 | if (err.code !== "EEXIST") { 23 | throw err 24 | } 25 | } 26 | 27 | const snapshotPath = path.join( 28 | snapshotDir, 29 | `${fileNameWithoutSpaces}-sch.snapshot.png` 30 | ) 31 | await writeFile(snapshotPath, pngBuffer) 32 | } 33 | -------------------------------------------------------------------------------- /tests/fixtures/setup-debug-logging.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import fs from "node:fs" 3 | const randomString = () => Math.random().toString(36).substring(2, 15) 4 | globalThis.logTmpFile = (prefix, obj) => { 5 | fs.mkdirSync("./tmp-debug-logs", { recursive: true }) 6 | const filePath = `./tmp-debug-logs/${prefix}-${Date.now()}-${randomString()}.json` 7 | fs.writeFileSync(filePath, JSON.stringify(obj, null, 2)) 8 | console.log(`Wrote debug log to ${filePath}`) 9 | } 10 | -------------------------------------------------------------------------------- /tests/footprints/basic-pcb-trace.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "../fixtures/get-test-fixture" 3 | import { su } from "@tscircuit/soup-util" 4 | 5 | test("basic pcb trace (manual placement)", async (t) => { 6 | const { pb, logSoup } = getTestFixture(t) 7 | 8 | pb.add("component", (cb) => { 9 | cb.footprint.add("pcbtrace", (ptb) => { 10 | ptb.setProps({ 11 | route: [ 12 | { x: "0mm", y: "0mm" }, 13 | { x: "10mm", y: "0mm" }, 14 | ], 15 | thickness: "0.1mm", 16 | }) 17 | }) 18 | }) 19 | 20 | const soup = await pb.build() 21 | 22 | const pcb_trace = su(soup).pcb_trace.list()[0]! 23 | 24 | await logSoup(soup) 25 | t.is(pcb_trace.route[0].x, 0) 26 | t.is(pcb_trace.route[1].x, 10) 27 | }) 28 | -------------------------------------------------------------------------------- /tests/footprints/move-footprint.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createFootprintBuilder, createProjectBuilder } from "../../src" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("set pcb_x/pcb_y of bug", async (t) => { 6 | const pb = createProjectBuilder() 7 | const result = await pb 8 | .add("bug", (bb) => 9 | bb 10 | .setProps({ 11 | port_arrangement: { 12 | left_size: 2, 13 | right_size: 2, 14 | }, 15 | port_labels: { 16 | "1": "D0", 17 | "2": "D1", 18 | }, 19 | pcb_x: 3, 20 | pcb_y: 0, 21 | }) 22 | .setFootprint( 23 | createFootprintBuilder(pb).add("smtpad", (b) => 24 | b.setProps({ 25 | width: 1, 26 | height: 1, 27 | x: 0, 28 | y: 0, 29 | shape: "rect", 30 | }) 31 | ) 32 | ) 33 | ) 34 | .build() 35 | 36 | await logLayout(`pcb-shifted bug`, result) 37 | t.pass() 38 | }) 39 | -------------------------------------------------------------------------------- /tests/footprints/pcb-component-size.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "../fixtures/get-test-fixture" 3 | 4 | test("pcb component width-height calculation from footprint", async (t) => { 5 | const { logSoup, pb } = await getTestFixture(t) 6 | 7 | const soup = await pb 8 | .add("resistor", (rb) => 9 | rb.setProps({ 10 | footprint: "0805", 11 | resistance: "1k", 12 | name: "R1", 13 | }) 14 | ) 15 | .build() 16 | 17 | await logSoup(soup) 18 | 19 | const pcb_component = soup.find((e) => e.type === "pcb_component")! 20 | 21 | t.is(pcb_component.width, 2.6) 22 | t.is(pcb_component.height, 1.2) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/group-builder/routing-disabled.test.ts: -------------------------------------------------------------------------------- 1 | import { createProjectBuilder } from "../../src/lib/builder/project-builder" 2 | import test from "ava" 3 | 4 | test("group builder with routingDisabled", async (t) => { 5 | const project = createProjectBuilder() 6 | 7 | project.setProps({ routingDisabled: true }) 8 | const g = project 9 | 10 | g.addResistor((r) => { 11 | r.setProps({ resistance: "1kohm", name: "R1" }) 12 | }) 13 | 14 | g.addCapacitor((c) => { 15 | c.setProps({ capacitance: "10uF", name: "C1" }) 16 | }) 17 | 18 | g.addTrace((t) => { 19 | t.addConnections([".R1 > .left", ".C1 > .left"]) 20 | }) 21 | 22 | const elements = await project.build() 23 | 24 | // Check that schematic traces are present 25 | const schematicTraces = elements.filter((el) => el.type === "schematic_trace") 26 | t.true(schematicTraces.length > 0) 27 | 28 | // Check that source traces are present 29 | const sourceTraces = elements.filter((el) => el.type === "source_trace") 30 | t.true(sourceTraces.length > 0) 31 | 32 | // Check that PCB traces and vias are not present 33 | const pcbTraces = elements.filter((el) => el.type === "pcb_trace") 34 | t.is(pcbTraces.length, 0) 35 | 36 | const pcbVias = elements.filter((el) => el.type === "pcb_via") 37 | t.is(pcbVias.length, 0) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/match-pcb-ports-with-footprint.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { matchPCBPortsWithFootprintAndMutate } from "lib/builder/trace-builder/match-pcb-ports-with-footprint" 3 | 4 | test("match pcb ports with footprint", async (t) => { 5 | const scenario: Parameters[0] = { 6 | footprint_elements: [ 7 | { 8 | type: "pcb_smtpad" as const, 9 | pcb_smtpad_id: "pcb_smtpad_0", 10 | shape: "rect" as const, 11 | x: -0.5, 12 | y: 0, 13 | width: 0.6, 14 | height: 0.6, 15 | layer: "top", 16 | pcb_component_id: "pcb_component_simple_resistor_0", 17 | port_hints: ["left", "1"], 18 | }, 19 | { 20 | type: "pcb_smtpad" as const, 21 | pcb_smtpad_id: "pcb_smtpad_1", 22 | shape: "rect" as const, 23 | x: 0.5, 24 | y: 0, 25 | width: 0.6, 26 | height: 0.6, 27 | layer: "top", 28 | pcb_component_id: "pcb_component_simple_resistor_0", 29 | port_hints: ["right", "2"], 30 | }, 31 | ], 32 | pcb_ports: [ 33 | { 34 | type: "pcb_port" as const, 35 | pcb_port_id: "pcb_port_0", 36 | source_port_id: "source_port_0", 37 | pcb_component_id: "simple_resistor_0", 38 | }, 39 | { 40 | type: "pcb_port" as const, 41 | pcb_port_id: "pcb_port_1", 42 | source_port_id: "source_port_1", 43 | pcb_component_id: "simple_resistor_0", 44 | }, 45 | ], 46 | source_ports: [ 47 | { 48 | type: "source_port" as const, 49 | name: "left", 50 | source_port_id: "source_port_0", 51 | source_component_id: "simple_resistor_0", 52 | }, 53 | { 54 | type: "source_port" as const, 55 | name: "right", 56 | source_port_id: "source_port_1", 57 | source_component_id: "simple_resistor_0", 58 | }, 59 | ], 60 | } 61 | matchPCBPortsWithFootprintAndMutate(scenario) 62 | t.is((scenario.pcb_ports[0] as any).x, -0.5) 63 | t.is((scenario.pcb_ports[1] as any).x, 0.5) 64 | }) 65 | -------------------------------------------------------------------------------- /tests/net-builder/__snapshots__/net-builder-1-pcb.snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscircuit/builder/0e9ac742c2c714fdb7daf6b57cca2a49f325b575/tests/net-builder/__snapshots__/net-builder-1-pcb.snapshot.png -------------------------------------------------------------------------------- /tests/net-builder/net-builder-1.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { su } from "@tscircuit/soup-util" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("net builder 1", async (t) => { 6 | const { pb, logSoup, writePcbSnapshotPng } = await getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("resistor", (rb) => rb.setProps({ resistance: 100, name: "R1" })) 10 | .add("net", (nb) => nb.setProps({ name: "N1", trace_width: 0.2 })) 11 | .add("trace", (tb) => tb.setProps({ from: ".R1 > .right", to: "net.N1" })) 12 | .build() 13 | 14 | const [source_net] = su(soup).source_net.list() 15 | t.is(source_net.name, "N1") 16 | 17 | const errors = soup.filter((e) => e.type.includes("_error")) 18 | 19 | if (errors.length > 0) { 20 | console.log(errors) 21 | } 22 | t.is(errors.length, 0) 23 | 24 | const [net_label] = su(soup).schematic_net_label.list() 25 | 26 | t.truthy(net_label) 27 | 28 | const [trace] = su(soup).schematic_trace.list() 29 | 30 | t.truthy(trace) 31 | 32 | await writePcbSnapshotPng(soup) 33 | await logSoup(soup) 34 | }) 35 | -------------------------------------------------------------------------------- /tests/net-builder/net-builder-2.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { su } from "@tscircuit/soup-util" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("net builder 2 (infer nets from trace paths)", async (t) => { 6 | const { pb, logSoup } = await getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("resistor", (rb) => rb.setProps({ resistance: 100, name: "R1" })) 10 | // Instead of adding the net, we'll let it be inferred from the trace path 11 | // .add("net", (nb) => nb.setProps({ name: "N1" })) 12 | .add("trace", (tb) => tb.setProps({ from: ".R1 > .right", to: "net.N1" })) 13 | .build() 14 | 15 | const [source_net] = su(soup).source_net.list() 16 | if (!source_net) { 17 | t.fail("source net wasn't created") 18 | } 19 | t.is(source_net.name, "N1") 20 | 21 | const errors = soup.filter((e) => e.type.includes("_error")) 22 | 23 | if (errors.length > 0) { 24 | console.log(errors) 25 | } 26 | t.is(errors.length, 0) 27 | 28 | const [net_label] = su(soup).schematic_net_label.list() 29 | 30 | t.truthy(net_label) 31 | 32 | const [trace] = su(soup).schematic_trace.list() 33 | 34 | t.truthy(trace) 35 | 36 | await logSoup(soup) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/net-builder/net-builder-3.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { su } from "@tscircuit/soup-util" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("net builder 3 (create multiple net labels for same net)", async (t) => { 6 | const { pb, logSoup } = await getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("resistor", (rb) => 10 | rb.setProps({ resistance: 100, name: "R1" }).setSchematicCenter(-2, 0) 11 | ) 12 | .add("resistor", (rb) => 13 | rb.setProps({ resistance: 100, name: "R2" }).setSchematicCenter(2, 0) 14 | ) 15 | .add("resistor", (rb) => 16 | rb 17 | .setProps({ resistance: 100, name: "R3", rotation: "90deg" }) 18 | .setSchematicCenter(0, 2) 19 | ) 20 | .add("resistor", (rb) => 21 | rb 22 | .setProps({ resistance: 100, name: "R4", rotation: "-90deg" }) 23 | .setSchematicCenter(0, -2) 24 | ) 25 | .add("trace", (tb) => 26 | tb.setProps({ from: ".R1 > .right", to: "net.LONGLONG" }) 27 | ) 28 | .add("trace", (tb) => 29 | tb.setProps({ from: ".R2 > .left", to: "net.MEDIUM" }) 30 | ) 31 | .add("trace", (tb) => tb.setProps({ from: ".R3 > .left", to: "net.N1" })) 32 | .add("trace", (tb) => tb.setProps({ from: ".R4 > .left", to: "net.SML" })) 33 | .build() 34 | await logSoup(soup) 35 | 36 | t.is(su(soup).source_net.list().length, 4) 37 | 38 | const errors = soup.filter((e) => e.type.includes("_error")) 39 | 40 | if (errors.length > 0) { 41 | console.log(errors) 42 | } 43 | t.is(errors.length, 0) 44 | 45 | const [net_label] = su(soup).schematic_net_label.list() 46 | 47 | t.truthy(net_label) 48 | 49 | const [trace] = su(soup).schematic_trace.list() 50 | 51 | t.truthy(trace) 52 | }) 53 | -------------------------------------------------------------------------------- /tests/net-builder/net-builder-4.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 3 | 4 | test("No traces overlapping net labels", async (t) => { 5 | const { pb, logSoup } = await getTestFixture(t) 6 | 7 | const soup = await pb 8 | .add("resistor", (rb) => 9 | rb.setProps({ resistance: 100, name: "R1" }).setSchematicCenter(-1, 0) 10 | ) 11 | .add("resistor", (rb) => 12 | rb 13 | .setProps({ resistance: 100, name: "R2", rotation: "90deg" }) 14 | .setSchematicCenter(0, 2) 15 | ) 16 | .add("resistor", (rb) => 17 | rb 18 | .setProps({ resistance: 100, name: "R3", rotation: "-90deg" }) 19 | .setSchematicCenter(0, -2) 20 | ) 21 | .add("trace", (tb) => 22 | tb.setProps({ from: ".R1 > .right", to: "net.LONGLONG" }) 23 | ) 24 | .add("trace", (tb) => 25 | tb.setProps({ from: ".R2 > .left", to: ".R3 > .left" }) 26 | ) 27 | .build() 28 | 29 | await logSoup(soup) 30 | t.pass() 31 | }) 32 | -------------------------------------------------------------------------------- /tests/net-builder/net-builder-5.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { su } from "@tscircuit/soup-util" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | /** 6 | * This test has R1, R2 connected by a net. The test makes sure that a pcb trace 7 | * is created between R1 and R2, and not multiple 8 | */ 9 | test("net builder 5", async (t) => { 10 | const { pb, logSoup } = await getTestFixture(t) 11 | 12 | const soup = await pb 13 | .add("resistor", (rb) => 14 | rb.setProps({ 15 | resistance: 100, 16 | name: "R1", 17 | footprint: "0402", 18 | pcbX: -5, 19 | pcbY: 0, 20 | }) 21 | ) 22 | .add("resistor", (rb) => 23 | rb.setProps({ 24 | resistance: 100, 25 | name: "R2", 26 | footprint: "0402", 27 | pcbX: 5, 28 | pcbY: 0, 29 | }) 30 | ) 31 | .add("component", (cb) => cb.setName("Obstacle").setFootprint("dip64_p1")) 32 | .add("net", (nb) => nb.setProps({ name: "N1" })) 33 | .add("trace", (tb) => tb.setProps({ from: ".R1 > .right", to: "net.N1" })) 34 | .add("trace", (tb) => tb.setProps({ from: ".R2 > .left", to: "net.N1" })) 35 | .build() 36 | 37 | await logSoup(soup) 38 | 39 | const pcb_traces = su(soup).pcb_trace.list() 40 | 41 | // NOTE: there is a bug where there are two pcb traces created (starting at 42 | // each port that's in the net) 43 | // t.is(pcb_traces.length, 1, "should only have one pcb trace") 44 | t.truthy(pcb_traces.length > 0) 45 | 46 | const pcb_errors = su(soup).pcb_error.list() 47 | // NOTE: since there is a bug where there are two pcb traces created, there 48 | // are also two errors 49 | // t.is(pcb_errors.length, 2, "should have two pcb errors") 50 | t.truthy(pcb_errors.length > 0) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/pcb-manual-layout/group-builder-pcb-manual-layout-1.test.ts: -------------------------------------------------------------------------------- 1 | import { layout } from "@tscircuit/layout" 2 | import type { PCBComponent } from "@tscircuit/soup" 3 | import test from "ava" 4 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 5 | 6 | test("pcb manual layout in group builder", async (t) => { 7 | const { logSoup, pb } = getTestFixture(t) 8 | 9 | const soup = await pb 10 | .setProps({ 11 | layout: layout().manualPcbPlacement([ 12 | { 13 | selector: ".R1", 14 | center: { x: -1, y: 1 }, 15 | }, 16 | ]), 17 | }) 18 | .add("resistor", (rb) => 19 | rb.setProps({ 20 | resistance: "10k", 21 | name: "R1", 22 | footprint: "0805", 23 | }) 24 | ) 25 | .build() 26 | 27 | const pcb_component = soup.find( 28 | (e) => e.type === "pcb_component" 29 | )! as PCBComponent 30 | 31 | t.deepEqual(pcb_component.center, { x: -1, y: 1 }) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/pcb-manual-layout/group-builder-pcb-manual-layout-2.test.ts: -------------------------------------------------------------------------------- 1 | import { layout } from "@tscircuit/layout" 2 | import type { PCBComponent } from "@tscircuit/soup" 3 | import { su } from "@tscircuit/soup-util" 4 | import test from "ava" 5 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 6 | 7 | test("pcb manual layout in group builder", async (t) => { 8 | const { logSoup, pb } = getTestFixture(t) 9 | 10 | const soup = await pb 11 | .setProps({ 12 | layout: layout().manualPcbPlacement([ 13 | { 14 | selector: ".R1", 15 | center: { 16 | x: -10, 17 | y: 10, 18 | }, 19 | }, 20 | ]), 21 | }) 22 | .add("resistor", (rb) => 23 | rb.setProps({ 24 | resistance: "10k", 25 | name: "R1", 26 | footprint: "0805", 27 | }) 28 | ) 29 | .build() 30 | 31 | const pcb_component = soup.find( 32 | (e) => e.type === "pcb_component" 33 | )! as PCBComponent 34 | 35 | t.deepEqual(pcb_component.center, { x: -10, y: 10 }) 36 | 37 | // Check that the port also moved 38 | const pcb_ports = su(soup).pcb_port.list({ 39 | pcb_component_id: pcb_component.pcb_component_id, 40 | }) 41 | 42 | t.is(pcb_ports?.length, 2) 43 | 44 | t.truthy(pcb_ports?.every((p) => Math.abs(p.x - -10) < 2)) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/pcb-manual-layout/group-builder-pcb-manual-trace-hint.test.ts: -------------------------------------------------------------------------------- 1 | import { layout } from "@tscircuit/layout" 2 | import { su } from "@tscircuit/soup-util" 3 | import test from "ava" 4 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 5 | 6 | test("pcb trace hint in group builder", async (t) => { 7 | const { logSoup, pb } = getTestFixture(t) 8 | 9 | const soup = await pb 10 | .setProps({ 11 | layout: layout().manualEdits({ 12 | manual_trace_hints: [ 13 | { 14 | pcb_port_selector: ".R1 > .right", 15 | offsets: [ 16 | { 17 | x: 0, 18 | y: 10, 19 | }, 20 | ], 21 | }, 22 | ], 23 | }), 24 | }) 25 | .add("resistor", (rb) => 26 | rb.setProps({ 27 | resistance: "10k", 28 | name: "R1", 29 | footprint: "0805", 30 | }) 31 | ) 32 | .add("resistor", (rb) => 33 | rb 34 | .setProps({ 35 | resistance: "10k", 36 | name: "R2", 37 | footprint: "0805", 38 | }) 39 | .setFootprintCenter(3, 0) 40 | ) 41 | .add("trace", (tb) => 42 | tb.setProps({ 43 | from: ".R1 > .right", 44 | to: ".R2 > .left", 45 | }) 46 | ) 47 | .build() 48 | 49 | const ths = su(soup).pcb_trace_hint.list() 50 | 51 | const trace = su(soup).pcb_trace.list()[0] 52 | 53 | t.is(ths.length, 1) 54 | t.truthy(trace.route.some((r) => r.y === 10)) 55 | }) 56 | -------------------------------------------------------------------------------- /tests/primitives/schematic-path.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createProjectBuilder } from "lib/builder" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("render a simple schematic path", async (t) => { 6 | const soup = await createProjectBuilder() 7 | .add("component", (cb) => 8 | cb.modifySchematic((sb) => 9 | sb.add("schematic_path", (sp) => 10 | sp.setProps({ 11 | is_filled: true, 12 | points: [ 13 | { x: 0, y: 0 }, 14 | { x: "1mm", y: "1mm" }, 15 | { x: 0, y: "1mm" }, 16 | ], 17 | }) 18 | ) 19 | ) 20 | ) 21 | .build() 22 | 23 | await logLayout("custom schematic symbol with path", soup) 24 | t.pass() 25 | }) 26 | -------------------------------------------------------------------------------- /tests/routing-bugs/dont-route-through-plateholes.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { FootprintBuilder } from "index" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | const axial = (fb: FootprintBuilder) => 6 | fb 7 | .add("platedhole", (pb) => 8 | pb.setProps({ 9 | x: "-0.05in", 10 | y: 0, 11 | hole_diameter: 0.8, 12 | outer_diameter: 1.2, 13 | port_hints: ["left"], 14 | }) 15 | ) 16 | .add("platedhole", (pb) => 17 | pb.setProps({ 18 | x: "0.05in", 19 | y: 0, 20 | hole_diameter: 0.8, 21 | outer_diameter: 1.2, 22 | port_hints: ["right"], 23 | }) 24 | ) 25 | 26 | test("don't route through plated holes", async (t) => { 27 | const { pb, logSoup } = await getTestFixture(t) 28 | 29 | // three resistors, each with a plated holes as footprint 30 | const soup = await pb 31 | .add("resistor", (rb) => 32 | rb 33 | .setProps({ 34 | name: "R1", 35 | resistance: "10k", 36 | center: [0, 0], 37 | }) 38 | .modifyFootprint(axial) 39 | ) 40 | .add("resistor", (rb) => 41 | rb 42 | .setProps({ 43 | name: "R2", 44 | resistance: "10k", 45 | center: [0, 0], 46 | pcb_x: 10, 47 | pcb_y: 0, 48 | }) 49 | .modifyFootprint(axial) 50 | ) 51 | .add("resistor", (rb) => 52 | rb 53 | .setProps({ 54 | name: "R3", 55 | resistance: "10k", 56 | center: [0, 0], 57 | pcb_x: 5, 58 | pcb_y: "0.05in", 59 | pcb_rotation: "90deg", 60 | }) 61 | .modifyFootprint(axial) 62 | ) 63 | .add("trace", (tb) => 64 | tb.setProps({ 65 | from: ".R1 > .right", 66 | to: ".R2 > .left", 67 | }) 68 | ) 69 | .build() 70 | 71 | await logSoup(soup) 72 | t.pass() 73 | }) 74 | -------------------------------------------------------------------------------- /tests/routing-bugs/pcb-port-without-xy.test.ts: -------------------------------------------------------------------------------- 1 | // REPRODUCTION IN REACT 2 | // export const MyExample = () => { 3 | // return ( 4 | // 5 | // 9 | // 17 | // 25 | // 26 | // } 27 | // > 28 | // 29 | // 30 | // 31 | // 38 | // 39 | // 40 | // ) 41 | // } 42 | 43 | import test from "ava" 44 | import { FootprintBuilder } from "index" 45 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 46 | 47 | const axial = (fb: FootprintBuilder) => 48 | fb 49 | .add("platedhole", (pb) => 50 | pb.setProps({ 51 | x: "-0.5in", 52 | y: 0, 53 | hole_diameter: 0.8, 54 | outer_diameter: 1.2, 55 | port_hints: ["1"], 56 | }) 57 | ) 58 | .add("platedhole", (pb) => 59 | pb.setProps({ 60 | x: "0.5in", 61 | y: 0, 62 | hole_diameter: 0.8, 63 | outer_diameter: 1.2, 64 | port_hints: ["2"], 65 | }) 66 | ) 67 | 68 | test("should not have a pcb trace error or port without x/y", async (t) => { 69 | const { pb, logSoup } = await getTestFixture(t) 70 | 71 | const soup = await pb 72 | .add("resistor", (rb) => 73 | rb 74 | .setProps({ 75 | name: "R1", 76 | resistance: "10k", 77 | center: [0, 0], 78 | footprint: "0805", 79 | pcb_x: 4, 80 | pcb_y: 0, 81 | }) 82 | .modifyFootprint(axial) 83 | ) 84 | .add("component", (cb) => 85 | cb 86 | .setProps({ name: "B1" }) 87 | .modifyFootprint(axial) 88 | .modifyPorts((pb) => 89 | pb 90 | .add("port", (pb) => 91 | pb.setProps({ 92 | x: 0, 93 | y: -0.7, 94 | name: "plus", 95 | pin_number: 1, 96 | direction: "up", 97 | }) 98 | ) 99 | .add("port", (pb) => 100 | pb.setProps({ 101 | x: 0, 102 | y: 0.7, 103 | name: "minus", 104 | pin_number: 2, 105 | direction: "down", 106 | }) 107 | ) 108 | ) 109 | ) 110 | .add("trace", (tb) => 111 | tb.setProps({ 112 | from: ".R1 > .left", 113 | to: ".B1 > .minus", 114 | }) 115 | ) 116 | .build() 117 | const errors = soup.filter((e) => e.type.includes("error")) 118 | 119 | await logSoup(soup) 120 | t.is(errors.length, 0) 121 | }) 122 | -------------------------------------------------------------------------------- /tests/routing-bugs/second-degree-port-aliasing.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 3 | 4 | test("second degree port aliasing", async (t) => { 5 | const { logSoup, pb } = await getTestFixture(t) 6 | 7 | // A resistor connected to a diode via a trace 8 | const soup = await pb 9 | .add("resistor", (rb) => 10 | rb.setProps({ 11 | name: "R1", 12 | footprint: "0805", 13 | resistance: "1k", 14 | center: [0, 0], 15 | pcb_x: 0, 16 | pcb_y: 0, 17 | }) 18 | ) 19 | .add("diode", (rb) => 20 | rb.setProps({ 21 | name: "D1", 22 | footprint: "0805", 23 | center: [3, 0], 24 | pcb_x: 5, 25 | pcb_y: 0, 26 | }) 27 | ) 28 | .add("trace", (rb) => 29 | rb.setProps({ 30 | from: ".R1 > .right", 31 | to: ".D1 > .left", 32 | }) 33 | ) 34 | .build() 35 | 36 | await logSoup(soup) 37 | t.pass() 38 | }) 39 | -------------------------------------------------------------------------------- /tests/routing/pcb-routing-hints.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createProjectBuilder } from "../../src" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("trace with pcb routing hints", async (t) => { 6 | const result = await createProjectBuilder() 7 | .add("resistor", (rb) => 8 | rb 9 | .setSourceProperties({ 10 | resistance: "1kohm", 11 | name: "R1", 12 | }) 13 | .setFootprint("0805") 14 | .setFootprintCenter(0, 0) 15 | .setSchematicCenter(0, 0) 16 | ) 17 | .add("resistor", (rb) => 18 | rb 19 | .setSourceProperties({ 20 | resistance: "1kohm", 21 | name: "R2", 22 | }) 23 | .setFootprint("0805") 24 | .setSchematicCenter(3, 0) 25 | .setFootprintCenter(10, 0) 26 | ) 27 | .add("resistor", (rb) => 28 | rb 29 | .setSourceProperties({ 30 | resistance: "1kohm", 31 | name: "R3", 32 | }) 33 | .setFootprint("0805") 34 | .setSchematicCenter(6, 0) 35 | .setFootprintCenter(4, 0) 36 | .setFootprintRotation("90deg") 37 | ) 38 | .add("trace", (tb) => 39 | tb.addConnections([".R1 > port.right", ".R2 > port.left"]).setProps({ 40 | pcb_route_hints: [ 41 | { 42 | x: "3mm", 43 | y: -4, 44 | }, 45 | { 46 | x: 6, 47 | y: 4, 48 | }, 49 | ], 50 | }) 51 | ) 52 | // .add("trace", (tb) => 53 | // tb.addConnections([".R1 > port.left", ".R3 > port.right"]).setProps({ 54 | // pcb_route_hints: [ 55 | // { 56 | // x: "2mm", 57 | // y: 4, 58 | // }, 59 | // ], 60 | // }) 61 | // ) 62 | // .add("trace", (tb) => 63 | // tb.addConnections([".R2 > port.right", ".R1 > port.left"]).setProps({ 64 | // pcb_route_hints: [ 65 | // { 66 | // x: "3mm", 67 | // y: -6, 68 | // }, 69 | // ], 70 | // }) 71 | // ) 72 | .build() 73 | 74 | await logLayout("trace with pcb routing hints", result) 75 | t.pass() 76 | }) 77 | -------------------------------------------------------------------------------- /tests/routing/schematic-routing-hints.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createProjectBuilder } from "../../src" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("trace with schematic routing hints", async (t) => { 6 | // Add two resistors, add a trace between them, give a hint that the trace must 7 | // follow 8 | 9 | const result = await createProjectBuilder() 10 | .add("resistor", (rb) => 11 | rb 12 | .setSourceProperties({ 13 | resistance: "1kohm", 14 | name: "R1", 15 | }) 16 | .setSchematicCenter(0, 0) 17 | ) 18 | .add("resistor", (rb) => 19 | rb 20 | .setSourceProperties({ 21 | resistance: "1kohm", 22 | name: "R2", 23 | }) 24 | .setSchematicCenter(3, 0) 25 | ) 26 | .add("trace", (tb) => 27 | tb.addConnections([".R1 > port.right", ".R2 > port.left"]).setProps({ 28 | schematic_route_hints: [ 29 | { 30 | x: 1, 31 | y: -2, 32 | }, 33 | { 34 | x: 2, 35 | y: 2, 36 | }, 37 | ], 38 | }) 39 | ) 40 | .build() 41 | 42 | await logLayout("trace with schematic routing hints", result) 43 | t.pass() 44 | }) 45 | -------------------------------------------------------------------------------- /tests/schematic-autolayout/automatic-schematic-layout.test.ts: -------------------------------------------------------------------------------- 1 | import { layout } from "@tscircuit/layout" 2 | import type { SchematicComponent } from "@tscircuit/soup" 3 | import test from "ava" 4 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 5 | 6 | test("automatic schematic layout 1", async (t) => { 7 | const { logSoup, pb } = getTestFixture(t) 8 | 9 | const soup = await pb 10 | .setProps({ 11 | layout: layout().autoLayoutSchematic(), 12 | }) 13 | .add("bug", (cb) => 14 | cb 15 | .setProps({ 16 | name: "U1", 17 | port_arrangement: { 18 | left_size: 4, 19 | right_size: 4, 20 | }, 21 | }) 22 | .labelPort(1, "GND") 23 | .labelPort(2, "TRG") 24 | .labelPort(3, "OUT") 25 | .labelPort(4, "RES") 26 | .labelPort(5, "CRL") 27 | .labelPort(6, "TRE") 28 | .labelPort(7, "DIS") 29 | .labelPort(8, "VCC") 30 | ) 31 | .add("diode", (lb) => lb.setProps({ name: "LED" })) 32 | .add("resistor", (rb) => rb.setProps({ resistance: "1k ohm", name: "R1" })) 33 | .add("resistor", (rb) => rb.setProps({ resistance: "10k ohm", name: "R2" })) 34 | .add("resistor", (rb) => rb.setProps({ resistance: "220 ohm", name: "R3" })) 35 | .add("capacitor", (cb) => cb.setProps({ capacitance: "10uF", name: "C1" })) 36 | 37 | // run addTrace([".U1 > .GND", ".R2 >...."]) to configure the 555timer in an astable configuration 38 | .add("trace", (tb) => tb.addConnections([".R1 > .left", ".R2 > .left"])) 39 | .add("trace", (tb) => tb.addConnections([".R2 > .left", ".C1 > .right"])) 40 | .add("trace", (tb) => tb.addConnections([".R2 > .left", ".U1 > port.TRE"])) 41 | .add("trace", (tb) => tb.addConnections([".C1 > .left", ".U1 > port.GND"])) 42 | .add("trace", (tb) => 43 | tb.addConnections([".U1 > port.DIS", ".U1 > port.TRG"]) 44 | ) 45 | .add("trace", (tb) => 46 | tb.addConnections([".U1 > port.OUT", ".LED > .anode"]) 47 | ) 48 | .add("trace", (tb) => tb.addConnections([".LED > .cathode", ".R3 > .left"])) 49 | // .add("trace", (tb) => tb.addConnections([".U1 > port.VCC+", "5-12V line"])) 50 | .build() 51 | 52 | await logSoup(soup) 53 | const r1_source_component = soup.find( 54 | (c): c is SchematicComponent => 55 | c.type === "source_component" && c.name === "R2" 56 | )! 57 | 58 | t.not( 59 | soup.find( 60 | (c): c is SchematicComponent => 61 | c.type === "schematic_component" && 62 | c.source_component_id === r1_source_component.source_component_id 63 | )!.center.x, 64 | 0 65 | ) 66 | }) 67 | -------------------------------------------------------------------------------- /tests/smoke-tests/diode.test.ts: -------------------------------------------------------------------------------- 1 | import { createProjectBuilder } from "lib/builder" 2 | import test from "ava" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("[smoke] diode", async (t) => { 6 | const projectBuilder = await createProjectBuilder().add("diode", (db) => 7 | db 8 | .setSourceProperties({ 9 | name: "D1", 10 | }) 11 | .setSchematicCenter(2, 1) 12 | ) 13 | 14 | const projectBuilderOutput = await projectBuilder.build() 15 | 16 | const srcComp: any = projectBuilderOutput.find( 17 | (e) => e.type === "source_component" 18 | ) 19 | 20 | t.is(srcComp.name, "D1") 21 | t.is(srcComp.ftype, "simple_diode") 22 | 23 | await logLayout("diode", projectBuilderOutput) 24 | t.pass() 25 | }) 26 | -------------------------------------------------------------------------------- /tests/smoke-tests/fabrication.test.ts: -------------------------------------------------------------------------------- 1 | import { su } from "@tscircuit/soup-util" 2 | import test from "ava" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("[smoke] fabrication", async (t) => { 6 | const { logSoup, pb } = await getTestFixture(t) 7 | const soup = await pb 8 | .add("component", (cb) => 9 | cb.modifyFootprint((fb) => 10 | fb 11 | .add("fabricationnotetext", (stb) => 12 | stb.setProps({ 13 | pcbX: 0, 14 | pcbY: 0, 15 | text: "Hello World", 16 | }) 17 | ) 18 | .add("fabricationnotepath", (stb) => { 19 | stb.setProps({ 20 | route: [ 21 | { 22 | x: 0, 23 | y: 0, 24 | }, 25 | { 26 | x: 2, 27 | y: 2, 28 | }, 29 | ], 30 | }) 31 | }) 32 | ) 33 | ) 34 | .build() 35 | 36 | const [fbt] = su(soup).pcb_fabrication_note_text.list() 37 | t.is(fbt.text, "Hello World") 38 | }) 39 | -------------------------------------------------------------------------------- /tests/smoke-tests/net-alias.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import type { SchematicComponent } from "@tscircuit/soup" 3 | import { createProjectBuilder } from "lib/builder" 4 | import { logLayout } from "../utils/log-layout" 5 | 6 | test("[smoke] netalias", async (t) => { 7 | const projectBuilder = await createProjectBuilder().add("net_alias", (nab) => 8 | nab 9 | .setSourceProperties({ 10 | net: "gnd", 11 | }) 12 | .setSchematicCenter(2, 1) 13 | ) 14 | 15 | const soup = await projectBuilder.build() 16 | 17 | const net_alias = soup.filter( 18 | (s) => s.type === "schematic_component" 19 | )?.[0] as SchematicComponent 20 | 21 | t.not(net_alias.size.width, 0) 22 | t.not(net_alias.size.height, 0) 23 | 24 | await logLayout("netalias", soup) 25 | t.pass() 26 | }) 27 | -------------------------------------------------------------------------------- /tests/smoke-tests/resistor.test.ts: -------------------------------------------------------------------------------- 1 | import { createProjectBuilder } from "lib/builder" 2 | import test from "ava" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("[smoke] resistor", async (t) => { 6 | const projectBuilder = await createProjectBuilder().add("resistor", (rb) => 7 | rb 8 | .setSourceProperties({ 9 | resistance: "10 ohm", 10 | name: "R1", 11 | }) 12 | .setSchematicCenter("2mm", 1) 13 | .setFootprint("0402") 14 | ) 15 | 16 | const projectBuilderOutput = await projectBuilder.build() 17 | 18 | await logLayout("resistor", projectBuilderOutput) 19 | t.pass() 20 | }) 21 | -------------------------------------------------------------------------------- /tests/smoke-tests/silkscreen.test.ts: -------------------------------------------------------------------------------- 1 | import { su } from "@tscircuit/soup-util" 2 | import test from "ava" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("[smoke] silkscreen", async (t) => { 6 | const { logSoup, pb } = await getTestFixture(t) 7 | const soup = await pb 8 | .add("component", (cb) => 9 | cb.modifyFootprint((fb) => 10 | fb.add("silkscreentext", (stb) => 11 | stb.setProps({ 12 | pcbX: 0, 13 | pcbY: 0, 14 | text: "Hello World", 15 | }) 16 | ) 17 | ) 18 | ) 19 | .build() 20 | 21 | const [silkscreen_text] = su(soup).pcb_silkscreen_text.list() 22 | t.is(silkscreen_text.text, "Hello World") 23 | }) 24 | -------------------------------------------------------------------------------- /tests/stories/footprint-with-traces.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createProjectBuilder } from "lib/builder/project-builder" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("footprint-with-traces", async (t) => { 6 | const projectBuilder = await createProjectBuilder() 7 | .add("resistor", (rb) => 8 | rb 9 | .setSourceProperties({ 10 | resistance: "10 ohm", 11 | name: "R1", 12 | }) 13 | .setSchematicCenter(2, 1) 14 | .setFootprint("0805") 15 | ) 16 | .add("resistor", (rb) => 17 | rb 18 | .setSourceProperties({ 19 | resistance: "10 ohm", 20 | name: "R2", 21 | }) 22 | .setSchematicCenter(5, 1) 23 | .setFootprintCenter(3, 1) 24 | .setFootprintRotation("90deg") 25 | .setFootprint("0603") 26 | ) 27 | .add("trace", (tb) => tb.addConnections([".R1 > .right", ".R2 > .left"])) 28 | 29 | const projectBuilderOutput = await projectBuilder.build() 30 | 31 | await logLayout("footprint-with-traces", projectBuilderOutput) 32 | // t.snapshot(projectBuilderOutput) 33 | t.pass() 34 | }) 35 | -------------------------------------------------------------------------------- /tests/stories/generic-component-builder.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { 3 | createBoxBuilder, 4 | createPlatedHoleBuilder, 5 | createPortBuilder, 6 | createProjectBuilder, 7 | createSchematicLineBuilder, 8 | createSchematicSymbolBuilder, 9 | PortBuilder, 10 | } from "lib/builder" 11 | import { logLayout } from "../utils/log-layout" 12 | 13 | test("component build from scratch without .add function (only appendChild)", async (t) => { 14 | const pb = await createProjectBuilder() 15 | 16 | const pins = ["rx", "tx", "d2"] 17 | 18 | pb.add("component", (cb) => { 19 | cb.setName("PIN3_HEADER") 20 | const port_map: Record = {} 21 | for (let i = 0; i < pins.length; i++) { 22 | const pin = pins[i] 23 | const phb = createPlatedHoleBuilder(pb).setProps({ 24 | // net: pin, 25 | x: `${i * 2.5}mm`, 26 | y: 0, 27 | outer_diameter: "2mm", 28 | hole_diameter: "1mm", 29 | }) 30 | cb.appendChild(phb) 31 | 32 | const portb = createPortBuilder(pb) 33 | port_map[pin] = portb 34 | cb.appendChild(portb) 35 | } 36 | // Symbol 37 | const ssb = createSchematicSymbolBuilder(pb) 38 | 39 | const box = createBoxBuilder(pb).setProps({ 40 | width: "0.1in", 41 | height: "0.3in", 42 | x: 0, 43 | y: 0, 44 | }) 45 | ssb.appendChild(box) 46 | 47 | for (let i = 0; i < pins.length; i++) { 48 | const pin = pins[i] 49 | const y1 = `${(0.3 / 3) * i - 0.1}in` as const 50 | const slb = createSchematicLineBuilder(pb).setProps({ 51 | x1: "0.04in", 52 | y1, 53 | x2: "0.07in", 54 | y2: y1, 55 | }) 56 | ssb.appendChild(slb) 57 | const portb = port_map[pin] 58 | portb.schematic_position = { 59 | x: "0.07in", 60 | y: y1, 61 | } 62 | } 63 | 64 | cb.appendChild(ssb) 65 | }) 66 | 67 | await logLayout("generic-component-builder", await pb.build()) 68 | 69 | t.pass() 70 | // t.snapshot(await pb.build(), "Generic Component Builder Output") 71 | }) 72 | -------------------------------------------------------------------------------- /tests/stories/schematic-text.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createProjectBuilder } from "lib/builder" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("render a schematic with text", async (t) => { 6 | const pb = createProjectBuilder() 7 | 8 | const soup = await pb 9 | .add("component", (cb) => 10 | cb.modifySchematic((scb) => 11 | scb.add("schematic_text", (st) => 12 | st.setProps({ 13 | position: { x: 0, y: 0 }, 14 | text: "Hello, World!", 15 | }) 16 | ) 17 | ) 18 | ) 19 | .build() 20 | 21 | await logLayout("custom schematic symbol with text", soup) 22 | t.pass() 23 | }) 24 | -------------------------------------------------------------------------------- /tests/stories/set-footprint-as-builder.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { createFootprintBuilder, createProjectBuilder } from "lib/builder" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("set footprint as builder", async (t) => { 6 | const pb = await createProjectBuilder() 7 | 8 | pb.add("diode", (db) => 9 | db.setProps({ 10 | footprint: createFootprintBuilder(pb).add("smtpad", (sp) => 11 | sp.setShape("rect").setSize(1, 1).setPosition(0, 0) 12 | ), 13 | }) 14 | ) 15 | 16 | const soup = await pb.build() 17 | 18 | logLayout("set footprint as builder", soup) 19 | t.truthy(soup.some((elm) => elm.type === "pcb_smtpad")) 20 | }) 21 | -------------------------------------------------------------------------------- /tests/stories/snapshots/resistor.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscircuit/builder/0e9ac742c2c714fdb7daf6b57cca2a49f325b575/tests/stories/snapshots/resistor.test.ts.snap -------------------------------------------------------------------------------- /tests/stories/snapshots/sparkfun-footprint.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscircuit/builder/0e9ac742c2c714fdb7daf6b57cca2a49f325b575/tests/stories/snapshots/sparkfun-footprint.test.ts.snap -------------------------------------------------------------------------------- /tests/trace-builder/__snapshots__/multi-layer-route-2-pcb.snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscircuit/builder/0e9ac742c2c714fdb7daf6b57cca2a49f325b575/tests/trace-builder/__snapshots__/multi-layer-route-2-pcb.snapshot.png -------------------------------------------------------------------------------- /tests/trace-builder/__snapshots__/multi-layer-route-2.snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscircuit/builder/0e9ac742c2c714fdb7daf6b57cca2a49f325b575/tests/trace-builder/__snapshots__/multi-layer-route-2.snapshot.png -------------------------------------------------------------------------------- /tests/trace-builder/auto-route-segment-size-benchmark.test.ts: -------------------------------------------------------------------------------- 1 | import { schematicPortArrangement } from "@tscircuit/props" 2 | import test from "ava" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("auto route segment size benchmark", async (t) => { 6 | const { logSoup, pb } = await getTestFixture(t) 7 | 8 | // Two components with interconnections, on the left is a fine-pitched TSSOP 9 | // and on the right is a fine-pitched BGA 10 | 11 | const soup = await pb 12 | .add("board", (board) => 13 | board 14 | .setProps({ 15 | width: 10, 16 | height: 10, 17 | }) 18 | .add("bug", (bb) => 19 | bb.setProps({ 20 | name: "U1", 21 | footprint: "tssop12_p0.65mm_pw0.35mm_pl2.05mm_w9.05mm", 22 | pcbX: -6, 23 | pcbY: 0, 24 | }) 25 | ) 26 | .add("bug", (bb) => 27 | bb.setProps({ 28 | name: "U2", 29 | footprint: "bga16", 30 | pcbX: 6, 31 | pcbY: 0, 32 | }) 33 | ) 34 | .add("trace", (tb) => 35 | tb.setProps({ from: ".U1 > port.10", to: ".U2 > port.B3" }) 36 | ) 37 | ) 38 | .build() 39 | 40 | await logSoup(soup) 41 | t.pass() 42 | }) 43 | -------------------------------------------------------------------------------- /tests/trace-builder/bad-schematic-route-1.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 3 | 4 | test("bad schematic route 1", async (t) => { 5 | const { logSoup, pb } = await getTestFixture(t) 6 | 7 | const soup = await pb 8 | .add("resistor", (rb) => 9 | rb.setProps({ 10 | resistance: 100, 11 | name: "R1", 12 | rotation: "90deg", 13 | schX: -2, 14 | schY: -2, 15 | }) 16 | ) 17 | .add("bug", (bb) => 18 | bb.setProps({ 19 | name: "U1", 20 | port_arrangement: { 21 | left_size: 1, 22 | right_size: 1, 23 | }, 24 | port_labels: { 25 | 1: "A", 26 | 2: "B", 27 | }, 28 | }) 29 | ) 30 | .add("trace", (tb) => tb.setProps({ from: ".R1 > .left", to: ".U1 > .A" })) 31 | .build() 32 | 33 | await logSoup(soup) 34 | t.pass() 35 | }) 36 | -------------------------------------------------------------------------------- /tests/trace-builder/default-trace-builder.test.ts: -------------------------------------------------------------------------------- 1 | import { createProjectBuilder } from "lib/builder" 2 | import test from "ava" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("default-trace-builder", async (t) => { 6 | const projectBuilder = await createProjectBuilder() 7 | .add("diode", (db) => 8 | db 9 | .setSourceProperties({ 10 | name: "D1", 11 | }) 12 | .setSchematicCenter(2, 1) 13 | ) 14 | .add("resistor", (rb) => 15 | rb 16 | .setSourceProperties({ resistance: "1kohm", name: "R1" }) 17 | .setSchematicCenter(0, 0) 18 | ) 19 | .add( 20 | "trace", 21 | (tb) => tb.addConnections([".R1 > .right", ".D1 > .left"]) 22 | // .setRouteSolver("route1") 23 | ) 24 | 25 | const projectBuilderOutput = await projectBuilder.build() 26 | await logLayout("default-trace-builder", projectBuilderOutput) 27 | t.pass() 28 | }) 29 | -------------------------------------------------------------------------------- /tests/trace-builder/find-possible-trace-layer-combinations.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { findPossibleTraceLayerCombinations } from "lib/builder/trace-builder/pcb-routing/find-possible-trace-layer-combinations" 3 | 4 | test("find possible trace layer combinations 1", (t) => { 5 | const candidates = findPossibleTraceLayerCombinations([ 6 | { 7 | layers: ["top", "bottom"], 8 | }, 9 | { 10 | via: true, 11 | }, 12 | { 13 | layers: ["top", "bottom"], 14 | }, 15 | ]) 16 | t.is( 17 | candidates.map((c) => c.layer_path.join(",")).join("\n"), 18 | ` 19 | 20 | top,bottom,bottom 21 | bottom,top,top 22 | 23 | 24 | `.trim() 25 | ) 26 | }) 27 | 28 | test("find possible trace layer combinations 2", (t) => { 29 | const candidates = findPossibleTraceLayerCombinations([ 30 | { 31 | layers: ["top", "bottom", "inner1"], 32 | }, 33 | { 34 | via: true, 35 | }, 36 | { 37 | layers: ["top", "bottom", "inner1"], 38 | }, 39 | ]) 40 | t.is(candidates.map((c) => c.layer_path.join(",")).length, 6) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/trace-builder/intersection-test.test.ts: -------------------------------------------------------------------------------- 1 | import { createProjectBuilder } from "lib/builder" 2 | import test from "ava" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("intersection test", async (t) => { 6 | const projectBuilder = await createProjectBuilder() 7 | .add("diode", (db) => 8 | db 9 | .setSourceProperties({ 10 | name: "D1", 11 | }) 12 | .setSchematicRotation("180deg") 13 | .setSchematicCenter(-2, 2) 14 | ) 15 | .add("resistor", (rb) => 16 | rb 17 | .setSourceProperties({ resistance: "1kohm", name: "R1" }) 18 | .setSchematicCenter(0, 0) 19 | ) 20 | .add("trace", (tb) => tb.addConnections([".R1 > .right", ".D1 > .left"])) 21 | .add( 22 | "resistor", 23 | (rb) => rb.setSchematicCenter(-0.5, 1) //.setSchematicRotation("-90deg") 24 | ) 25 | .add( 26 | "resistor", 27 | (rb) => rb.setSchematicCenter(1, 1) //.setSchematicRotation("-90deg") 28 | ) 29 | 30 | const projectBuilderOutput = await projectBuilder.build() 31 | 32 | await logLayout("intersection test", projectBuilderOutput) 33 | t.pass() 34 | }) 35 | -------------------------------------------------------------------------------- /tests/trace-builder/multi-layer-route-1.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 3 | 4 | test("multi-layer route 1", async (t) => { 5 | const { pb, logSoup } = await getTestFixture(t) 6 | 7 | const soup = await pb 8 | .add("resistor", (rb) => 9 | rb.setProps({ 10 | x: -2, 11 | y: 0, 12 | pcb_x: -2, 13 | pcb_y: 0, 14 | resistance: "1k", 15 | name: "R1", 16 | footprint: "0402", 17 | pcb_layer: "top", 18 | }) 19 | ) 20 | .add("resistor", (rb) => 21 | rb.setProps({ 22 | x: 2, 23 | y: 0, 24 | pcb_x: 2, 25 | pcb_y: 0, 26 | resistance: "10k", 27 | name: "R2", 28 | footprint: "0402", 29 | pcb_layer: "bottom", 30 | }) 31 | ) 32 | .add("trace", (tb) => 33 | tb.setProps({ 34 | path: [".R1 > .right", ".R2 > .left"], 35 | pcb_route_hints: [ 36 | { 37 | x: 0, 38 | y: 0, 39 | via: true, 40 | }, 41 | ], 42 | }) 43 | ) 44 | .build() 45 | 46 | await logSoup(soup) 47 | t.is( 48 | soup.filter((s) => s.type.includes("error")).length, 49 | 0, 50 | "should be no errors" 51 | ) 52 | t.truthy(soup.find((elm) => elm.type === "pcb_via")) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/trace-builder/multi-layer-route-2.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 3 | 4 | test("multi-layer route 2", async (t) => { 5 | const { pb, logSoup, writePcbSnapshotPng } = await getTestFixture(t) 6 | 7 | const soup = await pb 8 | .add("resistor", (rb) => 9 | rb.setProps({ 10 | x: -2, 11 | y: 0, 12 | pcb_x: -2, 13 | pcb_y: 0, 14 | resistance: "1k", 15 | name: "R1", 16 | footprint: "0402", 17 | pcb_layer: "top", 18 | }) 19 | ) 20 | .add("resistor", (rb) => 21 | rb.setProps({ 22 | x: 2, 23 | y: 0, 24 | pcb_x: 2, 25 | pcb_y: 0, 26 | resistance: "10k", 27 | name: "R2", 28 | footprint: "0402", 29 | pcb_layer: "bottom", 30 | }) 31 | ) 32 | .add("trace", (tb) => 33 | tb.setProps({ 34 | path: [".R1 > .right", ".R2 > .left"], 35 | pcb_route_hints: [ 36 | { 37 | x: 0, 38 | y: -2, 39 | via: true, 40 | }, 41 | { 42 | x: 0, 43 | y: 2, 44 | via: true, 45 | }, 46 | ], 47 | }) 48 | ) 49 | .build() 50 | 51 | await writePcbSnapshotPng(soup) 52 | await logSoup(soup) 53 | t.is( 54 | soup.filter((s) => s.type.includes("error")).length, 55 | 0, 56 | "should be no errors" 57 | ) 58 | t.truthy(soup.find((elm) => elm.type === "pcb_via")) 59 | }) 60 | -------------------------------------------------------------------------------- /tests/trace-builder/no-traces-through-pcb-pads.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import * as Type from "lib/types" 3 | import { createProjectBuilder } from "../../src" 4 | import { logLayout } from "../utils/log-layout" 5 | 6 | test("no traces through pcb pads", async (t) => { 7 | const result = await createProjectBuilder() 8 | .add("resistor", (bb) => 9 | bb 10 | .setSourceProperties({ name: "R1", resistance: "10kohm" }) 11 | .setFootprint("0805") 12 | .setFootprintCenter(0, 0) 13 | .setFootprintRotation("90deg") 14 | .setSchematicCenter(0, 0) 15 | ) 16 | .add("resistor", (bb) => 17 | bb 18 | .setSourceProperties({ name: "R2", resistance: "10kohm" }) 19 | .setFootprint("0805") 20 | .setFootprintCenter(2, 0) 21 | .setFootprintRotation("90deg") 22 | .setSchematicCenter(2, 0) 23 | ) 24 | .add("resistor", (bb) => 25 | bb 26 | .setSourceProperties({ name: "R3", resistance: "10kohm" }) 27 | .setFootprint("0805") 28 | .setFootprintCenter(4, 0) 29 | .setFootprintRotation("90deg") 30 | .setSchematicCenter(4, 0) 31 | ) 32 | .add("trace", (tb) => tb.addConnections([".R1 > .left", ".R3 > .left"])) 33 | .build() 34 | 35 | await logLayout(`no traces through pcb pads`, result) 36 | t.pass() 37 | }) 38 | -------------------------------------------------------------------------------- /tests/trace-builder/no-traces-through-schematic-pins.test.ts: -------------------------------------------------------------------------------- 1 | import { createProjectBuilder } from "lib/builder" 2 | import test from "ava" 3 | import { logLayout } from "../utils/log-layout" 4 | 5 | test("no-traces-through-schematic-pins", async (t) => { 6 | const projectBuilder = await createProjectBuilder() 7 | .add("diode", (db) => 8 | db 9 | .setSourceProperties({ 10 | name: "D1", 11 | }) 12 | .setSchematicRotation("180deg") 13 | .setSchematicCenter(0, -2) 14 | ) 15 | .add("resistor", (rb) => 16 | rb 17 | .setSourceProperties({ resistance: "1kohm", name: "R1" }) 18 | .setSchematicCenter(0, 2) 19 | ) 20 | .add("trace", (tb) => tb.addConnections([".R1 > .right", ".D1 > .left"])) 21 | .add("resistor", (rb) => rb.setSchematicCenter(0, 0)) 22 | 23 | const projectBuilderOutput = await projectBuilder.build() 24 | 25 | await logLayout("no-traces-through-schematic-pins", projectBuilderOutput) 26 | t.pass() 27 | }) 28 | -------------------------------------------------------------------------------- /tests/trace-builder/route-to-nets.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 3 | 4 | test("route to nets", async (t) => { 5 | const { logSoup, pb } = await getTestFixture(t) 6 | 7 | const soup = await pb 8 | .add("resistor", (rb) => 9 | rb 10 | // .setProps({ 11 | // resistance: "10 ohm", 12 | // name: "R1", 13 | // }) 14 | .setSourceProperties({ 15 | resistance: "10 ohm", 16 | name: "R1", 17 | }) 18 | .setSchematicCenter("2mm", 1) 19 | .setFootprint("0402") 20 | ) 21 | .add("resistor", (rb) => 22 | rb 23 | // .setProps({ 24 | // resistance: "10 ohm", 25 | // name: "R2", 26 | // pcbX: "10mm", 27 | // pcbY: "0mm", 28 | // }) 29 | .setSourceProperties({ 30 | resistance: "10 ohm", 31 | name: "R2", 32 | }) 33 | .setSchematicCenter("2mm", 1) 34 | .setFootprintCenter("10mm", 0) 35 | .setFootprint("0402") 36 | ) 37 | .add("net", (nb) => nb.setProps({ name: "N1" })) 38 | .add("trace", (tb) => 39 | tb.setProps({ 40 | from: ".R1 > .right", 41 | to: ".N1", 42 | }) 43 | ) 44 | .add("trace", (tb) => 45 | tb.setProps({ 46 | from: ".R2 > .left", 47 | to: ".N1", 48 | }) 49 | ) 50 | .build() 51 | 52 | await logSoup(soup) 53 | t.pass() 54 | }) 55 | -------------------------------------------------------------------------------- /tests/trace-builder/simple-trace-test.test.ts: -------------------------------------------------------------------------------- 1 | import { su } from "@tscircuit/soup-util" 2 | import test from "ava" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("simple trace test", async (t) => { 6 | const { logSoup, pb } = await getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("resistor", (rb) => 10 | rb.setProps({ 11 | resistance: "10 ohm", 12 | name: "R1", 13 | footprint: "0805", 14 | pcbX: -5, 15 | pcbY: 0, 16 | }) 17 | ) 18 | .add("resistor", (rb) => 19 | rb.setProps({ 20 | resistance: "10 ohm", 21 | name: "R2", 22 | footprint: "0805", 23 | pcbX: 5, 24 | pcbY: 0, 25 | }) 26 | ) 27 | .add("trace", (tb) => 28 | tb.setProps({ 29 | from: ".R1 > .right", 30 | to: ".R2 > .left", 31 | }) 32 | ) 33 | .build() 34 | 35 | const traces = su(soup).pcb_trace.list() 36 | const startTrace = traces[0] 37 | t.is( 38 | startTrace.route.some( 39 | (r) => r.route_type === "wire" && r.start_pcb_port_id !== undefined 40 | ), 41 | true 42 | ) 43 | 44 | const endTrace = traces[traces.length - 1] 45 | t.is( 46 | endTrace.route.some( 47 | (r) => r.route_type === "wire" && r.start_pcb_port_id !== undefined 48 | ), 49 | true 50 | ) 51 | 52 | await logSoup(soup) 53 | t.pass() 54 | }) 55 | -------------------------------------------------------------------------------- /tests/tracehint/trace-hint-1.test.ts: -------------------------------------------------------------------------------- 1 | import { su } from "@tscircuit/soup-util" 2 | import test from "ava" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("trace hint 1: basic trace_hint for pad", async (t) => { 6 | const { logSoup, pb } = await getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("resistor", (rb) => 10 | rb.setProps({ resistance: "10k", name: "R1", footprint: "0805" }) 11 | ) 12 | .add("trace_hint", (th) => 13 | th.setProps({ 14 | for: ".R1 > .left", 15 | offset: { x: -1, y: 1 }, 16 | }) 17 | ) 18 | .build() 19 | 20 | const trace_hints = su(soup).pcb_trace_hint.list({})! 21 | 22 | t.is(trace_hints.length, 1) 23 | 24 | const trace_hint = trace_hints[0] 25 | 26 | // waiting for select 27 | const pcb_port = su(soup).pcb_port.select(".R1 > .left")! 28 | 29 | if (pcb_port) { 30 | t.is(trace_hint.route[0]?.x, pcb_port.x - 1) 31 | t.is(trace_hint.route[0]?.y, pcb_port.y + 1) 32 | } else { 33 | t.fail("pcb_port is undefined") 34 | } 35 | 36 | logSoup(soup) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/tracehint/trace-hint-2.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { su } from "@tscircuit/soup-util" 3 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 4 | 5 | test("trace hint 2: pcb trace follows trace hint", async (t) => { 6 | const { logSoup, pb } = await getTestFixture(t) 7 | 8 | const soup = await pb 9 | .add("resistor", (rb) => 10 | rb.setProps({ 11 | resistance: "10k", 12 | name: "R1", 13 | footprint: "0805", 14 | pcb_x: -5, 15 | pcb_y: 0, 16 | }) 17 | ) 18 | .add("resistor", (rb) => 19 | rb.setProps({ 20 | resistance: "10k", 21 | name: "R2", 22 | footprint: "0805", 23 | pcb_x: 5, 24 | pcb_y: 0, 25 | }) 26 | ) 27 | .add("trace_hint", (th) => 28 | th.setProps({ 29 | for: ".R1 > .left", 30 | offset: { x: -5, y: 5 }, 31 | }) 32 | ) 33 | .add("trace", (tb) => 34 | tb.setProps({ 35 | from: ".R1 > .left", 36 | to: ".R2 > .right", 37 | }) 38 | ) 39 | .build() 40 | 41 | const trace_hints = su(soup).pcb_trace_hint.list({})! 42 | 43 | t.is(trace_hints.length, 1) 44 | 45 | const trace_hint = trace_hints[0] 46 | 47 | // waiting for select 48 | const pcb_port = su(soup).pcb_port.select(".R1 > .left")! 49 | 50 | // check that the trace goes up to y=5 for the trace hint before coming 51 | // back down to connect to the other resistor 52 | const route = su(soup).pcb_trace.list({})[0].route 53 | t.truthy(route.some((e) => e.y === 5)) 54 | 55 | await logSoup(soup) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/transformations/port-rotation.test.ts: -------------------------------------------------------------------------------- 1 | import { createProjectBuilder } from "lib/builder" 2 | import test from "ava" 3 | import { logLayout } from "../utils/log-layout" 4 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 5 | import { su } from "@tscircuit/soup-util" 6 | import { AnySoupElement } from "@tscircuit/soup" 7 | 8 | test("port rotation (facing_direction)", async (t) => { 9 | const { pb, logSoup } = await getTestFixture(t) 10 | 11 | const soup = await pb 12 | .add("resistor", (rb) => 13 | rb 14 | .setProps({ 15 | name: "R1", 16 | resistance: "10 ohm", 17 | }) 18 | .setSchematicCenter(0, 0) 19 | .setSchematicRotation(`90deg`) 20 | .setFootprint("0402") 21 | ) 22 | .build() 23 | 24 | t.is(su(soup).schematic_port.select(".R1 > .right")?.facing_direction, "up") 25 | t.is(su(soup).schematic_port.select(".R1 > .left")?.facing_direction, "down") 26 | 27 | await logSoup(soup) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/utils/get-explicit-to-normal-pin-mapping.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { 3 | getExplicitToNormalPinMapping, 4 | getNormalToExplicitPinMapping, 5 | } from "index" 6 | 7 | test("get normal to explicit pin mapping", async (t) => { 8 | const port_arrangement = { 9 | left_side: { 10 | pins: [1, 8], 11 | }, 12 | bottom_side: { 13 | pins: [3], 14 | }, 15 | right_side: { 16 | pin_definition_direction: "top-to-bottom", 17 | pins: [2, 4], 18 | }, 19 | } 20 | 21 | const normal_to_explicit_mapping = getNormalToExplicitPinMapping( 22 | port_arrangement as any 23 | ) 24 | 25 | /** 26 | * 1 2 27 | * 8 4 28 | * 3 29 | */ 30 | 31 | t.deepEqual(normal_to_explicit_mapping, [0, 1, 8, 3, 4, 2]) 32 | 33 | const explicit_to_normal_mapping = getExplicitToNormalPinMapping( 34 | port_arrangement as any 35 | ) 36 | 37 | t.deepEqual(explicit_to_normal_mapping, [ 38 | 0, // 0 39 | 1, // 1 40 | 5, // 2 41 | 3, // 3 42 | 4, // 4 43 | undefined, // 5 44 | undefined, // 6 45 | undefined, // 7 46 | 2, // 8 47 | ]) 48 | }) 49 | -------------------------------------------------------------------------------- /tests/utils/log-layout.ts: -------------------------------------------------------------------------------- 1 | import defaultAxios from "redaxios" 2 | import { AnyElement } from "lib/types" 3 | 4 | const DEBUG_SRV = `https://debug.tscircuit.com` 5 | 6 | const axios = defaultAxios.create({ 7 | baseURL: DEBUG_SRV, 8 | }) 9 | 10 | function findSource(elm: AnyElement, sources: Array) { 11 | if ("source_component_id" in elm) { 12 | return sources.find( 13 | (s) => 14 | "source_component_id" in s && 15 | s.source_component_id === elm.source_component_id && 16 | s.type === "source_component" 17 | ) 18 | } 19 | if ("source_port_id" in elm) { 20 | return sources.find( 21 | (s) => 22 | "source_port_id" in s && 23 | s.source_port_id === elm.source_port_id && 24 | s.type === "source_port" 25 | ) 26 | } 27 | return null 28 | } 29 | 30 | let layout_server_healthy: boolean | null = null 31 | export const logLayout = async ( 32 | layout_group_name: string, 33 | objects: Array 34 | ) => { 35 | if (globalThis?.process?.env?.CI) return 36 | if (globalThis?.process?.env?.FULL_TEST) return 37 | if (layout_server_healthy === false) return 38 | 39 | if (layout_server_healthy === null) { 40 | try { 41 | await axios.get("/api/health", { 42 | timeout: 1000, 43 | } as any) 44 | layout_server_healthy = true 45 | } catch (e) { 46 | layout_server_healthy = false 47 | return 48 | } 49 | } 50 | 51 | await axios 52 | .post("/api/soup_group/add_soup", { 53 | soup_group_name: `builder: ${layout_group_name}`, 54 | soup_name: "all", 55 | username: "tmp", 56 | content: { 57 | elements: objects.map((o: any) => ({ 58 | ...o, 59 | source: findSource(o, objects), 60 | })), 61 | }, 62 | }) 63 | .catch((e) => { 64 | console.warn(`Couldn't log layout: ${layout_group_name}`) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /tests/via-builder/via-builder.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | import { getTestFixture } from "tests/fixtures/get-test-fixture" 3 | 4 | test("[smoke] via builder", async (t) => { 5 | const { pb, logSoup } = await getTestFixture(t) 6 | 7 | const soup = await pb 8 | .add("via", (vb) => 9 | vb.setProps({ 10 | center: [0, 0], 11 | name: "V1", 12 | from_layer: "top", 13 | to_layer: "bottom", 14 | hole_diameter: "0.2mm", 15 | outer_diameter: "0.5mm", 16 | }) 17 | ) 18 | .add("resistor", (rb) => 19 | rb.setProps({ 20 | x: -2, 21 | y: 0, 22 | pcb_x: -2, 23 | pcb_y: 0, 24 | resistance: "1k", 25 | name: "R1", 26 | footprint: "0402", 27 | }) 28 | ) 29 | .add("resistor", (rb) => 30 | rb.setProps({ 31 | x: 2, 32 | y: 0, 33 | pcb_x: 2, 34 | pcb_y: 0, 35 | resistance: "10k", 36 | name: "R2", 37 | footprint: "0402", 38 | pcb_layer: "bottom", 39 | }) 40 | ) 41 | .add("trace", (tb) => 42 | tb.setProps({ 43 | path: [".R1 > .right", ".V1 > .top"], 44 | }) 45 | ) 46 | .add("trace", (tb) => 47 | tb.setProps({ 48 | path: [".V1 > .bottom", ".R2 > .left"], 49 | }) 50 | ) 51 | .build() 52 | 53 | await logSoup(soup) 54 | t.pass() 55 | }) 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": false, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "react-jsx", 17 | "baseUrl": "./src", 18 | "paths": { 19 | "tests/*": ["../tests/*"] 20 | }, 21 | "strictNullChecks": true 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts"], 24 | "exclude": ["node_modules", "**.ts.snap", "dist"] 25 | } 26 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | export default defineConfig({ 4 | tsconfig: "./tsconfig.json", 5 | entry: ["./src/index.ts"], 6 | format: ["cjs"], 7 | treeshake: true, 8 | dts: true, 9 | sourcemap: true, 10 | clean: true, 11 | }) 12 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitRevision": "main", 3 | "entryPoints": "src/index.ts" 4 | } 5 | --------------------------------------------------------------------------------