kicadts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
  2. package/.vscode/settings.json +16 -0
  3. package/AGENTS.md +30 -0
  4. package/README.md +206 -0
  5. package/biome.json +93 -0
  6. package/bun.lock +48 -0
  7. package/bunfig.toml +5 -0
  8. package/lib/index.ts +1 -0
  9. package/lib/sexpr/base-classes/SxClass.ts +164 -0
  10. package/lib/sexpr/base-classes/SxPrimitiveBoolean.ts +35 -0
  11. package/lib/sexpr/base-classes/SxPrimitiveNumber.ts +26 -0
  12. package/lib/sexpr/base-classes/SxPrimitiveString.ts +26 -0
  13. package/lib/sexpr/classes/At.ts +38 -0
  14. package/lib/sexpr/classes/Bus.ts +83 -0
  15. package/lib/sexpr/classes/BusEntry.ts +142 -0
  16. package/lib/sexpr/classes/Color.ts +29 -0
  17. package/lib/sexpr/classes/Dnp.ts +8 -0
  18. package/lib/sexpr/classes/EmbeddedFonts.ts +70 -0
  19. package/lib/sexpr/classes/ExcludeFromSim.ts +8 -0
  20. package/lib/sexpr/classes/FieldsAutoplaced.ts +8 -0
  21. package/lib/sexpr/classes/Footprint.ts +719 -0
  22. package/lib/sexpr/classes/FootprintAttr.ts +102 -0
  23. package/lib/sexpr/classes/FootprintAutoplaceCost180.ts +9 -0
  24. package/lib/sexpr/classes/FootprintAutoplaceCost90.ts +9 -0
  25. package/lib/sexpr/classes/FootprintClearance.ts +9 -0
  26. package/lib/sexpr/classes/FootprintDescr.ts +44 -0
  27. package/lib/sexpr/classes/FootprintLocked.ts +32 -0
  28. package/lib/sexpr/classes/FootprintModel.ts +145 -0
  29. package/lib/sexpr/classes/FootprintNetTiePadGroups.ts +50 -0
  30. package/lib/sexpr/classes/FootprintPad.ts +705 -0
  31. package/lib/sexpr/classes/FootprintPath.ts +44 -0
  32. package/lib/sexpr/classes/FootprintPlaced.ts +32 -0
  33. package/lib/sexpr/classes/FootprintPrivateLayers.ts +56 -0
  34. package/lib/sexpr/classes/FootprintSheetfile.ts +44 -0
  35. package/lib/sexpr/classes/FootprintSheetname.ts +44 -0
  36. package/lib/sexpr/classes/FootprintSolderMaskMargin.ts +9 -0
  37. package/lib/sexpr/classes/FootprintSolderPasteMargin.ts +9 -0
  38. package/lib/sexpr/classes/FootprintSolderPasteRatio.ts +9 -0
  39. package/lib/sexpr/classes/FootprintTags.ts +44 -0
  40. package/lib/sexpr/classes/FootprintTedit.ts +21 -0
  41. package/lib/sexpr/classes/FootprintThermalGap.ts +9 -0
  42. package/lib/sexpr/classes/FootprintThermalWidth.ts +9 -0
  43. package/lib/sexpr/classes/FootprintZoneConnect.ts +9 -0
  44. package/lib/sexpr/classes/FpArc.ts +289 -0
  45. package/lib/sexpr/classes/FpCircle.ts +293 -0
  46. package/lib/sexpr/classes/FpLine.ts +288 -0
  47. package/lib/sexpr/classes/FpPoly.ts +266 -0
  48. package/lib/sexpr/classes/FpPolyFill.ts +48 -0
  49. package/lib/sexpr/classes/FpPolyLocked.ts +40 -0
  50. package/lib/sexpr/classes/FpRect.ts +293 -0
  51. package/lib/sexpr/classes/FpText.ts +341 -0
  52. package/lib/sexpr/classes/FpTextBox.ts +412 -0
  53. package/lib/sexpr/classes/GrLine.ts +245 -0
  54. package/lib/sexpr/classes/GrLineAngle.ts +32 -0
  55. package/lib/sexpr/classes/GrLineEnd.ts +61 -0
  56. package/lib/sexpr/classes/GrLineLocked.ts +40 -0
  57. package/lib/sexpr/classes/GrLineStart.ts +61 -0
  58. package/lib/sexpr/classes/GrText.ts +202 -0
  59. package/lib/sexpr/classes/Image.ts +256 -0
  60. package/lib/sexpr/classes/InBom.ts +8 -0
  61. package/lib/sexpr/classes/Junction.ts +134 -0
  62. package/lib/sexpr/classes/KicadPcb.ts +313 -0
  63. package/lib/sexpr/classes/KicadSch.ts +303 -0
  64. package/lib/sexpr/classes/KicadSchGenerator.ts +32 -0
  65. package/lib/sexpr/classes/KicadSchGeneratorVersion.ts +30 -0
  66. package/lib/sexpr/classes/KicadSchVersion.ts +22 -0
  67. package/lib/sexpr/classes/Label.ts +136 -0
  68. package/lib/sexpr/classes/Layer.ts +51 -0
  69. package/lib/sexpr/classes/Layers.ts +47 -0
  70. package/lib/sexpr/classes/LibSymbols.ts +61 -0
  71. package/lib/sexpr/classes/NoConnect.ts +73 -0
  72. package/lib/sexpr/classes/OnBoard.ts +8 -0
  73. package/lib/sexpr/classes/PadChamfer.ts +50 -0
  74. package/lib/sexpr/classes/PadChamferRatio.ts +9 -0
  75. package/lib/sexpr/classes/PadClearance.ts +9 -0
  76. package/lib/sexpr/classes/PadDieLength.ts +9 -0
  77. package/lib/sexpr/classes/PadDrill.ts +145 -0
  78. package/lib/sexpr/classes/PadDrillOffset.ts +54 -0
  79. package/lib/sexpr/classes/PadLayers.ts +59 -0
  80. package/lib/sexpr/classes/PadNet.ts +56 -0
  81. package/lib/sexpr/classes/PadOptions.ts +182 -0
  82. package/lib/sexpr/classes/PadPinFunction.ts +9 -0
  83. package/lib/sexpr/classes/PadPinType.ts +9 -0
  84. package/lib/sexpr/classes/PadPrimitiveGrArc.ts +254 -0
  85. package/lib/sexpr/classes/PadPrimitiveGrCircle.ts +279 -0
  86. package/lib/sexpr/classes/PadPrimitiveGrLine.ts +126 -0
  87. package/lib/sexpr/classes/PadPrimitives.ts +289 -0
  88. package/lib/sexpr/classes/PadRectDelta.ts +57 -0
  89. package/lib/sexpr/classes/PadRoundrectRratio.ts +9 -0
  90. package/lib/sexpr/classes/PadSize.ts +54 -0
  91. package/lib/sexpr/classes/PadSolderMaskMargin.ts +9 -0
  92. package/lib/sexpr/classes/PadSolderPasteMargin.ts +9 -0
  93. package/lib/sexpr/classes/PadSolderPasteMarginRatio.ts +9 -0
  94. package/lib/sexpr/classes/PadTeardrops.ts +208 -0
  95. package/lib/sexpr/classes/PadThermalBridgeAngle.ts +9 -0
  96. package/lib/sexpr/classes/PadThermalGap.ts +9 -0
  97. package/lib/sexpr/classes/PadThermalWidth.ts +9 -0
  98. package/lib/sexpr/classes/PadZoneConnect.ts +9 -0
  99. package/lib/sexpr/classes/Paper.ts +119 -0
  100. package/lib/sexpr/classes/PcbGeneral.ts +75 -0
  101. package/lib/sexpr/classes/PcbGeneralLegacyTeardrops.ts +44 -0
  102. package/lib/sexpr/classes/PcbGeneralThickness.ts +9 -0
  103. package/lib/sexpr/classes/PcbGenerator.ts +16 -0
  104. package/lib/sexpr/classes/PcbGeneratorVersion.ts +16 -0
  105. package/lib/sexpr/classes/PcbLayerDefinition.ts +102 -0
  106. package/lib/sexpr/classes/PcbLayers.ts +34 -0
  107. package/lib/sexpr/classes/PcbNet.ts +56 -0
  108. package/lib/sexpr/classes/PcbVersion.ts +9 -0
  109. package/lib/sexpr/classes/Property.ts +246 -0
  110. package/lib/sexpr/classes/PropertyHide.ts +9 -0
  111. package/lib/sexpr/classes/PropertyUnlocked.ts +9 -0
  112. package/lib/sexpr/classes/Pts.ts +65 -0
  113. package/lib/sexpr/classes/RenderCache.ts +221 -0
  114. package/lib/sexpr/classes/SchematicText.ts +141 -0
  115. package/lib/sexpr/classes/Segment.ts +222 -0
  116. package/lib/sexpr/classes/SegmentEnd.ts +59 -0
  117. package/lib/sexpr/classes/SegmentLocked.ts +33 -0
  118. package/lib/sexpr/classes/SegmentNet.ts +62 -0
  119. package/lib/sexpr/classes/SegmentStart.ts +59 -0
  120. package/lib/sexpr/classes/Setup/PcbPlotParams.ts +729 -0
  121. package/lib/sexpr/classes/Setup/PcbPlotParamsBase.ts +9 -0
  122. package/lib/sexpr/classes/Setup/PcbPlotParamsNumericProperties.ts +105 -0
  123. package/lib/sexpr/classes/Setup/PcbPlotParamsStringPropertiesA.ts +104 -0
  124. package/lib/sexpr/classes/Setup/PcbPlotParamsStringPropertiesB.ts +105 -0
  125. package/lib/sexpr/classes/Setup/Setup.ts +573 -0
  126. package/lib/sexpr/classes/Setup/SetupPropertyTypes.ts +119 -0
  127. package/lib/sexpr/classes/Setup/Stackup.ts +140 -0
  128. package/lib/sexpr/classes/Setup/StackupLayer.ts +233 -0
  129. package/lib/sexpr/classes/Setup/StackupLayerProperties.ts +78 -0
  130. package/lib/sexpr/classes/Setup/StackupProperties.ts +41 -0
  131. package/lib/sexpr/classes/Setup/base.ts +167 -0
  132. package/lib/sexpr/classes/Setup/index.ts +14 -0
  133. package/lib/sexpr/classes/Setup/setupMultiValueProperties.ts +54 -0
  134. package/lib/sexpr/classes/Setup/setupNumericProperties.ts +151 -0
  135. package/lib/sexpr/classes/Setup/setupPropertyHandlers.ts +90 -0
  136. package/lib/sexpr/classes/Setup/setupStringProperties.ts +75 -0
  137. package/lib/sexpr/classes/Sheet.ts +205 -0
  138. package/lib/sexpr/classes/SheetFill.ts +44 -0
  139. package/lib/sexpr/classes/SheetInstances.ts +168 -0
  140. package/lib/sexpr/classes/SheetInstancesRoot.ts +165 -0
  141. package/lib/sexpr/classes/SheetPin.ts +122 -0
  142. package/lib/sexpr/classes/SheetProperty.ts +115 -0
  143. package/lib/sexpr/classes/SheetSize.ts +44 -0
  144. package/lib/sexpr/classes/Stroke.ts +58 -0
  145. package/lib/sexpr/classes/StrokeType.ts +34 -0
  146. package/lib/sexpr/classes/Symbol.ts +1541 -0
  147. package/lib/sexpr/classes/TextEffects.ts +444 -0
  148. package/lib/sexpr/classes/TitleBlock.ts +352 -0
  149. package/lib/sexpr/classes/Unit.ts +28 -0
  150. package/lib/sexpr/classes/Uuid.ts +8 -0
  151. package/lib/sexpr/classes/Via.ts +328 -0
  152. package/lib/sexpr/classes/ViaNet.ts +59 -0
  153. package/lib/sexpr/classes/Width.ts +8 -0
  154. package/lib/sexpr/classes/Wire.ts +91 -0
  155. package/lib/sexpr/classes/Xy.ts +35 -0
  156. package/lib/sexpr/classes/Zone.ts +41 -0
  157. package/lib/sexpr/index.ts +130 -0
  158. package/lib/sexpr/parseKicadSexpr.ts +5 -0
  159. package/lib/sexpr/parseToPrimitiveSExpr.ts +240 -0
  160. package/lib/sexpr/utils/indentLines.ts +3 -0
  161. package/lib/sexpr/utils/parseYesNo.ts +12 -0
  162. package/lib/sexpr/utils/quoteSExprString.ts +8 -0
  163. package/lib/sexpr/utils/strokeFromArgs.ts +19 -0
  164. package/lib/sexpr/utils/toNumberValue.ts +13 -0
  165. package/lib/sexpr/utils/toStringValue.ts +10 -0
  166. package/package.json +26 -0
  167. package/scripts/download-references.ts +66 -0
  168. package/tests/fixtures/expectEqualPrimitiveSExpr.ts +200 -0
  169. package/tests/sexpr/KicadPcbDemos.test.ts +48 -0
  170. package/tests/sexpr/KicadSchDemos.test.ts +49 -0
  171. package/tests/sexpr/classes/Footprint.test.ts +277 -0
  172. package/tests/sexpr/classes/FootprintPad.test.ts +71 -0
  173. package/tests/sexpr/classes/FpArc.test.ts +45 -0
  174. package/tests/sexpr/classes/FpCircle.test.ts +39 -0
  175. package/tests/sexpr/classes/FpPoly.test.ts +43 -0
  176. package/tests/sexpr/classes/FpRect.test.ts +40 -0
  177. package/tests/sexpr/classes/FpTextBox.test.ts +84 -0
  178. package/tests/sexpr/classes/Image.test.ts +50 -0
  179. package/tests/sexpr/classes/KicadSch.test.ts +97 -0
  180. package/tests/sexpr/classes/Paper.test.ts +30 -0
  181. package/tests/sexpr/classes/Property.test.ts +48 -0
  182. package/tests/sexpr/classes/Setup.test.ts +189 -0
  183. package/tests/sexpr/classes/Sheet.test.ts +107 -0
  184. package/tests/sexpr/classes/Stroke.test.ts +15 -0
  185. package/tests/sexpr/classes/Symbol.test.ts +96 -0
  186. package/tests/sexpr/classes/TextEffects.test.ts +56 -0
  187. package/tests/sexpr/classes/TitleBlock.test.ts +40 -0
  188. package/tsconfig.json +35 -0
@@ -0,0 +1,111 @@
1
+ ---
2
+ description: Use Bun instead of Node.js, npm, pnpm, or vite.
3
+ globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
4
+ alwaysApply: false
5
+ ---
6
+
7
+ Default to using Bun instead of Node.js.
8
+
9
+ - Use `bun <file>` instead of `node <file>` or `ts-node <file>`
10
+ - Use `bun test` instead of `jest` or `vitest`
11
+ - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
12
+ - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
13
+ - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
14
+ - Bun automatically loads .env, so don't use dotenv.
15
+
16
+ ## APIs
17
+
18
+ - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
19
+ - `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
20
+ - `Bun.redis` for Redis. Don't use `ioredis`.
21
+ - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
22
+ - `WebSocket` is built-in. Don't use `ws`.
23
+ - Prefer `Bun.file` over `node:fs`'s readFile/writeFile
24
+ - Bun.$`ls` instead of execa.
25
+
26
+ ## Testing
27
+
28
+ Use `bun test` to run tests.
29
+
30
+ ```ts#index.test.ts
31
+ import { test, expect } from "bun:test";
32
+
33
+ test("hello world", () => {
34
+ expect(1).toBe(1);
35
+ });
36
+ ```
37
+
38
+ ## Frontend
39
+
40
+ Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
41
+
42
+ Server:
43
+
44
+ ```ts#index.ts
45
+ import index from "./index.html"
46
+
47
+ Bun.serve({
48
+ routes: {
49
+ "/": index,
50
+ "/api/users/:id": {
51
+ GET: (req) => {
52
+ return new Response(JSON.stringify({ id: req.params.id }));
53
+ },
54
+ },
55
+ },
56
+ // optional websocket support
57
+ websocket: {
58
+ open: (ws) => {
59
+ ws.send("Hello, world!");
60
+ },
61
+ message: (ws, message) => {
62
+ ws.send(message);
63
+ },
64
+ close: (ws) => {
65
+ // handle close
66
+ }
67
+ },
68
+ development: {
69
+ hmr: true,
70
+ console: true,
71
+ }
72
+ })
73
+ ```
74
+
75
+ HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
76
+
77
+ ```html#index.html
78
+ <html>
79
+ <body>
80
+ <h1>Hello, world!</h1>
81
+ <script type="module" src="./frontend.tsx"></script>
82
+ </body>
83
+ </html>
84
+ ```
85
+
86
+ With the following `frontend.tsx`:
87
+
88
+ ```tsx#frontend.tsx
89
+ import React from "react";
90
+
91
+ // import .css files directly and it works
92
+ import './index.css';
93
+
94
+ import { createRoot } from "react-dom/client";
95
+
96
+ const root = createRoot(document.body);
97
+
98
+ export default function Frontend() {
99
+ return <h1>Hello, world!</h1>;
100
+ }
101
+
102
+ root.render(<Frontend />);
103
+ ```
104
+
105
+ Then, run index.ts
106
+
107
+ ```sh
108
+ bun --hot ./index.ts
109
+ ```
110
+
111
+ For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
@@ -0,0 +1,16 @@
1
+ {
2
+ "workbench.colorCustomizations": {
3
+ "commandCenter.border": "#15202b99",
4
+ "sash.hoverBorder": "#18f7e8",
5
+ "statusBar.background": "#07d5c7",
6
+ "statusBar.foreground": "#15202b",
7
+ "statusBarItem.hoverBackground": "#05a499",
8
+ "statusBarItem.remoteBackground": "#07d5c7",
9
+ "statusBarItem.remoteForeground": "#15202b",
10
+ "titleBar.activeBackground": "#07d5c7",
11
+ "titleBar.activeForeground": "#15202b",
12
+ "titleBar.inactiveBackground": "#07d5c799",
13
+ "titleBar.inactiveForeground": "#15202b99"
14
+ },
15
+ "peacock.color": "#07d5c7"
16
+ }
package/AGENTS.md ADDED
@@ -0,0 +1,30 @@
1
+ This is the `kicadts` typescript library. It's still in early developement and
2
+ we're trying to make sure we parse the entire KiCad S-expression specification.
3
+
4
+ You can find the difference specifications in the `references` directory, the
5
+ most extensive one is `references/SEXPR_MAIN.adoc` (use `bun run scripts/download-references.ts` to download if it's not already there)
6
+
7
+ - `references/SCHEMATIC_SEXPR.adoc`
8
+ - `references/PCB_SEXPR.adoc`
9
+ - `references/FOOTPRINT_SEXPR.adoc`
10
+ - `references/SCH_SYM_SEXPR.adoc`
11
+ - `references/SEXPR_MAIN.adoc`
12
+
13
+ ### Tips
14
+
15
+ - Every S-expression token must have a corresponding `SxClass` subclass and be registered via `SxClass.register`; missing registrations cause parse failures.
16
+ - Tokens reused under different parents (for example `type` under `stroke`) require `static parentToken` overrides so the correct class resolves during parsing.
17
+ - When a class accepts unordered child tokens, set `_propertyMap` using `loadProperties` to make getters convenient and keep `getString()` deterministic.
18
+ - Snapshot tests with `bun test` provide quick verification that `getString()` matches the KiCad formatting expectations.
19
+
20
+ ### Major Refactor Notice
21
+
22
+ - The code in this repo is only partially migrated to the new pattern
23
+ - NEW PATTERN: Constructors never take `PrimitiveSExpr` arguments
24
+ - NEW PATTERN: All classes have a `fromSexprPrimitives` static method that takes a `PrimitiveSExpr` array and returns an instance of the class
25
+ - NEW PATTERN: Classes have ergonomic getters and setters for properties
26
+ - NEW PATTERN: Never has "extras" property, everything becomes an `SxClass`
27
+ - NEW PATTERN: Never have "switch cases" that switch on the token. Always use the `SxClass.parsePrimitiveSexpr` or `SxClass.parsePrimitivesToClassProperties` methods to parse arrays of `PrimitiveSExpr` into instances of the correct class
28
+ - NEW PATTERN: Never track the order of children unless absolutely necessary, use the `_sx*` properties and `getChildren` to return the children in a predefined order
29
+ - NEW PATTERN: Never track unknown children, throw an error/allow an error to be thrown if you encounter a child PrimitiveSExpr that can't be parsed. You can introduce new `_sx*` properties and a new class to prevent the error
30
+ - NEW PATTERN: One class per file
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # kicadts
2
+
3
+ `kicadts` is a TypeScript-first toolkit for reading, editing, and generating KiCad S-expression documents. Every KiCad token is modeled as a class, so you can compose schematics, boards, and footprints entirely in TypeScript and emit KiCad-compatible files with deterministic formatting.
4
+
5
+ ## Local Setup
6
+
7
+ This repository uses [Bun](https://bun.sh) for scripts and testing.
8
+
9
+ - `bun install`
10
+ - `bun test` — optional, but handy to confirm we still round-trip the KiCad demo files
11
+
12
+ ## Build KiCad Schematics
13
+
14
+ The high-level classes (`KicadSch`, `Sheet`, `SchematicSymbol`, `Wire`, …) expose setters and getters for their children. Populate the model, then call `getString()` to emit KiCad’s S-expression.
15
+
16
+ ```ts
17
+ import { promises as fs } from "node:fs"
18
+ import {
19
+ At,
20
+ KicadSch,
21
+ Paper,
22
+ Property,
23
+ Sheet,
24
+ SchematicSymbol,
25
+ TitleBlock,
26
+ Wire,
27
+ Pts,
28
+ Xy,
29
+ } from "kicadts"
30
+
31
+ const schematic = new KicadSch()
32
+ schematic.version = 20240101
33
+ schematic.generator = "kicadts-demo"
34
+
35
+ const title = new TitleBlock()
36
+ title.title = "Demo Schematic"
37
+ title.company = "Example Labs"
38
+ schematic.titleBlock = title
39
+
40
+ const paper = new Paper()
41
+ paper.size = "A4"
42
+ schematic.paper = paper
43
+ schematic.properties = [new Property("Sheetfile", "demo.kicad_sch")]
44
+
45
+ const sheet = new Sheet()
46
+ sheet.position = new At([0, 0, 0])
47
+ sheet.size = { width: 100, height: 80 }
48
+ schematic.sheets = [sheet]
49
+
50
+ const symbol = new SchematicSymbol()
51
+ symbol.libraryId = "Device:R"
52
+ symbol.at = new At([25.4, 12.7])
53
+ schematic.symbols = [symbol]
54
+
55
+ const wire = new Wire()
56
+ wire.points = new Pts([new Xy(0, 0), new Xy(25.4, 12.7)])
57
+ schematic.wires = [wire]
58
+
59
+ await fs.writeFile("demo.kicad_sch", schematic.getString())
60
+ ```
61
+
62
+ ## Build KiCad PCBs
63
+
64
+ Boards follow the same pattern. Compose `KicadPcb` with nets, footprints, segments, and zones. Footprints are reusable whether you embed them on a board or export them as `.kicad_mod` files.
65
+
66
+ ```ts
67
+ import { promises as fs } from "node:fs"
68
+ import {
69
+ At,
70
+ Footprint,
71
+ FootprintPad,
72
+ FpText,
73
+ KicadPcb,
74
+ PadLayers,
75
+ PadNet,
76
+ PadSize,
77
+ PcbNet,
78
+ TextEffects,
79
+ } from "kicadts"
80
+
81
+ const pcb = new KicadPcb()
82
+ pcb.version = 20240101
83
+ pcb.generator = "kicadts-demo"
84
+
85
+ const netGnd = new PcbNet(1, "GND")
86
+ const netSignal = new PcbNet(2, "Net-(R1-Pad2)")
87
+ pcb.nets = [netGnd, netSignal]
88
+
89
+ const footprint = new Footprint()
90
+ footprint.libraryLink = "Resistor_SMD:R_0603"
91
+ footprint.layer = "F.Cu"
92
+ footprint.position = new At([10, 5, 90])
93
+
94
+ const makeText = (type: string, text: string, x: number, y: number, layer: string) => {
95
+ const fpText = new FpText()
96
+ fpText.type = type
97
+ fpText.text = text
98
+ fpText.position = new At([x, y])
99
+ fpText.layer = layer
100
+ const effects = new TextEffects()
101
+ effects.font.size = { height: 1, width: 1 }
102
+ effects.font.thickness = 0.15
103
+ fpText.effects = effects
104
+ return fpText
105
+ }
106
+
107
+ footprint.fpTexts = [
108
+ makeText("reference", "R1", 0, -1.5, "F.SilkS"),
109
+ makeText("value", "10k", 0, 1.5, "F.Fab"),
110
+ ]
111
+
112
+ const pad = (number: string, x: number, net: PcbNet) => {
113
+ const fpPad = new FootprintPad(number, "smd", "roundrect")
114
+ fpPad.at = new At([x, 0])
115
+ fpPad.size = new PadSize(1.05, 0.95)
116
+ fpPad.layers = new PadLayers(["F.Cu", "F.Paste", "F.Mask"])
117
+ fpPad.roundrectRatio = 0.25
118
+ fpPad.net = new PadNet(net.id, net.name)
119
+ fpPad.pinfunction = number
120
+ fpPad.pintype = "passive"
121
+ return fpPad
122
+ }
123
+
124
+ footprint.fpPads = [pad("1", -0.8, netGnd), pad("2", 0.8, netSignal)]
125
+ pcb.footprints = [footprint]
126
+
127
+ await fs.writeFile("demo.kicad_pcb", pcb.getString())
128
+ ```
129
+
130
+ ## Build Stand-Alone Footprints
131
+
132
+ Footprints can live outside a board file (for library `.kicad_mod` entries). Populate `Footprint` and write the S-expression to disk.
133
+
134
+ ```ts
135
+ import { promises as fs } from "node:fs"
136
+ import {
137
+ At,
138
+ Footprint,
139
+ FootprintPad,
140
+ FpText,
141
+ PadLayers,
142
+ PadSize,
143
+ TextEffects,
144
+ } from "kicadts"
145
+
146
+ const footprint = new Footprint()
147
+ footprint.libraryLink = "Demo:TestPad"
148
+ footprint.layer = "F.Cu"
149
+ footprint.position = new At([0, 0])
150
+
151
+ const label = new FpText()
152
+ label.type = "reference"
153
+ label.text = "REF**"
154
+ label.position = new At([0, -1.5])
155
+ label.layer = "F.SilkS"
156
+ const effects = new TextEffects()
157
+ effects.font.size = { height: 1, width: 1 }
158
+ effects.font.thickness = 0.15
159
+ label.effects = effects
160
+
161
+ footprint.fpTexts = [label]
162
+
163
+ const pad = new FootprintPad("1", "smd", "rect")
164
+ pad.at = new At([0, 0])
165
+ pad.size = new PadSize(1.5, 1.5)
166
+ pad.layers = new PadLayers(["F.Cu", "F.Paste", "F.Mask"])
167
+ footprint.fpPads = [pad]
168
+
169
+ await fs.writeFile("Demo_TestPad.kicad_mod", footprint.getString())
170
+ ```
171
+
172
+ ## Load Existing KiCad Files
173
+
174
+ Parsing works for schematics, boards, footprints, or any KiCad S-expression. `parseKicadSexpr` returns an array of `SxClass` instances; narrow to the concrete class with `instanceof` and mutate as needed.
175
+
176
+ ```ts
177
+ import { promises as fs } from "node:fs"
178
+ import { KicadPcb, KicadSch, Footprint, parseKicadSexpr } from "kicadts"
179
+
180
+ const load = async (path: string) => {
181
+ const raw = await fs.readFile(path, "utf8")
182
+ const [root] = parseKicadSexpr(raw)
183
+
184
+ if (root instanceof KicadSch) {
185
+ root.generator = "kicadts"
186
+ return root
187
+ }
188
+
189
+ if (root instanceof KicadPcb) {
190
+ root.generatorVersion = "(generated programmatically)"
191
+ return root
192
+ }
193
+
194
+ if (root instanceof Footprint) {
195
+ root.descr = "Imported with kicadts"
196
+ return root
197
+ }
198
+
199
+ throw new Error(`Unsupported root token: ${root.token}`)
200
+ }
201
+
202
+ const updated = await load("existing.kicad_sch")
203
+ await fs.writeFile("existing.kicad_sch", updated.getString())
204
+ ```
205
+
206
+ Any class exposes `getChildren()` if you need to walk the tree manually, and snapshot tests (`bun test`) ensure the emitted S-expression stays identical to KiCad’s own formatting.
package/biome.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
3
+ "assist": { "actions": { "source": { "organizeImports": "on" } } },
4
+ "formatter": {
5
+ "enabled": true,
6
+ "indentStyle": "space"
7
+ },
8
+ "files": {
9
+ "includes": ["**", "!**/cosmos-export", "!**/dist", "!**/package.json"]
10
+ },
11
+ "javascript": {
12
+ "formatter": {
13
+ "jsxQuoteStyle": "double",
14
+ "quoteProperties": "asNeeded",
15
+ "trailingCommas": "all",
16
+ "semicolons": "asNeeded",
17
+ "arrowParentheses": "always",
18
+ "bracketSpacing": true,
19
+ "bracketSameLine": false
20
+ }
21
+ },
22
+ "linter": {
23
+ "enabled": true,
24
+ "rules": {
25
+ "recommended": true,
26
+ "suspicious": {
27
+ "noExplicitAny": "off"
28
+ },
29
+ "complexity": {
30
+ "noForEach": "error",
31
+ "useLiteralKeys": "off"
32
+ },
33
+ "a11y": {
34
+ "noAccessKey": "off",
35
+ "noAriaHiddenOnFocusable": "off",
36
+ "noAriaUnsupportedElements": "off",
37
+ "noAutofocus": "off",
38
+ "noDistractingElements": "off",
39
+ "noHeaderScope": "off",
40
+ "noInteractiveElementToNoninteractiveRole": "off",
41
+ "noLabelWithoutControl": "off",
42
+ "noNoninteractiveElementToInteractiveRole": "off",
43
+ "noNoninteractiveTabindex": "off",
44
+ "noPositiveTabindex": "off",
45
+ "noRedundantAlt": "off",
46
+ "noRedundantRoles": "off",
47
+ "noStaticElementInteractions": "off",
48
+ "noSvgWithoutTitle": "off",
49
+ "useAltText": "off",
50
+ "useAnchorContent": "off",
51
+ "useAriaActivedescendantWithTabindex": "off",
52
+ "useAriaPropsForRole": "off",
53
+ "useAriaPropsSupportedByRole": "off",
54
+ "useButtonType": "off",
55
+ "useFocusableInteractive": "off",
56
+ "useHeadingContent": "off",
57
+ "useHtmlLang": "off",
58
+ "useIframeTitle": "off",
59
+ "useKeyWithClickEvents": "off",
60
+ "useKeyWithMouseEvents": "off",
61
+ "useMediaCaption": "off",
62
+ "useSemanticElements": "off",
63
+ "useValidAnchor": "off",
64
+ "useValidAriaProps": "off",
65
+ "useValidAriaRole": "off",
66
+ "useValidAriaValues": "off",
67
+ "useValidAutocomplete": "off",
68
+ "useValidLang": "off"
69
+ },
70
+ "style": {
71
+ "useSingleVarDeclarator": "error",
72
+ "noParameterAssign": "off",
73
+ "noUselessElse": "off",
74
+ "noNonNullAssertion": "off",
75
+ "useNumberNamespace": "off",
76
+ "noUnusedTemplateLiteral": "off",
77
+ "useFilenamingConvention": {
78
+ "level": "error",
79
+ "options": {
80
+ "strictCase": true,
81
+ "requireAscii": true,
82
+ "filenameCases": ["kebab-case", "export"]
83
+ }
84
+ },
85
+ "useAsConstAssertion": "error",
86
+ "useDefaultParameterLast": "error",
87
+ "useEnumInitializers": "error",
88
+ "useSelfClosingElements": "error",
89
+ "noInferrableTypes": "error"
90
+ }
91
+ }
92
+ }
93
+ }
package/bun.lock ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "kicadts",
6
+ "devDependencies": {
7
+ "@biomejs/biome": "^2.2.4",
8
+ "@types/bun": "latest",
9
+ },
10
+ "peerDependencies": {
11
+ "typescript": "^5",
12
+ },
13
+ },
14
+ },
15
+ "packages": {
16
+ "@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
17
+
18
+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
19
+
20
+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
21
+
22
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
23
+
24
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
25
+
26
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
27
+
28
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
29
+
30
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
31
+
32
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
33
+
34
+ "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
35
+
36
+ "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
37
+
38
+ "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
39
+
40
+ "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
41
+
42
+ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
43
+
44
+ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
45
+
46
+ "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
47
+ }
48
+ }
package/bunfig.toml ADDED
@@ -0,0 +1,5 @@
1
+ # [test]
2
+ # preload = ["./tests/fixtures/preload.ts"]
3
+
4
+ [install.lockfile]
5
+ save = false
package/lib/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./sexpr"
@@ -0,0 +1,164 @@
1
+ import {
2
+ parseToPrimitiveSExpr,
3
+ type PrimitiveSExpr,
4
+ } from "../parseToPrimitiveSExpr"
5
+
6
+ const DEFAULT_PARENT_TOKEN = "__default__"
7
+
8
+ export abstract class SxClass {
9
+ abstract token: string
10
+ static token: string
11
+
12
+ /**
13
+ * Token strings are sometimes re-used (e.g. a "type" token) but the class
14
+ * varies based on the parent token
15
+ */
16
+ static parentToken?: string
17
+
18
+ isSxClass = true
19
+
20
+ getChildren(): SxClass[] {
21
+ // By default, return any properties found in this instance that have the _sx* prefix
22
+ return Object.keys(this)
23
+ .filter((k) => k.startsWith("_sx"))
24
+ .map((k) => (this as any)[k])
25
+ .filter((v) => v && typeof v === "object" && v.isSxClass)
26
+ }
27
+
28
+ getStringIndented(): string {
29
+ return this.getString()
30
+ .split("\n")
31
+ .map((line) => ` ${line}`)
32
+ .join("\n")
33
+ }
34
+
35
+ getString(): string {
36
+ const children = this.getChildren()
37
+ if (children.length === 0) {
38
+ return `(${this.token})`
39
+ }
40
+
41
+ const lines = [`(${this.token}`]
42
+ for (const p of children) {
43
+ lines.push(p.getStringIndented())
44
+ }
45
+ lines.push(")")
46
+ return lines.join("\n")
47
+ }
48
+ get [Symbol.toStringTag](): string {
49
+ return this.getString()
50
+ }
51
+ [Symbol.for("nodejs.util.inspect.custom")]() {
52
+ return this.getString()
53
+ }
54
+
55
+ // =========================== STATIC METHODS ===========================
56
+
57
+ static classes: Record<string, Record<string, any>> = {}
58
+
59
+ /**
60
+ * Should be called after class definition to register the class for parsing
61
+ */
62
+ static register(newClass: any) {
63
+ if (!newClass.token) {
64
+ throw new Error("Class must have a static override token")
65
+ }
66
+ const parentKey = newClass.parentToken ?? DEFAULT_PARENT_TOKEN
67
+ const existing = SxClass.classes[newClass.token] ?? {}
68
+ existing[parentKey] = newClass
69
+ SxClass.classes[newClass.token] = existing
70
+ }
71
+
72
+ /**
73
+ * Parse an S-expression string into registered SxClass instances
74
+ */
75
+ static parse(sexpr: string): SxClass[] {
76
+ const primitiveSexpr = parseToPrimitiveSExpr(sexpr)
77
+
78
+ return SxClass.parsePrimitiveSexpr(primitiveSexpr) as any
79
+ }
80
+
81
+ static fromSexprPrimitives(primitiveSexprs: PrimitiveSExpr[]): SxClass {
82
+ throw new Error(
83
+ `"${this.name}" class has not implemented fromSexprPrimitives`,
84
+ )
85
+ }
86
+
87
+ static parsePrimitiveSexpr(
88
+ primitiveSexpr: PrimitiveSExpr,
89
+ options: { parentToken?: string } = {},
90
+ ): SxClass | SxClass[] | number | string | boolean | null {
91
+ const parentToken = options.parentToken
92
+
93
+ if (
94
+ Array.isArray(primitiveSexpr) &&
95
+ primitiveSexpr.length >= 1 &&
96
+ typeof primitiveSexpr[0] === "string"
97
+ ) {
98
+ const classToken = primitiveSexpr[0] as string
99
+ const classGroup = SxClass.classes[classToken]
100
+ if (!classGroup) {
101
+ throw new Error(
102
+ `Class "${classToken}" not registered via SxClass.register`,
103
+ )
104
+ }
105
+ const parentKey = parentToken ?? DEFAULT_PARENT_TOKEN
106
+ const ClassDef: any =
107
+ classGroup[parentKey] ?? classGroup[DEFAULT_PARENT_TOKEN]
108
+ if (!ClassDef) {
109
+ throw new Error(
110
+ `Class "${classToken}" not registered for parent "${parentToken ?? "<root>"}"`,
111
+ )
112
+ }
113
+ const args = primitiveSexpr.slice(1) as PrimitiveSExpr[]
114
+ if (!("fromSexprPrimitives" in ClassDef)) {
115
+ throw new Error(
116
+ `Class "${classToken}" does not have a fromSexprPrimitives method`,
117
+ )
118
+ }
119
+ const classInstance = ClassDef.fromSexprPrimitives(args)
120
+ return classInstance
121
+ }
122
+
123
+ if (Array.isArray(primitiveSexpr)) {
124
+ return primitiveSexpr.map((item) =>
125
+ SxClass.parsePrimitiveSexpr(item, options),
126
+ ) as any[]
127
+ }
128
+
129
+ if (
130
+ typeof primitiveSexpr === "number" ||
131
+ typeof primitiveSexpr === "string" ||
132
+ typeof primitiveSexpr === "boolean" ||
133
+ primitiveSexpr === null
134
+ ) {
135
+ return primitiveSexpr as number | string | boolean | null
136
+ }
137
+
138
+ throw new Error(
139
+ `Couldn't parse primitive S-expression: ${JSON.stringify(primitiveSexpr)}`,
140
+ )
141
+ }
142
+
143
+ // =========================== STATIC UTILITIES ===========================
144
+ static parsePrimitivesToClassProperties(
145
+ primitiveSexprs: PrimitiveSExpr[],
146
+ parentToken?: string,
147
+ ): {
148
+ propertyMap: Record<string, SxClass>
149
+ arrayPropertyMap: Record<string, SxClass[]>
150
+ } {
151
+ const propertyMap = {} as Record<string, SxClass>
152
+ const arrayPropertyMap = {} as Record<string, SxClass[]>
153
+ for (const primitiveSexpr of primitiveSexprs) {
154
+ const sxClass = SxClass.parsePrimitiveSexpr(primitiveSexpr, {
155
+ parentToken,
156
+ }) as SxClass
157
+ if (!sxClass.isSxClass) continue
158
+ propertyMap[sxClass.token] = sxClass
159
+ arrayPropertyMap[sxClass.token] ??= []
160
+ arrayPropertyMap[sxClass.token]!.push(sxClass)
161
+ }
162
+ return { propertyMap, arrayPropertyMap }
163
+ }
164
+ }