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
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "kicadts",
3
+ "module": "dist/index.js",
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/tscircuit/kicadts"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup-node lib/index.ts --format esm --dts",
12
+ "download-references": "bun run scripts/download-references.ts",
13
+ "download-demos": "git clone --depth 1 --filter=blob:none --sparse git@gitlab.com:kicad/code/kicad.git kicad-demos && cd kicad-demos && git sparse-checkout set demos",
14
+ "typecheck": "bunx tsc --noEmit",
15
+ "format": "biome format --write .",
16
+ "format:check": "biome format ."
17
+ },
18
+ "devDependencies": {
19
+ "@biomejs/biome": "^2.2.4",
20
+ "@types/bun": "latest",
21
+ "tsup": "^8.5.0"
22
+ },
23
+ "peerDependencies": {
24
+ "typescript": "^5"
25
+ }
26
+ }
@@ -0,0 +1,66 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { resolve, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ type ReferenceSpec = {
6
+ readonly url: string;
7
+ readonly filename: string;
8
+ };
9
+
10
+ const references: ReferenceSpec[] = [
11
+ {
12
+ url: "https://gitlab.com/kicad/services/kicad-dev-docs/-/raw/master/content/file-formats/sexpr-intro/_index.en.adoc",
13
+ filename: "SEXPR_MAIN.adoc",
14
+ },
15
+ {
16
+ url: "https://gitlab.com/kicad/services/kicad-dev-docs/-/raw/master/content/file-formats/sexpr-schematic/_index.en.adoc",
17
+ filename: "SCHEMATIC_SEXPR.adoc",
18
+ },
19
+ {
20
+ url: "https://gitlab.com/kicad/services/kicad-dev-docs/-/raw/master/content/file-formats/sexpr-pcb/_index.en.adoc",
21
+ filename: "PCB_SEXPR.adoc",
22
+ },
23
+ {
24
+ url: "https://gitlab.com/kicad/services/kicad-dev-docs/-/raw/master/content/file-formats/sexpr-footprint/_index.en.adoc",
25
+ filename: "FOOTPRINT_SEXPR.adoc",
26
+ },
27
+ {
28
+ url: "https://gitlab.com/kicad/services/kicad-dev-docs/-/raw/master/content/file-formats/sexpr-symbol-lib/_index.en.adoc",
29
+ filename: "SCH_SYM_SEXPR.adoc",
30
+ },
31
+ ];
32
+
33
+ const __dirname = dirname(fileURLToPath(import.meta.url));
34
+ const repoRoot = resolve(__dirname, "..");
35
+ const referencesDir = resolve(repoRoot, "references");
36
+
37
+ async function fetchReference(reference: ReferenceSpec): Promise<void> {
38
+ const response = await fetch(reference.url);
39
+
40
+ if (!response.ok) {
41
+ throw new Error(`${reference.url} (${response.status} ${response.statusText})`);
42
+ }
43
+
44
+ const content = await response.text();
45
+ const targetPath = resolve(referencesDir, reference.filename);
46
+
47
+ await writeFile(targetPath, content, "utf8");
48
+ console.log(`Saved ${reference.filename}`);
49
+ }
50
+
51
+ async function main(): Promise<void> {
52
+ await mkdir(referencesDir, { recursive: true });
53
+
54
+ await Promise.all(
55
+ references.map(async (reference) => {
56
+ try {
57
+ await fetchReference(reference);
58
+ } catch (error) {
59
+ console.error(`Failed to download ${reference.filename}:`, error);
60
+ process.exitCode = 1;
61
+ }
62
+ }),
63
+ );
64
+ }
65
+
66
+ await main();
@@ -0,0 +1,200 @@
1
+ import { expect } from "bun:test"
2
+ import type { PrimitiveSExpr } from "lib/sexpr/parseToPrimitiveSExpr"
3
+
4
+ interface ExpectEqualPrimitiveSExprOptions {
5
+ context?: Record<string, unknown>
6
+ path?: string
7
+ }
8
+
9
+ type CanonicalPrimitiveSExpr =
10
+ | string
11
+ | number
12
+ | boolean
13
+ | null
14
+ | CanonicalPrimitiveSExpr[]
15
+
16
+ const ORDERED_TAGS = new Set(["xy"])
17
+
18
+ const canonicalKey = (value: CanonicalPrimitiveSExpr): string =>
19
+ JSON.stringify(value)
20
+
21
+ const canonicalizePrimitiveSExpr = (
22
+ value: PrimitiveSExpr,
23
+ ): CanonicalPrimitiveSExpr => {
24
+ if (!Array.isArray(value)) {
25
+ return value
26
+ }
27
+
28
+ if (value.length === 0) {
29
+ return []
30
+ }
31
+
32
+ const first = value[0]
33
+
34
+ if (typeof first === "string") {
35
+ return canonicalizeNode(value as CanonicalPrimitiveSExpr[])
36
+ }
37
+
38
+ if (Array.isArray(first)) {
39
+ const canonicalChildren = value.map((child) =>
40
+ canonicalizePrimitiveSExpr(child),
41
+ ) as CanonicalPrimitiveSExpr[]
42
+
43
+ canonicalChildren.sort((a, b) =>
44
+ canonicalKey(a).localeCompare(canonicalKey(b)),
45
+ )
46
+
47
+ return canonicalChildren
48
+ }
49
+
50
+ return value.map((child) => canonicalizePrimitiveSExpr(child))
51
+ }
52
+
53
+ const canonicalizeNode = (
54
+ list: CanonicalPrimitiveSExpr[],
55
+ ): CanonicalPrimitiveSExpr => {
56
+ const [head, ...tail] = list
57
+
58
+ if (head === undefined) {
59
+ return []
60
+ }
61
+
62
+ const positional: CanonicalPrimitiveSExpr[] = []
63
+ const grouped = new Map<string, CanonicalPrimitiveSExpr[]>()
64
+
65
+ for (const element of tail) {
66
+ if (Array.isArray(element)) {
67
+ const tag = element[0]
68
+ if (typeof tag === "string" && !ORDERED_TAGS.has(tag)) {
69
+ const canonicalChild = canonicalizePrimitiveSExpr(element)
70
+ const bucket = grouped.get(tag)
71
+ if (bucket) {
72
+ bucket.push(canonicalChild)
73
+ } else {
74
+ grouped.set(tag, [canonicalChild])
75
+ }
76
+ continue
77
+ }
78
+ }
79
+
80
+ positional.push(canonicalizePrimitiveSExpr(element))
81
+ }
82
+
83
+ const groupedValues: CanonicalPrimitiveSExpr[] = []
84
+ const sortedTags = [...grouped.keys()].sort()
85
+ for (const tag of sortedTags) {
86
+ const items = grouped.get(tag)!
87
+ items.sort((a, b) => canonicalKey(a).localeCompare(canonicalKey(b)))
88
+ groupedValues.push(...items)
89
+ }
90
+
91
+ return [canonicalizePrimitiveSExpr(head), ...positional, ...groupedValues]
92
+ }
93
+
94
+ export const expectEqualPrimitiveSExpr = (
95
+ actual: PrimitiveSExpr,
96
+ expected: PrimitiveSExpr,
97
+ options: ExpectEqualPrimitiveSExprOptions = {},
98
+ ): void => {
99
+ const normalizedActual = canonicalizePrimitiveSExpr(actual)
100
+ const normalizedExpected = canonicalizePrimitiveSExpr(expected)
101
+
102
+ const rootPath = options.path ?? "sexpr"
103
+ const contextEntries = options.context
104
+ ? Object.entries(options.context)
105
+ : []
106
+
107
+ const formatPath = (segments: readonly string[]): string => segments.join("")
108
+
109
+ const buildPayload = (path: string, value: unknown): Record<string, unknown> => {
110
+ if (contextEntries.length === 0) {
111
+ return { path, value }
112
+ }
113
+
114
+ const payload: Record<string, unknown> = {}
115
+ for (const [key, contextValue] of contextEntries) {
116
+ payload[key] = contextValue
117
+ }
118
+ payload.path = path
119
+ payload.value = value
120
+ return payload
121
+ }
122
+
123
+ const expectWithPath = (
124
+ segments: readonly string[],
125
+ actualValue: unknown,
126
+ expectedValue: unknown,
127
+ ): void => {
128
+ const path = formatPath(segments)
129
+ expect(buildPayload(path, actualValue)).toEqual(
130
+ buildPayload(path, expectedValue),
131
+ )
132
+ }
133
+
134
+ const getNodeToken = (
135
+ value: CanonicalPrimitiveSExpr,
136
+ ): string | undefined => {
137
+ if (!Array.isArray(value)) {
138
+ return undefined
139
+ }
140
+
141
+ const [head] = value
142
+ return typeof head === "string" ? head : undefined
143
+ }
144
+
145
+ const describePathSegment = (
146
+ child: CanonicalPrimitiveSExpr,
147
+ index: number,
148
+ ): string => {
149
+ const token = getNodeToken(child)
150
+ if (token) {
151
+ return `[${index}:${token}]`
152
+ }
153
+ return `[${index}]`
154
+ }
155
+
156
+ const describeType = (value: CanonicalPrimitiveSExpr): string => {
157
+ if (Array.isArray(value)) {
158
+ return "array"
159
+ }
160
+ if (value === null) {
161
+ return "null"
162
+ }
163
+ return typeof value
164
+ }
165
+
166
+ const compare = (
167
+ currentActual: CanonicalPrimitiveSExpr,
168
+ currentExpected: CanonicalPrimitiveSExpr,
169
+ path: readonly string[],
170
+ ): void => {
171
+ const actualType = describeType(currentActual)
172
+ const expectedType = describeType(currentExpected)
173
+
174
+ if (actualType !== expectedType) {
175
+ expectWithPath([...path, ".type"], actualType, expectedType)
176
+ return
177
+ }
178
+
179
+ if (Array.isArray(currentActual) && Array.isArray(currentExpected)) {
180
+ expectWithPath(
181
+ [...path, ".length"],
182
+ currentActual.length,
183
+ currentExpected.length,
184
+ )
185
+
186
+ const limit = Math.min(currentActual.length, currentExpected.length)
187
+ for (let index = 0; index < limit; index += 1) {
188
+ const childActual = currentActual[index]!
189
+ const childExpected = currentExpected[index]!
190
+ const childSegment = describePathSegment(childActual, index)
191
+ compare(childActual, childExpected, [...path, childSegment])
192
+ }
193
+ return
194
+ }
195
+
196
+ expectWithPath(path, currentActual, currentExpected)
197
+ }
198
+
199
+ compare(normalizedActual, normalizedExpected, [rootPath])
200
+ }
@@ -0,0 +1,48 @@
1
+ import { expect, test } from "bun:test"
2
+ import { KicadPcb, SxClass } from "lib/sexpr"
3
+ import { parseToPrimitiveSExpr } from "lib/sexpr/parseToPrimitiveSExpr"
4
+ import type { PrimitiveSExpr } from "lib/sexpr/parseToPrimitiveSExpr"
5
+
6
+ import { expectEqualPrimitiveSExpr } from "../fixtures/expectEqualPrimitiveSExpr"
7
+
8
+ const numericLike = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/
9
+
10
+ const normalizePrimitive = (value: PrimitiveSExpr): PrimitiveSExpr => {
11
+ if (Array.isArray(value)) {
12
+ return value.map((item) => normalizePrimitive(item))
13
+ }
14
+ if (typeof value === "string" && numericLike.test(value)) {
15
+ const parsed = Number(value)
16
+ if (!Number.isNaN(parsed)) {
17
+ return parsed
18
+ }
19
+ }
20
+ return value
21
+ }
22
+
23
+ const demoPcbPaths = [
24
+ "kicad-demos/demos/flat_hierarchy/flat_hierarchy.kicad_pcb",
25
+ "kicad-demos/demos/custom_pads_test/custom_pads_test.kicad_pcb",
26
+ ]
27
+
28
+ test("kicad_pcb round-trips selected KiCad demo boards", async () => {
29
+ for (const path of demoPcbPaths) {
30
+ const original = await Bun.file(path).text()
31
+ const classes = SxClass.parse(original)
32
+
33
+ expect(classes).toHaveLength(1)
34
+ expect(classes[0]).toBeInstanceOf(KicadPcb)
35
+
36
+ const roundTrip = classes.map((instance) => instance.getString()).join("\n")
37
+
38
+ const originalPrimitive = parseToPrimitiveSExpr(original).map((form) =>
39
+ normalizePrimitive(form),
40
+ )
41
+
42
+ const roundTripPrimitive = parseToPrimitiveSExpr(roundTrip).map((form) =>
43
+ normalizePrimitive(form),
44
+ )
45
+
46
+ expectEqualPrimitiveSExpr(roundTripPrimitive, originalPrimitive, { path })
47
+ }
48
+ })
@@ -0,0 +1,49 @@
1
+ import { expect, test } from "bun:test"
2
+ import { KicadSch, SxClass } from "lib/sexpr"
3
+ import { parseToPrimitiveSExpr } from "lib/sexpr/parseToPrimitiveSExpr"
4
+ import type { PrimitiveSExpr } from "lib/sexpr/parseToPrimitiveSExpr"
5
+
6
+ import { expectEqualPrimitiveSExpr } from "../fixtures/expectEqualPrimitiveSExpr"
7
+
8
+ const numericLike = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/
9
+
10
+ const normalizePrimitive = (value: PrimitiveSExpr): PrimitiveSExpr => {
11
+ if (Array.isArray(value)) {
12
+ return value.map((item) => normalizePrimitive(item))
13
+ }
14
+ if (typeof value === "string" && numericLike.test(value)) {
15
+ const parsed = Number(value)
16
+ if (!Number.isNaN(parsed)) {
17
+ return parsed
18
+ }
19
+ }
20
+ return value
21
+ }
22
+
23
+ const demoSchematics = [
24
+ "kicad-demos/demos/simulation/rectifier/rectifier.kicad_sch",
25
+ "kicad-demos/demos/simulation/up-down-counter/up-down-c.kicad_sch",
26
+ "kicad-demos/demos/simulation/analog-multiplier/a-multi.kicad_sch",
27
+ ]
28
+
29
+ test("kicad_sch round-trips kicad demo schematics", async () => {
30
+ for (const path of demoSchematics) {
31
+ const original = await Bun.file(path).text()
32
+ const classes = SxClass.parse(original)
33
+
34
+ expect(classes).toHaveLength(1)
35
+ expect(classes[0]).toBeInstanceOf(KicadSch)
36
+
37
+ const roundTrip = classes.map((instance) => instance.getString()).join("\n")
38
+
39
+ const originalPrimitive = parseToPrimitiveSExpr(original).map((form) =>
40
+ normalizePrimitive(form),
41
+ )
42
+
43
+ const roundTripPrimitive = parseToPrimitiveSExpr(roundTrip).map((form) =>
44
+ normalizePrimitive(form),
45
+ )
46
+
47
+ expectEqualPrimitiveSExpr(roundTripPrimitive, originalPrimitive, { path })
48
+ }
49
+ })
@@ -0,0 +1,277 @@
1
+ import {
2
+ At,
3
+ Footprint,
4
+ FootprintAttr,
5
+ FootprintPrivateLayers,
6
+ FootprintSolderMaskMargin,
7
+ FpArc,
8
+ FpCircle,
9
+ FpPoly,
10
+ FpRect,
11
+ FpText,
12
+ FpTextBox,
13
+ Layer,
14
+ SxClass,
15
+ } from "lib/sexpr"
16
+ import { expect, test } from "bun:test"
17
+
18
+ test("Footprint", () => {
19
+ const [fp] = SxClass.parse(`
20
+ (footprint "Resistor_SMD:R_0603"
21
+ locked
22
+ placed
23
+ (layer F.Cu)
24
+ (tedit 5E4C0E65)
25
+ (uuid 12345678-1234-1234-1234-123456789abc)
26
+ (at 10.16 5.08 90)
27
+ (descr "0603 chip resistor")
28
+ (tags "resistor smd")
29
+ (property "Sheetfile" "example.kicad_sch")
30
+ (path "/abcdef01")
31
+ (autoplace_cost90 4)
32
+ (solder_mask_margin 0.05)
33
+ (attr smd exclude_from_bom)
34
+ (private_layers F.CrtYd B.CrtYd)
35
+ (net_tie_pad_groups "A,B" "C,D")
36
+ (fp_text reference R1 (at 0 0) (layer F.SilkS)
37
+ (effects (font (size 1 1) (thickness 0.15)))
38
+ )
39
+ (fp_text_box locked "Label"
40
+ (start -1 -1)
41
+ (end 1 1)
42
+ (pts (xy -1 -1) (xy 1 -1) (xy 1 1) (xy -1 1))
43
+ (layer F.SilkS)
44
+ (effects (font (size 1.2 1.2) (thickness 0.18)))
45
+ (uuid 99999999-aaaa-bbbb-cccc-dddddddddddd)
46
+ )
47
+ (fp_rect
48
+ (start -2 -2)
49
+ (end 2 2)
50
+ (layer F.Cu)
51
+ (stroke (width 0.1) (type solid) (color 0 0 0 1))
52
+ (uuid 22222222-3333-4444-5555-666666666666)
53
+ )
54
+ (fp_circle locked
55
+ (center 0 0)
56
+ (end 1 0)
57
+ (layer F.SilkS)
58
+ (stroke (width 0.05) (type dash) (color 0.5 0.5 0.5 1))
59
+ (uuid 77777777-8888-9999-aaaa-bbbbbbbbbbbb)
60
+ )
61
+ (fp_arc
62
+ (start -3 0)
63
+ (mid 0 1)
64
+ (end 3 0)
65
+ (layer F.SilkS)
66
+ (stroke (width 0.08) (type solid) (color 0 0 0 1))
67
+ (uuid 44444444-5555-6666-7777-888888888888)
68
+ )
69
+ (fp_poly
70
+ (pts (xy 0 0) (xy 2 0) (xy 2 2) (xy 0 2))
71
+ (layer F.Mask)
72
+ (stroke (width 0.05) (type solid) (color 0 0 0 1))
73
+ (fill yes)
74
+ (uuid 99999999-aaaa-bbbb-cccc-eeeeeeeeeeee)
75
+ )
76
+ (pad "1" smd roundrect
77
+ (at 0 0 90)
78
+ (size 1.2 0.6)
79
+ (layers F.Cu F.Paste F.Mask)
80
+ (roundrect_rratio 0.25)
81
+ (solder_mask_margin 0.05)
82
+ (clearance 0.1)
83
+ (net 1 "GND")
84
+ (pinfunction "GND")
85
+ (pintype "passive")
86
+ (uuid 12121212-3434-5656-7878-909090909090)
87
+ )
88
+ )
89
+ `)
90
+
91
+ expect(fp).toBeInstanceOf(Footprint)
92
+ const footprint = fp as Footprint
93
+
94
+ expect(footprint.libraryLink).toBe("Resistor_SMD:R_0603")
95
+ expect(footprint.locked).toBe(true)
96
+ expect(footprint.placed).toBe(true)
97
+ expect(footprint.layer).toBeInstanceOf(Layer)
98
+ expect(footprint.tedit?.value).toBe("5E4C0E65")
99
+ expect(footprint.uuid?.value).toBe("12345678-1234-1234-1234-123456789abc")
100
+ const position = footprint.position
101
+ expect(position).toBeDefined()
102
+ expect(position?.x).toBe(10.16)
103
+ expect(position?.y).toBe(5.08)
104
+ expect(position).toBeInstanceOf(At)
105
+ if (position instanceof At) {
106
+ expect(position.angle).toBe(90)
107
+ }
108
+ expect(footprint.descr?.value).toBe("0603 chip resistor")
109
+ expect(footprint.tags?.value).toBe("resistor smd")
110
+ expect(footprint.properties[0]?.key).toBe("Sheetfile")
111
+ expect(footprint.properties[0]?.value).toBe("example.kicad_sch")
112
+ expect(footprint.path?.value).toBe("/abcdef01")
113
+ expect(footprint.autoplaceCost90?.value).toBe(4)
114
+ expect(footprint.solderMaskMargin?.value).toBe(0.05)
115
+ expect(footprint.attr).toBeInstanceOf(FootprintAttr)
116
+ expect(footprint.attr?.type).toBe("smd")
117
+ expect(footprint.attr?.excludeFromBom).toBe(true)
118
+ expect(footprint.privateLayers).toBeInstanceOf(FootprintPrivateLayers)
119
+ expect(footprint.privateLayers?.layers).toEqual(["F.CrtYd", "B.CrtYd"])
120
+ expect(footprint.netTiePadGroups?.groups).toEqual(["A,B", "C,D"])
121
+ expect(footprint.fpTexts.length).toBe(1)
122
+ const text = footprint.fpTexts[0] as FpText
123
+ expect(text.type).toBe("reference")
124
+ expect(text.text).toBe("R1")
125
+ expect(text.layer?.names).toEqual(["F.SilkS"])
126
+ expect(text.effects).toBeDefined()
127
+ expect(footprint.fpTextBoxes.length).toBe(1)
128
+ const textBox = footprint.fpTextBoxes[0] as FpTextBox
129
+ expect(textBox.locked).toBe(true)
130
+ expect(textBox.text).toBe("Label")
131
+ expect(textBox.start?.x).toBe(-1)
132
+ expect(textBox.end?.y).toBe(1)
133
+ expect(footprint.fpRects.length).toBe(1)
134
+ const rect = footprint.fpRects[0] as FpRect
135
+ expect(rect.start?.x).toBe(-2)
136
+ expect(rect.end?.y).toBe(2)
137
+ expect(rect.layer?.names).toEqual(["F.Cu"])
138
+ expect(rect.stroke).toBeDefined()
139
+ expect(footprint.fpCircles.length).toBe(1)
140
+ const circle = footprint.fpCircles[0] as FpCircle
141
+ expect(circle.center?.x).toBe(0)
142
+ expect(circle.end?.x).toBe(1)
143
+ expect(footprint.fpArcs.length).toBe(1)
144
+ const arc = footprint.fpArcs[0] as FpArc
145
+ expect(arc.start?.x).toBe(-3)
146
+ expect(arc.mid?.y).toBe(1)
147
+ expect(arc.end?.x).toBe(3)
148
+ expect(footprint.fpPolys.length).toBe(1)
149
+ const poly = footprint.fpPolys[0] as FpPoly
150
+ expect(poly.points?.points.length).toBe(4)
151
+ expect(poly.fill?.filled).toBe(true)
152
+ expect(footprint.fpPads.length).toBe(1)
153
+ const pad = footprint.fpPads[0]!
154
+ expect(pad.number).toBe("1")
155
+ expect(pad.padType).toBe("smd")
156
+ expect(pad.layers?.layers).toEqual(["F.Cu", "F.Paste", "F.Mask"])
157
+ expect(pad.net?.name).toBe("GND")
158
+
159
+ footprint.locked = false
160
+ footprint.placed = false
161
+ footprint.solderMaskMargin = new FootprintSolderMaskMargin(0.09)
162
+
163
+ expect(footprint.getString()).toMatchInlineSnapshot(`
164
+ "(footprint
165
+ \"Resistor_SMD:R_0603\"
166
+ (layer F.Cu)
167
+ (tedit 5E4C0E65)
168
+ (uuid 12345678-1234-1234-1234-123456789abc)
169
+ (at 10.16 5.08 90)
170
+ (descr \"0603 chip resistor\")
171
+ (tags \"resistor smd\")
172
+ (path \"/abcdef01\")
173
+ (autoplace_cost90 4)
174
+ (solder_mask_margin 0.09)
175
+ (attr smd exclude_from_bom)
176
+ (private_layers F.CrtYd B.CrtYd)
177
+ (net_tie_pad_groups \"A,B\" \"C,D\")
178
+ (property \"Sheetfile\" \"example.kicad_sch\"
179
+ )
180
+ (fp_text
181
+ reference
182
+ \"R1\"
183
+ (at 0 0)
184
+ (layer F.SilkS)
185
+ (effects
186
+ (font
187
+ (size 1 1)
188
+ (thickness 0.15)
189
+ )
190
+ )
191
+ )
192
+ (fp_text_box
193
+ locked
194
+ \"Label\"
195
+ (start -1 -1)
196
+ (end 1 1)
197
+ (pts
198
+ (xy -1 -1)
199
+ (xy 1 -1)
200
+ (xy 1 1)
201
+ (xy -1 1)
202
+ )
203
+ (layer F.SilkS)
204
+ (effects
205
+ (font
206
+ (size 1.2 1.2)
207
+ (thickness 0.18)
208
+ )
209
+ )
210
+ (uuid 99999999-aaaa-bbbb-cccc-dddddddddddd)
211
+ )
212
+ (fp_rect
213
+ (start -2 -2)
214
+ (end 2 2)
215
+ (layer F.Cu)
216
+ (stroke
217
+ (width 0.1)
218
+ (type solid)
219
+ (color 0 0 0 1)
220
+ )
221
+ (uuid 22222222-3333-4444-5555-666666666666)
222
+ )
223
+ (fp_circle
224
+ (center 0 0)
225
+ (end 1 0)
226
+ (layer F.SilkS)
227
+ (stroke
228
+ (width 0.05)
229
+ (type dash)
230
+ (color 0.5 0.5 0.5 1)
231
+ )
232
+ (uuid 77777777-8888-9999-aaaa-bbbbbbbbbbbb)
233
+ locked
234
+ )
235
+ (fp_arc
236
+ (start -3 0)
237
+ (mid 0 1)
238
+ (end 3 0)
239
+ (layer F.SilkS)
240
+ (stroke
241
+ (width 0.08)
242
+ (type solid)
243
+ (color 0 0 0 1)
244
+ )
245
+ (uuid 44444444-5555-6666-7777-888888888888)
246
+ )
247
+ (fp_poly
248
+ (pts
249
+ (xy 0 0)
250
+ (xy 2 0)
251
+ (xy 2 2)
252
+ (xy 0 2)
253
+ )
254
+ (layer F.Mask)
255
+ (stroke
256
+ (width 0.05)
257
+ (type solid)
258
+ (color 0 0 0 1)
259
+ )
260
+ (fill yes)
261
+ (uuid 99999999-aaaa-bbbb-cccc-eeeeeeeeeeee)
262
+ )
263
+ (pad \"1\" smd roundrect
264
+ (at 0 0 90)
265
+ (size 1.2 0.6)
266
+ (layers F.Cu F.Paste F.Mask)
267
+ (roundrect_rratio 0.25)
268
+ (net 1 \"GND\")
269
+ (pinfunction GND)
270
+ (pintype passive)
271
+ (solder_mask_margin 0.05)
272
+ (clearance 0.1)
273
+ (uuid 12121212-3434-5656-7878-909090909090)
274
+ )
275
+ )"
276
+ `)
277
+ })